summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-06-20 11:10:13 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-06-20 11:10:13 +0000
commit0ea3fcec397b69815975647f5e2aa5fe944a8486 (patch)
tree7979381b89d26011bcf9bdc989a40fcc2f1ed4ff /app
parent72123183a20411a36d607d70b12d57c484394c8e (diff)
downloadgitlab-ce-0ea3fcec397b69815975647f5e2aa5fe944a8486.tar.gz
Add latest changes from gitlab-org/gitlab@15-1-stable-eev15.1.0-rc42
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/auth_buttons/gitlab_64.pngbin2070 -> 1622 bytes
-rw-r--r--app/assets/images/ext_snippet_icons/ext_snippet_icons.pngbin1319 -> 1334 bytes
-rw-r--r--app/assets/javascripts/access_tokens/components/access_token_table_app.vue168
-rw-r--r--app/assets/javascripts/access_tokens/components/constants.js61
-rw-r--r--app/assets/javascripts/access_tokens/components/expires_at_field.vue10
-rw-r--r--app/assets/javascripts/access_tokens/components/new_access_token_app.vue130
-rw-r--r--app/assets/javascripts/access_tokens/index.js71
-rw-r--r--app/assets/javascripts/activities.js4
-rw-r--r--app/assets/javascripts/admin/application_settings/inactive_project_deletion/components/form.vue249
-rw-r--r--app/assets/javascripts/admin/application_settings/inactive_project_deletion/index.js36
-rw-r--r--app/assets/javascripts/admin/users/components/user_actions.vue2
-rw-r--r--app/assets/javascripts/admin/users/components/users_table.vue14
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue18
-rw-r--r--app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue8
-rw-r--r--app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue6
-rw-r--r--app/assets/javascripts/api.js18
-rw-r--r--app/assets/javascripts/api/projects_api.js11
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue4
-rw-r--r--app/assets/javascripts/awards_handler.js40
-rw-r--r--app/assets/javascripts/batch_comments/components/draft_note.vue2
-rw-r--r--app/assets/javascripts/batch_comments/components/drafts_count.vue9
-rw-r--r--app/assets/javascripts/batch_comments/components/preview_dropdown.vue5
-rw-r--r--app/assets/javascripts/batch_comments/components/review_bar.vue7
-rw-r--r--app/assets/javascripts/batch_comments/components/submit_dropdown.vue115
-rw-r--r--app/assets/javascripts/batch_comments/services/drafts_service.js4
-rw-r--r--app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js20
-rw-r--r--app/assets/javascripts/behaviors/components/sandboxed_mermaid.vue77
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js20
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_kroki.js4
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js10
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js9
-rw-r--r--app/assets/javascripts/blob/blob_line_permalink_updater.js5
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue6
-rw-r--r--app/assets/javascripts/blob/components/blob_header_default_actions.vue17
-rw-r--r--app/assets/javascripts/blob/components/blob_header_filepath.vue4
-rw-r--r--app/assets/javascripts/blob/viewer/index.js17
-rw-r--r--app/assets/javascripts/boards/components/board_app.vue11
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue29
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue19
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue67
-rw-r--r--app/assets/javascripts/boards/graphql/board_create.mutation.graphql5
-rw-r--r--app/assets/javascripts/boards/graphql/board_update.mutation.graphql5
-rw-r--r--app/assets/javascripts/boards/index.js1
-rw-r--r--app/assets/javascripts/boards/stores/actions.js31
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js15
-rw-r--r--app/assets/javascripts/boards/stores/state.js2
-rw-r--r--app/assets/javascripts/breadcrumb.js2
-rw-r--r--app/assets/javascripts/ci_secure_files/components/secure_files_list.vue2
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue3
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue27
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_environments_dropdown.vue81
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue426
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue32
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue199
-rw-r--r--app/assets/javascripts/ci_variable_list/index.js63
-rw-r--r--app/assets/javascripts/clusters/agents/components/create_token_button.vue7
-rw-r--r--app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue51
-rw-r--r--app/assets/javascripts/clusters_list/clusters_util.js10
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_token.vue11
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters.vue10
-rw-r--r--app/assets/javascripts/clusters_list/components/install_agent_modal.vue7
-rw-r--r--app/assets/javascripts/code_navigation/utils/index.js4
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue21
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue157
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue34
-rw-r--r--app/assets/javascripts/content_editor/components/top_toolbar.vue14
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/code_block.vue79
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/footnote_definition.vue28
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue7
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/diagram.js22
-rw-r--r--app/assets/javascripts/content_editor/extensions/footnote_definition.js27
-rw-r--r--app/assets/javascripts/content_editor/extensions/footnote_reference.js15
-rw-r--r--app/assets/javascripts/content_editor/extensions/footnotes_section.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/sourcemap.js18
-rw-r--r--app/assets/javascripts/content_editor/services/asset_resolver.js15
-rw-r--r--app/assets/javascripts/content_editor/services/code_block_language_loader.js8
-rw-r--r--app/assets/javascripts/content_editor/services/content_editor.js4
-rw-r--r--app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js329
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js39
-rw-r--r--app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js180
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js222
-rw-r--r--app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue4
-rw-r--r--app/assets/javascripts/custom_metrics/index.js4
-rw-r--r--app/assets/javascripts/cycle_analytics/components/path_navigation.vue11
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_table.vue30
-rw-r--r--app/assets/javascripts/cycle_analytics/constants.js1
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/render.js8
-rw-r--r--app/assets/javascripts/design_management/components/delete_button.vue2
-rw-r--r--app/assets/javascripts/design_management/components/design_presentation.vue13
-rw-r--r--app/assets/javascripts/design_management/components/design_sidebar.vue180
-rw-r--r--app/assets/javascripts/design_management/components/list/item.vue2
-rw-r--r--app/assets/javascripts/design_management/components/toolbar/design_navigation.vue4
-rw-r--r--app/assets/javascripts/design_management/components/toolbar/index.vue12
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue165
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue12
-rw-r--r--app/assets/javascripts/diff.js3
-rw-r--r--app/assets/javascripts/diffs/components/app.vue16
-rw-r--r--app/assets/javascripts/diffs/components/collapsed_files_warning.vue2
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue9
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue47
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue7
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue13
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue8
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue43
-rw-r--r--app/assets/javascripts/diffs/components/merge_conflict_warning.vue4
-rw-r--r--app/assets/javascripts/diffs/store/utils.js8
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js12
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js27
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_webide_ext.js28
-rw-r--r--app/assets/javascripts/editor/schema/ci.json198
-rw-r--r--app/assets/javascripts/editor/source_editor.js5
-rw-r--r--app/assets/javascripts/editor/source_editor_extension.js2
-rw-r--r--app/assets/javascripts/emoji/constants.js2
-rw-r--r--app/assets/javascripts/emoji/index.js18
-rw-r--r--app/assets/javascripts/emoji/utils.js8
-rw-r--r--app/assets/javascripts/environments/components/container.vue2
-rw-r--r--app/assets/javascripts/environments/components/deploy_board_wrapper.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_folder.vue4
-rw-r--r--app/assets/javascripts/environments/components/environment_form.vue33
-rw-r--r--app/assets/javascripts/environments/components/environments_detail_header.vue4
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue2
-rw-r--r--app/assets/javascripts/environments/components/new_environment_item.vue2
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue7
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_actions.vue2
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/empty_state.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags.vue13
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags_table.vue20
-rw-r--r--app/assets/javascripts/feature_flags/components/form.vue12
-rw-r--r--app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/new_feature_flag.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/strategy.vue2
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_operator.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js2
-rw-r--r--app/assets/javascripts/filtered_search/droplab/drop_down.js4
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js20
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue6
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js2
-rw-r--r--app/assets/javascripts/gl_form.js5
-rw-r--r--app/assets/javascripts/google_cloud/components/gcp_regions_list.vue2
-rw-r--r--app/assets/javascripts/google_cloud/components/revoke_oauth.vue2
-rw-r--r--app/assets/javascripts/google_cloud/components/service_accounts_list.vue2
-rw-r--r--app/assets/javascripts/google_tag_manager/index.js8
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql3
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json4
-rw-r--r--app/assets/javascripts/group_settings/components/shared_runners_form.vue7
-rw-r--r--app/assets/javascripts/group_settings/constants.js3
-rw-r--r--app/assets/javascripts/group_settings/stale_runner_cleanup.js3
-rw-r--r--app/assets/javascripts/groups/components/app.vue42
-rw-r--r--app/assets/javascripts/groups/components/empty_state.vue91
-rw-r--r--app/assets/javascripts/groups/components/group_folder.vue2
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue4
-rw-r--r--app/assets/javascripts/groups/components/group_name_and_path.vue279
-rw-r--r--app/assets/javascripts/groups/components/groups.vue7
-rw-r--r--app/assets/javascripts/groups/components/item_caret.vue4
-rw-r--r--app/assets/javascripts/groups/components/item_type_icon.vue4
-rw-r--r--app/assets/javascripts/groups/create_edit_form.js29
-rw-r--r--app/assets/javascripts/groups/index.js25
-rw-r--r--app/assets/javascripts/groups/settings/api/access_dropdown_api.js16
-rw-r--r--app/assets/javascripts/groups/settings/components/access_dropdown.vue194
-rw-r--r--app/assets/javascripts/groups/settings/constants.js3
-rw-r--r--app/assets/javascripts/groups/settings/init_access_dropdown.js36
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue27
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue28
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue48
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue55
-rw-r--r--app/assets/javascripts/ide/components/ide.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue6
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue7
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue6
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/description.vue2
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue74
-rw-r--r--app/assets/javascripts/ide/components/terminal/empty_state.vue2
-rw-r--r--app/assets/javascripts/ide/components/terminal/session.vue2
-rw-r--r--app/assets/javascripts/ide/lib/editor_options.js1
-rw-r--r--app/assets/javascripts/ide/utils.js3
-rw-r--r--app/assets/javascripts/image_diff/helpers/dom_helper.js2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue54
-rw-r--r--app/assets/javascripts/import_entities/import_groups/constants.js3
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue2
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue4
-rw-r--r--app/assets/javascripts/incidents/list.js1
-rw-r--r--app/assets/javascripts/incidents_settings/components/pagerduty_form.vue24
-rw-r--r--app/assets/javascripts/integrations/constants.js36
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue10
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_upgrade_cta.vue51
-rw-r--r--app/assets/javascripts/integrations/edit/components/sections/configuration.vue38
-rw-r--r--app/assets/javascripts/integrations/edit/components/sections/trigger.vue26
-rw-r--r--app/assets/javascripts/integrations/edit/components/trigger_field.vue46
-rw-r--r--app/assets/javascripts/integrations/edit/index.js4
-rw-r--r--app/assets/javascripts/integrations/overrides/components/integration_overrides.vue2
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue13
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_trigger.vue13
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue16
-rw-r--r--app/assets/javascripts/invite_members/components/user_limit_notification.vue33
-rw-r--r--app/assets/javascripts/invite_members/constants.js13
-rw-r--r--app/assets/javascripts/issuable/components/csv_export_modal.vue34
-rw-r--r--app/assets/javascripts/issuable/components/csv_import_modal.vue4
-rw-r--r--app/assets/javascripts/issuable/components/issuable_header_warnings.vue2
-rw-r--r--app/assets/javascripts/issuable/components/related_issuable_item.vue5
-rw-r--r--app/assets/javascripts/issuable/components/status_box.vue5
-rw-r--r--app/assets/javascripts/issuable/issuable_form.js3
-rw-r--r--app/assets/javascripts/issuable/popover/components/issue_popover.vue83
-rw-r--r--app/assets/javascripts/issuable/popover/components/mr_popover.vue (renamed from app/assets/javascripts/mr_popover/components/mr_popover.vue)35
-rw-r--r--app/assets/javascripts/issuable/popover/constants.js (renamed from app/assets/javascripts/mr_popover/constants.js)0
-rw-r--r--app/assets/javascripts/issuable/popover/index.js85
-rw-r--r--app/assets/javascripts/issuable/popover/queries/issue.query.graphql11
-rw-r--r--app/assets/javascripts/issuable/popover/queries/merge_request.query.graphql (renamed from app/assets/javascripts/mr_popover/queries/merge_request.query.graphql)4
-rw-r--r--app/assets/javascripts/issues/create_merge_request_dropdown.js49
-rw-r--r--app/assets/javascripts/issues/index.js4
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue99
-rw-r--r--app/assets/javascripts/issues/list/constants.js6
-rw-r--r--app/assets/javascripts/issues/list/index.js10
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues_counts_without_crm.query.graphql136
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues_without_crm.query.graphql94
-rw-r--r--app/assets/javascripts/issues/list/utils.js11
-rw-r--r--app/assets/javascripts/issues/new/components/title_suggestions.vue28
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue80
-rw-r--r--app/assets/javascripts/issues/show/components/edit_actions.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql21
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue21
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue73
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_list_item.vue71
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue70
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/utils.js18
-rw-r--r--app/assets/javascripts/issues/show/components/title.vue2
-rw-r--r--app/assets/javascripts/issues/show/index.js5
-rw-r--r--app/assets/javascripts/jira_connect/branches/pages/index.vue2
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue2
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/app.vue86
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue2
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue25
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions_page.vue2
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_app.vue2
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_form.vue4
-rw-r--r--app/assets/javascripts/jobs/components/job_container_item.vue3
-rw-r--r--app/assets/javascripts/jobs/components/log/line_header.vue2
-rw-r--r--app/assets/javascripts/jobs/components/manual_variables_form.vue2
-rw-r--r--app/assets/javascripts/jobs/components/stuck_block.vue23
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_app.vue2
-rw-r--r--app/assets/javascripts/labels/components/promote_label_modal.vue2
-rw-r--r--app/assets/javascripts/lazy_loader.js9
-rw-r--r--app/assets/javascripts/lib/gfm/index.js27
-rw-r--r--app/assets/javascripts/lib/graphql.js13
-rw-r--r--app/assets/javascripts/lib/utils/color_utils.js2
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js2
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js11
-rw-r--r--app/assets/javascripts/lib/utils/forms.js4
-rw-r--r--app/assets/javascripts/lib/utils/rails_ujs.js4
-rw-r--r--app/assets/javascripts/lib/utils/table_utility.js10
-rw-r--r--app/assets/javascripts/lib/utils/users_cache.js7
-rw-r--r--app/assets/javascripts/logo.js4
-rw-r--r--app/assets/javascripts/logs/utils.js21
-rw-r--r--app/assets/javascripts/main.js7
-rw-r--r--app/assets/javascripts/members/components/members_tabs.vue4
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue33
-rw-r--r--app/assets/javascripts/members/components/table/role_dropdown.vue2
-rw-r--r--app/assets/javascripts/members/constants.js12
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue4
-rw-r--r--app/assets/javascripts/merge_request.js10
-rw-r--r--app/assets/javascripts/merge_request_tabs.js3
-rw-r--r--app/assets/javascripts/milestones/components/promote_milestone_modal.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/create_dashboard_modal.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue14
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_header.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue8
-rw-r--r--app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue27
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue2
-rw-r--r--app/assets/javascripts/monitoring/services/alerts_service.js43
-rw-r--r--app/assets/javascripts/mr_popover/index.js67
-rw-r--r--app/assets/javascripts/nav/components/responsive_header.vue2
-rw-r--r--app/assets/javascripts/notes/components/comment_field_layout.vue4
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue34
-rw-r--r--app/assets/javascripts/notes/components/diff_discussion_header.vue9
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue11
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue12
-rw-r--r--app/assets/javascripts/notes/components/email_participants_warning.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue14
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue114
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue9
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue5
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue6
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue2
-rw-r--r--app/assets/javascripts/notes/constants.js2
-rw-r--r--app/assets/javascripts/notes/i18n.js4
-rw-r--r--app/assets/javascripts/notes/stores/actions.js14
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js17
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue5
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue5
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue3
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_files.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue25
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue75
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata_loader.vue30
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/composer.vue10
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/conan.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/maven.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue14
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue10
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue52
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history_loader.vue24
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue19
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/constants.js8
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql32
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql39
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_pipelines.query.graphql24
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue27
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue124
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue (renamed from app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue119
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/constants.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/cli_commands.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/payload_downloader.js4
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/payload_previewer.js6
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/repository/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/groups/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/groups/new/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/impersonation_tokens/index.js10
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js6
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js22
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js25
-rw-r--r--app/assets/javascripts/pages/groups/new/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/settings/access_tokens/index.js8
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue2
-rw-r--r--app/assets/javascripts/pages/import/history/components/import_error_details.vue2
-rw-r--r--app/assets/javascripts/pages/import/history/components/import_history_app.vue2
-rw-r--r--app/assets/javascripts/pages/profiles/personal_access_tokens/index.js10
-rw-r--r--app/assets/javascripts/pages/profiles/two_factor_auths/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue11
-rw-r--r--app/assets/javascripts/pages/projects/incidents/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js32
-rw-r--r--app/assets/javascripts/pages/projects/issues/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js9
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js7
-rw-r--r--app/assets/javascripts/pages/projects/settings/access_tokens/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/settings/branch_rules/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/settings/integrations/edit/index.js (renamed from app/assets/javascripts/pages/projects/services/edit/index.js)0
-rw-r--r--app/assets/javascripts/pages/projects/settings/integrations/index/index.js (renamed from app/assets/javascripts/pages/projects/settings/integrations/show/index.js)0
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue131
-rw-r--r--app/assets/javascripts/pages/projects/shared/save_project_loader.js16
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/static_site_editor/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/tags/index/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/tags/remove_tag.js16
-rw-r--r--app/assets/javascripts/pages/projects/tags/show/index.js9
-rw-r--r--app/assets/javascripts/pages/shared/nav/sidebar_tracking.js10
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js2
-rw-r--r--app/assets/javascripts/performance_bar/components/add_request.vue34
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue4
-rw-r--r--app/assets/javascripts/performance_bar/components/request_selector.vue2
-rw-r--r--app/assets/javascripts/performance_bar/index.js4
-rw-r--r--app/assets/javascripts/performance_bar/performance_bar_log.js2
-rw-r--r--app/assets/javascripts/performance_bar/services/performance_bar_service.js15
-rw-r--r--app/assets/javascripts/performance_bar/stores/performance_bar_store.js10
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js4
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue6
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue11
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue43
-rw-r--r--app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue13
-rw-r--r--app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue2
-rw-r--r--app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue8
-rw-r--r--app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue2
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue23
-rw-r--r--app/assets/javascripts/pipeline_editor/components/popovers/file_tree_popover.vue7
-rw-r--r--app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue65
-rw-r--r--app/assets/javascripts/pipeline_editor/constants.js21
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js2
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue7
-rw-r--r--app/assets/javascripts/pipeline_wizard/components/input_wrapper.vue (renamed from app/assets/javascripts/pipeline_wizard/components/input.vue)0
-rw-r--r--app/assets/javascripts/pipeline_wizard/components/step.vue2
-rw-r--r--app/assets/javascripts/pipeline_wizard/templates/.gitkeep0
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue40
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/jobs_app.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/notification/deprecated_type_keyword_notification.vue102
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_tabs.vue31
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue43
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue23
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue58
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary.vue2
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_pipeline_warnings.query.graphql12
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js28
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_notification.js31
-rw-r--r--app/assets/javascripts/pipelines/pipeline_tabs.js40
-rw-r--r--app/assets/javascripts/pipelines/pipeline_test_details.js11
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue5
-rw-r--r--app/assets/javascripts/project_select.js204
-rw-r--r--app/assets/javascripts/projects/clusters_deprecation_alert/components/clusters_deprecation_alert.vue23
-rw-r--r--app/assets/javascripts/projects/clusters_deprecation_alert/index.js21
-rw-r--r--app/assets/javascripts/projects/commit/components/form_modal.vue2
-rw-r--r--app/assets/javascripts/projects/commits/components/author_select.vue2
-rw-r--r--app/assets/javascripts/projects/compare/components/app.vue4
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_card.vue10
-rw-r--r--app/assets/javascripts/projects/default_project_templates.js8
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue12
-rw-r--r--app/assets/javascripts/projects/project_import_gitlab_project.js15
-rw-r--r--app/assets/javascripts/projects/project_new.js154
-rw-r--r--app/assets/javascripts/projects/project_visibility.js6
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/branch_dropdown.vue110
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/rule_edit.vue38
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js26
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/queries/branches.query.graphql8
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/app.vue16
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js13
-rw-r--r--app/assets/javascripts/related_issues/constants.js11
-rw-r--r--app/assets/javascripts/related_issues/index.js3
-rw-r--r--app/assets/javascripts/releases/components/app_index.vue2
-rw-r--r--app/assets/javascripts/releases/components/release_block_header.vue2
-rw-r--r--app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql1
-rw-r--r--app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql1
-rw-r--r--app/assets/javascripts/releases/graphql/mutations/create_release.mutation.graphql1
-rw-r--r--app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql1
-rw-r--r--app/assets/javascripts/reports/components/summary_row.vue2
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue23
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/index.js1
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/sketch_viewer.vue31
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue2
-rw-r--r--app/assets/javascripts/repository/components/preview/index.vue2
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue14
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue8
-rw-r--r--app/assets/javascripts/rest_api.js1
-rw-r--r--app/assets/javascripts/right_sidebar.js2
-rw-r--r--app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue30
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue18
-rw-r--r--app/assets/javascripts/runner/admin_runners/index.js4
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_status_cell.vue7
-rw-r--r--app/assets/javascripts/runner/components/registration/registration_dropdown.vue26
-rw-r--r--app/assets/javascripts/runner/components/registration/registration_token.vue18
-rw-r--r--app/assets/javascripts/runner/components/runner_details.vue27
-rw-r--r--app/assets/javascripts/runner/components/runner_jobs.vue8
-rw-r--r--app/assets/javascripts/runner/components/runner_list.vue2
-rw-r--r--app/assets/javascripts/runner/components/runner_list_empty_state.vue75
-rw-r--r--app/assets/javascripts/runner/components/runner_projects.vue8
-rw-r--r--app/assets/javascripts/runner/components/runner_update_form.vue12
-rw-r--r--app/assets/javascripts/runner/graphql/list/admin_runners.query.graphql2
-rw-r--r--app/assets/javascripts/runner/graphql/list/group_runners.query.graphql2
-rw-r--r--app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql21
-rw-r--r--app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql20
-rw-r--r--app/assets/javascripts/runner/graphql/show/runner.query.graphql40
-rw-r--r--app/assets/javascripts/runner/graphql/show/runner_details.fragment.graphql5
-rw-r--r--app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql39
-rw-r--r--app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue114
-rw-r--r--app/assets/javascripts/runner/group_runner_show/index.js36
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue17
-rw-r--r--app/assets/javascripts/runner/group_runners/index.js4
-rw-r--r--app/assets/javascripts/runner/runner_search_utils.js14
-rw-r--r--app/assets/javascripts/search/store/actions.js18
-rw-r--r--app/assets/javascripts/search_autocomplete.js2
-rw-r--r--app/assets/javascripts/search_settings/components/search_settings.vue2
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue28
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js4
-rw-r--r--app/assets/javascripts/security_configuration/graphql/current_license.query.graphql6
-rw-r--r--app/assets/javascripts/security_configuration/index.js1
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/attention_requested_toggle.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue13
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/graphql/cache_update.js20
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/graphql/mutations/delete_timelog.mutation.graphql5
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/report.vue92
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue1
-rw-r--r--app/assets/javascripts/sidebar/graphql.js8
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js7
-rw-r--r--app/assets/javascripts/sidebar/queries/escalation_status.fragment.graphql4
-rw-r--r--app/assets/javascripts/sidebar/queries/update_escalation_status.mutation.graphql4
-rw-r--r--app/assets/javascripts/sidebar/utils.js1
-rw-r--r--app/assets/javascripts/single_file_diff.js2
-rw-r--r--app/assets/javascripts/static_site_editor/components/app.vue13
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_area.vue190
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_drawer.vue27
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_header.vue23
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_meta_controls.vue130
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_meta_modal.vue126
-rw-r--r--app/assets/javascripts/static_site_editor/components/front_matter_controls.vue57
-rw-r--r--app/assets/javascripts/static_site_editor/components/invalid_content_message.vue29
-rw-r--r--app/assets/javascripts/static_site_editor/components/publish_toolbar.vue57
-rw-r--r--app/assets/javascripts/static_site_editor/components/skeleton_loader.vue19
-rw-r--r--app/assets/javascripts/static_site_editor/components/submit_changes_error.vue24
-rw-r--r--app/assets/javascripts/static_site_editor/components/unsaved_changes_confirm_dialog.vue27
-rw-r--r--app/assets/javascripts/static_site_editor/constants.js35
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/index.js47
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/mutations/has_submitted_changes.mutation.graphql5
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql7
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql17
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/queries/saved_content_meta.query.graphql3
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql10
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/resolvers/file.js11
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/resolvers/has_submitted_changes.js25
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js47
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/typedefs.graphql58
-rw-r--r--app/assets/javascripts/static_site_editor/image_repository.js25
-rw-r--r--app/assets/javascripts/static_site_editor/index.js56
-rw-r--r--app/assets/javascripts/static_site_editor/pages/home.vue169
-rw-r--r--app/assets/javascripts/static_site_editor/pages/success.vue106
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/constants.js57
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/modals/add_image/add_image_modal.vue134
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/modals/add_image/upload_image_tab.vue56
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/modals/insert_video_modal.vue93
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/rich_content_editor.vue150
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/build_custom_renderer.js42
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/build_html_to_markdown_renderer.js109
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/editor_service.js116
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token.js63
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_attribute_definition.js7
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_embedded_ruby_text.js9
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_font_awesome_html_inline.js11
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_heading.js6
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_html_block.js23
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text.js40
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph.js40
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_list_item.js6
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_softbreak.js7
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_utils.js38
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/sanitize_html.js22
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/toolbar_item.vue31
-rw-r--r--app/assets/javascripts/static_site_editor/router/constants.js2
-rw-r--r--app/assets/javascripts/static_site_editor/router/index.js15
-rw-r--r--app/assets/javascripts/static_site_editor/router/routes.js21
-rw-r--r--app/assets/javascripts/static_site_editor/services/formatter.js56
-rw-r--r--app/assets/javascripts/static_site_editor/services/front_matterify.js75
-rw-r--r--app/assets/javascripts/static_site_editor/services/generate_branch_name.js8
-rw-r--r--app/assets/javascripts/static_site_editor/services/image_service.js8
-rw-r--r--app/assets/javascripts/static_site_editor/services/load_source_content.js15
-rw-r--r--app/assets/javascripts/static_site_editor/services/parse_source_file.js46
-rw-r--r--app/assets/javascripts/static_site_editor/services/renderers/render_image.js89
-rw-r--r--app/assets/javascripts/static_site_editor/services/submit_content_changes.js145
-rw-r--r--app/assets/javascripts/static_site_editor/services/templater.js89
-rw-r--r--app/assets/javascripts/tags/components/delete_tag_modal.vue192
-rw-r--r--app/assets/javascripts/tags/event_hub.js3
-rw-r--r--app/assets/javascripts/tags/init_delete_tag_modal.js14
-rw-r--r--app/assets/javascripts/terraform/components/states_table.vue58
-rw-r--r--app/assets/javascripts/terraform/components/states_table_actions.vue10
-rw-r--r--app/assets/javascripts/terraform/components/terraform_list.vue7
-rw-r--r--app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql1
-rw-r--r--app/assets/javascripts/terraform/index.js4
-rw-r--r--app/assets/javascripts/token_access/components/token_access.vue2
-rw-r--r--app/assets/javascripts/token_access/components/token_projects_table.vue6
-rw-r--r--app/assets/javascripts/user_popovers.js139
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue97
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/humanized_text.js23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue84
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js207
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue100
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/issues.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/index.js10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue36
-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.js14
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue10
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue4
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/constants.js9
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/index.js16
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/clone_dropdown.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue214
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js30
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue109
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue53
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue31
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue43
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql9
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql9
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js15
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js14
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue37
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/registry_search.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue123
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js44
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/constants.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js13
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js39
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue125
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue2
-rw-r--r--app/assets/javascripts/vue_shared/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue1
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue59
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue7
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue17
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/constants.js7
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue5
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue12
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue7
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue28
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue6
-rw-r--r--app/assets/javascripts/whats_new/components/app.vue2
-rw-r--r--app/assets/javascripts/whats_new/store/actions.js3
-rw-r--r--app/assets/javascripts/whats_new/utils/notification.js2
-rw-r--r--app/assets/javascripts/work_items/components/item_state.vue18
-rw-r--r--app/assets/javascripts/work_items/components/item_title.vue9
-rw-r--r--app/assets/javascripts/work_items/components/update_work_item.js23
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue111
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue234
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue53
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail_modal.vue17
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/index.js37
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue165
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue28
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state.vue46
-rw-r--r--app/assets/javascripts/work_items/components/work_item_title.vue50
-rw-r--r--app/assets/javascripts/work_items/components/work_item_weight.vue26
-rw-r--r--app/assets/javascripts/work_items/constants.js18
-rw-r--r--app/assets/javascripts/work_items/graphql/local_update_work_item.mutation.graphql9
-rw-r--r--app/assets/javascripts/work_items/graphql/provider.js84
-rw-r--r--app/assets/javascripts/work_items/graphql/typedefs.graphql36
-rw-r--r--app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql8
-rw-r--r--app/assets/javascripts/work_items/graphql/update_work_item_widgets.mutation.graphql10
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql7
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.query.graphql16
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_links.query.graphql28
-rw-r--r--app/assets/javascripts/work_items/index.js4
-rw-r--r--app/assets/javascripts/work_items/pages/work_item_root.vue4
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss1
-rw-r--r--app/assets/stylesheets/components/avatar.scss4
-rw-r--r--app/assets/stylesheets/components/feature_highlight.scss26
-rw-r--r--app/assets/stylesheets/components/related_items_list.scss6
-rw-r--r--app/assets/stylesheets/components/upload_dropzone/upload_dropzone.scss33
-rw-r--r--app/assets/stylesheets/errors.scss4
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/awards.scss6
-rw-r--r--app/assets/stylesheets/framework/buttons.scss64
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss16
-rw-r--r--app/assets/stylesheets/framework/diffs.scss41
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss26
-rw-r--r--app/assets/stylesheets/framework/forms.scss5
-rw-r--r--app/assets/stylesheets/framework/header.scss14
-rw-r--r--app/assets/stylesheets/framework/highlight.scss1
-rw-r--r--app/assets/stylesheets/framework/icons.scss106
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss40
-rw-r--r--app/assets/stylesheets/framework/mixins.scss4
-rw-r--r--app/assets/stylesheets/framework/page_title.scss2
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss23
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss34
-rw-r--r--app/assets/stylesheets/framework/timeline.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss23
-rw-r--r--app/assets/stylesheets/framework/variables.scss14
-rw-r--r--app/assets/stylesheets/highlight/common.scss38
-rw-r--r--app/assets/stylesheets/highlight/themes/dark.scss4
-rw-r--r--app/assets/stylesheets/highlight/themes/monokai.scss23
-rw-r--r--app/assets/stylesheets/highlight/themes/none.scss12
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-dark.scss23
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-light.scss4
-rw-r--r--app/assets/stylesheets/highlight/white_base.scss13
-rw-r--r--app/assets/stylesheets/notify_enhanced.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/_pipeline_mixins.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/build.scss13
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss19
-rw-r--r--app/assets/stylesheets/page_bundles/issues_show.scss28
-rw-r--r--app/assets/stylesheets/page_bundles/jira_connect.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss71
-rw-r--r--app/assets/stylesheets/page_bundles/pipelines.scss12
-rw-r--r--app/assets/stylesheets/page_bundles/wiki.scss1
-rw-r--r--app/assets/stylesheets/pages/commits.scss6
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss19
-rw-r--r--app/assets/stylesheets/pages/groups.scss51
-rw-r--r--app/assets/stylesheets/pages/issuable.scss119
-rw-r--r--app/assets/stylesheets/pages/labels.scss15
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss52
-rw-r--r--app/assets/stylesheets/pages/note_form.scss27
-rw-r--r--app/assets/stylesheets/pages/notes.scss8
-rw-r--r--app/assets/stylesheets/pages/pages.scss55
-rw-r--r--app/assets/stylesheets/pages/profile.scss4
-rw-r--r--app/assets/stylesheets/pages/profiles/preferences.scss12
-rw-r--r--app/assets/stylesheets/snippets.scss7
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss120
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss116
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss6
-rw-r--r--app/assets/stylesheets/themes/_dark.scss1
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss12
-rw-r--r--app/assets/stylesheets/themes/theme_gray.scss (renamed from app/assets/stylesheets/themes/theme_dark.scss)2
-rw-r--r--app/assets/stylesheets/themes/theme_helper.scss3
-rw-r--r--app/assets/stylesheets/themes/theme_light_gray.scss (renamed from app/assets/stylesheets/themes/theme_light.scss)10
-rw-r--r--app/assets/stylesheets/utilities.scss47
-rw-r--r--app/components/diffs/overflow_warning_component.html.haml2
-rw-r--r--app/components/diffs/overflow_warning_component.rb7
-rw-r--r--app/components/diffs/stats_component.rb8
-rw-r--r--app/components/pajamas/alert_component.html.haml9
-rw-r--r--app/components/pajamas/alert_component.rb14
-rw-r--r--app/components/pajamas/banner_component.html.haml23
-rw-r--r--app/components/pajamas/banner_component.rb61
-rw-r--r--app/components/pajamas/button_component.html.haml8
-rw-r--r--app/components/pajamas/button_component.rb118
-rw-r--r--app/components/pajamas/card_component.html.haml9
-rw-r--r--app/components/pajamas/card_component.rb21
-rw-r--r--app/components/pajamas/checkbox_component.html.haml6
-rw-r--r--app/components/pajamas/checkbox_component.rb56
-rw-r--r--app/components/pajamas/component.rb12
-rw-r--r--app/components/pajamas/concerns/checkbox_radio_label_with_help_text.rb30
-rw-r--r--app/components/pajamas/concerns/checkbox_radio_options.rb11
-rw-r--r--app/components/pajamas/radio_component.html.haml5
-rw-r--r--app/components/pajamas/radio_component.rb51
-rw-r--r--app/controllers/admin/application_settings/appearances_controller.rb1
-rw-r--r--app/controllers/admin/application_settings_controller.rb6
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb1
-rw-r--r--app/controllers/admin/dashboard_controller.rb2
-rw-r--r--app/controllers/admin/groups_controller.rb1
-rw-r--r--app/controllers/admin/runners_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb2
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/controllers/autocomplete_controller.rb4
-rw-r--r--app/controllers/clusters/clusters_controller.rb106
-rw-r--r--app/controllers/concerns/gitlab_recaptcha.rb3
-rw-r--r--app/controllers/concerns/integrations/actions.rb2
-rw-r--r--app/controllers/concerns/issuable_actions.rb3
-rw-r--r--app/controllers/concerns/issues_calendar.rb2
-rw-r--r--app/controllers/concerns/membership_actions.rb4
-rw-r--r--app/controllers/concerns/notes_actions.rb42
-rw-r--r--app/controllers/concerns/product_analytics_tracking.rb11
-rw-r--r--app/controllers/concerns/project_stats_refresh_conflicts_guard.rb13
-rw-r--r--app/controllers/concerns/snippets_actions.rb2
-rw-r--r--app/controllers/concerns/sorting_preference.rb17
-rw-r--r--app/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support.rb2
-rw-r--r--app/controllers/concerns/wiki_actions.rb2
-rw-r--r--app/controllers/concerns/zuora_csp.rb26
-rw-r--r--app/controllers/confirmations_controller.rb2
-rw-r--r--app/controllers/groups/autocomplete_sources_controller.rb3
-rw-r--r--app/controllers/groups/email_campaigns_controller.rb1
-rw-r--r--app/controllers/groups/group_members_controller.rb4
-rw-r--r--app/controllers/groups/runners_controller.rb2
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb5
-rw-r--r--app/controllers/groups_controller.rb8
-rw-r--r--app/controllers/help_controller.rb26
-rw-r--r--app/controllers/ide_controller.rb5
-rw-r--r--app/controllers/import/fogbugz_controller.rb14
-rw-r--r--app/controllers/import/gitea_controller.rb2
-rw-r--r--app/controllers/import/github_controller.rb2
-rw-r--r--app/controllers/jira_connect/oauth_application_ids_controller.rb23
-rw-r--r--app/controllers/jwks_controller.rb4
-rw-r--r--app/controllers/mailgun/webhooks_controller.rb55
-rw-r--r--app/controllers/members/mailgun/permanent_failures_controller.rb65
-rw-r--r--app/controllers/oauth/authorizations_controller.rb17
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb10
-rw-r--r--app/controllers/profiles/accounts_controller.rb2
-rw-r--r--app/controllers/profiles/active_sessions_controller.rb2
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb14
-rw-r--r--app/controllers/profiles_controller.rb5
-rw-r--r--app/controllers/projects/ci/pipeline_editor_controller.rb2
-rw-r--r--app/controllers/projects/commits_controller.rb2
-rw-r--r--app/controllers/projects/compare_controller.rb19
-rw-r--r--app/controllers/projects/environments/prometheus_api_controller.rb1
-rw-r--r--app/controllers/projects/environments/sample_metrics_controller.rb1
-rw-r--r--app/controllers/projects/environments_controller.rb6
-rw-r--r--app/controllers/projects/error_tracking/base_controller.rb1
-rw-r--r--app/controllers/projects/error_tracking/projects_controller.rb1
-rw-r--r--app/controllers/projects/google_cloud/base_controller.rb1
-rw-r--r--app/controllers/projects/grafana_api_controller.rb1
-rw-r--r--app/controllers/projects/incidents_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb26
-rw-r--r--app/controllers/projects/jobs_controller.rb4
-rw-r--r--app/controllers/projects/logs_controller.rb1
-rw-r--r--app/controllers/projects/mattermosts_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/drafts_controller.rb13
-rw-r--r--app/controllers/projects/merge_requests_controller.rb10
-rw-r--r--app/controllers/projects/metrics/dashboards/builder_controller.rb1
-rw-r--r--app/controllers/projects/metrics_dashboard_controller.rb1
-rw-r--r--app/controllers/projects/performance_monitoring/dashboards_controller.rb1
-rw-r--r--app/controllers/projects/pipeline_schedules_controller.rb2
-rw-r--r--app/controllers/projects/pipelines/tests_controller.rb4
-rw-r--r--app/controllers/projects/pipelines_controller.rb22
-rw-r--r--app/controllers/projects/prometheus/alerts_controller.rb10
-rw-r--r--app/controllers/projects/prometheus/metrics_controller.rb7
-rw-r--r--app/controllers/projects/releases_controller.rb18
-rw-r--r--app/controllers/projects/service_hook_logs_controller.rb23
-rw-r--r--app/controllers/projects/services_controller.rb122
-rw-r--r--app/controllers/projects/settings/branch_rules_controller.rb15
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb10
-rw-r--r--app/controllers/projects/settings/integration_hook_logs_controller.rb27
-rw-r--r--app/controllers/projects/settings/integrations_controller.rb130
-rw-r--r--app/controllers/projects/settings/packages_and_registries_controller.rb7
-rw-r--r--app/controllers/projects/settings/repository_controller.rb1
-rw-r--r--app/controllers/projects/static_site_editor_controller.rb51
-rw-r--r--app/controllers/projects/tags_controller.rb9
-rw-r--r--app/controllers/projects/tracings_controller.rb1
-rw-r--r--app/controllers/projects/usage_quotas_controller.rb1
-rw-r--r--app/controllers/projects/work_items_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb8
-rw-r--r--app/controllers/pwa_controller.rb4
-rw-r--r--app/controllers/registrations/welcome_controller.rb2
-rw-r--r--app/controllers/registrations_controller.rb1
-rw-r--r--app/controllers/search_controller.rb3
-rw-r--r--app/controllers/sessions_controller.rb6
-rw-r--r--app/controllers/users/callouts_controller.rb1
-rw-r--r--app/controllers/users/terms_controller.rb2
-rw-r--r--app/controllers/users_controller.rb7
-rw-r--r--app/controllers/whats_new_controller.rb1
-rw-r--r--app/events/pages/page_deleted_event.rb16
-rw-r--r--app/experiments/application_experiment.rb12
-rw-r--r--app/experiments/templates/new_project_readme_content/readme_advanced.md.tt46
-rw-r--r--app/finders/clusters/agents_finder.rb2
-rw-r--r--app/finders/crm/contacts_finder.rb39
-rw-r--r--app/finders/crm/organizations_finder.rb76
-rw-r--r--app/finders/issuable_finder.rb4
-rw-r--r--app/finders/issuable_finder/params.rb6
-rw-r--r--app/finders/issuables/label_filter.rb6
-rw-r--r--app/finders/issues_finder.rb14
-rw-r--r--app/finders/packages/pypi/packages_finder.rb2
-rw-r--r--app/finders/work_items/work_items_finder.rb19
-rw-r--r--app/graphql/gitlab_schema.rb28
-rw-r--r--app/graphql/mutations/base_mutation.rb18
-rw-r--r--app/graphql/mutations/ci/pipeline/destroy.rb13
-rw-r--r--app/graphql/mutations/ci/runner/update.rb4
-rw-r--r--app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb21
-rw-r--r--app/graphql/mutations/incident_management/timeline_event/create.rb4
-rw-r--r--app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb3
-rw-r--r--app/graphql/mutations/issues/set_crm_contacts.rb2
-rw-r--r--app/graphql/mutations/merge_requests/set_draft.rb4
-rw-r--r--app/graphql/mutations/packages/cleanup/policy/update.rb48
-rw-r--r--app/graphql/mutations/packages/destroy_files.rb54
-rw-r--r--app/graphql/mutations/releases/create.rb4
-rw-r--r--app/graphql/mutations/security/ci_configuration/configure_sast.rb2
-rw-r--r--app/graphql/mutations/terraform/state/delete.rb4
-rw-r--r--app/graphql/mutations/user_preferences/update.rb17
-rw-r--r--app/graphql/mutations/work_items/create.rb3
-rw-r--r--app/graphql/mutations/work_items/create_from_task.rb2
-rw-r--r--app/graphql/mutations/work_items/delete.rb2
-rw-r--r--app/graphql/mutations/work_items/delete_task.rb3
-rw-r--r--app/graphql/mutations/work_items/update.rb13
-rw-r--r--app/graphql/mutations/work_items/update_task.rb77
-rw-r--r--app/graphql/mutations/work_items/update_widgets.rb59
-rw-r--r--app/graphql/resolvers/base_issues_resolver.rb7
-rw-r--r--app/graphql/resolvers/ci/runner_owner_project_resolver.rb65
-rw-r--r--app/graphql/resolvers/clusters/agent_tokens_resolver.rb2
-rw-r--r--app/graphql/resolvers/clusters/agents_resolver.rb2
-rw-r--r--app/graphql/resolvers/concerns/issue_resolver_arguments.rb5
-rw-r--r--app/graphql/resolvers/concerns/resolves_groups.rb2
-rw-r--r--app/graphql/resolvers/concerns/resolves_merge_requests.rb1
-rw-r--r--app/graphql/resolvers/crm/contacts_resolver.rb36
-rw-r--r--app/graphql/resolvers/crm/organizations_resolver.rb36
-rw-r--r--app/graphql/resolvers/design_management/versions_resolver.rb2
-rw-r--r--app/graphql/resolvers/merge_request_pipelines_resolver.rb7
-rw-r--r--app/graphql/resolvers/milestones_resolver.rb23
-rw-r--r--app/graphql/resolvers/paginated_tree_resolver.rb2
-rw-r--r--app/graphql/resolvers/tree_resolver.rb2
-rw-r--r--app/graphql/resolvers/user_resolver.rb8
-rw-r--r--app/graphql/resolvers/users_resolver.rb5
-rw-r--r--app/graphql/resolvers/work_items_resolver.rb60
-rw-r--r--app/graphql/types/base_field.rb29
-rw-r--r--app/graphql/types/ci/detailed_status_type.rb2
-rw-r--r--app/graphql/types/ci/job_type.rb2
-rw-r--r--app/graphql/types/ci/pipeline_merge_request_event_type_enum.rb19
-rw-r--r--app/graphql/types/ci/pipeline_type.rb3
-rw-r--r--app/graphql/types/ci/runner_type.rb29
-rw-r--r--app/graphql/types/ci/runner_web_url_edge.rb11
-rw-r--r--app/graphql/types/ci/status_action_type.rb3
-rw-r--r--app/graphql/types/concerns/find_closest.rb15
-rw-r--r--app/graphql/types/customer_relations/contact_state_enum.rb17
-rw-r--r--app/graphql/types/customer_relations/organization_state_enum.rb17
-rw-r--r--app/graphql/types/global_id_type.rb3
-rw-r--r--app/graphql/types/group_member_type.rb2
-rw-r--r--app/graphql/types/group_type.rb6
-rw-r--r--app/graphql/types/issue_sort_enum.rb6
-rw-r--r--app/graphql/types/issue_type.rb7
-rw-r--r--app/graphql/types/limited_countable_connection_type.rb26
-rw-r--r--app/graphql/types/merge_requests/interacts_with_merge_request.rb10
-rw-r--r--app/graphql/types/milestone_type.rb4
-rw-r--r--app/graphql/types/mutation_type.rb14
-rw-r--r--app/graphql/types/packages/cleanup/keep_duplicated_package_files_enum.rb25
-rw-r--r--app/graphql/types/packages/cleanup/policy_type.rb23
-rw-r--r--app/graphql/types/permission_types/base_permission_type.rb14
-rw-r--r--app/graphql/types/project_type.rb15
-rw-r--r--app/graphql/types/query_complexity_type.rb4
-rw-r--r--app/graphql/types/query_type.rb10
-rw-r--r--app/graphql/types/release_asset_link_type.rb9
-rw-r--r--app/graphql/types/release_type.rb3
-rw-r--r--app/graphql/types/terraform/state_type.rb4
-rw-r--r--app/graphql/types/time_type.rb3
-rw-r--r--app/graphql/types/todo_type.rb4
-rw-r--r--app/graphql/types/work_item_sort_enum.rb11
-rw-r--r--app/graphql/types/work_item_type.rb2
-rw-r--r--app/graphql/types/work_items/updated_task_input_type.rb11
-rw-r--r--app/graphql/types/work_items/widget_interface.rb28
-rw-r--r--app/graphql/types/work_items/widget_type_enum.rb14
-rw-r--r--app/graphql/types/work_items/widgets/description_input_type.rb15
-rw-r--r--app/graphql/types/work_items/widgets/description_type.rb25
-rw-r--r--app/graphql/types/work_items/widgets/hierarchy_type.rb30
-rw-r--r--app/helpers/access_tokens_helper.rb4
-rw-r--r--app/helpers/admin/application_settings/settings_helper.rb16
-rw-r--r--app/helpers/application_helper.rb1
-rw-r--r--app/helpers/application_settings_helper.rb28
-rw-r--r--app/helpers/breadcrumbs_helper.rb2
-rw-r--r--app/helpers/ci/pipeline_editor_helper.rb1
-rw-r--r--app/helpers/ci/runners_helper.rb8
-rw-r--r--app/helpers/custom_metrics_helper.rb2
-rw-r--r--app/helpers/diff_helper.rb27
-rw-r--r--app/helpers/emails_helper.rb2
-rw-r--r--app/helpers/environments_helper.rb3
-rw-r--r--app/helpers/form_helper.rb58
-rw-r--r--app/helpers/groups/crm_settings_helper.rb9
-rw-r--r--app/helpers/groups/group_members_helper.rb15
-rw-r--r--app/helpers/groups_helper.rb34
-rw-r--r--app/helpers/integrations_helper.rb10
-rw-r--r--app/helpers/invite_members_helper.rb2
-rw-r--r--app/helpers/issues_helper.rb20
-rw-r--r--app/helpers/jira_connect_helper.rb4
-rw-r--r--app/helpers/markup_helper.rb33
-rw-r--r--app/helpers/members_helper.rb2
-rw-r--r--app/helpers/merge_requests_helper.rb6
-rw-r--r--app/helpers/nav/new_dropdown_helper.rb2
-rw-r--r--app/helpers/nav/top_nav_helper.rb76
-rw-r--r--app/helpers/nav_helper.rb2
-rw-r--r--app/helpers/notes_helper.rb6
-rw-r--r--app/helpers/projects/pipeline_helper.rb8
-rw-r--r--app/helpers/projects/project_members_helper.rb29
-rw-r--r--app/helpers/projects_helper.rb61
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/helpers/snippets_helper.rb12
-rw-r--r--app/helpers/sorting_helper.rb4
-rw-r--r--app/helpers/storage_helper.rb30
-rw-r--r--app/helpers/system_note_helper.rb8
-rw-r--r--app/helpers/tags_helper.rb9
-rw-r--r--app/helpers/todos_helper.rb23
-rw-r--r--app/helpers/tooling/visual_review_helper.rb26
-rw-r--r--app/helpers/users_helper.rb2
-rw-r--r--app/helpers/work_items_helper.rb10
-rw-r--r--app/mailers/emails/admin_notification.rb13
-rw-r--r--app/mailers/emails/auto_devops.rb8
-rw-r--r--app/mailers/emails/issues.rb14
-rw-r--r--app/mailers/emails/members.rb21
-rw-r--r--app/mailers/emails/merge_requests.rb7
-rw-r--r--app/mailers/emails/pipelines.rb8
-rw-r--r--app/mailers/emails/profile.rb17
-rw-r--r--app/mailers/emails/projects.rb8
-rw-r--r--app/mailers/notify.rb7
-rw-r--r--app/mailers/previews/notify_preview.rb4
-rw-r--r--app/models/application_setting.rb7
-rw-r--r--app/models/application_setting_implementation.rb29
-rw-r--r--app/models/award_emoji.rb2
-rw-r--r--app/models/bulk_imports/entity.rb13
-rw-r--r--app/models/bulk_imports/export_status.rb17
-rw-r--r--app/models/bulk_imports/file_transfer/project_config.rb9
-rw-r--r--app/models/bulk_imports/tracker.rb2
-rw-r--r--app/models/ci/bridge.rb22
-rw-r--r--app/models/ci/build.rb81
-rw-r--r--app/models/ci/job_artifact.rb35
-rw-r--r--app/models/ci/pipeline.rb44
-rw-r--r--app/models/ci/runner.rb12
-rw-r--r--app/models/ci/secure_file.rb2
-rw-r--r--app/models/ci/sources/pipeline.rb4
-rw-r--r--app/models/clusters/agent.rb8
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/cluster_enabled_grant.rb9
-rw-r--r--app/models/clusters/integrations/prometheus.rb18
-rw-r--r--app/models/commit.rb7
-rw-r--r--app/models/commit_signatures/ssh_signature.rb9
-rw-r--r--app/models/compare.rb5
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage_event_model.rb7
-rw-r--r--app/models/concerns/as_cte.rb12
-rw-r--r--app/models/concerns/async_devise_email.rb5
-rw-r--r--app/models/concerns/awardable.rb16
-rw-r--r--app/models/concerns/cache_markdown_field.rb7
-rw-r--r--app/models/concerns/ci/artifactable.rb2
-rw-r--r--app/models/concerns/enums/ci/pipeline.rb2
-rw-r--r--app/models/concerns/file_store_mounter.rb14
-rw-r--r--app/models/concerns/integrations/base_data_fields.rb29
-rw-r--r--app/models/concerns/integrations/has_data_fields.rb3
-rw-r--r--app/models/concerns/issuable.rb20
-rw-r--r--app/models/concerns/limitable.rb26
-rw-r--r--app/models/concerns/pg_full_text_searchable.rb11
-rw-r--r--app/models/concerns/project_features_compatibility.rb4
-rw-r--r--app/models/concerns/sensitive_serializable_hash.rb2
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb14
-rw-r--r--app/models/container_registry/event.rb2
-rw-r--r--app/models/customer_relations/contact.rb21
-rw-r--r--app/models/customer_relations/organization.rb21
-rw-r--r--app/models/deployment.rb77
-rw-r--r--app/models/environment.rb30
-rw-r--r--app/models/error_tracking/client_key.rb1
-rw-r--r--app/models/error_tracking/error_event.rb54
-rw-r--r--app/models/group.rb19
-rw-r--r--app/models/hooks/project_hook.rb5
-rw-r--r--app/models/hooks/web_hook.rb52
-rw-r--r--app/models/hooks/web_hook_log.rb13
-rw-r--r--app/models/integration.rb31
-rw-r--r--app/models/integrations/bamboo.rb20
-rw-r--r--app/models/integrations/base_chat_notification.rb2
-rw-r--r--app/models/integrations/buildkite.rb12
-rw-r--r--app/models/integrations/drone_ci.rb8
-rw-r--r--app/models/integrations/field.rb7
-rw-r--r--app/models/integrations/harbor.rb2
-rw-r--r--app/models/integrations/irker.rb55
-rw-r--r--app/models/integrations/jenkins.rb14
-rw-r--r--app/models/integrations/jira.rb5
-rw-r--r--app/models/integrations/microsoft_teams.rb30
-rw-r--r--app/models/integrations/mock_ci.rb2
-rw-r--r--app/models/integrations/prometheus.rb2
-rw-r--r--app/models/integrations/teamcity.rb10
-rw-r--r--app/models/integrations/zentao_tracker_data.rb13
-rw-r--r--app/models/issue.rb22
-rw-r--r--app/models/key.rb24
-rw-r--r--app/models/label.rb2
-rw-r--r--app/models/member.rb2
-rw-r--r--app/models/members/group_member.rb34
-rw-r--r--app/models/members/last_group_owner_assigner.rb3
-rw-r--r--app/models/members/project_member.rb16
-rw-r--r--app/models/merge_request.rb33
-rw-r--r--app/models/merge_request/cleanup_schedule.rb12
-rw-r--r--app/models/merge_request_diff_file.rb6
-rw-r--r--app/models/namespace.rb64
-rw-r--r--app/models/namespace/root_storage_statistics.rb31
-rw-r--r--app/models/namespace_setting.rb9
-rw-r--r--app/models/namespaces/project_namespace.rb7
-rw-r--r--app/models/namespaces/traversal/linear.rb10
-rw-r--r--app/models/namespaces/traversal/linear_scopes.rb151
-rw-r--r--app/models/namespaces/traversal/recursive.rb8
-rw-r--r--app/models/note.rb1
-rw-r--r--app/models/packages/cleanup/policy.rb2
-rw-r--r--app/models/packages/package.rb17
-rw-r--r--app/models/project.rb78
-rw-r--r--app/models/project_feature.rb20
-rw-r--r--app/models/project_statistics.rb4
-rw-r--r--app/models/project_team.rb14
-rw-r--r--app/models/projects/build_artifacts_size_refresh.rb14
-rw-r--r--app/models/protected_tag.rb6
-rw-r--r--app/models/release.rb7
-rw-r--r--app/models/repository.rb5
-rw-r--r--app/models/resource_event.rb1
-rw-r--r--app/models/route.rb10
-rw-r--r--app/models/terraform/state.rb12
-rw-r--r--app/models/terraform/state_version.rb1
-rw-r--r--app/models/time_tracking/timelog_category.rb35
-rw-r--r--app/models/user.rb57
-rw-r--r--app/models/user_detail.rb3
-rw-r--r--app/models/users/callout.rb6
-rw-r--r--app/models/wiki.rb47
-rw-r--r--app/models/work_item.rb19
-rw-r--r--app/models/work_items/parent_link.rb53
-rw-r--r--app/models/work_items/type.rb16
-rw-r--r--app/models/work_items/widgets/base.rb25
-rw-r--r--app/models/work_items/widgets/description.rb13
-rw-r--r--app/models/work_items/widgets/hierarchy.rb19
-rw-r--r--app/policies/group_policy.rb2
-rw-r--r--app/policies/issuable_policy.rb4
-rw-r--r--app/policies/issue_policy.rb2
-rw-r--r--app/policies/packages/cleanup/policy_policy.rb9
-rw-r--r--app/policies/project_policy.rb28
-rw-r--r--app/policies/work_item_policy.rb5
-rw-r--r--app/presenters/blob_presenter.rb4
-rw-r--r--app/presenters/ci/pipeline_presenter.rb3
-rw-r--r--app/presenters/merge_request_presenter.rb14
-rw-r--r--app/presenters/packages/pypi/package_presenter.rb96
-rw-r--r--app/presenters/packages/pypi/simple_index_presenter.rb50
-rw-r--r--app/presenters/packages/pypi/simple_package_versions_presenter.rb58
-rw-r--r--app/presenters/packages/pypi/simple_presenter_base.rb53
-rw-r--r--app/presenters/project_presenter.rb17
-rw-r--r--app/presenters/projects/security/configuration_presenter.rb3
-rw-r--r--app/presenters/projects/settings/deploy_keys_presenter.rb2
-rw-r--r--app/presenters/releases/link_presenter.rb12
-rw-r--r--app/presenters/service_hook_presenter.rb4
-rw-r--r--app/presenters/snippet_blob_presenter.rb2
-rw-r--r--app/serializers/analytics_issue_entity.rb4
-rw-r--r--app/serializers/deploy_key_entity.rb39
-rw-r--r--app/serializers/deploy_key_serializer.rb5
-rw-r--r--app/serializers/deploy_keys/basic_deploy_key_entity.rb28
-rw-r--r--app/serializers/deploy_keys/basic_deploy_key_serializer.rb7
-rw-r--r--app/serializers/deploy_keys/deploy_key_entity.rb22
-rw-r--r--app/serializers/deploy_keys/deploy_key_serializer.rb7
-rw-r--r--app/serializers/diff_file_entity.rb12
-rw-r--r--app/serializers/environment_serializer.rb22
-rw-r--r--app/serializers/integrations/event_entity.rb46
-rw-r--r--app/serializers/integrations/event_serializer.rb7
-rw-r--r--app/serializers/integrations/field_entity.rb49
-rw-r--r--app/serializers/integrations/field_serializer.rb7
-rw-r--r--app/serializers/issue_board_entity.rb9
-rw-r--r--app/serializers/issue_entity.rb9
-rw-r--r--app/serializers/linked_issue_entity.rb9
-rw-r--r--app/serializers/merge_request_poll_cached_widget_entity.rb3
-rw-r--r--app/serializers/merge_request_poll_widget_entity.rb14
-rw-r--r--app/serializers/merge_request_widget_entity.rb14
-rw-r--r--app/serializers/prometheus_alert_entity.rb4
-rw-r--r--app/serializers/service_event_entity.rb44
-rw-r--r--app/serializers/service_event_serializer.rb5
-rw-r--r--app/serializers/service_field_entity.rb47
-rw-r--r--app/serializers/service_field_serializer.rb5
-rw-r--r--app/services/auto_merge/base_service.rb5
-rw-r--r--app/services/boards/base_items_list_service.rb7
-rw-r--r--app/services/boards/issues/list_service.rb2
-rw-r--r--app/services/bulk_create_integration_service.rb14
-rw-r--r--app/services/bulk_imports/create_pipeline_trackers_service.rb68
-rw-r--r--app/services/bulk_imports/file_export_service.rb4
-rw-r--r--app/services/bulk_imports/lfs_objects_export_service.rb2
-rw-r--r--app/services/bulk_imports/repository_bundle_export_service.rb23
-rw-r--r--app/services/bulk_update_integration_service.rb17
-rw-r--r--app/services/ci/job_artifacts/destroy_all_expired_service.rb2
-rw-r--r--app/services/ci/job_artifacts/destroy_batch_service.rb37
-rw-r--r--app/services/ci/pipeline_artifacts/coverage_report_service.rb38
-rw-r--r--app/services/ci/pipeline_artifacts/destroy_all_expired_service.rb2
-rw-r--r--app/services/ci/runners/reset_registration_token_service.rb7
-rw-r--r--app/services/clusters/applications/schedule_update_service.rb40
-rw-r--r--app/services/concerns/integrations/bulk_operation_hashes.rb31
-rw-r--r--app/services/concerns/members/bulk_create_users.rb86
-rw-r--r--app/services/environments/stop_service.rb6
-rw-r--r--app/services/event_create_service.rb18
-rw-r--r--app/services/git/branch_push_service.rb1
-rw-r--r--app/services/import/base_service.rb4
-rw-r--r--app/services/import/bitbucket_server_service.rb4
-rw-r--r--app/services/import/fogbugz_service.rb107
-rw-r--r--app/services/import/github_service.rb4
-rw-r--r--app/services/incident_management/timeline_events/base_service.rb2
-rw-r--r--app/services/incident_management/timeline_events/create_service.rb5
-rw-r--r--app/services/incident_management/timeline_events/destroy_service.rb1
-rw-r--r--app/services/incident_management/timeline_events/update_service.rb6
-rw-r--r--app/services/issuable/clone/base_service.rb7
-rw-r--r--app/services/issuable/common_system_notes_service.rb2
-rw-r--r--app/services/issues/create_service.rb8
-rw-r--r--app/services/issues/move_service.rb1
-rw-r--r--app/services/jira_connect_subscriptions/create_service.rb13
-rw-r--r--app/services/markdown_content_rewriter_service.rb62
-rw-r--r--app/services/members/approve_access_request_service.rb17
-rw-r--r--app/services/members/base_service.rb13
-rw-r--r--app/services/members/create_service.rb13
-rw-r--r--app/services/members/creator_service.rb119
-rw-r--r--app/services/members/destroy_service.rb11
-rw-r--r--app/services/members/groups/bulk_creator_service.rb9
-rw-r--r--app/services/members/groups/creator_service.rb6
-rw-r--r--app/services/members/mailgun/process_webhook_service.rb39
-rw-r--r--app/services/members/projects/bulk_creator_service.rb9
-rw-r--r--app/services/members/projects/creator_service.rb24
-rw-r--r--app/services/members/update_service.rb17
-rw-r--r--app/services/merge_requests/base_service.rb22
-rw-r--r--app/services/merge_requests/build_service.rb36
-rw-r--r--app/services/merge_requests/create_pipeline_service.rb8
-rw-r--r--app/services/merge_requests/merge_service.rb8
-rw-r--r--app/services/merge_requests/mergeability/run_checks_service.rb12
-rw-r--r--app/services/merge_requests/post_merge_service.rb2
-rw-r--r--app/services/merge_requests/refresh_service.rb10
-rw-r--r--app/services/merge_requests/reload_merge_head_diff_service.rb2
-rw-r--r--app/services/merge_requests/update_service.rb12
-rw-r--r--app/services/metrics/dashboard/base_service.rb1
-rw-r--r--app/services/metrics/dashboard/panel_preview_service.rb1
-rw-r--r--app/services/metrics/dashboard/system_dashboard_service.rb3
-rw-r--r--app/services/note_summary.rb4
-rw-r--r--app/services/notes/copy_service.rb17
-rw-r--r--app/services/notification_recipients/build_service.rb4
-rw-r--r--app/services/notification_recipients/builder/new_release.rb25
-rw-r--r--app/services/notification_service.rb4
-rw-r--r--app/services/packages/cleanup/update_policy_service.rb35
-rw-r--r--app/services/packages/go/create_package_service.rb3
-rw-r--r--app/services/packages/maven/metadata/append_package_file_service.rb4
-rw-r--r--app/services/packages/rubygems/create_gemspec_service.rb4
-rw-r--r--app/services/pages/delete_service.rb13
-rw-r--r--app/services/pages_domains/create_acme_order_service.rb10
-rw-r--r--app/services/projects/after_rename_service.rb14
-rw-r--r--app/services/projects/destroy_rollback_service.rb31
-rw-r--r--app/services/projects/destroy_service.rb19
-rw-r--r--app/services/projects/import_export/export_service.rb7
-rw-r--r--app/services/projects/open_issues_count_service.rb6
-rw-r--r--app/services/projects/operations/update_service.rb2
-rw-r--r--app/services/projects/transfer_service.rb8
-rw-r--r--app/services/projects/update_pages_service.rb8
-rw-r--r--app/services/releases/base_service.rb4
-rw-r--r--app/services/releases/create_service.rb2
-rw-r--r--app/services/repositories/base_service.rb12
-rw-r--r--app/services/repositories/changelog_service.rb32
-rw-r--r--app/services/repositories/destroy_rollback_service.rb25
-rw-r--r--app/services/repositories/destroy_service.rb36
-rw-r--r--app/services/repositories/shell_destroy_service.rb15
-rw-r--r--app/services/resource_access_tokens/create_service.rb10
-rw-r--r--app/services/resource_events/base_change_timebox_service.rb2
-rw-r--r--app/services/resource_events/base_synthetic_notes_builder_service.rb9
-rw-r--r--app/services/service_ping/submit_service.rb11
-rw-r--r--app/services/service_response.rb18
-rw-r--r--app/services/snippets/bulk_destroy_service.rb14
-rw-r--r--app/services/snippets/destroy_service.rb5
-rw-r--r--app/services/static_site_editor/config_service.rb85
-rw-r--r--app/services/system_notes/issuables_service.rb15
-rw-r--r--app/services/system_notes/merge_requests_service.rb2
-rw-r--r--app/services/terraform/remote_state_handler.rb14
-rw-r--r--app/services/terraform/states/destroy_service.rb34
-rw-r--r--app/services/terraform/states/trigger_destroy_service.rb43
-rw-r--r--app/services/two_factor/destroy_service.rb8
-rw-r--r--app/services/user_project_access_changed_service.rb6
-rw-r--r--app/services/web_hook_service.rb85
-rw-r--r--app/services/web_hooks/destroy_service.rb70
-rw-r--r--app/services/web_hooks/log_destroy_service.rb19
-rw-r--r--app/services/work_items/update_service.rb22
-rw-r--r--app/uploaders/gitlab_uploader.rb8
-rw-r--r--app/uploaders/metric_image_uploader.rb4
-rw-r--r--app/validators/json_schemas/web_hooks_url_variables.json14
-rw-r--r--app/views/abuse_reports/new.html.haml4
-rw-r--r--app/views/admin/abuse_reports/index.html.haml2
-rw-r--r--app/views/admin/application_settings/_abuse.html.haml2
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml2
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml4
-rw-r--r--app/views/admin/application_settings/_default_branch.html.haml2
-rw-r--r--app/views/admin/application_settings/_diff_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_eks.html.haml4
-rw-r--r--app/views/admin/application_settings/_email.html.haml2
-rw-r--r--app/views/admin/application_settings/_external_authorization_service_form.html.haml6
-rw-r--r--app/views/admin/application_settings/_floc.html.haml4
-rw-r--r--app/views/admin/application_settings/_git_lfs_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_gitaly.html.haml2
-rw-r--r--app/views/admin/application_settings/_gitpod.html.haml2
-rw-r--r--app/views/admin/application_settings/_help_page.html.haml2
-rw-r--r--app/views/admin/application_settings/_import_export_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_ip_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_issue_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_jira_connect_application_key.html.haml21
-rw-r--r--app/views/admin/application_settings/_kroki.html.haml4
-rw-r--r--app/views/admin/application_settings/_localization.html.haml2
-rw-r--r--app/views/admin/application_settings/_mailgun.html.haml2
-rw-r--r--app/views/admin/application_settings/_network_rate_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_note_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_outbound.html.haml2
-rw-r--r--app/views/admin/application_settings/_package_registry.html.haml4
-rw-r--r--app/views/admin/application_settings/_pages.html.haml4
-rw-r--r--app/views/admin/application_settings/_performance.html.haml2
-rw-r--r--app/views/admin/application_settings/_pipeline_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_plantuml.html.haml2
-rw-r--r--app/views/admin/application_settings/_protected_paths.html.haml2
-rw-r--r--app/views/admin/application_settings/_realtime.html.haml2
-rw-r--r--app/views/admin/application_settings/_repository_check.html.haml4
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml2
-rw-r--r--app/views/admin/application_settings/_search_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_sentry.html.haml2
-rw-r--r--app/views/admin/application_settings/_sidekiq_job_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_signin.html.haml2
-rw-r--r--app/views/admin/application_settings/_signup.html.haml20
-rw-r--r--app/views/admin/application_settings/_snowplow.html.haml2
-rw-r--r--app/views/admin/application_settings/_sourcegraph.html.haml4
-rw-r--r--app/views/admin/application_settings/_terms.html.haml2
-rw-r--r--app/views/admin/application_settings/_third_party_offers.html.haml2
-rw-r--r--app/views/admin/application_settings/_usage.html.haml2
-rw-r--r--app/views/admin/application_settings/_users_api_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml5
-rw-r--r--app/views/admin/application_settings/_whats_new.html.haml13
-rw-r--r--app/views/admin/application_settings/ci/_header.html.haml2
-rw-r--r--app/views/admin/application_settings/ci_cd.html.haml6
-rw-r--r--app/views/admin/application_settings/general.html.haml19
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml10
-rw-r--r--app/views/admin/application_settings/network.html.haml28
-rw-r--r--app/views/admin/application_settings/preferences.html.haml16
-rw-r--r--app/views/admin/application_settings/reporting.html.haml4
-rw-r--r--app/views/admin/application_settings/repository.html.haml10
-rw-r--r--app/views/admin/application_settings/service_usage_data.html.haml12
-rw-r--r--app/views/admin/applications/edit.html.haml2
-rw-r--r--app/views/admin/applications/index.html.haml2
-rw-r--r--app/views/admin/applications/new.html.haml2
-rw-r--r--app/views/admin/applications/show.html.haml2
-rw-r--r--app/views/admin/background_jobs/show.html.haml2
-rw-r--r--app/views/admin/broadcast_messages/index.html.haml8
-rw-r--r--app/views/admin/dashboard/_security_newsletter_callout.html.haml10
-rw-r--r--app/views/admin/dashboard/index.html.haml48
-rw-r--r--app/views/admin/deploy_keys/edit.html.haml2
-rw-r--r--app/views/admin/deploy_keys/new.html.haml2
-rw-r--r--app/views/admin/gitaly_servers/index.html.haml2
-rw-r--r--app/views/admin/groups/_form.html.haml2
-rw-r--r--app/views/admin/groups/edit.html.haml2
-rw-r--r--app/views/admin/groups/new.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml4
-rw-r--r--app/views/admin/health_check/show.html.haml8
-rw-r--r--app/views/admin/hook_logs/show.html.haml7
-rw-r--r--app/views/admin/hooks/_form.html.haml2
-rw-r--r--app/views/admin/identities/_form.html.haml2
-rw-r--r--app/views/admin/identities/edit.html.haml2
-rw-r--r--app/views/admin/identities/index.html.haml2
-rw-r--r--app/views/admin/identities/new.html.haml2
-rw-r--r--app/views/admin/jobs/index.html.haml11
-rw-r--r--app/views/admin/labels/edit.html.haml2
-rw-r--r--app/views/admin/labels/index.html.haml2
-rw-r--r--app/views/admin/labels/new.html.haml2
-rw-r--r--app/views/admin/projects/index.html.haml19
-rw-r--r--app/views/admin/projects/show.html.haml8
-rw-r--r--app/views/admin/spam_logs/index.html.haml2
-rw-r--r--app/views/admin/topics/_form.html.haml2
-rw-r--r--app/views/admin/topics/edit.html.haml2
-rw-r--r--app/views/admin/topics/new.html.haml2
-rw-r--r--app/views/admin/users/_access_levels.html.haml19
-rw-r--r--app/views/admin/users/_admin_notes.html.haml5
-rw-r--r--app/views/admin/users/_form.html.haml58
-rw-r--r--app/views/admin/users/_head.html.haml4
-rw-r--r--app/views/admin/users/_users.html.haml2
-rw-r--r--app/views/admin/users/edit.html.haml3
-rw-r--r--app/views/admin/users/new.html.haml3
-rw-r--r--app/views/award_emoji/_awards_block.html.haml6
-rw-r--r--app/views/ci/variables/_header.html.haml2
-rw-r--r--app/views/ci/variables/_index.html.haml6
-rw-r--r--app/views/clusters/clusters/_advanced_settings.html.haml2
-rw-r--r--app/views/clusters/clusters/_banner.html.haml4
-rw-r--r--app/views/clusters/clusters/_deprecation_alert.html.haml2
-rw-r--r--app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml4
-rw-r--r--app/views/dashboard/_activity_head.html.haml2
-rw-r--r--app/views/dashboard/_groups_head.html.haml2
-rw-r--r--app/views/dashboard/_projects_head.html.haml11
-rw-r--r--app/views/dashboard/_projects_nav.html.haml2
-rw-r--r--app/views/dashboard/_snippets_head.html.haml2
-rw-r--r--app/views/dashboard/issues.html.haml2
-rw-r--r--app/views/dashboard/merge_requests.html.haml2
-rw-r--r--app/views/dashboard/milestones/index.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml2
-rw-r--r--app/views/devise/confirmations/new.html.haml2
-rw-r--r--app/views/devise/registrations/new.html.haml1
-rw-r--r--app/views/devise/shared/_signup_box.html.haml1
-rw-r--r--app/views/devise/shared/_signup_omniauth_provider_list.haml4
-rw-r--r--app/views/doorkeeper/applications/edit.html.haml2
-rw-r--r--app/views/doorkeeper/applications/new.html.haml2
-rw-r--r--app/views/doorkeeper/applications/show.html.haml2
-rw-r--r--app/views/doorkeeper/authorizations/error.html.haml2
-rw-r--r--app/views/doorkeeper/authorizations/redirect.html.haml2
-rw-r--r--app/views/doorkeeper/authorizations/show.html.haml2
-rw-r--r--app/views/errors/access_denied.html.haml2
-rw-r--r--app/views/explore/topics/_head.html.haml11
-rw-r--r--app/views/groups/_create_chat_team.html.haml15
-rw-r--r--app/views/groups/_group_admin_settings.html.haml12
-rw-r--r--app/views/groups/_invite_members_side_nav_link.html.haml3
-rw-r--r--app/views/groups/_new_group_fields.html.haml2
-rw-r--r--app/views/groups/_personalize.html.haml6
-rw-r--r--app/views/groups/_subgroups_and_projects.html.haml5
-rw-r--r--app/views/groups/edit.html.haml9
-rw-r--r--app/views/groups/group_members/index.html.haml4
-rw-r--r--app/views/groups/issues.html.haml28
-rw-r--r--app/views/groups/labels/edit.html.haml3
-rw-r--r--app/views/groups/labels/index.html.haml8
-rw-r--r--app/views/groups/labels/new.html.haml3
-rw-r--r--app/views/groups/merge_requests.html.haml2
-rw-r--r--app/views/groups/milestones/_form.html.haml2
-rw-r--r--app/views/groups/milestones/edit.html.haml2
-rw-r--r--app/views/groups/milestones/new.html.haml2
-rw-r--r--app/views/groups/new.html.haml2
-rw-r--r--app/views/groups/projects.html.haml1
-rw-r--r--app/views/groups/runners/_settings.html.haml24
-rw-r--r--app/views/groups/runners/edit.html.haml2
-rw-r--r--app/views/groups/runners/show.html.haml5
-rw-r--r--app/views/groups/settings/_advanced.html.haml4
-rw-r--r--app/views/groups/settings/_export.html.haml4
-rw-r--r--app/views/groups/settings/_git_access_protocols.html.haml7
-rw-r--r--app/views/groups/settings/_permissions.html.haml14
-rw-r--r--app/views/groups/settings/_remove_button.html.haml2
-rw-r--r--app/views/groups/settings/_transfer.html.haml2
-rw-r--r--app/views/groups/settings/applications/edit.html.haml2
-rw-r--r--app/views/groups/settings/applications/show.html.haml2
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml9
-rw-r--r--app/views/groups/settings/repository/_default_branch.html.haml2
-rw-r--r--app/views/groups/settings/repository/show.html.haml1
-rw-r--r--app/views/groups/show.html.haml3
-rw-r--r--app/views/import/bitbucket/status.html.haml2
-rw-r--r--app/views/import/bitbucket_server/new.html.haml2
-rw-r--r--app/views/import/bitbucket_server/status.html.haml2
-rw-r--r--app/views/import/fogbugz/new.html.haml2
-rw-r--r--app/views/import/fogbugz/new_user_map.html.haml2
-rw-r--r--app/views/import/fogbugz/status.html.haml2
-rw-r--r--app/views/import/gitea/new.html.haml3
-rw-r--r--app/views/import/gitea/status.html.haml4
-rw-r--r--app/views/import/github/new.html.haml8
-rw-r--r--app/views/import/github/status.html.haml2
-rw-r--r--app/views/import/gitlab/status.html.haml2
-rw-r--r--app/views/import/gitlab_projects/new.html.haml2
-rw-r--r--app/views/import/manifest/_form.html.haml2
-rw-r--r--app/views/import/manifest/new.html.haml2
-rw-r--r--app/views/import/manifest/status.html.haml2
-rw-r--r--app/views/import/phabricator/new.html.haml2
-rw-r--r--app/views/import/shared/_errors.html.haml2
-rw-r--r--app/views/invites/show.html.haml2
-rw-r--r--app/views/jira_connect/subscriptions/index.html.haml6
-rw-r--r--app/views/kaminari/gitlab/_keyset_paginator.html.haml8
-rw-r--r--app/views/kaminari/gitlab/_next_page.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_prev_page.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_without_count.html.haml4
-rw-r--r--app/views/layouts/_head.html.haml1
-rw-r--r--app/views/layouts/_page.html.haml5
-rw-r--r--app/views/layouts/_visual_review.html.haml1
-rw-r--r--app/views/layouts/application.html.haml1
-rw-r--r--app/views/layouts/header/_default.html.haml11
-rw-r--r--app/views/layouts/header/_registration_enabled_callout.html.haml8
-rw-r--r--app/views/layouts/header/_storage_enforcement_banner.html.haml10
-rw-r--r--app/views/layouts/nav/breadcrumbs/_collapsed_inline_list.html.haml4
-rw-r--r--app/views/layouts/notify.html.haml4
-rw-r--r--app/views/layouts/service_desk.html.haml4
-rw-r--r--app/views/layouts/terms.html.haml25
-rw-r--r--app/views/notify/approved_merge_request_email.html.haml2
-rw-r--r--app/views/notify/merge_when_pipeline_succeeds_email.html.haml2
-rw-r--r--app/views/notify/unapproved_merge_request_email.html.haml2
-rw-r--r--app/views/notify/user_auto_banned_email.html.haml9
-rw-r--r--app/views/notify/user_auto_banned_email.text.erb7
-rw-r--r--app/views/profiles/_email_settings.html.haml6
-rw-r--r--app/views/profiles/accounts/show.html.haml8
-rw-r--r--app/views/profiles/chat_names/_chat_name.html.haml4
-rw-r--r--app/views/profiles/chat_names/new.html.haml2
-rw-r--r--app/views/profiles/emails/index.html.haml2
-rw-r--r--app/views/profiles/keys/_form.html.haml7
-rw-r--r--app/views/profiles/keys/_key_details.html.haml2
-rw-r--r--app/views/profiles/passwords/new.html.haml2
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml12
-rw-r--r--app/views/profiles/preferences/show.html.haml15
-rw-r--r--app/views/profiles/show.html.haml20
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml5
-rw-r--r--app/views/projects/_clusters_deprecation_alert.html.haml2
-rw-r--r--app/views/projects/_commit_button.html.haml7
-rw-r--r--app/views/projects/_deletion_failed.html.haml2
-rw-r--r--app/views/projects/_errors.html.haml2
-rw-r--r--app/views/projects/_import_project_pane.html.haml8
-rw-r--r--app/views/projects/_invite_members_modal.html.haml2
-rw-r--r--app/views/projects/_invite_members_side_nav_link.html.haml3
-rw-r--r--app/views/projects/_last_push.html.haml4
-rw-r--r--app/views/projects/_merge_request_merge_method_settings.html.haml58
-rw-r--r--app/views/projects/_merge_request_squash_options_settings.html.haml47
-rw-r--r--app/views/projects/_merge_request_target_project_settings.html.haml21
-rw-r--r--app/views/projects/_new_project_fields.html.haml2
-rw-r--r--app/views/projects/_service_desk_settings.html.haml2
-rw-r--r--app/views/projects/_visibility_modal.html.haml2
-rw-r--r--app/views/projects/activity.html.haml1
-rw-r--r--app/views/projects/blob/_new_dir.html.haml2
-rw-r--r--app/views/projects/blob/_remove.html.haml2
-rw-r--r--app/views/projects/blob/_upload.html.haml30
-rw-r--r--app/views/projects/blob/edit.html.haml4
-rw-r--r--app/views/projects/blob/new.html.haml2
-rw-r--r--app/views/projects/blob/show.html.haml3
-rw-r--r--app/views/projects/branch_rules/_show.html.haml12
-rw-r--r--app/views/projects/branches/new.html.haml5
-rw-r--r--app/views/projects/buttons/_remove_tag.html.haml9
-rw-r--r--app/views/projects/cleanup/_show.html.haml2
-rw-r--r--app/views/projects/commit/show.html.haml2
-rw-r--r--app/views/projects/commits/_commits.html.haml4
-rw-r--r--app/views/projects/compare/index.html.haml4
-rw-r--r--app/views/projects/compare/show.html.haml2
-rw-r--r--app/views/projects/confluences/show.html.haml2
-rw-r--r--app/views/projects/default_branch/_show.html.haml2
-rw-r--r--app/views/projects/deploy_keys/edit.html.haml2
-rw-r--r--app/views/projects/edit.html.haml14
-rw-r--r--app/views/projects/empty.html.haml7
-rw-r--r--app/views/projects/environments/terminal.html.haml2
-rw-r--r--app/views/projects/forks/error.html.haml2
-rw-r--r--app/views/projects/graphs/show.html.haml4
-rw-r--r--app/views/projects/hook_logs/show.html.haml7
-rw-r--r--app/views/projects/import/jira/show.html.haml2
-rw-r--r--app/views/projects/imports/new.html.haml2
-rw-r--r--app/views/projects/issuable/_show.html.haml2
-rw-r--r--app/views/projects/issues/_by_email_description.html.haml6
-rw-r--r--app/views/projects/issues/_work_item_links.html.haml2
-rw-r--r--app/views/projects/issues/edit.html.haml2
-rw-r--r--app/views/projects/issues/index.html.haml31
-rw-r--r--app/views/projects/issues/new.html.haml4
-rw-r--r--app/views/projects/issues/service_desk.html.haml8
-rw-r--r--app/views/projects/issues/service_desk/_alert_moved_from_service_desk.html.haml (renamed from app/views/projects/issues/_alert_moved_from_service_desk.html.haml)2
-rw-r--r--app/views/projects/issues/service_desk/_nav_btns.html.haml (renamed from app/views/projects/issues/_nav_btns.html.haml)0
-rw-r--r--app/views/projects/issues/service_desk/_service_desk_empty_state.html.haml (renamed from app/views/projects/issues/_service_desk_empty_state.html.haml)2
-rw-r--r--app/views/projects/issues/service_desk/_service_desk_info_content.html.haml (renamed from app/views/projects/issues/_service_desk_info_content.html.haml)2
-rw-r--r--app/views/projects/jobs/index.html.haml2
-rw-r--r--app/views/projects/labels/edit.html.haml4
-rw-r--r--app/views/projects/labels/index.html.haml13
-rw-r--r--app/views/projects/labels/new.html.haml4
-rw-r--r--app/views/projects/mattermosts/_no_teams.html.haml2
-rw-r--r--app/views/projects/mattermosts/_team_selection.html.haml4
-rw-r--r--app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml25
-rw-r--r--app/views/projects/merge_requests/_code_dropdown.html.haml2
-rw-r--r--app/views/projects/merge_requests/_mr_box.html.haml10
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml31
-rw-r--r--app/views/projects/merge_requests/conflicts/show.html.haml4
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml46
-rw-r--r--app/views/projects/merge_requests/creations/_new_submit.html.haml6
-rw-r--r--app/views/projects/merge_requests/edit.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml17
-rw-r--r--app/views/projects/milestones/edit.html.haml2
-rw-r--r--app/views/projects/milestones/new.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml4
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml8
-rw-r--r--app/views/projects/mirrors/_ssh_host_keys.html.haml4
-rw-r--r--app/views/projects/new.html.haml6
-rw-r--r--app/views/projects/no_repo.html.haml2
-rw-r--r--app/views/projects/notes/_actions.html.haml13
-rw-r--r--app/views/projects/notes/_more_actions_dropdown.html.haml4
-rw-r--r--app/views/projects/pages/_list.html.haml27
-rw-r--r--app/views/projects/pages/show.html.haml2
-rw-r--r--app/views/projects/pages_domains/_dns.html.haml4
-rw-r--r--app/views/projects/pages_domains/new.html.haml2
-rw-r--r--app/views/projects/pages_domains/show.html.haml9
-rw-r--r--app/views/projects/pipeline_schedules/_form.html.haml6
-rw-r--r--app/views/projects/pipeline_schedules/edit.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/index.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/new.html.haml3
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml42
-rw-r--r--app/views/projects/pipelines/index.html.haml2
-rw-r--r--app/views/projects/pipelines/new.html.haml2
-rw-r--r--app/views/projects/pipelines/show.html.haml1
-rw-r--r--app/views/projects/project_members/index.html.haml13
-rw-r--r--app/views/projects/prometheus/metrics/edit.html.haml2
-rw-r--r--app/views/projects/prometheus/metrics/new.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_index.html.haml4
-rw-r--r--app/views/projects/protected_tags/shared/_index.html.haml4
-rw-r--r--app/views/projects/readme_templates/default.md.tt2
-rw-r--r--app/views/projects/releases/edit.html.haml2
-rw-r--r--app/views/projects/runners/edit.html.haml2
-rw-r--r--app/views/projects/settings/_archive.html.haml4
-rw-r--r--app/views/projects/settings/access_tokens/index.html.haml2
-rw-r--r--app/views/projects/settings/branch_rules/index.html.haml6
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml24
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml26
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml16
-rw-r--r--app/views/projects/settings/integrations/_form.html.haml (renamed from app/views/projects/services/_form.html.haml)0
-rw-r--r--app/views/projects/settings/integrations/edit.html.haml (renamed from app/views/projects/services/edit.html.haml)0
-rw-r--r--app/views/projects/settings/integrations/index.html.haml (renamed from app/views/projects/settings/integrations/show.html.haml)0
-rw-r--r--app/views/projects/settings/operations/_alert_management.html.haml2
-rw-r--r--app/views/projects/settings/operations/_error_tracking.html.haml2
-rw-r--r--app/views/projects/settings/operations/_tracing.html.haml2
-rw-r--r--app/views/projects/settings/repository/show.html.haml2
-rw-r--r--app/views/projects/show.html.haml3
-rw-r--r--app/views/projects/snippets/edit.html.haml2
-rw-r--r--app/views/projects/snippets/new.html.haml2
-rw-r--r--app/views/projects/static_site_editor/show.html.haml1
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tags/index.html.haml3
-rw-r--r--app/views/projects/tags/new.html.haml16
-rw-r--r--app/views/projects/tags/show.html.haml3
-rw-r--r--app/views/projects/tracings/show.html.haml2
-rw-r--r--app/views/projects/usage_quotas/index.html.haml10
-rw-r--r--app/views/projects/work_items/index.html.haml2
-rw-r--r--app/views/pwa/manifest.json.erb27
-rw-r--r--app/views/registrations/welcome/show.html.haml5
-rw-r--r--app/views/search/show.html.haml4
-rw-r--r--app/views/sent_notifications/unsubscribe.html.haml2
-rw-r--r--app/views/shared/_alert_info.html.haml7
-rw-r--r--app/views/shared/_auto_devops_callout.html.haml24
-rw-r--r--app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml6
-rw-r--r--app/views/shared/_broadcast_message.html.haml14
-rw-r--r--app/views/shared/_captcha_check.html.haml2
-rw-r--r--app/views/shared/_group_form.html.haml78
-rw-r--r--app/views/shared/_import_form.html.haml4
-rw-r--r--app/views/shared/_label.html.haml27
-rw-r--r--app/views/shared/_no_password.html.haml4
-rw-r--r--app/views/shared/_no_ssh.html.haml4
-rw-r--r--app/views/shared/_project_limit.html.haml2
-rw-r--r--app/views/shared/_remote_mirror_update_button.html.haml5
-rw-r--r--app/views/shared/_service_ping_consent.html.haml4
-rw-r--r--app/views/shared/_sidebar_toggle_button.html.haml2
-rw-r--r--app/views/shared/_two_factor_auth_recovery_settings_check.html.haml10
-rw-r--r--app/views/shared/access_tokens/_form.html.haml5
-rw-r--r--app/views/shared/builds/_tabs.html.haml2
-rw-r--r--app/views/shared/deploy_keys/_form.html.haml2
-rw-r--r--app/views/shared/deploy_keys/_index.html.haml4
-rw-r--r--app/views/shared/deploy_tokens/_index.html.haml4
-rw-r--r--app/views/shared/empty_states/_wikis.html.haml2
-rw-r--r--app/views/shared/errors/_gitaly_unavailable.html.haml2
-rw-r--r--app/views/shared/form_elements/_description.html.haml33
-rw-r--r--app/views/shared/groups/_group_name_and_path_fields.html.haml5
-rw-r--r--app/views/shared/hook_logs/_content.html.haml7
-rw-r--r--app/views/shared/integrations/overrides.html.haml2
-rw-r--r--app/views/shared/issuable/_bulk_update_sidebar.html.haml6
-rw-r--r--app/views/shared/issuable/_form.html.haml46
-rw-r--r--app/views/shared/issuable/_label_page_create.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml7
-rw-r--r--app/views/shared/issuable/_status_box.html.haml3
-rw-r--r--app/views/shared/issuable/form/_branch_chooser.html.haml14
-rw-r--r--app/views/shared/issuable/form/_merge_params.html.haml5
-rw-r--r--app/views/shared/issuable/form/_metadata.html.haml37
-rw-r--r--app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml6
-rw-r--r--app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml4
-rw-r--r--app/views/shared/issuable/form/_title.html.haml7
-rw-r--r--app/views/shared/issuable/form/_type_selector.html.haml53
-rw-r--r--app/views/shared/issue_type/_details_content.html.haml5
-rw-r--r--app/views/shared/labels/_form.html.haml17
-rw-r--r--app/views/shared/labels/_nav.html.haml9
-rw-r--r--app/views/shared/members/_manage_access_button.html.haml2
-rw-r--r--app/views/shared/milestones/_milestone.html.haml9
-rw-r--r--app/views/shared/milestones/_milestone_complete_alert.html.haml2
-rw-r--r--app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml3
-rw-r--r--app/views/shared/namespaces/cascading_settings/_lock_icon.html.haml7
-rw-r--r--app/views/shared/notes/_edit_form.html.haml4
-rw-r--r--app/views/shared/projects/_inactive_project_deletion_alert.html.haml7
-rw-r--r--app/views/shared/projects/_search_bar.html.haml3
-rw-r--r--app/views/shared/promotions/_promote_servicedesk.html.haml19
-rw-r--r--app/views/shared/runners/_runner_details.html.haml2
-rw-r--r--app/views/shared/runners/_runner_type_alert.html.haml6
-rw-r--r--app/views/shared/snippets/_embed.html.haml2
-rw-r--r--app/views/shared/snippets/show.js.haml1
-rw-r--r--app/views/shared/topics/_topic.html.haml5
-rw-r--r--app/views/shared/users/_user.html.haml4
-rw-r--r--app/views/shared/wikis/_form.html.haml2
-rw-r--r--app/views/shared/wikis/_sidebar.html.haml2
-rw-r--r--app/views/shared/wikis/diff.html.haml2
-rw-r--r--app/views/shared/wikis/edit.html.haml2
-rw-r--r--app/views/shared/wikis/history.html.haml2
-rw-r--r--app/views/shared/wikis/pages.html.haml2
-rw-r--r--app/views/snippets/edit.html.haml2
-rw-r--r--app/views/snippets/new.html.haml2
-rw-r--r--app/views/snippets/notes/_actions.html.haml14
-rw-r--r--app/views/users/unsubscribes/show.html.haml2
-rw-r--r--app/workers/all_queues.yml1124
-rw-r--r--app/workers/background_migration/single_database_worker.rb13
-rw-r--r--app/workers/build_success_worker.rb4
-rw-r--r--app/workers/bulk_import_worker.rb2
-rw-r--r--app/workers/bulk_imports/pipeline_worker.rb112
-rw-r--r--app/workers/ci/archive_trace_worker.rb10
-rw-r--r--app/workers/ci/pipeline_artifacts/coverage_report_worker.rb2
-rw-r--r--app/workers/clusters/applications/activate_integration_worker.rb25
-rw-r--r--app/workers/clusters/applications/activate_service_worker.rb23
-rw-r--r--app/workers/clusters/applications/deactivate_integration_worker.rb39
-rw-r--r--app/workers/clusters/applications/deactivate_service_worker.rb30
-rw-r--r--app/workers/concerns/limited_capacity/job_tracker.rb2
-rw-r--r--app/workers/concerns/worker_attributes.rb26
-rw-r--r--app/workers/container_registry/migration/enqueuer_worker.rb57
-rw-r--r--app/workers/container_registry/migration/guard_worker.rb31
-rw-r--r--app/workers/database/batched_background_migration/ci_database_worker.rb6
-rw-r--r--app/workers/database/batched_background_migration/single_database_worker.rb24
-rw-r--r--app/workers/database/batched_background_migration_worker.rb4
-rw-r--r--app/workers/database/ci_namespace_mirrors_consistency_check_worker.rb4
-rw-r--r--app/workers/database/ci_project_mirrors_consistency_check_worker.rb4
-rw-r--r--app/workers/delete_container_repository_worker.rb37
-rw-r--r--app/workers/expire_job_cache_worker.rb22
-rw-r--r--app/workers/expire_pipeline_cache_worker.rb27
-rw-r--r--app/workers/gitlab_service_ping_worker.rb20
-rw-r--r--app/workers/integrations/execute_worker.rb27
-rw-r--r--app/workers/integrations/irker_worker.rb193
-rw-r--r--app/workers/irker_worker.rb192
-rw-r--r--app/workers/issue_placement_worker.rb71
-rw-r--r--app/workers/issue_rebalancing_worker.rb56
-rw-r--r--app/workers/loose_foreign_keys/cleanup_worker.rb2
-rw-r--r--app/workers/merge_requests/create_pipeline_worker.rb9
-rw-r--r--app/workers/merge_requests/update_head_pipeline_worker.rb2
-rw-r--r--app/workers/namespaceless_project_destroy_worker.rb42
-rw-r--r--app/workers/namespaces/process_sync_events_worker.rb4
-rw-r--r--app/workers/pages_transfer_worker.rb10
-rw-r--r--app/workers/pipeline_hooks_worker.rb1
-rw-r--r--app/workers/pipeline_notification_worker.rb1
-rw-r--r--app/workers/project_daily_statistics_worker.rb20
-rw-r--r--app/workers/project_service_worker.rb21
-rw-r--r--app/workers/projects/inactive_projects_deletion_cron_worker.rb67
-rw-r--r--app/workers/projects/process_sync_events_worker.rb4
-rw-r--r--app/workers/prometheus/create_default_alerts_worker.rb19
-rw-r--r--app/workers/repository_remove_remote_worker.rb35
-rw-r--r--app/workers/schedule_merge_request_cleanup_refs_worker.rb1
-rw-r--r--app/workers/terraform/states/destroy_worker.rb23
-rw-r--r--app/workers/update_merge_requests_worker.rb8
-rw-r--r--app/workers/web_hooks/destroy_worker.rb9
-rw-r--r--app/workers/web_hooks/log_destroy_worker.rb24
1714 files changed, 19722 insertions, 12832 deletions
diff --git a/app/assets/images/auth_buttons/gitlab_64.png b/app/assets/images/auth_buttons/gitlab_64.png
index f675678dc9d..860f9c1be9b 100644
--- a/app/assets/images/auth_buttons/gitlab_64.png
+++ b/app/assets/images/auth_buttons/gitlab_64.png
Binary files differ
diff --git a/app/assets/images/ext_snippet_icons/ext_snippet_icons.png b/app/assets/images/ext_snippet_icons/ext_snippet_icons.png
index c864e558bfd..e81115d311e 100644
--- a/app/assets/images/ext_snippet_icons/ext_snippet_icons.png
+++ b/app/assets/images/ext_snippet_icons/ext_snippet_icons.png
Binary files differ
diff --git a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
new file mode 100644
index 00000000000..944a2ef7f64
--- /dev/null
+++ b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
@@ -0,0 +1,168 @@
+<script>
+import { GlButton, GlIcon, GlLink, GlPagination, GlTable, GlTooltipDirective } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { __, sprintf } from '~/locale';
+import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import UserDate from '~/vue_shared/components/user_date.vue';
+import { EVENT_SUCCESS, FIELDS, FORM_SELECTOR, INITIAL_PAGE, PAGE_SIZE } from './constants';
+
+export default {
+ EVENT_SUCCESS,
+ FORM_SELECTOR,
+ PAGE_SIZE,
+ name: 'AccessTokenTableApp',
+ components: {
+ DomElementListener,
+ GlButton,
+ GlIcon,
+ GlLink,
+ GlPagination,
+ GlTable,
+ TimeAgoTooltip,
+ UserDate,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ lastUsedHelpLink: helpPagePath('/user/profile/personal_access_tokens.md', {
+ anchor: 'view-the-last-time-a-token-was-used',
+ }),
+ i18n: {
+ emptyField: __('Never'),
+ expired: __('Expired'),
+ header: __('Active %{accessTokenTypePlural} (%{totalAccessTokens})'),
+ modalMessage: __(
+ 'Are you sure you want to revoke this %{accessTokenType}? This action cannot be undone.',
+ ),
+ revokeButton: __('Revoke'),
+ tokenValidity: __('Token valid until revoked'),
+ },
+ inject: [
+ 'accessTokenType',
+ 'accessTokenTypePlural',
+ 'initialActiveAccessTokens',
+ 'noActiveTokensMessage',
+ 'showRole',
+ ],
+ data() {
+ return {
+ activeAccessTokens: this.initialActiveAccessTokens,
+ currentPage: INITIAL_PAGE,
+ };
+ },
+ computed: {
+ filteredFields() {
+ return this.showRole ? FIELDS : FIELDS.filter((field) => field.key !== 'role');
+ },
+ header() {
+ return sprintf(this.$options.i18n.header, {
+ accessTokenTypePlural: this.accessTokenTypePlural,
+ totalAccessTokens: this.activeAccessTokens.length,
+ });
+ },
+ modalMessage() {
+ return sprintf(this.$options.i18n.modalMessage, {
+ accessTokenType: this.accessTokenType,
+ });
+ },
+ showPagination() {
+ return this.activeAccessTokens.length > PAGE_SIZE;
+ },
+ },
+ methods: {
+ onSuccess(event) {
+ const [{ active_access_tokens: activeAccessTokens }] = event.detail;
+ this.activeAccessTokens = convertObjectPropsToCamelCase(activeAccessTokens, { deep: true });
+ this.currentPage = INITIAL_PAGE;
+ },
+ sortingChanged(aRow, bRow, key) {
+ if (['createdAt', 'lastUsedAt', 'expiresAt'].includes(key)) {
+ // Transform `null` value to the latest possible date
+ // https://stackoverflow.com/a/11526569/18428169
+ const maxEpoch = 8640000000000000;
+ const a = new Date(aRow[key] ?? maxEpoch).getTime();
+ const b = new Date(bRow[key] ?? maxEpoch).getTime();
+ return a - b;
+ }
+
+ // For other columns the default sorting works OK
+ return false;
+ },
+ },
+};
+</script>
+
+<template>
+ <dom-element-listener :selector="$options.FORM_SELECTOR" @[$options.EVENT_SUCCESS]="onSuccess">
+ <div>
+ <hr />
+ <h5>{{ header }}</h5>
+
+ <gl-table
+ data-testid="active-tokens"
+ :empty-text="noActiveTokensMessage"
+ :fields="filteredFields"
+ :items="activeAccessTokens"
+ :per-page="$options.PAGE_SIZE"
+ :current-page="currentPage"
+ :sort-compare="sortingChanged"
+ show-empty
+ >
+ <template #cell(createdAt)="{ item: { createdAt } }">
+ <user-date :date="createdAt" />
+ </template>
+
+ <template #head(lastUsedAt)="{ label }">
+ <span>{{ label }}</span>
+ <gl-link :href="$options.lastUsedHelpLink"
+ ><gl-icon name="question-o" /><span class="gl-sr-only">{{
+ s__('AccessTokens|The last time a token was used')
+ }}</span></gl-link
+ >
+ </template>
+
+ <template #cell(lastUsedAt)="{ item: { lastUsedAt } }">
+ <time-ago-tooltip v-if="lastUsedAt" :time="lastUsedAt" />
+ <template v-else> {{ $options.i18n.emptyField }}</template>
+ </template>
+
+ <template #cell(expiresAt)="{ item: { expiresAt, expired, expiresSoon } }">
+ <template v-if="expiresAt">
+ <span v-if="expired" class="text-danger">{{ $options.i18n.expired }}</span>
+ <time-ago-tooltip v-else :class="{ 'text-warning': expiresSoon }" :time="expiresAt" />
+ </template>
+ <span v-else v-gl-tooltip :title="$options.i18n.tokenValidity">{{
+ $options.i18n.emptyField
+ }}</span>
+ </template>
+
+ <template #cell(action)="{ item: { revokePath, expiresAt } }">
+ <gl-button
+ variant="danger"
+ :category="expiresAt ? 'primary' : 'secondary'"
+ :aria-label="$options.i18n.revokeButton"
+ :data-confirm="modalMessage"
+ data-confirm-btn-variant="danger"
+ data-qa-selector="revoke_button"
+ data-method="put"
+ :href="revokePath"
+ icon="remove"
+ />
+ </template>
+ </gl-table>
+ <gl-pagination
+ v-if="showPagination"
+ v-model="currentPage"
+ :per-page="$options.PAGE_SIZE"
+ :total-items="activeAccessTokens.length"
+ :prev-text="__('Prev')"
+ :next-text="__('Next')"
+ :label-next-page="__('Go to next page')"
+ :label-prev-page="__('Go to previous page')"
+ align="center"
+ />
+ </div>
+ </dom-element-listener>
+</template>
diff --git a/app/assets/javascripts/access_tokens/components/constants.js b/app/assets/javascripts/access_tokens/components/constants.js
new file mode 100644
index 00000000000..84e50bc099f
--- /dev/null
+++ b/app/assets/javascripts/access_tokens/components/constants.js
@@ -0,0 +1,61 @@
+import { __, s__ } from '~/locale';
+
+export const EVENT_ERROR = 'ajax:error';
+export const EVENT_SUCCESS = 'ajax:success';
+export const FORM_SELECTOR = '#js-new-access-token-form';
+
+export const INITIAL_PAGE = 1;
+export const PAGE_SIZE = 100;
+
+export const FIELDS = [
+ {
+ key: 'name',
+ label: __('Token name'),
+ sortable: true,
+ tdClass: `gl-text-black-normal`,
+ thClass: `gl-text-black-normal`,
+ },
+ {
+ formatter(scopes) {
+ return scopes?.length ? scopes.join(', ') : __('no scopes selected');
+ },
+ key: 'scopes',
+ label: __('Scopes'),
+ sortable: true,
+ tdClass: `gl-text-black-normal`,
+ thClass: `gl-text-black-normal`,
+ },
+ {
+ key: 'createdAt',
+ label: s__('AccessTokens|Created'),
+ sortable: true,
+ tdClass: `gl-text-black-normal`,
+ thClass: `gl-text-black-normal`,
+ },
+ {
+ key: 'lastUsedAt',
+ label: __('Last Used'),
+ sortable: true,
+ tdClass: `gl-text-black-normal`,
+ thClass: `gl-text-black-normal`,
+ },
+ {
+ key: 'expiresAt',
+ label: __('Expires'),
+ sortable: true,
+ tdClass: `gl-text-black-normal`,
+ thClass: `gl-text-black-normal`,
+ },
+ {
+ key: 'role',
+ label: __('Role'),
+ tdClass: `gl-text-black-normal`,
+ thClass: `gl-text-black-normal`,
+ sortable: true,
+ },
+ {
+ key: 'action',
+ label: __('Action'),
+ thClass: `gl-text-black-normal`,
+ },
+];
diff --git a/app/assets/javascripts/access_tokens/components/expires_at_field.vue b/app/assets/javascripts/access_tokens/components/expires_at_field.vue
index 561b2617c5f..147de529eea 100644
--- a/app/assets/javascripts/access_tokens/components/expires_at_field.vue
+++ b/app/assets/javascripts/access_tokens/components/expires_at_field.vue
@@ -21,17 +21,17 @@ export default {
required: false,
default: () => ({}),
},
+ minDate: {
+ type: Date,
+ required: false,
+ default: () => new Date(),
+ },
maxDate: {
type: Date,
required: false,
default: () => null,
},
},
- data() {
- return {
- minDate: new Date(),
- };
- },
};
</script>
diff --git a/app/assets/javascripts/access_tokens/components/new_access_token_app.vue b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue
new file mode 100644
index 00000000000..904052688f3
--- /dev/null
+++ b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue
@@ -0,0 +1,130 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { createAlert, VARIANT_INFO } from '~/flash';
+import { __, n__, sprintf } from '~/locale';
+import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
+import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
+import { EVENT_ERROR, EVENT_SUCCESS, FORM_SELECTOR } from './constants';
+
+export default {
+ EVENT_ERROR,
+ EVENT_SUCCESS,
+ FORM_SELECTOR,
+ name: 'NewAccessTokenApp',
+ components: { DomElementListener, GlAlert, InputCopyToggleVisibility },
+ i18n: {
+ alertInfoMessage: __('Your new %{accessTokenType} has been created.'),
+ copyButtonTitle: __('Copy %{accessTokenType}'),
+ description: __("Make sure you save it - you won't be able to access it again."),
+ label: __('Your new %{accessTokenType}'),
+ },
+ tokenInputId: 'new-access-token',
+ inject: ['accessTokenType'],
+ data() {
+ return { errors: null, infoAlert: null, newToken: null };
+ },
+ computed: {
+ alertInfoMessage() {
+ return sprintf(this.$options.i18n.alertInfoMessage, {
+ accessTokenType: this.accessTokenType,
+ });
+ },
+ alertDangerTitle() {
+ return n__(
+ 'The form contains the following error:',
+ 'The form contains the following errors:',
+ this.errors?.length ?? 0,
+ );
+ },
+ copyButtonTitle() {
+ return sprintf(this.$options.i18n.copyButtonTitle, { accessTokenType: this.accessTokenType });
+ },
+ formInputGroupProps() {
+ return {
+ id: this.$options.tokenInputId,
+ class: 'qa-created-access-token',
+ 'data-qa-selector': 'created_access_token_field',
+ name: this.$options.tokenInputId,
+ };
+ },
+ label() {
+ return sprintf(this.$options.i18n.label, { accessTokenType: this.accessTokenType });
+ },
+ },
+ mounted() {
+ /** @type {HTMLFormElement} */
+ this.form = document.querySelector(FORM_SELECTOR);
+
+ /** @type {HTMLInputElement} */
+ this.submitButton = this.form.querySelector('input[type=submit]');
+ },
+ methods: {
+ beforeDisplayResults() {
+ this.infoAlert?.dismiss();
+ this.$refs.container.scrollIntoView(false);
+
+ this.errors = null;
+ this.newToken = null;
+ },
+ onError(event) {
+ this.beforeDisplayResults();
+
+ const [{ errors }] = event.detail;
+ this.errors = errors;
+
+ this.submitButton.classList.remove('disabled');
+ },
+ onSuccess(event) {
+ this.beforeDisplayResults();
+
+ const [{ new_token: newToken }] = event.detail;
+ this.newToken = newToken;
+
+ this.infoAlert = createAlert({ message: this.alertInfoMessage, variant: VARIANT_INFO });
+
+ this.form.reset();
+ },
+ },
+};
+</script>
+
+<template>
+ <dom-element-listener
+ :selector="$options.FORM_SELECTOR"
+ @[$options.EVENT_ERROR]="onError"
+ @[$options.EVENT_SUCCESS]="onSuccess"
+ >
+ <div ref="container">
+ <template v-if="newToken">
+ <!--
+ After issue https://gitlab.com/gitlab-org/gitlab/-/issues/360921 is
+ closed remove the `initial-visibility`.
+ -->
+ <input-copy-toggle-visibility
+ :copy-button-title="copyButtonTitle"
+ :label="label"
+ :label-for="$options.tokenInputId"
+ :value="newToken"
+ initial-visibility
+ :form-input-group-props="formInputGroupProps"
+ >
+ <template #description>
+ {{ $options.i18n.description }}
+ </template>
+ </input-copy-toggle-visibility>
+ <hr />
+ </template>
+
+ <template v-if="errors">
+ <gl-alert :title="alertDangerTitle" variant="danger" @dismiss="errors = null">
+ <ul class="m-0">
+ <li v-for="error in errors" :key="error">
+ {{ error }}
+ </li>
+ </ul>
+ </gl-alert>
+ <hr />
+ </template>
+ </div>
+ </dom-element-listener>
+</template>
diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js
index c59bd445539..a7a03523e7f 100644
--- a/app/assets/javascripts/access_tokens/index.js
+++ b/app/assets/javascripts/access_tokens/index.js
@@ -3,12 +3,57 @@ import Vue from 'vue';
import createFlash from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { parseRailsFormFields } from '~/lib/utils/forms';
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
+import AccessTokenTableApp from './components/access_token_table_app.vue';
import ExpiresAtField from './components/expires_at_field.vue';
+import NewAccessTokenApp from './components/new_access_token_app.vue';
import TokensApp from './components/tokens_app.vue';
import { FEED_TOKEN, INCOMING_EMAIL_TOKEN, STATIC_OBJECT_TOKEN } from './constants';
+export const initAccessTokenTableApp = () => {
+ const el = document.querySelector('#js-access-token-table-app');
+
+ if (!el) {
+ return null;
+ }
+
+ const {
+ accessTokenType,
+ accessTokenTypePlural,
+ initialActiveAccessTokens: initialActiveAccessTokensJson,
+ noActiveTokensMessage: noTokensMessage,
+ } = el.dataset;
+
+ // Default values
+ const noActiveTokensMessage =
+ noTokensMessage ||
+ sprintf(__('This user has no active %{accessTokenTypePlural}.'), { accessTokenTypePlural });
+ const showRole = 'showRole' in el.dataset;
+
+ const initialActiveAccessTokens = convertObjectPropsToCamelCase(
+ JSON.parse(initialActiveAccessTokensJson),
+ {
+ deep: true,
+ },
+ );
+
+ return new Vue({
+ el,
+ name: 'AccessTokenTableRoot',
+ provide: {
+ accessTokenType,
+ accessTokenTypePlural,
+ initialActiveAccessTokens,
+ noActiveTokensMessage,
+ showRole,
+ },
+ render(h) {
+ return h(AccessTokenTableApp);
+ },
+ });
+};
+
export const initExpiresAtField = () => {
const el = document.querySelector('.js-access-tokens-expires-at');
@@ -17,7 +62,7 @@ export const initExpiresAtField = () => {
}
const { expiresAt: inputAttrs } = parseRailsFormFields(el);
- const { maxDate } = el.dataset;
+ const { minDate, maxDate } = el.dataset;
return new Vue({
el,
@@ -25,6 +70,7 @@ export const initExpiresAtField = () => {
return h(ExpiresAtField, {
props: {
inputAttrs,
+ minDate: minDate ? new Date(minDate) : undefined,
maxDate: maxDate ? new Date(maxDate) : undefined,
},
});
@@ -32,6 +78,27 @@ export const initExpiresAtField = () => {
});
};
+export const initNewAccessTokenApp = () => {
+ const el = document.querySelector('#js-new-access-token-app');
+
+ if (!el) {
+ return null;
+ }
+
+ const { accessTokenType } = el.dataset;
+
+ return new Vue({
+ el,
+ name: 'NewAccessTokenRoot',
+ provide: {
+ accessTokenType,
+ },
+ render(h) {
+ return h(NewAccessTokenApp);
+ },
+ });
+};
+
export const initProjectsField = () => {
const el = document.querySelector('.js-access-tokens-projects');
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index 7a78ccdb0cd..6fc37e9331f 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -2,7 +2,7 @@
import $ from 'jquery';
import { setCookie } from '~/lib/utils/common_utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import { localTimeAgo } from './lib/utils/datetime_utility';
import Pager from './pager';
@@ -31,7 +31,7 @@ export default class Activities {
prepareData: (data) => data,
successCallback: () => this.updateTooltips(),
errorCallback: () =>
- createFlash({
+ createAlert({
message: s__(
'Activity|An error occurred while retrieving activity. Reload the page to try again.',
),
diff --git a/app/assets/javascripts/admin/application_settings/inactive_project_deletion/components/form.vue b/app/assets/javascripts/admin/application_settings/inactive_project_deletion/components/form.vue
new file mode 100644
index 00000000000..ef4a5319eec
--- /dev/null
+++ b/app/assets/javascripts/admin/application_settings/inactive_project_deletion/components/form.vue
@@ -0,0 +1,249 @@
+<script>
+import {
+ GlFormCheckbox,
+ GlFormGroup,
+ GlFormInputGroup,
+ GlFormInput,
+ GlFormText,
+ GlLink,
+ GlSprintf,
+} from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { __, s__ } from '~/locale';
+
+export default {
+ name: 'InactiveProjectDeletionForm',
+ components: {
+ GlFormCheckbox,
+ GlFormGroup,
+ GlFormInputGroup,
+ GlFormInput,
+ GlFormText,
+ GlLink,
+ GlSprintf,
+ },
+ props: {
+ deleteInactiveProjects: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ inactiveProjectsDeleteAfterMonths: {
+ type: Number,
+ required: false,
+ default: 2,
+ },
+ inactiveProjectsMinSizeMb: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ inactiveProjectsSendWarningEmailAfterMonths: {
+ type: Number,
+ required: false,
+ default: 1,
+ },
+ },
+ data() {
+ return {
+ enabled: this.deleteInactiveProjects,
+ deleteAfterMonths: this.inactiveProjectsDeleteAfterMonths,
+ minSizeMb: this.inactiveProjectsMinSizeMb,
+ sendWarningEmailAfterMonths: this.inactiveProjectsSendWarningEmailAfterMonths,
+ };
+ },
+ computed: {
+ isMinSizeMbValid() {
+ return parseInt(this.minSizeMb, 10) >= 0;
+ },
+ isDeleteAfterMonthsValid() {
+ return (
+ parseInt(this.deleteAfterMonths, 10) > 0 &&
+ parseInt(this.deleteAfterMonths, 10) > parseInt(this.sendWarningEmailAfterMonths, 10)
+ );
+ },
+ isSendWarningEmailAfterMonthsValid() {
+ return parseInt(this.sendWarningEmailAfterMonths, 10) > 0;
+ },
+ },
+ methods: {
+ checkValidity(ref, feedback, valid) {
+ // These form fields are used within a HAML created form and we don't have direct access to the submit button
+ // So we set the validity of the field so the HAML form can't be submitted until this is set back to blank
+ if (valid) {
+ ref.$el.setCustomValidity('');
+ } else {
+ ref.$el.setCustomValidity(feedback);
+ }
+ },
+ },
+ i18n: {
+ checkboxLabel: s__('AdminSettings|Delete inactive projects'),
+ checkboxHelp: s__(
+ 'AdminSettings|Configure when inactive projects should be automatically deleted. %{linkStart}What are inactive projects?%{linkEnd}',
+ ),
+ checkboxHelpDocLink: helpPagePath('administration/inactive_project_deletion'),
+ minSizeMbLabel: s__('AdminSettings|When to delete inactive projects'),
+ minSizeMbDescription: s__('AdminSettings|Delete inactive projects that exceed'),
+ minSizeMbInvalidFeedback: s__('AdminSettings|Minimum size must be at least 0.'),
+ deleteAfterMonthsLabel: s__('AdminSettings|Delete project after'),
+ deleteAfterMonthsInvalidFeedback: s__(
+ "AdminSettings|You can't delete projects before the warning email is sent.",
+ ),
+ sendWarningEmailAfterMonthsLabel: s__('AdminSettings|Send warning email'),
+ sendWarningEmailAfterMonthsDescription: s__(
+ 'AdminSettings|Send email to maintainers after project is inactive for',
+ ),
+ sendWarningEmailAfterMonthsHelp: s__(
+ 'AdminSettings|Requires %{linkStart}email notifications%{linkEnd}',
+ ),
+ sendWarningEmailAfterMonthsDocLink: helpPagePath('user/profile/notifications'),
+ sendWarningEmailAfterMonthsInvalidFeedback: s__(
+ 'AdminSettings|Setting must be greater than 0.',
+ ),
+ mbAppend: __('MB'),
+ monthsAppend: __('months'),
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-form-group>
+ <input name="application_setting[delete_inactive_projects]" type="hidden" :value="enabled" />
+ <gl-form-checkbox v-model="enabled">
+ {{ $options.i18n.checkboxLabel }}
+
+ <template #help>
+ <gl-sprintf :message="$options.i18n.checkboxHelp">
+ <template #link="{ content }">
+ <gl-link :href="$options.i18n.checkboxHelpDocLink" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-form-checkbox>
+ </gl-form-group>
+
+ <div v-if="enabled" class="gl-ml-6" data-testid="inactive-project-deletion-settings">
+ <gl-form-group
+ :label="$options.i18n.minSizeMbLabel"
+ :state="isMinSizeMbValid"
+ data-testid="min-size-group"
+ >
+ <template #invalid-feedback>
+ <div class="gl-w-40p">{{ $options.i18n.minSizeMbInvalidFeedback }}</div>
+ </template>
+ <gl-form-text class="gl-mt-0 gl-mb-3 gl-text-body!">
+ {{ $options.i18n.minSizeMbDescription }}
+ </gl-form-text>
+ <gl-form-input-group data-testid="min-size-input-group">
+ <gl-form-input
+ ref="minSizeMbInput"
+ v-model="minSizeMb"
+ :state="isMinSizeMbValid"
+ name="application_setting[inactive_projects_min_size_mb]"
+ size="md"
+ type="number"
+ :min="0"
+ data-testid="min-size-input"
+ @change="
+ checkValidity(
+ $refs.minSizeMbInput,
+ $options.i18n.minSizeMbInvalidFeedback,
+ isMinSizeMbValid,
+ )
+ "
+ />
+
+ <template #append>
+ <div class="input-group-text">{{ $options.i18n.mbAppend }}</div>
+ </template>
+ </gl-form-input-group>
+ </gl-form-group>
+
+ <div class="gl-pl-6 gl-border-l">
+ <gl-form-group
+ :label="$options.i18n.deleteAfterMonthsLabel"
+ :state="isDeleteAfterMonthsValid"
+ data-testid="delete-after-months-group"
+ >
+ <template #invalid-feedback>
+ <div class="gl-w-30p">{{ $options.i18n.deleteAfterMonthsInvalidFeedback }}</div>
+ </template>
+ <gl-form-input-group data-testid="delete-after-months-input-group">
+ <gl-form-input
+ ref="deleteAfterMonthsInput"
+ v-model="deleteAfterMonths"
+ :state="isDeleteAfterMonthsValid"
+ name="application_setting[inactive_projects_delete_after_months]"
+ size="sm"
+ type="number"
+ :min="0"
+ data-testid="delete-after-months-input"
+ @change="
+ checkValidity(
+ $refs.deleteAfterMonthsInput,
+ $options.i18n.deleteAfterMonthsInvalidFeedback,
+ isDeleteAfterMonthsValid,
+ )
+ "
+ />
+
+ <template #append>
+ <div class="input-group-text">{{ $options.i18n.monthsAppend }}</div>
+ </template>
+ </gl-form-input-group>
+ </gl-form-group>
+
+ <gl-form-group
+ :label="$options.i18n.sendWarningEmailAfterMonthsLabel"
+ :state="isSendWarningEmailAfterMonthsValid"
+ data-testid="send-warning-email-after-months-group"
+ >
+ <template #invalid-feedback>
+ <div class="gl-w-30p">
+ {{ $options.i18n.sendWarningEmailAfterMonthsInvalidFeedback }}
+ </div>
+ </template>
+ <gl-form-text class="gl-max-w-26 gl-mt-0 gl-mb-3 gl-text-body!">
+ {{ $options.i18n.sendWarningEmailAfterMonthsDescription }}
+ </gl-form-text>
+ <gl-form-input-group data-testid="send-warning-email-after-months-input-group">
+ <gl-form-input
+ ref="sendWarningEmailAfterMonthsInput"
+ v-model="sendWarningEmailAfterMonths"
+ :state="isSendWarningEmailAfterMonthsValid"
+ name="application_setting[inactive_projects_send_warning_email_after_months]"
+ size="sm"
+ type="number"
+ :min="0"
+ data-testid="send-warning-email-after-months-input"
+ @change="
+ checkValidity(
+ $refs.sendWarningEmailAfterMonthsInput,
+ $options.i18n.sendWarningEmailAfterMonthsInvalidFeedback,
+ isSendWarningEmailAfterMonthsValid,
+ )
+ "
+ />
+
+ <template #append>
+ <div class="input-group-text">{{ $options.i18n.monthsAppend }}</div>
+ </template>
+ </gl-form-input-group>
+
+ <template #description>
+ <gl-sprintf :message="$options.i18n.sendWarningEmailAfterMonthsHelp">
+ <template #link="{ content }">
+ <gl-link :href="$options.i18n.sendWarningEmailAfterMonthsDocLink" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-form-group>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/application_settings/inactive_project_deletion/index.js b/app/assets/javascripts/admin/application_settings/inactive_project_deletion/index.js
new file mode 100644
index 00000000000..43e6902885c
--- /dev/null
+++ b/app/assets/javascripts/admin/application_settings/inactive_project_deletion/index.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import Form from './components/form.vue';
+
+export default () => {
+ const el = document.querySelector('.js-inactive-project-deletion-form');
+
+ if (!el) {
+ return false;
+ }
+
+ const {
+ deleteInactiveProjects,
+ inactiveProjectsDeleteAfterMonths,
+ inactiveProjectsMinSizeMb,
+ inactiveProjectsSendWarningEmailAfterMonths,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ name: 'InactiveProjectDeletion',
+ render(createElement) {
+ return createElement(Form, {
+ props: {
+ deleteInactiveProjects: parseBoolean(deleteInactiveProjects),
+ inactiveProjectsDeleteAfterMonths: parseInt(inactiveProjectsDeleteAfterMonths, 10),
+ inactiveProjectsMinSizeMb: parseInt(inactiveProjectsMinSizeMb, 10),
+ inactiveProjectsSendWarningEmailAfterMonths: parseInt(
+ inactiveProjectsSendWarningEmailAfterMonths,
+ 10,
+ ),
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/admin/users/components/user_actions.vue b/app/assets/javascripts/admin/users/components/user_actions.vue
index 829174d7593..40e5f8d9d70 100644
--- a/app/assets/javascripts/admin/users/components/user_actions.vue
+++ b/app/assets/javascripts/admin/users/components/user_actions.vue
@@ -100,7 +100,7 @@ export default {
<gl-button
v-else
v-gl-tooltip="$options.i18n.edit"
- icon="pencil-square"
+ icon="pencil"
v-bind="editButtonAttrs"
:aria-label="$options.i18n.edit"
/>
diff --git a/app/assets/javascripts/admin/users/components/users_table.vue b/app/assets/javascripts/admin/users/components/users_table.vue
index ede5c26e487..b4b84594276 100644
--- a/app/assets/javascripts/admin/users/components/users_table.vue
+++ b/app/assets/javascripts/admin/users/components/users_table.vue
@@ -2,7 +2,7 @@
import { GlSkeletonLoader, GlTable } from '@gitlab/ui';
import createFlash from '~/flash';
import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils';
-import { thWidthClass } from '~/lib/utils/table_utility';
+import { thWidthPercent } from '~/lib/utils/table_utility';
import { s__, __ } from '~/locale';
import UserDate from '~/vue_shared/components/user_date.vue';
import getUsersGroupCountsQuery from '../graphql/queries/get_users_group_counts.query.graphql';
@@ -70,32 +70,32 @@ export default {
{
key: 'name',
label: __('Name'),
- thClass: thWidthClass(40),
+ thClass: thWidthPercent(40),
},
{
key: 'projectsCount',
label: __('Projects'),
- thClass: thWidthClass(10),
+ thClass: thWidthPercent(10),
},
{
key: 'groupCount',
label: __('Groups'),
- thClass: thWidthClass(10),
+ thClass: thWidthPercent(10),
},
{
key: 'createdAt',
label: __('Created on'),
- thClass: thWidthClass(15),
+ thClass: thWidthPercent(15),
},
{
key: 'lastActivityOn',
label: __('Last activity'),
- thClass: thWidthClass(15),
+ thClass: thWidthPercent(15),
},
{
key: 'settings',
label: '',
- thClass: thWidthClass(10),
+ thClass: thWidthPercent(10),
},
],
};
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
index f4cc0678c38..3860831169e 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
@@ -42,6 +42,20 @@ const bodyTrClass =
export default {
i18n,
typeSet,
+ modal: {
+ actionPrimary: {
+ text: i18n.deleteIntegration,
+ attributes: {
+ variant: 'danger',
+ },
+ },
+ actionSecondary: {
+ text: __('Cancel'),
+ attributes: {
+ variant: 'default',
+ },
+ },
+ },
components: {
GlButtonGroup,
GlButton,
@@ -204,8 +218,8 @@ export default {
<gl-modal
modal-id="deleteIntegration"
:title="$options.i18n.deleteIntegration"
- :ok-title="$options.i18n.deleteIntegration"
- ok-variant="danger"
+ :action-primary="$options.modal.actionPrimary"
+ :action-secondary="$options.modal.actionSecondary"
@ok="deleteIntegration"
>
<gl-sprintf
diff --git a/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
index 6ac1bce4032..567e534d9cf 100644
--- a/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
+++ b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlSkeletonLoader } from '@gitlab/ui';
import { flatten, isEqual, keyBy } from 'lodash';
import createFlash from '~/flash';
import { sprintf, s__ } from '~/locale';
@@ -48,7 +48,7 @@ const groupRawMetrics = (groups = [], rawData = []) => {
export default {
name: 'ValueStreamMetrics',
components: {
- GlSkeletonLoading,
+ GlSkeletonLoader,
MetricTile,
},
props: {
@@ -119,8 +119,8 @@ export default {
};
</script>
<template>
- <div class="gl-display-flex gl-mt-6" data-testid="vsa-metrics">
- <gl-skeleton-loading v-if="isLoading" class="gl-h-auto gl-py-3 gl-pr-9 gl-my-6" />
+ <div class="gl-display-flex" data-testid="vsa-metrics" :class="isLoading ? 'gl-my-6' : 'gl-mt-6'">
+ <gl-skeleton-loader v-if="isLoading" />
<template v-else>
<div v-if="hasGroupedMetrics" class="gl-flex-direction-column">
<div
diff --git a/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue b/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
index 63ec40d4ec6..457a52d3807 100644
--- a/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
+++ b/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlSkeletonLoader } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import createFlash from '~/flash';
import { number } from '~/lib/utils/unit_format';
@@ -11,7 +11,7 @@ const defaultPrecision = 0;
export default {
name: 'UsageCounts',
components: {
- GlSkeletonLoading,
+ GlSkeletonLoader,
GlSingleStat,
},
data() {
@@ -65,7 +65,7 @@ export default {
<div
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-my-6 gl-align-items-flex-start"
>
- <gl-skeleton-loading v-if="$apollo.queries.counts.loading" />
+ <gl-skeleton-loader v-if="$apollo.queries.counts.loading" />
<template v-else>
<gl-single-stat
v-for="count in counts"
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 8d46ea76be1..0c870a89760 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { joinPaths } from './lib/utils/url_utility';
@@ -444,7 +444,7 @@ const Api = {
},
// Return group projects list. Filtered by query
- groupProjects(groupId, query, options, callback = () => {}, useCustomErrorHandler = false) {
+ groupProjects(groupId, query, options, callback = () => {}) {
const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
const defaults = {
search: query,
@@ -456,19 +456,7 @@ const Api = {
})
.then(({ data, headers }) => {
callback(data);
-
return { data, headers };
- })
- .catch((error) => {
- if (useCustomErrorHandler) {
- throw error;
- }
-
- createFlash({
- message: __('Something went wrong while fetching projects'),
- });
-
- callback();
});
},
@@ -654,7 +642,7 @@ const Api = {
})
.then(({ data }) => callback(data))
.catch(() =>
- createFlash({
+ createAlert({
message: __('Something went wrong while fetching projects'),
}),
);
diff --git a/app/assets/javascripts/api/projects_api.js b/app/assets/javascripts/api/projects_api.js
index 7666f558eb5..667aa878261 100644
--- a/app/assets/javascripts/api/projects_api.js
+++ b/app/assets/javascripts/api/projects_api.js
@@ -2,10 +2,9 @@ import { DEFAULT_PER_PAGE } from '~/api';
import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
-export * from './alert_management_alerts_api';
-
const PROJECTS_PATH = '/api/:version/projects.json';
const PROJECT_IMPORT_MEMBERS_PATH = '/api/:version/projects/:id/import_project_members/:project_id';
+const PROJECT_REPOSITORY_SIZE_PATH = '/api/:version/projects/:id/repository_size';
export function getProjects(query, options, callback = () => {}) {
const url = buildApiUrl(PROJECTS_PATH);
@@ -35,3 +34,11 @@ export function importProjectMembers(sourceId, targetId) {
.replace(':project_id', targetId);
return axios.post(url);
}
+
+export function updateRepositorySize(projectPath) {
+ const url = buildApiUrl(PROJECT_REPOSITORY_SIZE_PATH).replace(
+ ':id',
+ encodeURIComponent(projectPath),
+ );
+ return axios.post(url);
+}
diff --git a/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue b/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
index fe801cd460f..9cf41750efe 100644
--- a/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
+++ b/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
@@ -101,9 +101,9 @@ export default {
<template>
<div>
- <h3 class="page-title">
+ <h1 class="page-title gl-font-size-h-display">
{{ $options.i18n.pageTitle }}
- </h3>
+ </h1>
<hr />
<gl-alert variant="info" :dismissible="false">
{{ $options.i18n.alertTitle }}
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index aa735df7da5..a030797c698 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -3,9 +3,9 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
import { uniq } from 'lodash';
+import { getEmojiScoreWithIntent } from '~/emoji/utils';
import { getCookie, setCookie, scrollToElement } from '~/lib/utils/common_utils';
import * as Emoji from '~/emoji';
-
import { dispose, fixTitle } from '~/tooltips';
import createFlash from './flash';
import axios from './lib/utils/axios_utils';
@@ -559,13 +559,45 @@ export class AwardsHandler {
}
}
+ getEmojiScore(emojis, value) {
+ const elem = $(value).find('[data-name]').get(0);
+ const emoji = emojis.filter((x) => x.emoji.name === elem.dataset.name)[0];
+ elem.dataset.score = emoji.score;
+
+ return emoji.score;
+ }
+
+ sortEmojiElements(emojis, $elements) {
+ const scores = new WeakMap();
+
+ return $elements.sort((a, b) => {
+ let aScore = scores.get(a);
+ let bScore = scores.get(b);
+
+ if (!aScore) {
+ aScore = this.getEmojiScore(emojis, a);
+ scores.set(a, aScore);
+ }
+
+ if (!bScore) {
+ bScore = this.getEmojiScore(emojis, b);
+ scores.set(b, bScore);
+ }
+
+ return aScore - bScore;
+ });
+ }
+
findMatchingEmojiElements(query) {
- const emojiMatches = this.emoji.searchEmoji(query).map((x) => x.emoji.name);
+ const matchingEmoji = this.emoji
+ .searchEmoji(query)
+ .map((x) => ({ ...x, score: getEmojiScoreWithIntent(x.emoji.name, x.score) }));
+ const matchingEmojiNames = matchingEmoji.map((x) => x.emoji.name);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const $matchingElements = $emojiElements.filter(
- (i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0,
+ (i, elm) => matchingEmojiNames.indexOf(elm.dataset.name) >= 0,
);
- return $matchingElements.closest('li').clone();
+ return this.sortEmojiElements(matchingEmoji, $matchingElements.closest('li').clone());
}
/* showMenuElement and hideMenuElement are performance optimizations. We use
diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue
index c8130c47f5b..2b1ab911fbe 100644
--- a/app/assets/javascripts/batch_comments/components/draft_note.vue
+++ b/app/assets/javascripts/batch_comments/components/draft_note.vue
@@ -113,7 +113,7 @@ export default {
class="referenced-commands draft-note-commands"
></div>
- <p class="draft-note-actions d-flex">
+ <p class="draft-note-actions d-flex" data-qa-selector="draft_note_content">
<publish-button
:show-count="true"
:should-publish="false"
diff --git a/app/assets/javascripts/batch_comments/components/drafts_count.vue b/app/assets/javascripts/batch_comments/components/drafts_count.vue
index 61718b766d8..0cd093823bc 100644
--- a/app/assets/javascripts/batch_comments/components/drafts_count.vue
+++ b/app/assets/javascripts/batch_comments/components/drafts_count.vue
@@ -6,13 +6,20 @@ export default {
components: {
GlBadge,
},
+ props: {
+ variant: {
+ type: String,
+ required: false,
+ default: 'info',
+ },
+ },
computed: {
...mapGetters('batchComments', ['draftsCount']),
},
};
</script>
<template>
- <gl-badge size="sm" variant="info" class="gl-ml-2">
+ <gl-badge size="sm" :variant="variant" class="gl-ml-2">
{{ draftsCount }}
<span class="sr-only"> {{ n__('draft', 'drafts', draftsCount) }} </span>
</gl-badge>
diff --git a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
index e90c29e939f..f839056daf8 100644
--- a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
@@ -1,7 +1,9 @@
<script>
import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import PreviewItem from './preview_item.vue';
+import DraftsCount from './drafts_count.vue';
export default {
components: {
@@ -9,7 +11,9 @@ export default {
GlDropdownItem,
GlIcon,
PreviewItem,
+ DraftsCount,
},
+ mixins: [glFeatureFlagMixin()],
computed: {
...mapState('diffs', ['viewDiffsFileByFile']),
...mapGetters('batchComments', ['draftsCount', 'sortedDrafts']),
@@ -39,6 +43,7 @@ export default {
>
<template #button-content>
{{ __('Pending comments') }}
+ <drafts-count v-if="glFeatures.mrReviewSubmitComment" variant="neutral" />
<gl-icon class="dropdown-chevron" name="chevron-up" />
</template>
<gl-dropdown-item
diff --git a/app/assets/javascripts/batch_comments/components/review_bar.vue b/app/assets/javascripts/batch_comments/components/review_bar.vue
index bce13751448..3cd1a2525e9 100644
--- a/app/assets/javascripts/batch_comments/components/review_bar.vue
+++ b/app/assets/javascripts/batch_comments/components/review_bar.vue
@@ -1,14 +1,18 @@
<script>
import { mapActions, mapGetters } from 'vuex';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { REVIEW_BAR_VISIBLE_CLASS_NAME } from '../constants';
import PreviewDropdown from './preview_dropdown.vue';
import PublishButton from './publish_button.vue';
+import SubmitDropdown from './submit_dropdown.vue';
export default {
components: {
PreviewDropdown,
PublishButton,
+ SubmitDropdown,
},
+ mixins: [glFeatureFlagMixin()],
computed: {
...mapGetters(['isNotesFetched']),
},
@@ -38,7 +42,8 @@ export default {
data-qa-selector="review_bar_content"
>
<preview-dropdown />
- <publish-button class="gl-ml-3" show-count />
+ <publish-button v-if="!glFeatures.mrReviewSubmitComment" class="gl-ml-3" show-count />
+ <submit-dropdown v-else />
</div>
</nav>
</div>
diff --git a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
new file mode 100644
index 00000000000..5f4a1e44ea3
--- /dev/null
+++ b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
@@ -0,0 +1,115 @@
+<script>
+import { GlDropdown, GlButton, GlIcon, GlForm, GlFormGroup } from '@gitlab/ui';
+import { mapGetters, mapActions } from 'vuex';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import { scrollToElement } from '~/lib/utils/common_utils';
+
+export default {
+ components: {
+ GlDropdown,
+ GlButton,
+ GlIcon,
+ GlForm,
+ GlFormGroup,
+ MarkdownField,
+ },
+ data() {
+ return {
+ isSubmitting: false,
+ note: '',
+ };
+ },
+ computed: {
+ ...mapGetters(['getNotesData', 'getNoteableData', 'noteableType', 'getCurrentUserLastNote']),
+ },
+ methods: {
+ ...mapActions('batchComments', ['publishReview']),
+ async submitReview() {
+ const noteData = {
+ noteable_type: this.noteableType,
+ noteable_id: this.getNoteableData.id,
+ note: this.note,
+ };
+
+ this.isSubmitting = true;
+
+ await this.publishReview(noteData);
+
+ if (window.mrTabs && this.note) {
+ window.location.hash = `note_${this.getCurrentUserLastNote.id}`;
+ window.mrTabs.tabShown('show');
+
+ setTimeout(() =>
+ scrollToElement(document.getElementById(`note_${this.getCurrentUserLastNote.id}`)),
+ );
+ }
+
+ this.isSubmitting = false;
+ },
+ },
+ restrictedToolbarItems: ['full-screen'],
+};
+</script>
+
+<template>
+ <gl-dropdown right class="submit-review-dropdown" variant="info" category="secondary">
+ <template #button-content>
+ {{ __('Finish review') }}
+ <gl-icon class="dropdown-chevron" name="chevron-up" />
+ </template>
+ <gl-form data-testid="submit-gl-form" @submit.prevent="submitReview">
+ <gl-form-group
+ :label="__('Summary comment (optional)')"
+ label-for="review-note-body"
+ label-class="gl-mb-2"
+ >
+ <div class="common-note-form gfm-form">
+ <div
+ class="comment-warning-wrapper gl-border-solid gl-border-1 gl-rounded-base gl-border-gray-100"
+ >
+ <markdown-field
+ :is-submitting="isSubmitting"
+ :add-spacing-classes="false"
+ :textarea-value="note"
+ :markdown-preview-path="getNoteableData.preview_note_path"
+ :markdown-docs-path="getNotesData.markdownDocsPath"
+ :quick-actions-docs-path="getNotesData.quickActionsDocsPath"
+ :restricted-tool-bar-items="$options.restrictedToolbarItems"
+ :force-autosize="false"
+ class="js-no-autosize"
+ >
+ <template #textarea>
+ <textarea
+ id="review-note-body"
+ ref="textarea"
+ v-model="note"
+ dir="auto"
+ :disabled="isSubmitting"
+ name="review[note]"
+ class="note-textarea js-gfm-input markdown-area"
+ data-supports-quick-actions="true"
+ data-testid="comment-textarea"
+ :aria-label="__('Comment')"
+ :placeholder="__('Write a comment or drag your files here…')"
+ @keydown.meta.enter="submitReview"
+ @keydown.ctrl.enter="submitReview"
+ ></textarea>
+ </template>
+ </markdown-field>
+ </div>
+ </div>
+ </gl-form-group>
+ <div class="gl-display-flex gl-justify-content-end gl-mt-5">
+ <gl-button
+ :loading="isSubmitting"
+ variant="confirm"
+ type="submit"
+ class="js-no-auto-disable"
+ data-testid="submit-review-button"
+ >
+ {{ __('Submit review') }}
+ </gl-button>
+ </div>
+ </gl-form>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/batch_comments/services/drafts_service.js b/app/assets/javascripts/batch_comments/services/drafts_service.js
index 36d2f8df612..b52e573d55d 100644
--- a/app/assets/javascripts/batch_comments/services/drafts_service.js
+++ b/app/assets/javascripts/batch_comments/services/drafts_service.js
@@ -19,8 +19,8 @@ export default {
fetchDrafts(endpoint) {
return axios.get(endpoint);
},
- publish(endpoint) {
- return axios.post(endpoint);
+ publish(endpoint, noteData) {
+ return axios.post(endpoint, noteData);
},
discard(endpoint) {
return axios.delete(endpoint);
diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
index 4ee22918463..908cbfd6dc8 100644
--- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
+++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
@@ -77,28 +77,22 @@ export const publishSingleDraft = ({ commit, dispatch, getters }, draftId) => {
.catch(() => commit(types.RECEIVE_PUBLISH_DRAFT_ERROR, draftId));
};
-export const publishReview = ({ commit, dispatch, getters }) => {
+export const publishReview = ({ commit, dispatch, getters }, noteData = {}) => {
commit(types.REQUEST_PUBLISH_REVIEW);
return service
- .publish(getters.getNotesData.draftsPublishPath)
+ .publish(getters.getNotesData.draftsPublishPath, noteData)
.then(() => dispatch('updateDiscussionsAfterPublish'))
.then(() => commit(types.RECEIVE_PUBLISH_REVIEW_SUCCESS))
.catch(() => commit(types.RECEIVE_PUBLISH_REVIEW_ERROR));
};
export const updateDiscussionsAfterPublish = async ({ dispatch, getters, rootGetters }) => {
- if (window.gon?.features?.paginatedNotes) {
- await dispatch('stopPolling', null, { root: true });
- await dispatch('fetchData', null, { root: true });
- await dispatch('restartPolling', null, { root: true });
- } else {
- await dispatch(
- 'fetchDiscussions',
- { path: getters.getNotesData.discussionsPath },
- { root: true },
- );
- }
+ await dispatch(
+ 'fetchDiscussions',
+ { path: getters.getNotesData.discussionsPath },
+ { root: true },
+ );
dispatch('diffs/assignDiscussionsToDiff', rootGetters.discussionsStructuredByLineCode, {
root: true,
diff --git a/app/assets/javascripts/behaviors/components/sandboxed_mermaid.vue b/app/assets/javascripts/behaviors/components/sandboxed_mermaid.vue
new file mode 100644
index 00000000000..6b4110cff02
--- /dev/null
+++ b/app/assets/javascripts/behaviors/components/sandboxed_mermaid.vue
@@ -0,0 +1,77 @@
+<script>
+import {
+ getSandboxFrameSrc,
+ BUFFER_IFRAME_HEIGHT,
+ SANDBOX_ATTRIBUTES,
+} from '../markdown/render_sandboxed_mermaid';
+
+export default {
+ name: 'SandboxedMermaid',
+
+ props: {
+ source: {
+ type: String,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ iframeHeight: BUFFER_IFRAME_HEIGHT,
+ sandboxFrameSrc: getSandboxFrameSrc(),
+ };
+ },
+
+ watch: {
+ source() {
+ this.updateDiagram();
+ },
+ },
+
+ mounted() {
+ window.addEventListener('message', this.onPostMessage, false);
+ },
+
+ destroyed() {
+ window.removeEventListener('message', this.onPostMessage);
+ },
+
+ methods: {
+ getSandboxFrameSrc,
+
+ onPostMessage(event) {
+ const container = this.$refs.diagramContainer;
+
+ if (event.source === container?.contentWindow) {
+ this.iframeHeight = Number(event.data.h) + BUFFER_IFRAME_HEIGHT;
+ }
+ },
+
+ updateDiagram() {
+ const container = this.$refs.diagramContainer;
+
+ // Potential risk associated with '*' discussed in below thread
+ // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74414#note_735183398
+ container.contentWindow?.postMessage(this.source, '*');
+ container.addEventListener('load', () => {
+ container.contentWindow?.postMessage(this.source, '*');
+ });
+ },
+ },
+
+ sandboxFrameSrc: getSandboxFrameSrc(),
+ sandboxAttributes: SANDBOX_ATTRIBUTES,
+};
+</script>
+<template>
+ <iframe
+ ref="diagramContainer"
+ :src="$options.sandboxFrameSrc"
+ :sandbox="$options.sandboxAttributes"
+ frameborder="0"
+ scrolling="no"
+ width="100%"
+ :height="iframeHeight"
+ >
+ </iframe>
+</template>
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index 063393c9cd1..c9ae3706383 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -1,10 +1,8 @@
import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
-import initUserPopovers from '../../user_popovers';
import highlightCurrentUser from './highlight_current_user';
import { renderKroki } from './render_kroki';
import renderMath from './render_math';
-import renderMermaid from './render_mermaid';
import renderSandboxedMermaid from './render_sandboxed_mermaid';
import renderMetrics from './render_metrics';
@@ -16,19 +14,15 @@ $.fn.renderGFM = function renderGFM() {
syntaxHighlight(this.find('.js-syntax-highlight').get());
renderKroki(this.find('.js-render-kroki[hidden]').get());
renderMath(this.find('.js-render-math'));
- if (gon.features?.sandboxedMermaid) {
- renderSandboxedMermaid(this.find('.js-render-mermaid'));
- } else {
- renderMermaid(this.find('.js-render-mermaid'));
- }
+ renderSandboxedMermaid(this.find('.js-render-mermaid'));
+
highlightCurrentUser(this.find('.gfm-project_member').get());
- initUserPopovers(this.find('.js-user-link').get());
- const mrPopoverElements = this.find('.gfm-merge_request').get();
- if (mrPopoverElements.length) {
- import(/* webpackChunkName: 'MrPopoverBundle' */ '~/mr_popover')
- .then(({ default: initMRPopovers }) => {
- initMRPopovers(mrPopoverElements);
+ const issuablePopoverElements = this.find('.gfm-issue, .gfm-merge_request').get();
+ if (issuablePopoverElements.length) {
+ import(/* webpackChunkName: 'IssuablePopoverBundle' */ '~/issuable/popover')
+ .then(({ default: initIssuablePopovers }) => {
+ initIssuablePopovers(issuablePopoverElements);
})
.catch(() => {});
}
diff --git a/app/assets/javascripts/behaviors/markdown/render_kroki.js b/app/assets/javascripts/behaviors/markdown/render_kroki.js
index abe71694d73..5fd910dd6cc 100644
--- a/app/assets/javascripts/behaviors/markdown/render_kroki.js
+++ b/app/assets/javascripts/behaviors/markdown/render_kroki.js
@@ -55,8 +55,8 @@ export function renderKroki(krokiImages) {
// A single Kroki image is processed multiple times for some reason,
// so this condition ensures we only create one alert per Kroki image
- if (!parent.hasAttribute('data-kroki-processed')) {
- parent.setAttribute('data-kroki-processed', 'true');
+ if (!Object.prototype.hasOwnProperty.call(parent.dataset, 'krokiProcessed')) {
+ parent.dataset.krokiProcessed = 'true';
parent.after(createAlert(krokiImage));
}
});
diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index 12f47255bdf..af7aac4cf36 100644
--- a/app/assets/javascripts/behaviors/markdown/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -1,6 +1,7 @@
import { spriteIcon } from '~/lib/utils/common_utils';
import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
import { s__ } from '~/locale';
+import { unrestrictedPages } from './constants';
// Renders math using KaTeX in any element with the
// `js-render-math` class
@@ -48,6 +49,7 @@ class SafeMathRenderer {
this.renderElement = this.renderElement.bind(this);
this.render = this.render.bind(this);
this.attachEvents = this.attachEvents.bind(this);
+ this.pageName = document.querySelector('body').dataset.page;
}
renderElement(chosenEl) {
@@ -56,7 +58,7 @@ class SafeMathRenderer {
}
const el = chosenEl || this.queue.shift();
- const forceRender = Boolean(chosenEl);
+ const forceRender = Boolean(chosenEl) || unrestrictedPages.includes(this.pageName);
const text = el.textContent;
el.removeAttribute('style');
@@ -79,7 +81,7 @@ class SafeMathRenderer {
'math|Displaying this math block may cause performance issues on this page',
)}</div>
<div class="gl-alert-actions">
- <button class="js-lazy-render-math btn gl-alert-action btn-primary btn-md gl-button">Display anyway</button>
+ <button class="js-lazy-render-math btn gl-alert-action btn-confirm btn-md gl-button">Display anyway</button>
</div>
</div>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
@@ -110,7 +112,7 @@ class SafeMathRenderer {
try {
displayContainer.innerHTML = this.katex.renderToString(text, {
- displayMode: el.getAttribute('data-math-style') === 'display',
+ displayMode: el.dataset.mathStyle === 'display',
throwOnError: true,
maxSize: 20,
maxExpand: 20,
@@ -143,7 +145,7 @@ class SafeMathRenderer {
this.elements.forEach((el) => {
const placeholder = document.createElement('span');
placeholder.style.display = 'none';
- placeholder.setAttribute('data-math-style', el.getAttribute('data-math-style'));
+ placeholder.dataset.mathStyle = el.dataset.mathStyle;
placeholder.textContent = el.textContent;
el.parentNode.replaceChild(placeholder, el);
this.queue.push(placeholder);
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
index f9cf3af98bb..2df0f7387fb 100644
--- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
@@ -160,7 +160,7 @@ function renderMermaids($els) {
'Warning: Displaying this diagram might cause performance issues on this page.',
)}</div>
<div class="gl-alert-actions">
- <button class="js-lazy-render-mermaid btn gl-alert-action btn-warning btn-md gl-button">Display</button>
+ <button class="js-lazy-render-mermaid btn gl-alert-action btn-confirm btn-md gl-button">Display</button>
</div>
</div>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
diff --git a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
index 3b9f6011c6d..077e96b2fee 100644
--- a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
@@ -32,7 +32,8 @@ const MAX_CHAR_LIMIT = 2000;
const MAX_MERMAID_BLOCK_LIMIT = 50;
// Max # of `&` allowed in Chaining of links syntax
const MAX_CHAINING_OF_LINKS_LIMIT = 30;
-const BUFFER_IFRAME_HEIGHT = 10;
+export const BUFFER_IFRAME_HEIGHT = 10;
+export const SANDBOX_ATTRIBUTES = 'allow-scripts allow-popups';
// Keep a map of mermaid blocks we've already rendered.
const elsProcessingMap = new WeakMap();
let renderedMermaidBlocks = 0;
@@ -56,7 +57,7 @@ function fixElementSource(el) {
return { source };
}
-function getSandboxFrameSrc() {
+export function getSandboxFrameSrc() {
const path = joinPaths(gon.relative_url_root || '', SANDBOX_FRAME_PATH);
if (!darkModeEnabled()) {
return path;
@@ -69,7 +70,7 @@ function renderMermaidEl(el, source) {
const iframeEl = document.createElement('iframe');
setAttributes(iframeEl, {
src: getSandboxFrameSrc(),
- sandbox: 'allow-scripts allow-popups',
+ sandbox: SANDBOX_ATTRIBUTES,
frameBorder: 0,
scrolling: 'no',
width: '100%',
@@ -138,7 +139,7 @@ function renderMermaids($els) {
<div>
<div class="js-warning-text"></div>
<div class="gl-alert-actions">
- <button type="button" class="js-lazy-render-mermaid btn gl-alert-action btn-warning btn-md gl-button">Display</button>
+ <button type="button" class="js-lazy-render-mermaid btn gl-alert-action btn-confirm btn-md gl-button">Display</button>
</div>
</div>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
diff --git a/app/assets/javascripts/blob/blob_line_permalink_updater.js b/app/assets/javascripts/blob/blob_line_permalink_updater.js
index a3dd241604d..0a5bcf326a1 100644
--- a/app/assets/javascripts/blob/blob_line_permalink_updater.js
+++ b/app/assets/javascripts/blob/blob_line_permalink_updater.js
@@ -9,10 +9,11 @@ const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => {
[].concat(Array.prototype.slice.call(linksToUpdate)).forEach((permalinkButton) => {
const baseHref =
- permalinkButton.getAttribute('data-original-href') ||
+ permalinkButton.dataset.originalHref ||
(() => {
const href = permalinkButton.getAttribute('href');
- permalinkButton.setAttribute('data-original-href', href);
+ // eslint-disable-next-line no-param-reassign
+ permalinkButton.dataset.originalHref = href;
return href;
})();
permalinkButton.setAttribute('href', `${baseHref}${hashUrlString}`);
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index f78d921fa90..716321430d2 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -47,6 +47,11 @@ export default {
required: false,
default: true,
},
+ overrideCopy: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -106,6 +111,7 @@ export default {
:environment-name="blob.environmentFormattedExternalUrl"
:environment-path="blob.environmentExternalUrlForRouteMap"
:is-empty="isEmpty"
+ :override-copy="overrideCopy"
@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 61baf4fa495..12a198f78ea 100644
--- a/app/assets/javascripts/blob/components/blob_header_default_actions.vue
+++ b/app/assets/javascripts/blob/components/blob_header_default_actions.vue
@@ -54,6 +54,11 @@ export default {
required: false,
default: false,
},
+ overrideCopy: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
downloadUrl() {
@@ -63,6 +68,10 @@ export default {
return this.activeViewer === RICH_BLOB_VIEWER;
},
getBlobHashTarget() {
+ if (this.overrideCopy) {
+ return null;
+ }
+
return `[data-blob-hash="${this.blobHash}"]`;
},
showCopyButton() {
@@ -74,6 +83,13 @@ export default {
});
},
},
+ methods: {
+ onCopy() {
+ if (this.overrideCopy) {
+ this.$emit('copy');
+ }
+ },
+ },
BTN_COPY_CONTENTS_TITLE,
BTN_DOWNLOAD_TITLE,
BTN_RAW_TITLE,
@@ -94,6 +110,7 @@ export default {
category="primary"
variant="default"
class="js-copy-blob-source-btn"
+ @click="onCopy"
/>
<gl-button
v-if="!isBinary"
diff --git a/app/assets/javascripts/blob/components/blob_header_filepath.vue b/app/assets/javascripts/blob/components/blob_header_filepath.vue
index 62355306655..fb99392ff48 100644
--- a/app/assets/javascripts/blob/components/blob_header_filepath.vue
+++ b/app/assets/javascripts/blob/components/blob_header_filepath.vue
@@ -46,7 +46,7 @@ export default {
<slot name="filepath-prepend"></slot>
<template v-if="fileName">
- <file-icon :file-name="fileName" :size="16" aria-hidden="true" css-classes="mr-2" />
+ <file-icon :file-name="fileName" :size="16" aria-hidden="true" css-classes="gl-mr-3" />
<strong
class="file-title-name mr-1 js-blob-header-filepath"
data-qa-selector="file_title_content"
@@ -62,7 +62,7 @@ export default {
css-class="btn-clipboard btn-transparent lh-100 position-static"
/>
- <small class="mr-2">{{ blobSize }}</small>
+ <small class="gl-mr-3">{{ blobSize }}</small>
<gl-badge v-if="showLfsBadge">{{ __('LFS') }}</gl-badge>
</div>
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index a6eed4ecae3..a0d4f7ef4f2 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -36,19 +36,19 @@ const loadRichBlobViewer = (type) => {
const loadViewer = (viewerParam) => {
const viewer = viewerParam;
- const url = viewer.getAttribute('data-url');
+ const { url } = viewer.dataset;
- if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
+ if (!url || viewer.dataset.loaded || viewer.dataset.loading) {
return Promise.resolve(viewer);
}
- viewer.setAttribute('data-loading', 'true');
+ viewer.dataset.loading = 'true';
return axios.get(url).then(({ data }) => {
viewer.innerHTML = data.html;
window.requestIdleCallback(() => {
- viewer.removeAttribute('data-loading');
+ delete viewer.dataset.loading;
});
return viewer;
@@ -108,7 +108,7 @@ export class BlobViewer {
switchToInitialViewer() {
const initialViewer = this.$fileHolder[0].querySelector('.blob-viewer:not(.hidden)');
- let initialViewerName = initialViewer.getAttribute('data-type');
+ let initialViewerName = initialViewer.dataset.type;
if (this.switcher && window.location.hash.indexOf('#L') === 0) {
initialViewerName = 'simple';
@@ -138,12 +138,12 @@ export class BlobViewer {
e.preventDefault();
- this.switchToViewer(target.getAttribute('data-viewer'));
+ this.switchToViewer(target.dataset.viewer);
}
toggleCopyButtonState() {
if (!this.copySourceBtn) return;
- if (this.simpleViewer.getAttribute('data-loaded')) {
+ if (this.simpleViewer.dataset.loaded) {
this.copySourceBtnTooltip.setAttribute('title', __('Copy file contents'));
this.copySourceBtn.classList.remove('disabled');
} else if (this.activeViewer === this.simpleViewer) {
@@ -199,7 +199,8 @@ export class BlobViewer {
this.$fileHolder.trigger('highlight:line');
handleLocationHash();
- viewer.setAttribute('data-loaded', 'true');
+ // eslint-disable-next-line no-param-reassign
+ viewer.dataset.loaded = 'true';
this.toggleCopyButtonState();
eventHub.$emit('showBlobInteractionZones', viewer.dataset.path);
});
diff --git a/app/assets/javascripts/boards/components/board_app.vue b/app/assets/javascripts/boards/components/board_app.vue
index 858aabb0f05..af753151be8 100644
--- a/app/assets/javascripts/boards/components/board_app.vue
+++ b/app/assets/javascripts/boards/components/board_app.vue
@@ -1,5 +1,6 @@
<script>
-import { mapActions, mapGetters } from 'vuex';
+import { mapGetters } from 'vuex';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
import BoardContent from '~/boards/components/board_content.vue';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import BoardTopBar from '~/boards/components/board_top_bar.vue';
@@ -14,11 +15,11 @@ export default {
computed: {
...mapGetters(['isSidebarOpen']),
},
- mounted() {
- this.performSearch();
+ created() {
+ window.addEventListener('popstate', refreshCurrentPage);
},
- methods: {
- ...mapActions(['performSearch']),
+ destroyed() {
+ window.removeEventListener('popstate', refreshCurrentPage);
},
};
</script>
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index 9d972860d06..9f359a25234 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -1,7 +1,8 @@
<script>
import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
-import { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { visitUrl, updateHistory, getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { formType } from '../constants';
@@ -170,17 +171,7 @@ export default {
}
},
methods: {
- ...mapActions(['setError', 'unsetError']),
- boardCreateResponse(data) {
- return data.createBoard.board.webPath;
- },
- boardUpdateResponse(data) {
- const path = data.updateBoard.board.webPath;
- const param = getParameterByName('group_by')
- ? `?group_by=${getParameterByName('group_by')}`
- : '';
- return `${path}${param}`;
- },
+ ...mapActions(['setError', 'unsetError', 'setBoard']),
cancel() {
this.$emit('cancel');
},
@@ -191,10 +182,10 @@ export default {
});
if (!this.board.id) {
- return this.boardCreateResponse(response.data);
+ return response.data.createBoard.board;
}
- return this.boardUpdateResponse(response.data);
+ return response.data.updateBoard.board;
},
async deleteBoard() {
await this.$apollo.mutate({
@@ -218,8 +209,14 @@ export default {
}
} else {
try {
- const url = await this.createOrUpdateBoard();
- visitUrl(url);
+ const board = await this.createOrUpdateBoard();
+ this.setBoard(board);
+ this.cancel();
+
+ const param = getParameterByName('group_by')
+ ? `?group_by=${getParameterByName('group_by')}`
+ : '';
+ updateHistory({ url: `${this.boardBaseUrl}/${getIdFromGraphQLId(board.id)}${param}` });
} catch {
this.setError({ message: this.$options.i18n.saveErrorMessage });
} finally {
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index a4298eb2544..a65269de743 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -45,9 +45,6 @@ export default {
},
mixins: [Tracking.mixin(), glFeatureFlagMixin()],
inject: {
- boardId: {
- default: '',
- },
weightFeatureAvailable: {
default: false,
},
@@ -78,7 +75,7 @@ export default {
},
},
computed: {
- ...mapState(['activeId', 'filterParams']),
+ ...mapState(['activeId', 'filterParams', 'boardId']),
...mapGetters(['isEpicBoard', 'isSwimlanesOn']),
isLoggedIn() {
return Boolean(this.currentUserId);
@@ -155,6 +152,12 @@ export default {
isLoading() {
return this.$apollo.queries.boardList.loading;
},
+ totalWeight() {
+ return this.boardList?.totalWeight;
+ },
+ canShowTotalWeight() {
+ return this.weightFeatureAvailable && !this.isLoading;
+ },
},
apollo: {
boardList: {
@@ -359,7 +362,7 @@ export default {
<div v-if="weightFeatureAvailable && !isLoading">
<gl-sprintf :message="__('%{totalWeight} total weight')">
- <template #totalWeight>{{ boardList.totalWeight }}</template>
+ <template #totalWeight>{{ totalWeight }}</template>
</gl-sprintf>
</div>
</gl-tooltip>
@@ -384,11 +387,11 @@ export default {
/>
</span>
<!-- EE start -->
- <template v-if="weightFeatureAvailable && !isEpicBoard && !isLoading">
+ <template v-if="canShowTotalWeight">
<gl-tooltip :target="() => $refs.weightTooltip" :title="weightCountToolTip" />
- <span ref="weightTooltip" class="gl-display-inline-flex gl-ml-3">
+ <span ref="weightTooltip" class="gl-display-inline-flex gl-ml-3" data-testid="weight">
<gl-icon class="gl-mr-2" name="weight" />
- {{ boardList.totalWeight }}
+ {{ totalWeight }}
</span>
</template>
<!-- EE end -->
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 2951eda1112..eaf3facb450 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -14,15 +14,16 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import BoardForm from 'ee_else_ce/boards/components/board_form.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { isMetaKey } from '~/lib/utils/common_utils';
+import { updateHistory } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import eventHub from '../eventhub';
import groupBoardsQuery from '../graphql/group_boards.query.graphql';
import projectBoardsQuery from '../graphql/project_boards.query.graphql';
-import groupBoardQuery from '../graphql/group_board.query.graphql';
-import projectBoardQuery from '../graphql/project_board.query.graphql';
import groupRecentBoardsQuery from '../graphql/group_recent_boards.query.graphql';
import projectRecentBoardsQuery from '../graphql/project_recent_boards.query.graphql';
+import { fullBoardId } from '../boards_util';
const MIN_BOARDS_TO_VIEW_RECENT = 10;
@@ -69,48 +70,15 @@ export default {
maxPosition: 0,
filterTerm: '',
currentPage: '',
- board: {},
};
},
- apollo: {
- board: {
- query() {
- return this.currentBoardQuery;
- },
- variables() {
- return {
- fullPath: this.fullPath,
- boardId: this.fullBoardId,
- };
- },
- update(data) {
- const board = data.workspace?.board;
- this.setBoardConfig(board);
- return {
- ...board,
- labels: board?.labels?.nodes,
- };
- },
- error() {
- this.setError({ message: this.$options.i18n.errorFetchingBoard });
- },
- },
- },
+
computed: {
- ...mapState(['boardType', 'fullBoardId']),
+ ...mapState(['boardType', 'board', 'isBoardLoading']),
...mapGetters(['isGroupBoard', 'isProjectBoard']),
parentType() {
return this.boardType;
},
- currentBoardQueryCE() {
- return this.isGroupBoard ? groupBoardQuery : projectBoardQuery;
- },
- currentBoardQuery() {
- return this.currentBoardQueryCE;
- },
- isBoardLoading() {
- return this.$apollo.queries.board.loading;
- },
loading() {
return this.loadingRecentBoards || Boolean(this.loadingBoards);
},
@@ -147,6 +115,9 @@ export default {
this.scrollFadeInitialized = false;
this.$nextTick(this.setScrollFade);
},
+ board(newBoard) {
+ document.title = newBoard.name;
+ },
},
created() {
eventHub.$on('showBoardModal', this.showPage);
@@ -155,7 +126,7 @@ export default {
eventHub.$off('showBoardModal', this.showPage);
},
methods: {
- ...mapActions(['setError', 'setBoardConfig']),
+ ...mapActions(['setError', 'fetchBoard', 'unsetActiveId']),
showPage(page) {
this.currentPage = page;
},
@@ -231,6 +202,22 @@ export default {
this.hasScrollFade = this.isScrolledUp();
},
+ fetchCurrentBoard(boardId) {
+ this.fetchBoard({
+ fullPath: this.fullPath,
+ fullBoardId: fullBoardId(boardId),
+ boardType: this.boardType,
+ });
+ },
+ async switchBoard(boardId, e) {
+ if (isMetaKey(e)) {
+ window.open(`${this.boardBaseUrl}/${boardId}`, '_blank');
+ } else {
+ this.unsetActiveId();
+ this.fetchCurrentBoard(boardId);
+ updateHistory({ url: `${this.boardBaseUrl}/${boardId}` });
+ }
+ },
},
i18n: {
errorFetchingBoard: s__('Board|An error occurred while fetching the board, please try again.'),
@@ -277,8 +264,8 @@ export default {
<gl-dropdown-item
v-for="recentBoard in recentBoards"
:key="`recent-${recentBoard.id}`"
- :href="`${boardBaseUrl}/${recentBoard.id}`"
data-testid="dropdown-item"
+ @click.prevent="switchBoard(recentBoard.id, $event)"
>
{{ recentBoard.name }}
</gl-dropdown-item>
@@ -293,8 +280,8 @@ export default {
<gl-dropdown-item
v-for="otherBoard in filteredBoards"
:key="otherBoard.id"
- :href="`${boardBaseUrl}/${otherBoard.id}`"
data-testid="dropdown-item"
+ @click.prevent="switchBoard(otherBoard.id, $event)"
>
{{ otherBoard.name }}
</gl-dropdown-item>
diff --git a/app/assets/javascripts/boards/graphql/board_create.mutation.graphql b/app/assets/javascripts/boards/graphql/board_create.mutation.graphql
index b3ea79d6443..42e164f4f3c 100644
--- a/app/assets/javascripts/boards/graphql/board_create.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/board_create.mutation.graphql
@@ -1,8 +1,9 @@
+#import "ee_else_ce/boards/graphql/board_scope.fragment.graphql"
+
mutation createBoard($input: CreateBoardInput!) {
createBoard(input: $input) {
board {
- id
- webPath
+ ...BoardScopeFragment
}
errors
}
diff --git a/app/assets/javascripts/boards/graphql/board_update.mutation.graphql b/app/assets/javascripts/boards/graphql/board_update.mutation.graphql
index 3abe09079c7..90de7713ff3 100644
--- a/app/assets/javascripts/boards/graphql/board_update.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/board_update.mutation.graphql
@@ -1,8 +1,9 @@
+#import "ee_else_ce/boards/graphql/board_scope.fragment.graphql"
+
mutation UpdateBoard($input: UpdateBoardInput!) {
updateBoard(input: $input) {
board {
- id
- webPath
+ ...BoardScopeFragment
}
errors
}
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 8af7da1e0aa..854717ed4c4 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -54,7 +54,6 @@ function mountBoardApp(el) {
apolloProvider,
provide: {
disabled: parseBoolean(el.dataset.disabled),
- boardId,
groupId: Number(groupId),
rootPath,
fullPath,
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index a84b678a5d9..791182af806 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -31,10 +31,12 @@ import {
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 totalCountAndWeightQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
+import { fetchPolicies } from '~/lib/graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
+import eventHub from '../eventhub';
import { gqlClient } from '../graphql';
import projectBoardQuery from '../graphql/project_board.query.graphql';
import groupBoardQuery from '../graphql/group_board.query.graphql';
@@ -49,6 +51,8 @@ import * as types from './mutation_types';
export default {
fetchBoard: ({ commit, dispatch }, { fullPath, fullBoardId, boardType }) => {
+ commit(types.REQUEST_CURRENT_BOARD);
+
const variables = {
fullPath,
boardId: fullBoardId,
@@ -60,9 +64,12 @@ export default {
variables,
})
.then(({ data }) => {
- const board = data.workspace?.board;
- commit(types.RECEIVE_BOARD_SUCCESS, board);
- dispatch('setBoardConfig', board);
+ if (data.workspace?.errors) {
+ commit(types.RECEIVE_BOARD_FAILURE);
+ } else {
+ const board = data.workspace?.board;
+ dispatch('setBoard', board);
+ }
})
.catch(() => commit(types.RECEIVE_BOARD_FAILURE));
},
@@ -87,6 +94,13 @@ export default {
commit(types.SET_BOARD_CONFIG, config);
},
+ setBoard: async ({ commit, dispatch }, board) => {
+ commit(types.RECEIVE_BOARD_SUCCESS, board);
+ await dispatch('setBoardConfig', board);
+ dispatch('performSearch', { resetLists: true });
+ eventHub.$emit('updateTokens');
+ },
+
setActiveId({ commit }, { id, sidebarType }) {
commit(types.SET_ACTIVE_ID, { id, sidebarType });
},
@@ -107,16 +121,16 @@ export default {
);
},
- performSearch({ dispatch }) {
+ performSearch({ dispatch }, { resetLists = false } = {}) {
dispatch(
'setFilters',
convertObjectPropsToCamelCase(queryToObject(window.location.search, { gatherArrays: true })),
);
- dispatch('fetchLists');
+ dispatch('fetchLists', { resetLists });
dispatch('resetIssues');
},
- fetchLists: ({ commit, state, dispatch }) => {
+ fetchLists: ({ commit, state, dispatch }, { resetLists = false } = {}) => {
const { boardType, filterParams, fullPath, fullBoardId, issuableType } = state;
const variables = {
@@ -133,6 +147,7 @@ export default {
.query({
query: listsQuery[issuableType].query,
variables,
+ ...(resetLists ? { fetchPolicy: fetchPolicies.NO_CACHE } : {}),
})
.then(({ data }) => {
const { lists, hideBacklogList } = data[boardType].board;
@@ -404,9 +419,6 @@ export default {
fetchItemsForList: ({ state, commit }, { listId, fetchNext = false }) => {
if (!listId) return null;
- if (!fetchNext) {
- commit(types.RESET_ITEMS_FOR_LIST, listId);
- }
commit(types.REQUEST_ITEMS_FOR_LIST, { listId, fetchNext });
const { fullPath, fullBoardId, boardType, filterParams } = state;
@@ -428,6 +440,7 @@ export default {
isSingleRequest: true,
},
variables,
+ ...(!fetchNext ? { fetchPolicy: fetchPolicies.NO_CACHE } : {}),
})
.then(({ data }) => {
const { lists } = data[boardType].board;
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 668a3b5e0f9..43268f21f96 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -1,3 +1,4 @@
+export const REQUEST_CURRENT_BOARD = 'REQUEST_CURRENT_BOARD';
export const RECEIVE_BOARD_SUCCESS = 'RECEIVE_BOARD_SUCCESS';
export const RECEIVE_BOARD_FAILURE = 'RECEIVE_BOARD_FAILURE';
export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA';
@@ -17,7 +18,6 @@ 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';
-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';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 9a50dcf05b8..04e7d3643e7 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -1,5 +1,6 @@
import { cloneDeep, pull, union } from 'lodash';
import Vue from 'vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__, __ } from '~/locale';
import { formatIssue } from '../boards_util';
import { issuableTypes } from '../constants';
@@ -33,15 +34,23 @@ export const addItemToList = ({ state, listId, itemId, moveBeforeId, moveAfterId
};
export default {
+ [mutationTypes.REQUEST_CURRENT_BOARD]: (state) => {
+ state.isBoardLoading = true;
+ },
+
[mutationTypes.RECEIVE_BOARD_SUCCESS]: (state, board) => {
state.board = {
...board,
labels: board?.labels?.nodes || [],
};
+ state.fullBoardId = board.id;
+ state.boardId = getIdFromGraphQLId(board.id);
+ state.isBoardLoading = false;
},
[mutationTypes.RECEIVE_BOARD_FAILURE]: (state) => {
state.error = s__('Boards|An error occurred while fetching the board. Please reload the page.');
+ state.isBoardLoading = false;
},
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
@@ -136,11 +145,6 @@ export default {
state.boardLists = listsBackup;
},
- [mutationTypes.RESET_ITEMS_FOR_LIST]: (state, listId) => {
- Vue.set(state, 'backupItemsList', state.boardItemsByListId[listId]);
- Vue.set(state.boardItemsByListId, listId, []);
- },
-
[mutationTypes.REQUEST_ITEMS_FOR_LIST]: (state, { listId, fetchNext }) => {
Vue.set(state.listsFlags, listId, { [fetchNext ? 'isLoadingMore' : 'isLoading']: true });
},
@@ -176,7 +180,6 @@ export default {
'Boards|An error occurred while fetching the board issues. Please reload the page.',
);
Vue.set(state.listsFlags, listId, { isLoading: false, isLoadingMore: false });
- Vue.set(state.boardItemsByListId, listId, state.backupItemsList);
},
[mutationTypes.RESET_ISSUES]: (state) => {
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index 7af4e5a8798..b62c032b921 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -2,6 +2,7 @@ import { inactiveId, ListType } from '~/boards/constants';
export default () => ({
board: {},
+ isBoardLoading: false,
boardType: null,
issuableType: null,
fullPath: null,
@@ -12,7 +13,6 @@ export default () => ({
boardLists: {},
listsFlags: {},
boardItemsByListId: {},
- backupItemsList: [],
isSettingAssignees: false,
pageInfoByListId: {},
boardItems: {},
diff --git a/app/assets/javascripts/breadcrumb.js b/app/assets/javascripts/breadcrumb.js
index b9d3742974c..113840dbc52 100644
--- a/app/assets/javascripts/breadcrumb.js
+++ b/app/assets/javascripts/breadcrumb.js
@@ -5,7 +5,7 @@ export const addTooltipToEl = (el) => {
if (textEl && textEl.scrollWidth > textEl.offsetWidth) {
el.setAttribute('title', el.textContent);
- el.setAttribute('data-container', 'body');
+ el.dataset.container = 'body';
el.classList.add('has-tooltip');
}
};
diff --git a/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue b/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
index dbc4565b19d..ebcc4b85ac4 100644
--- a/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
+++ b/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
@@ -191,7 +191,7 @@ export default {
<div class="col-md-12 col-lg-6">
<div class="gl-display-flex gl-flex-wrap gl-justify-content-end">
- <gl-button v-if="admin" class="gl-mt-3" variant="info" @click="loadFileSelctor">
+ <gl-button v-if="admin" class="gl-mt-3" variant="confirm" @click="loadFileSelctor">
<span v-if="uploading">
<gl-loading-icon size="sm" class="gl-my-5" inline />
{{ $options.i18n.uploadingLabel }}
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 3af89dc4a2c..557a8d6b5ba 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
@@ -369,7 +369,7 @@ export default {
:href="awsTipLearnLink"
target="_blank"
category="secondary"
- variant="info"
+ variant="confirm"
class="gl-overflow-wrap-break"
>{{ __('Learn more about deploying to AWS') }}</gl-button
>
@@ -416,6 +416,7 @@ export default {
:disabled="!canSubmit"
variant="confirm"
category="primary"
+ data-testid="ciUpdateOrAddVariableBtn"
data-qa-selector="ci_variable_save_button"
@click="updateOrAddVariable"
>{{ modalActionText }}
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
index 12bc5ad3549..4cc00eb01d9 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
@@ -1,32 +1,9 @@
<script>
-import { mapState, mapActions } from 'vuex';
-import CiVariableModal from './ci_variable_modal.vue';
-import CiVariableTable from './ci_variable_table.vue';
-
-export default {
- components: {
- CiVariableModal,
- CiVariableTable,
- },
- computed: {
- ...mapState(['isGroup']),
- },
- mounted() {
- if (!this.isGroup) {
- this.fetchEnvironments();
- }
- },
- methods: {
- ...mapActions(['fetchEnvironments']),
- },
-};
+export default {};
</script>
<template>
<div class="row">
- <div class="col-lg-12">
- <ci-variable-table />
- <ci-variable-modal />
- </div>
+ <div class="col-lg-12"></div>
</div>
</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_environments_dropdown.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_environments_dropdown.vue
new file mode 100644
index 00000000000..ecb39f214ec
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/components/legacy_ci_environments_dropdown.vue
@@ -0,0 +1,81 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlDropdownDivider, GlSearchBoxByType } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
+import { __, sprintf } from '~/locale';
+
+export default {
+ name: 'CiEnvironmentsDropdown',
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlSearchBoxByType,
+ },
+ props: {
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ searchTerm: '',
+ };
+ },
+ computed: {
+ ...mapGetters(['joinedEnvironments']),
+ composedCreateButtonLabel() {
+ return sprintf(__('Create wildcard: %{searchTerm}'), { searchTerm: this.searchTerm });
+ },
+ shouldRenderCreateButton() {
+ return this.searchTerm && !this.joinedEnvironments.includes(this.searchTerm);
+ },
+ filteredResults() {
+ const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
+ return this.joinedEnvironments.filter((resultString) =>
+ resultString.toLowerCase().includes(lowerCasedSearchTerm),
+ );
+ },
+ },
+ methods: {
+ selectEnvironment(selected) {
+ this.$emit('selectEnvironment', selected);
+ this.searchTerm = '';
+ },
+ createClicked() {
+ this.$emit('createClicked', this.searchTerm);
+ this.searchTerm = '';
+ },
+ isSelected(env) {
+ return this.value === env;
+ },
+ clearSearch() {
+ this.searchTerm = '';
+ },
+ },
+};
+</script>
+<template>
+ <gl-dropdown :text="value" @show="clearSearch">
+ <gl-search-box-by-type v-model.trim="searchTerm" data-testid="ci-environment-search" />
+ <gl-dropdown-item
+ v-for="environment in filteredResults"
+ :key="environment"
+ :is-checked="isSelected(environment)"
+ is-check-item
+ @click="selectEnvironment(environment)"
+ >
+ {{ environment }}
+ </gl-dropdown-item>
+ <gl-dropdown-item v-if="!filteredResults.length" ref="noMatchingResults">{{
+ __('No matching results')
+ }}</gl-dropdown-item>
+ <template v-if="shouldRenderCreateButton">
+ <gl-dropdown-divider />
+ <gl-dropdown-item data-testid="create-wildcard-button" @click="createClicked">
+ {{ composedCreateButtonLabel }}
+ </gl-dropdown-item>
+ </template>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue
new file mode 100644
index 00000000000..7dcc5ce42d7
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue
@@ -0,0 +1,426 @@
+<script>
+import {
+ GlAlert,
+ GlButton,
+ GlCollapse,
+ GlFormCheckbox,
+ GlFormCombobox,
+ GlFormGroup,
+ GlFormSelect,
+ GlFormInput,
+ GlFormTextarea,
+ GlIcon,
+ GlLink,
+ GlModal,
+ GlSprintf,
+} from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+import { getCookie, setCookie } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
+import Tracking from '~/tracking';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { mapComputed } from '~/vuex_shared/bindings';
+import {
+ AWS_TOKEN_CONSTANTS,
+ ADD_CI_VARIABLE_MODAL_ID,
+ AWS_TIP_DISMISSED_COOKIE_NAME,
+ AWS_TIP_MESSAGE,
+ CONTAINS_VARIABLE_REFERENCE_MESSAGE,
+ ENVIRONMENT_SCOPE_LINK_TITLE,
+ EVENT_LABEL,
+ EVENT_ACTION,
+} from '../constants';
+import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
+import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
+
+const trackingMixin = Tracking.mixin({ label: EVENT_LABEL });
+
+export default {
+ modalId: ADD_CI_VARIABLE_MODAL_ID,
+ tokens: awsTokens,
+ tokenList: awsTokenList,
+ awsTipMessage: AWS_TIP_MESSAGE,
+ containsVariableReferenceMessage: CONTAINS_VARIABLE_REFERENCE_MESSAGE,
+ environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
+ components: {
+ CiEnvironmentsDropdown,
+ GlAlert,
+ GlButton,
+ GlCollapse,
+ GlFormCheckbox,
+ GlFormCombobox,
+ GlFormGroup,
+ GlFormSelect,
+ GlFormInput,
+ GlFormTextarea,
+ GlIcon,
+ GlLink,
+ GlModal,
+ GlSprintf,
+ },
+ mixins: [glFeatureFlagsMixin(), trackingMixin],
+ data() {
+ return {
+ isTipDismissed: getCookie(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true',
+ validationErrorEventProperty: '',
+ };
+ },
+ computed: {
+ ...mapState([
+ 'projectId',
+ 'environments',
+ 'typeOptions',
+ 'variable',
+ 'variableBeingEdited',
+ 'isGroup',
+ 'maskableRegex',
+ 'selectedEnvironment',
+ 'isProtectedByDefault',
+ 'awsLogoSvgPath',
+ 'awsTipDeployLink',
+ 'awsTipCommandsLink',
+ 'awsTipLearnLink',
+ 'containsVariableReferenceLink',
+ 'protectedEnvironmentVariablesLink',
+ 'maskedEnvironmentVariablesLink',
+ 'environmentScopeLink',
+ ]),
+ ...mapComputed(
+ [
+ { key: 'key', updateFn: 'updateVariableKey' },
+ { key: 'secret_value', updateFn: 'updateVariableValue' },
+ { key: 'variable_type', updateFn: 'updateVariableType' },
+ { key: 'environment_scope', updateFn: 'setEnvironmentScope' },
+ { key: 'protected_variable', updateFn: 'updateVariableProtected' },
+ { key: 'masked', updateFn: 'updateVariableMasked' },
+ ],
+ false,
+ 'variable',
+ ),
+ isTipVisible() {
+ return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
+ },
+ canSubmit() {
+ return (
+ this.variableValidationState &&
+ this.variable.key !== '' &&
+ this.variable.secret_value !== ''
+ );
+ },
+ canMask() {
+ const regex = RegExp(this.maskableRegex);
+ return regex.test(this.variable.secret_value);
+ },
+ containsVariableReference() {
+ const regex = /\$/;
+ return regex.test(this.variable.secret_value);
+ },
+ displayMaskedError() {
+ return !this.canMask && this.variable.masked;
+ },
+ maskedState() {
+ if (this.displayMaskedError) {
+ return false;
+ }
+ return true;
+ },
+ modalActionText() {
+ return this.variableBeingEdited ? __('Update variable') : __('Add variable');
+ },
+ maskedFeedback() {
+ return this.displayMaskedError ? __('This variable can not be masked.') : '';
+ },
+ tokenValidationFeedback() {
+ const tokenSpecificFeedback = this.$options.tokens?.[this.variable.key]?.invalidMessage;
+ if (!this.tokenValidationState && tokenSpecificFeedback) {
+ return tokenSpecificFeedback;
+ }
+ return '';
+ },
+ tokenValidationState() {
+ const validator = this.$options.tokens?.[this.variable.key]?.validation;
+
+ if (validator) {
+ return validator(this.variable.secret_value);
+ }
+
+ return true;
+ },
+ scopedVariablesAvailable() {
+ return !this.isGroup || this.glFeatures.groupScopedCiVariables;
+ },
+ variableValidationFeedback() {
+ return `${this.tokenValidationFeedback} ${this.maskedFeedback}`;
+ },
+ variableValidationState() {
+ return this.variable.secret_value === '' || (this.tokenValidationState && this.maskedState);
+ },
+ },
+ watch: {
+ variable: {
+ handler() {
+ this.trackVariableValidationErrors();
+ },
+ deep: true,
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'addVariable',
+ 'updateVariable',
+ 'resetEditing',
+ 'displayInputValue',
+ 'clearModal',
+ 'deleteVariable',
+ 'setEnvironmentScope',
+ 'addWildCardScope',
+ 'resetSelectedEnvironment',
+ 'setSelectedEnvironment',
+ 'setVariableProtected',
+ ]),
+ dismissTip() {
+ setCookie(AWS_TIP_DISMISSED_COOKIE_NAME, 'true', { expires: 90 });
+ this.isTipDismissed = true;
+ },
+ deleteVarAndClose() {
+ this.deleteVariable();
+ this.hideModal();
+ },
+ hideModal() {
+ this.$refs.modal.hide();
+ },
+ resetModalHandler() {
+ if (this.variableBeingEdited) {
+ this.resetEditing();
+ }
+
+ this.clearModal();
+ this.resetSelectedEnvironment();
+ this.resetValidationErrorEvents();
+ },
+ updateOrAddVariable() {
+ if (this.variableBeingEdited) {
+ this.updateVariable();
+ } else {
+ this.addVariable();
+ }
+ this.hideModal();
+ },
+ setVariableProtectedByDefault() {
+ if (this.isProtectedByDefault && !this.variableBeingEdited) {
+ this.setVariableProtected();
+ }
+ },
+ trackVariableValidationErrors() {
+ const property = this.getTrackingErrorProperty();
+ if (!this.validationErrorEventProperty && property) {
+ this.track(EVENT_ACTION, { property });
+ this.validationErrorEventProperty = property;
+ }
+ },
+ getTrackingErrorProperty() {
+ let property;
+ if (this.variable.secret_value?.length && !property) {
+ if (this.displayMaskedError && this.maskableRegex?.length) {
+ const supportedChars = this.maskableRegex.replace('^', '').replace(/{(\d,)}\$/, '');
+ const regex = new RegExp(supportedChars, 'g');
+ property = this.variable.secret_value.replace(regex, '');
+ }
+ if (this.containsVariableReference) {
+ property = '$';
+ }
+ }
+
+ return property;
+ },
+ resetValidationErrorEvents() {
+ this.validationErrorEventProperty = '';
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="modal"
+ :modal-id="$options.modalId"
+ :title="modalActionText"
+ static
+ lazy
+ @hidden="resetModalHandler"
+ @shown="setVariableProtectedByDefault"
+ >
+ <form>
+ <gl-form-combobox
+ v-model="key"
+ :token-list="$options.tokenList"
+ :label-text="__('Key')"
+ data-qa-selector="ci_variable_key_field"
+ />
+
+ <gl-form-group
+ :label="__('Value')"
+ label-for="ci-variable-value"
+ :state="variableValidationState"
+ :invalid-feedback="variableValidationFeedback"
+ >
+ <gl-form-textarea
+ id="ci-variable-value"
+ ref="valueField"
+ v-model="secret_value"
+ :state="variableValidationState"
+ rows="3"
+ max-rows="6"
+ data-qa-selector="ci_variable_value_field"
+ class="gl-font-monospace!"
+ />
+ </gl-form-group>
+
+ <div class="d-flex">
+ <gl-form-group :label="__('Type')" label-for="ci-variable-type" class="w-50 gl-mr-5">
+ <gl-form-select id="ci-variable-type" v-model="variable_type" :options="typeOptions" />
+ </gl-form-group>
+
+ <gl-form-group label-for="ci-variable-env" class="w-50" data-testid="environment-scope">
+ <template #label>
+ {{ __('Environment scope') }}
+ <gl-link
+ :title="$options.environmentScopeLinkTitle"
+ :href="environmentScopeLink"
+ target="_blank"
+ data-testid="environment-scope-link"
+ >
+ <gl-icon name="question" :size="12" />
+ </gl-link>
+ </template>
+ <ci-environments-dropdown
+ v-if="scopedVariablesAvailable"
+ class="w-100"
+ :value="environment_scope"
+ @selectEnvironment="setEnvironmentScope"
+ @createClicked="addWildCardScope"
+ />
+
+ <gl-form-input v-else v-model="environment_scope" class="w-100" readonly />
+ </gl-form-group>
+ </div>
+
+ <gl-form-group :label="__('Flags')" label-for="ci-variable-flags">
+ <gl-form-checkbox
+ v-model="protected_variable"
+ class="mb-0"
+ data-testid="ci-variable-protected-checkbox"
+ >
+ {{ __('Protect variable') }}
+ <gl-link target="_blank" :href="protectedEnvironmentVariablesLink">
+ <gl-icon name="question" :size="12" />
+ </gl-link>
+ <p class="gl-mt-2 text-secondary">
+ {{ __('Export variable to pipelines running on protected branches and tags only.') }}
+ </p>
+ </gl-form-checkbox>
+
+ <gl-form-checkbox
+ ref="masked-ci-variable"
+ v-model="masked"
+ data-testid="ci-variable-masked-checkbox"
+ >
+ {{ __('Mask variable') }}
+ <gl-link target="_blank" :href="maskedEnvironmentVariablesLink">
+ <gl-icon name="question" :size="12" />
+ </gl-link>
+ <p class="gl-mt-2 gl-mb-0 text-secondary">
+ {{ __('Variable will be masked in job logs.') }}
+ <span
+ :class="{
+ 'bold text-plain': displayMaskedError,
+ }"
+ >
+ {{ __('Requires values to meet regular expression requirements.') }}</span
+ >
+ <gl-link target="_blank" :href="maskedEnvironmentVariablesLink">{{
+ __('More information')
+ }}</gl-link>
+ </p>
+ </gl-form-checkbox>
+ </gl-form-group>
+ </form>
+ <gl-collapse :visible="isTipVisible">
+ <gl-alert
+ :title="__('Deploying to AWS is easy with GitLab')"
+ variant="tip"
+ data-testid="aws-guidance-tip"
+ @dismiss="dismissTip"
+ >
+ <div class="gl-display-flex gl-flex-direction-row">
+ <div>
+ <p>
+ <gl-sprintf :message="$options.awsTipMessage">
+ <template #deployLink="{ content }">
+ <gl-link :href="awsTipDeployLink" target="_blank">{{ content }}</gl-link>
+ </template>
+ <template #commandsLink="{ content }">
+ <gl-link :href="awsTipCommandsLink" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <p>
+ <gl-button
+ :href="awsTipLearnLink"
+ target="_blank"
+ category="secondary"
+ variant="info"
+ class="gl-overflow-wrap-break"
+ >{{ __('Learn more about deploying to AWS') }}</gl-button
+ >
+ </p>
+ </div>
+ <img
+ class="gl-mt-3"
+ :alt="__('Amazon Web Services Logo')"
+ :src="awsLogoSvgPath"
+ height="32"
+ />
+ </div>
+ </gl-alert>
+ </gl-collapse>
+ <gl-alert
+ v-if="containsVariableReference"
+ :title="__('Value might 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
+ v-if="variableBeingEdited"
+ ref="deleteCiVariable"
+ variant="danger"
+ category="secondary"
+ data-qa-selector="ci_variable_delete_button"
+ @click="deleteVarAndClose"
+ >{{ __('Delete variable') }}</gl-button
+ >
+ <gl-button
+ ref="updateOrAddVariable"
+ :disabled="!canSubmit"
+ variant="confirm"
+ category="primary"
+ data-testid="ciUpdateOrAddVariableBtn"
+ data-qa-selector="ci_variable_save_button"
+ @click="updateOrAddVariable"
+ >{{ modalActionText }}
+ </gl-button>
+ </template>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue
new file mode 100644
index 00000000000..9acc9fbffb6
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue
@@ -0,0 +1,32 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import LegacyCiVariableModal from './legacy_ci_variable_modal.vue';
+import LegacyCiVariableTable from './legacy_ci_variable_table.vue';
+
+export default {
+ components: {
+ LegacyCiVariableModal,
+ LegacyCiVariableTable,
+ },
+ computed: {
+ ...mapState(['isGroup']),
+ },
+ mounted() {
+ if (!this.isGroup) {
+ this.fetchEnvironments();
+ }
+ },
+ methods: {
+ ...mapActions(['fetchEnvironments']),
+ },
+};
+</script>
+
+<template>
+ <div class="row">
+ <div class="col-lg-12">
+ <legacy-ci-variable-table />
+ <legacy-ci-variable-modal />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue
new file mode 100644
index 00000000000..f078234829a
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue
@@ -0,0 +1,199 @@
+<script>
+import { GlTable, GlButton, GlModalDirective, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import { s__, __ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
+import { ADD_CI_VARIABLE_MODAL_ID } from '../constants';
+import CiVariablePopover from './ci_variable_popover.vue';
+
+export default {
+ modalId: ADD_CI_VARIABLE_MODAL_ID,
+ trueIcon: 'mobile-issue-close',
+ falseIcon: 'close',
+ iconSize: 16,
+ fields: [
+ {
+ key: 'variable_type',
+ label: s__('CiVariables|Type'),
+ customStyle: { width: '70px' },
+ },
+ {
+ key: 'key',
+ label: s__('CiVariables|Key'),
+ tdClass: 'text-plain',
+ sortable: true,
+ customStyle: { width: '40%' },
+ },
+ {
+ key: 'value',
+ label: s__('CiVariables|Value'),
+ customStyle: { width: '40%' },
+ },
+ {
+ key: 'protected',
+ label: s__('CiVariables|Protected'),
+ customStyle: { width: '100px' },
+ },
+ {
+ key: 'masked',
+ label: s__('CiVariables|Masked'),
+ customStyle: { width: '100px' },
+ },
+ {
+ key: 'environment_scope',
+ label: s__('CiVariables|Environments'),
+ customStyle: { width: '20%' },
+ },
+ {
+ key: 'actions',
+ label: '',
+ tdClass: 'text-right',
+ customStyle: { width: '35px' },
+ },
+ ],
+ components: {
+ CiVariablePopover,
+ GlButton,
+ GlIcon,
+ GlTable,
+ TooltipOnTruncate,
+ },
+ directives: {
+ GlModalDirective,
+ GlTooltip: GlTooltipDirective,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ computed: {
+ ...mapState(['variables', 'valuesHidden', 'isLoading', 'isDeleting']),
+ valuesButtonText() {
+ return this.valuesHidden ? __('Reveal values') : __('Hide values');
+ },
+ isTableEmpty() {
+ return !this.variables || this.variables.length === 0;
+ },
+ fields() {
+ return this.$options.fields;
+ },
+ },
+ mounted() {
+ this.fetchVariables();
+ },
+ methods: {
+ ...mapActions(['fetchVariables', 'toggleValues', 'editVariable']),
+ },
+};
+</script>
+
+<template>
+ <div class="ci-variable-table" data-testid="ci-variable-table">
+ <gl-table
+ :fields="fields"
+ :items="variables"
+ tbody-tr-class="js-ci-variable-row"
+ data-qa-selector="ci_variable_table_content"
+ sort-by="key"
+ sort-direction="asc"
+ stacked="lg"
+ table-class="text-secondary"
+ fixed
+ show-empty
+ sort-icon-left
+ no-sort-reset
+ >
+ <template #table-colgroup="scope">
+ <col v-for="field in scope.fields" :key="field.key" :style="field.customStyle" />
+ </template>
+ <template #cell(key)="{ item }">
+ <div class="gl-display-flex gl-align-items-center">
+ <tooltip-on-truncate :title="item.key" truncate-target="child">
+ <span
+ :id="`ci-variable-key-${item.id}`"
+ class="gl-display-inline-block gl-max-w-full gl-text-truncate"
+ >{{ item.key }}</span
+ >
+ </tooltip-on-truncate>
+ <gl-button
+ v-gl-tooltip
+ category="tertiary"
+ icon="copy-to-clipboard"
+ :title="__('Copy key')"
+ :data-clipboard-text="item.key"
+ :aria-label="__('Copy to clipboard')"
+ />
+ </div>
+ </template>
+ <template #cell(value)="{ item }">
+ <div class="gl-display-flex gl-align-items-center">
+ <span v-if="valuesHidden">*********************</span>
+ <span
+ v-else
+ :id="`ci-variable-value-${item.id}`"
+ class="gl-display-inline-block gl-max-w-full gl-text-truncate"
+ >{{ item.value }}</span
+ >
+ <gl-button
+ v-gl-tooltip
+ category="tertiary"
+ icon="copy-to-clipboard"
+ :title="__('Copy value')"
+ :data-clipboard-text="item.value"
+ :aria-label="__('Copy to clipboard')"
+ />
+ </div>
+ </template>
+ <template #cell(protected)="{ item }">
+ <gl-icon v-if="item.protected" :size="$options.iconSize" :name="$options.trueIcon" />
+ <gl-icon v-else :size="$options.iconSize" :name="$options.falseIcon" />
+ </template>
+ <template #cell(masked)="{ item }">
+ <gl-icon v-if="item.masked" :size="$options.iconSize" :name="$options.trueIcon" />
+ <gl-icon v-else :size="$options.iconSize" :name="$options.falseIcon" />
+ </template>
+ <template #cell(environment_scope)="{ item }">
+ <div class="gl-display-flex">
+ <span
+ :id="`ci-variable-env-${item.id}`"
+ class="gl-display-inline-block gl-max-w-full gl-text-truncate"
+ >{{ item.environment_scope }}</span
+ >
+ <ci-variable-popover
+ :target="`ci-variable-env-${item.id}`"
+ :value="item.environment_scope"
+ :tooltip-text="__('Copy environment')"
+ />
+ </div>
+ </template>
+ <template #cell(actions)="{ item }">
+ <gl-button
+ v-gl-modal-directive="$options.modalId"
+ icon="pencil"
+ :aria-label="__('Edit')"
+ data-qa-selector="edit_ci_variable_button"
+ @click="editVariable(item)"
+ />
+ </template>
+ <template #empty>
+ <p class="gl-text-center gl-py-6 gl-text-black-normal gl-mb-0">
+ {{ __('There are no variables yet.') }}
+ </p>
+ </template>
+ </gl-table>
+ <div class="ci-variable-actions gl-display-flex gl-mt-5">
+ <gl-button
+ v-gl-modal-directive="$options.modalId"
+ class="gl-mr-3"
+ data-qa-selector="add_ci_variable_button"
+ variant="confirm"
+ category="primary"
+ >{{ __('Add variable') }}</gl-button
+ >
+ <gl-button
+ v-if="!isTableEmpty"
+ data-qa-selector="reveal_ci_variable_value_button"
+ @click="toggleValues(!valuesHidden)"
+ >{{ valuesButtonText }}</gl-button
+ >
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci_variable_list/index.js b/app/assets/javascripts/ci_variable_list/index.js
index f771751194c..2b54af6a2a4 100644
--- a/app/assets/javascripts/ci_variable_list/index.js
+++ b/app/assets/javascripts/ci_variable_list/index.js
@@ -1,10 +1,63 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import CiVariableSettings from './components/ci_variable_settings.vue';
+import LegacyCiVariableSettings from './components/legacy_ci_variable_settings.vue';
import createStore from './store';
const mountCiVariableListApp = (containerEl) => {
const {
+ awsLogoSvgPath,
+ awsTipCommandsLink,
+ awsTipDeployLink,
+ awsTipLearnLink,
+ containsVariableReferenceLink,
+ environmentScopeLink,
+ group,
+ maskedEnvironmentVariablesLink,
+ maskableRegex,
+ projectFullPath,
+ projectId,
+ protectedByDefault,
+ protectedEnvironmentVariablesLink,
+ } = containerEl.dataset;
+
+ const isGroup = parseBoolean(group);
+ const isProtectedByDefault = parseBoolean(protectedByDefault);
+
+ Vue.use(VueApollo);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ return new Vue({
+ el: containerEl,
+ apolloProvider,
+ provide: {
+ awsLogoSvgPath,
+ awsTipCommandsLink,
+ awsTipDeployLink,
+ awsTipLearnLink,
+ containsVariableReferenceLink,
+ environmentScopeLink,
+ isGroup,
+ isProtectedByDefault,
+ maskedEnvironmentVariablesLink,
+ maskableRegex,
+ projectFullPath,
+ projectId,
+ protectedEnvironmentVariablesLink,
+ },
+ render(createElement) {
+ return createElement(CiVariableSettings);
+ },
+ });
+};
+
+const mountLegacyCiVariableListApp = (containerEl) => {
+ const {
endpoint,
projectId,
group,
@@ -42,7 +95,7 @@ const mountCiVariableListApp = (containerEl) => {
el: containerEl,
store,
render(createElement) {
- return createElement(CiVariableSettings);
+ return createElement(LegacyCiVariableSettings);
},
});
};
@@ -50,5 +103,11 @@ const mountCiVariableListApp = (containerEl) => {
export default (containerId = 'js-ci-project-variables') => {
const el = document.getElementById(containerId);
- return !el ? {} : mountCiVariableListApp(el);
+ if (el) {
+ if (gon.features?.ciVariableSettingsGraphql) {
+ mountCiVariableListApp(el);
+ } else {
+ mountLegacyCiVariableListApp(el);
+ }
+ }
};
diff --git a/app/assets/javascripts/clusters/agents/components/create_token_button.vue b/app/assets/javascripts/clusters/agents/components/create_token_button.vue
index 3e1a8994fb8..74155d7819a 100644
--- a/app/assets/javascripts/clusters/agents/components/create_token_button.vue
+++ b/app/assets/javascripts/clusters/agents/components/create_token_button.vue
@@ -205,7 +205,12 @@ export default {
</gl-form-group>
</template>
- <agent-token v-else :agent-token="agentToken" :modal-id="$options.modalId" />
+ <agent-token
+ v-else
+ :agent-name="agentName"
+ :agent-token="agentToken"
+ :modal-id="$options.modalId"
+ />
<template #modal-footer>
<gl-button
diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
index dca89133931..8a997624a36 100644
--- a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
+++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
@@ -2,29 +2,9 @@
import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { s__ } from '~/locale';
-import SplitButton from '~/vue_shared/components/split_button.vue';
-
-const splitButtonActionItems = [
- {
- title: s__('ClusterIntegration|Remove integration and resources'),
- description: s__(
- 'ClusterIntegration|Deletes all GitLab resources attached to this cluster during removal',
- ),
- eventName: 'remove-cluster-and-cleanup',
- },
- {
- title: s__('ClusterIntegration|Remove integration'),
- description: s__(
- 'ClusterIntegration|Removes cluster from project but keeps associated resources',
- ),
- eventName: 'remove-cluster',
- },
-];
export default {
- splitButtonActionItems,
components: {
- SplitButton,
GlModal,
GlButton,
GlFormInput,
@@ -79,6 +59,9 @@ export default {
canCleanupResources() {
return !this.hasManagementProject;
},
+ buttonCategory() {
+ return !this.hasManagementProject ? 'secondary' : 'primary';
+ },
},
methods: {
handleClickRemoveCluster(cleanup = false) {
@@ -99,19 +82,20 @@ export default {
</script>
<template>
- <div class="gl-display-flex gl-justify-content-end">
- <split-button
+ <div class="gl-display-flex">
+ <gl-button
v-if="canCleanupResources"
- :action-items="$options.splitButtonActionItems"
- menu-class="dropdown-menu-large"
+ data-testid="remove-integration-and-resources-button"
+ class="gl-mr-3"
variant="danger"
- @remove-cluster="handleClickRemoveCluster(false)"
- @remove-cluster-and-cleanup="handleClickRemoveCluster(true)"
- />
+ @click="handleClickRemoveCluster(true)"
+ >
+ {{ s__('ClusterIntegration|Remove integration and resources') }}
+ </gl-button>
<gl-button
- v-else
+ data-testid="remove-integration-button"
+ :category="buttonCategory"
variant="danger"
- data-testid="btnRemove"
@click="handleClickRemoveCluster(false)"
>
{{ s__('ClusterIntegration|Remove integration') }}
@@ -163,13 +147,7 @@ export default {
<template v-if="confirmCleanup">
<gl-button
:disabled="!canSubmit"
- variant="warning"
- category="primary"
- @click="handleSubmit"
- >{{ s__('ClusterIntegration|Remove integration') }}</gl-button
- >
- <gl-button
- :disabled="!canSubmit"
+ data-testid="remove-integration-and-resources-modal-button"
variant="danger"
category="primary"
@click="handleSubmit(true)"
@@ -179,6 +157,7 @@ export default {
<template v-else>
<gl-button
:disabled="!canSubmit"
+ data-testid="remove-integration-modal-button"
variant="danger"
category="primary"
@click="handleSubmit"
diff --git a/app/assets/javascripts/clusters_list/clusters_util.js b/app/assets/javascripts/clusters_list/clusters_util.js
index 9eb01f593f5..e2d01723dde 100644
--- a/app/assets/javascripts/clusters_list/clusters_util.js
+++ b/app/assets/javascripts/clusters_list/clusters_util.js
@@ -1,12 +1,12 @@
-export function generateAgentRegistrationCommand(agentToken, kasAddress, kasVersion) {
+export function generateAgentRegistrationCommand({ name, token, version, address }) {
return `helm repo add gitlab https://charts.gitlab.io
helm repo update
-helm upgrade --install gitlab-agent gitlab/gitlab-agent \\
+helm upgrade --install ${name} gitlab/gitlab-agent \\
--namespace gitlab-agent \\
--create-namespace \\
- --set image.tag=v${kasVersion} \\
- --set config.token=${agentToken} \\
- --set config.kasAddress=${kasAddress}`;
+ --set image.tag=v${version} \\
+ --set config.token=${token} \\
+ --set config.kasAddress=${address}`;
}
export function getAgentConfigPath(clusterAgentName) {
diff --git a/app/assets/javascripts/clusters_list/components/agent_token.vue b/app/assets/javascripts/clusters_list/components/agent_token.vue
index 1597fcb9914..4dd6d84566c 100644
--- a/app/assets/javascripts/clusters_list/components/agent_token.vue
+++ b/app/assets/javascripts/clusters_list/components/agent_token.vue
@@ -21,6 +21,10 @@ export default {
},
inject: ['kasAddress', 'kasVersion'],
props: {
+ agentName: {
+ required: true,
+ type: String,
+ },
agentToken: {
required: true,
type: String,
@@ -32,7 +36,12 @@ export default {
},
computed: {
agentRegistrationCommand() {
- return generateAgentRegistrationCommand(this.agentToken, this.kasAddress, this.kasVersion);
+ return generateAgentRegistrationCommand({
+ name: this.agentName,
+ token: this.agentToken,
+ version: this.kasVersion,
+ address: this.kasAddress,
+ });
},
},
};
diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue
index fb3c8ff66b0..1ea5eff35d4 100644
--- a/app/assets/javascripts/clusters_list/components/clusters.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters.vue
@@ -4,7 +4,7 @@ import {
GlLink,
GlLoadingIcon,
GlPagination,
- GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+ GlSkeletonLoader,
GlSprintf,
GlTableLite,
GlTooltipDirective,
@@ -25,7 +25,7 @@ export default {
GlLink,
GlLoadingIcon,
GlPagination,
- GlSkeletonLoading,
+ GlSkeletonLoader,
GlSprintf,
GlTableLite,
NodeErrorHelpText,
@@ -267,7 +267,7 @@ export default {
<template #cell(node_size)="{ item }">
<span v-if="item.nodes">{{ item.nodes.length }}</span>
- <gl-skeleton-loading v-else-if="loadingNodes" :lines="1" :class="contentAlignClasses" />
+ <gl-skeleton-loader v-else-if="loadingNodes" :lines="1" :class="contentAlignClasses" />
<node-error-help-text
v-else-if="item.kubernetes_errors"
@@ -288,7 +288,7 @@ export default {
</gl-sprintf>
</span>
- <gl-skeleton-loading v-else-if="loadingNodes" :lines="1" :class="contentAlignClasses" />
+ <gl-skeleton-loader v-else-if="loadingNodes" :lines="1" :class="contentAlignClasses" />
<node-error-help-text
v-else-if="item.kubernetes_errors"
@@ -309,7 +309,7 @@ export default {
</gl-sprintf>
</span>
- <gl-skeleton-loading v-else-if="loadingNodes" :lines="1" :class="contentAlignClasses" />
+ <gl-skeleton-loader v-else-if="loadingNodes" :lines="1" :class="contentAlignClasses" />
<node-error-help-text
v-else-if="item.kubernetes_errors"
diff --git a/app/assets/javascripts/clusters_list/components/install_agent_modal.vue b/app/assets/javascripts/clusters_list/components/install_agent_modal.vue
index 3b39c3aac45..444b9ac2a14 100644
--- a/app/assets/javascripts/clusters_list/components/install_agent_modal.vue
+++ b/app/assets/javascripts/clusters_list/components/install_agent_modal.vue
@@ -268,7 +268,12 @@ export default {
</p>
</template>
- <agent-token v-else :agent-token="agentToken" :modal-id="$options.modalId" />
+ <agent-token
+ v-else
+ :agent-name="agentName"
+ :agent-token="agentToken"
+ :modal-id="$options.modalId"
+ />
</template>
<template v-else>
diff --git a/app/assets/javascripts/code_navigation/utils/index.js b/app/assets/javascripts/code_navigation/utils/index.js
index 0d72153d8fe..46038df2f86 100644
--- a/app/assets/javascripts/code_navigation/utils/index.js
+++ b/app/assets/javascripts/code_navigation/utils/index.js
@@ -32,8 +32,8 @@ export const addInteractionClass = ({ path, d, wrapTextNodes }) => {
});
if (el && !isTextNode(el)) {
- el.setAttribute('data-char-index', d.start_char);
- el.setAttribute('data-line-index', d.start_line);
+ el.dataset.charIndex = d.start_char;
+ el.dataset.lineIndex = d.start_line;
el.classList.add('cursor-pointer', 'code-navigation', 'js-code-navigation');
el.closest('.line').classList.add('code-navigation-line');
}
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 29530ddb7a2..4ff49433749 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -9,6 +9,7 @@ import PipelinesService from '~/pipelines/services/pipelines_service';
import PipelineStore from '~/pipelines/stores/pipelines_store';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { s__, __ } from '~/locale';
export default {
PipelineKeyOptions,
@@ -170,6 +171,20 @@ export default {
}
},
},
+ modal: {
+ actionPrimary: {
+ text: s__('Pipeline|Run pipeline'),
+ attributes: {
+ variant: 'danger',
+ },
+ },
+ actionCancel: {
+ text: __('Cancel'),
+ attributes: {
+ variant: 'default',
+ },
+ },
+ },
};
</script>
<template>
@@ -229,9 +244,9 @@ export default {
ref="modal"
:modal-id="modalId"
:title="s__('Pipelines|Are you sure you want to run this pipeline?')"
- :ok-title="s__('Pipeline|Run pipeline')"
- ok-variant="danger"
- @ok="onClickRunPipeline"
+ :action-primary="$options.modal.actionPrimary"
+ :action-cancel="$options.modal.actionCancel"
+ @primary="onClickRunPipeline"
>
<p>
{{
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue b/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue
index 518ddd7a09c..6c0ac8e54d2 100644
--- a/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue
@@ -1,5 +1,8 @@
<script>
import {
+ GlDropdownForm,
+ GlFormInput,
+ GlDropdownDivider,
GlButton,
GlButtonGroup,
GlDropdown,
@@ -20,23 +23,32 @@ const CODE_BLOCK_NODE_TYPES = [CodeBlockHighlight.name, Diagram.name, Frontmatte
export default {
components: {
BubbleMenu,
+ GlDropdownForm,
+ GlFormInput,
GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
+ GlDropdownDivider,
GlSearchBoxByType,
EditorStateObserver,
},
directives: {
GlTooltip,
},
- inject: ['tiptapEditor'],
+ inject: ['tiptapEditor', 'contentEditor'],
data() {
return {
codeBlockType: undefined,
- selectedLanguage: {},
filterTerm: '',
filteredLanguages: [],
+
+ showCustomLanguageInput: false,
+ customLanguageType: '',
+
+ selectedLanguage: {},
+ isDiagram: false,
+ showPreview: false,
};
},
watch: {
@@ -52,24 +64,39 @@ export default {
return CODE_BLOCK_NODE_TYPES.some((type) => editor.isActive(type));
},
- updateSelectedLanguage() {
+ async updateCodeBlockInfoToState() {
this.codeBlockType = CODE_BLOCK_NODE_TYPES.find((type) => this.tiptapEditor.isActive(type));
- if (this.codeBlockType) {
- const { language } = this.tiptapEditor.getAttributes(this.codeBlockType);
- this.selectedLanguage = codeBlockLanguageLoader.findLanguageBySyntax(language);
- }
+ if (!this.codeBlockType) return;
+
+ const { language, isDiagram, showPreview } = this.tiptapEditor.getAttributes(
+ this.codeBlockType,
+ );
+ this.selectedLanguage = codeBlockLanguageLoader.findOrCreateLanguageBySyntax(
+ language,
+ isDiagram,
+ );
+ this.isDiagram = isDiagram;
+ this.showPreview = showPreview;
},
- copyCodeBlockText() {
+ getCodeBlockText() {
const { view } = this.tiptapEditor;
const { from } = this.tiptapEditor.state.selection;
const node = getParentByTagName(view.domAtPos(from).node, 'pre');
+ return node?.textContent || '';
+ },
- navigator.clipboard.writeText(node?.textContent || '');
+ copyCodeBlockText() {
+ navigator.clipboard.writeText(this.getCodeBlockText());
},
- async applySelectedLanguage(language) {
+ togglePreview() {
+ this.showPreview = !this.showPreview;
+ this.tiptapEditor.commands.updateAttributes(Diagram.name, { showPreview: this.showPreview });
+ },
+
+ async applyLanguage(language) {
this.selectedLanguage = language;
await codeBlockLanguageLoader.loadLanguage(language.syntax);
@@ -77,6 +104,21 @@ export default {
this.tiptapEditor.commands.setCodeBlock({ language: this.selectedLanguage.syntax });
},
+ clearCustomLanguageForm() {
+ this.showCustomLanguageInput = false;
+ this.customLanguageType = '';
+ },
+
+ applyCustomLanguage() {
+ this.showCustomLanguageInput = false;
+
+ const language = codeBlockLanguageLoader.findOrCreateLanguageBySyntax(
+ this.customLanguageType,
+ );
+
+ this.applyLanguage(language);
+ },
+
getReferenceClientRect() {
const { view } = this.tiptapEditor;
const { from } = this.tiptapEditor.state.selection;
@@ -101,15 +143,36 @@ export default {
getReferenceClientRect,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
>
- <editor-state-observer @transaction="updateSelectedLanguage">
+ <editor-state-observer @transaction="updateCodeBlockInfoToState">
<gl-button-group>
<gl-dropdown
category="tertiary"
contenteditable="false"
boundary="viewport"
:text="selectedLanguage.label"
+ @hide="clearCustomLanguageForm"
>
- <template #header>
+ <template v-if="showCustomLanguageInput" #header>
+ <div class="gl-relative">
+ <gl-button
+ v-gl-tooltip
+ class="gl-absolute gl-mt-n3 gl-ml-2"
+ variant="default"
+ category="tertiary"
+ size="medium"
+ :aria-label="__('Go back')"
+ :title="__('Go back')"
+ icon="arrow-left"
+ @click.prevent.stop="showCustomLanguageInput = false"
+ />
+ <p
+ class="gl-text-center gl-new-dropdown-header-top gl-mb-0! gl-border-none! gl-pb-1!"
+ >
+ {{ __('Create custom type') }}
+ </p>
+ </div>
+ </template>
+ <template v-else #header>
<gl-search-box-by-type
v-model="filterTerm"
:clear-button-title="__('Clear')"
@@ -117,20 +180,59 @@ export default {
/>
</template>
- <template #highlighted-items>
+ <template v-if="!showCustomLanguageInput" #highlighted-items>
<gl-dropdown-item :key="selectedLanguage.syntax" is-check-item :is-checked="true">
{{ selectedLanguage.label }}
</gl-dropdown-item>
</template>
- <gl-dropdown-item
- v-for="language in filteredLanguages"
- v-show="selectedLanguage.syntax !== language.syntax"
- :key="language.syntax"
- @click="applySelectedLanguage(language)"
- >
- {{ language.label }}
- </gl-dropdown-item>
+ <template v-if="!showCustomLanguageInput" #default>
+ <gl-dropdown-item
+ v-for="language in filteredLanguages"
+ v-show="selectedLanguage.syntax !== language.syntax"
+ :key="language.syntax"
+ @click="applyLanguage(language)"
+ >
+ {{ language.label }}
+ </gl-dropdown-item>
+ </template>
+ <template v-else #default>
+ <gl-dropdown-form @submit.prevent="applyCustomLanguage">
+ <div class="gl-mx-4 gl-mt-2 gl-mb-3">
+ <gl-form-input v-model="customLanguageType" :placeholder="__('Language type')" />
+ </div>
+ <gl-dropdown-divider />
+ <div class="gl-mx-4 gl-mt-3 gl-display-flex gl-justify-content-end">
+ <gl-button
+ variant="default"
+ size="medium"
+ category="primary"
+ class="gl-mr-2 gl-w-auto!"
+ @click.prevent.stop="showCustomLanguageInput = false"
+ >
+ {{ __('Cancel') }}
+ </gl-button>
+ <gl-button
+ variant="confirm"
+ size="medium"
+ category="primary"
+ type="submit"
+ class="gl-w-auto!"
+ >
+ {{ __('Apply') }}
+ </gl-button>
+ </div>
+ </gl-dropdown-form>
+ </template>
+
+ <template v-if="!showCustomLanguageInput" #footer>
+ <gl-dropdown-item
+ data-testid="create-custom-type"
+ @click.capture.native.stop="showCustomLanguageInput = true"
+ >
+ {{ __('Create custom type') }}
+ </gl-dropdown-item>
+ </template>
</gl-dropdown>
<gl-button
v-gl-tooltip
@@ -144,6 +246,19 @@ export default {
@click="copyCodeBlockText"
/>
<gl-button
+ v-if="isDiagram"
+ v-gl-tooltip
+ variant="default"
+ category="tertiary"
+ size="medium"
+ :class="{ active: showPreview }"
+ data-testid="preview-diagram"
+ :aria-label="__('Preview diagram')"
+ :title="__('Preview diagram')"
+ icon="eye"
+ @click="togglePreview"
+ />
+ <gl-button
v-gl-tooltip
variant="default"
category="tertiary"
diff --git a/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue b/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue
new file mode 100644
index 00000000000..ecde593147c
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ directives: {
+ GlTooltip,
+ },
+ inject: ['tiptapEditor'],
+ methods: {
+ execute(contentType, attrs) {
+ this.tiptapEditor.chain().focus().setNode(contentType, attrs).run();
+
+ this.$emit('execute', { contentType });
+ },
+ },
+};
+</script>
+<template>
+ <gl-dropdown size="small" category="tertiary" icon="plus">
+ <gl-dropdown-item @click="execute('diagram', { language: 'mermaid' })">
+ {{ __('Mermaid diagram') }}
+ </gl-dropdown-item>
+ <gl-dropdown-item @click="execute('diagram', { language: 'plantuml' })">
+ {{ __('PlantUML diagram') }}
+ </gl-dropdown-item>
+ <gl-dropdown-item @click="execute('horizontalRule')">
+ {{ __('Horizontal rule') }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/top_toolbar.vue
index 19e150a4da9..b652e634b0c 100644
--- a/app/assets/javascripts/content_editor/components/top_toolbar.vue
+++ b/app/assets/javascripts/content_editor/components/top_toolbar.vue
@@ -5,6 +5,7 @@ import ToolbarImageButton from './toolbar_image_button.vue';
import ToolbarLinkButton from './toolbar_link_button.vue';
import ToolbarTableButton from './toolbar_table_button.vue';
import ToolbarTextStyleDropdown from './toolbar_text_style_dropdown.vue';
+import ToolbarMoreDropdown from './toolbar_more_dropdown.vue';
export default {
components: {
@@ -13,6 +14,7 @@ export default {
ToolbarLinkButton,
ToolbarTableButton,
ToolbarImageButton,
+ ToolbarMoreDropdown,
},
methods: {
trackToolbarControlExecution({ contentType, value }) {
@@ -117,16 +119,8 @@ export default {
:label="__('Add a collapsible section')"
@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')"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-table-button @execute="trackToolbarControlExecution" />
+ <toolbar-table-button data-testid="table" @execute="trackToolbarControlExecution" />
+ <toolbar-more-dropdown data-testid="more" @execute="trackToolbarControlExecution" />
</div>
</template>
<style>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/code_block.vue b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
index 1390b9b2daf..81f9b1f0af5 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
@@ -1,15 +1,26 @@
<script>
+import { debounce } from 'lodash';
import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
import { __ } from '~/locale';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import SandboxedMermaid from '~/behaviors/components/sandboxed_mermaid.vue';
import codeBlockLanguageLoader from '../../services/code_block_language_loader';
+import EditorStateObserver from '../editor_state_observer.vue';
export default {
name: 'CodeBlock',
components: {
NodeViewWrapper,
NodeViewContent,
+ EditorStateObserver,
+ SandboxedMermaid,
},
+ inject: ['contentEditor'],
props: {
+ editor: {
+ type: Object,
+ required: true,
+ },
node: {
type: Object,
required: true,
@@ -18,27 +29,75 @@ export default {
type: Function,
required: true,
},
+ selected: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ diagramUrl: '',
+ diagramSource: '',
+ };
},
async mounted() {
- const lang = codeBlockLanguageLoader.findLanguageBySyntax(this.node.attrs.language);
+ this.updateDiagramPreview = debounce(
+ this.updateDiagramPreview,
+ DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
+ );
+
+ const lang = codeBlockLanguageLoader.findOrCreateLanguageBySyntax(this.node.attrs.language);
await codeBlockLanguageLoader.loadLanguage(lang.syntax);
this.updateAttributes({ language: this.node.attrs.language });
},
+ methods: {
+ async updateDiagramPreview() {
+ if (!this.node.attrs.showPreview) {
+ this.diagramSource = '';
+ return;
+ }
+
+ if (!this.editor.isActive('diagram')) return;
+
+ this.diagramSource = this.$refs.nodeViewContent.$el.textContent;
+
+ if (this.node.attrs.language !== 'mermaid') {
+ this.diagramUrl = await this.contentEditor.renderDiagram(
+ this.diagramSource,
+ this.node.attrs.language,
+ );
+ }
+ },
+ },
i18n: {
frontmatter: __('frontmatter'),
},
+ userColorScheme: gon.user_color_scheme,
};
</script>
<template>
- <node-view-wrapper class="content-editor-code-block gl-relative code highlight" as="pre">
- <span
- v-if="node.attrs.isFrontmatter"
- data-testid="frontmatter-label"
- class="gl-absolute gl-top-0 gl-right-3"
- contenteditable="false"
- >{{ $options.i18n.frontmatter }}:{{ node.attrs.language }}</span
+ <editor-state-observer @transaction="updateDiagramPreview">
+ <node-view-wrapper
+ :class="`content-editor-code-block gl-relative code highlight ${$options.userColorScheme}`"
+ as="pre"
>
- <node-view-content as="code" />
- </node-view-wrapper>
+ <div
+ v-if="node.attrs.showPreview"
+ class="gl-mt-n3! gl-ml-n4! gl-mr-n4! gl-mb-3 gl-bg-white! gl-p-4 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
+ >
+ <sandboxed-mermaid v-if="node.attrs.language === 'mermaid'" :source="diagramSource" />
+ <img v-else ref="diagramContainer" :src="diagramUrl" />
+ </div>
+ <span
+ v-if="node.attrs.isFrontmatter"
+ data-testid="frontmatter-label"
+ class="gl-absolute gl-top-0 gl-right-3"
+ contenteditable="false"
+ >{{ $options.i18n.frontmatter }}:{{ node.attrs.language }}</span
+ >
+ <node-view-content ref="nodeViewContent" as="code" />
+ </node-view-wrapper>
+ </editor-state-observer>
</template>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/footnote_definition.vue b/app/assets/javascripts/content_editor/components/wrappers/footnote_definition.vue
new file mode 100644
index 00000000000..8b7b02605f7
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/wrappers/footnote_definition.vue
@@ -0,0 +1,28 @@
+<script>
+import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
+
+export default {
+ name: 'FootnoteDefinitionWrapper',
+ components: {
+ NodeViewWrapper,
+ NodeViewContent,
+ },
+ props: {
+ node: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <node-view-wrapper class="gl-display-flex gl-font-sm" as="div">
+ <span
+ data-testid="footnote-label"
+ contenteditable="false"
+ class="gl-display-inline-flex gl-mr-2"
+ >{{ node.attrs.label }}:</span
+ >
+ <node-view-content />
+ </node-view-wrapper>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
index 209e4629830..c0d6e32a739 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
@@ -101,6 +101,9 @@ export default {
deleteTable: __('Delete table'),
editTableActions: __('Edit table'),
},
+ dropdownPopperOpts: {
+ positionFixed: true,
+ },
};
</script>
<template>
@@ -124,9 +127,7 @@ export default {
no-caret
text-sr-only
:text="$options.i18n.editTableActions"
- :popper-opts="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
- positionFixed: true,
- } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ :popper-opts="$options.dropdownPopperOpts"
@hide="handleHide($event)"
>
<gl-dropdown-item @click="runCommand('addColumnBefore')">
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 cc4ba84a29d..61f6a233694 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -4,7 +4,8 @@ import { VueNodeViewRenderer } from '@tiptap/vue-2';
import languageLoader from '../services/code_block_language_loader';
import CodeBlockWrapper from '../components/wrappers/code_block.vue';
-const extractLanguage = (element) => element.getAttribute('lang');
+const extractLanguage = (element) => element.dataset.canonicalLang ?? element.getAttribute('lang');
+
export const backtickInputRegex = /^```([a-z]+)?[\s\n]$/;
export const tildeInputRegex = /^~~~([a-z]+)?[\s\n]$/;
diff --git a/app/assets/javascripts/content_editor/extensions/diagram.js b/app/assets/javascripts/content_editor/extensions/diagram.js
index f9dfeb92e9a..c59ca8a28b8 100644
--- a/app/assets/javascripts/content_editor/extensions/diagram.js
+++ b/app/assets/javascripts/content_editor/extensions/diagram.js
@@ -1,6 +1,10 @@
+import { textblockTypeInputRule } from '@tiptap/core';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
+import languageLoader from '../services/code_block_language_loader';
import CodeBlockHighlight from './code_block_highlight';
+const backtickInputRegex = /^```(mermaid|plantuml)[\s\n]$/;
+
export default CodeBlockHighlight.extend({
name: 'diagram',
@@ -17,6 +21,9 @@ export default CodeBlockHighlight.extend({
isDiagram: {
default: true,
},
+ showPreview: {
+ default: true,
+ },
};
},
@@ -24,6 +31,11 @@ export default CodeBlockHighlight.extend({
return [
{
priority: PARSE_HTML_PRIORITY_HIGHEST,
+ tag: 'pre[lang="mermaid"]',
+ getAttrs: () => ({ language: 'mermaid' }),
+ },
+ {
+ priority: PARSE_HTML_PRIORITY_HIGHEST,
tag: '[data-diagram]',
getContent(element, schema) {
const source = atob(element.dataset.diagramSrc.replace('data:text/plain;base64,', ''));
@@ -54,6 +66,14 @@ export default CodeBlockHighlight.extend({
},
addInputRules() {
- return [];
+ const getAttributes = (match) => languageLoader?.loadLanguageFromInputRule(match) || {};
+
+ return [
+ textblockTypeInputRule({
+ find: backtickInputRegex,
+ type: this.type,
+ getAttributes,
+ }),
+ ];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/footnote_definition.js b/app/assets/javascripts/content_editor/extensions/footnote_definition.js
index dbab0de3421..bf752918934 100644
--- a/app/assets/javascripts/content_editor/extensions/footnote_definition.js
+++ b/app/assets/javascripts/content_editor/extensions/footnote_definition.js
@@ -1,12 +1,27 @@
import { mergeAttributes, Node } from '@tiptap/core';
+import { VueNodeViewRenderer } from '@tiptap/vue-2';
+import FootnoteDefinitionWrapper from '../components/wrappers/footnote_definition.vue';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
+const extractFootnoteIdentifier = (idAttribute) => /^fn-(\w+)-\d+$/.exec(idAttribute)?.[1];
+
export default Node.create({
name: 'footnoteDefinition',
-
content: 'paragraph',
-
group: 'block',
+ isolating: true,
+ addAttributes() {
+ return {
+ identifier: {
+ default: null,
+ parseHTML: (element) => extractFootnoteIdentifier(element.getAttribute('id')),
+ },
+ label: {
+ default: null,
+ parseHTML: (element) => extractFootnoteIdentifier(element.getAttribute('id')),
+ },
+ };
+ },
parseHTML() {
return [
@@ -15,7 +30,11 @@ export default Node.create({
];
},
- renderHTML({ HTMLAttributes }) {
- return ['li', mergeAttributes(HTMLAttributes), 0];
+ renderHTML({ label, ...HTMLAttributes }) {
+ return ['div', mergeAttributes(HTMLAttributes), 0];
+ },
+
+ addNodeView() {
+ return new VueNodeViewRenderer(FootnoteDefinitionWrapper);
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/footnote_reference.js b/app/assets/javascripts/content_editor/extensions/footnote_reference.js
index 1ac8016f774..ae5b8edc7af 100644
--- a/app/assets/javascripts/content_editor/extensions/footnote_reference.js
+++ b/app/assets/javascripts/content_editor/extensions/footnote_reference.js
@@ -1,6 +1,9 @@
import { Node, mergeAttributes } from '@tiptap/core';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
+const extractFootnoteIdentifier = (element) =>
+ /^fnref-(\w+)-\d+$/.exec(element.querySelector('a')?.getAttribute('id'))?.[1];
+
export default Node.create({
name: 'footnoteReference',
@@ -16,13 +19,13 @@ export default Node.create({
addAttributes() {
return {
- footnoteId: {
+ identifier: {
default: null,
- parseHTML: (element) => element.querySelector('a').getAttribute('id'),
+ parseHTML: extractFootnoteIdentifier,
},
- footnoteNumber: {
+ label: {
default: null,
- parseHTML: (element) => element.textContent,
+ parseHTML: extractFootnoteIdentifier,
},
};
},
@@ -31,7 +34,7 @@ export default Node.create({
return [{ tag: 'sup.footnote-ref', priority: PARSE_HTML_PRIORITY_HIGHEST }];
},
- renderHTML({ HTMLAttributes: { footnoteNumber, footnoteId, ...HTMLAttributes } }) {
- return ['sup', mergeAttributes(HTMLAttributes), footnoteNumber];
+ renderHTML({ HTMLAttributes: { label, ...HTMLAttributes } }) {
+ return ['sup', mergeAttributes(HTMLAttributes), label];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/footnotes_section.js b/app/assets/javascripts/content_editor/extensions/footnotes_section.js
index 914a8934734..2b2c4177e1d 100644
--- a/app/assets/javascripts/content_editor/extensions/footnotes_section.js
+++ b/app/assets/javascripts/content_editor/extensions/footnotes_section.js
@@ -10,7 +10,10 @@ export default Node.create({
isolating: true,
parseHTML() {
- return [{ tag: 'section.footnotes > ol' }];
+ return [
+ { tag: 'section.footnotes', skip: true },
+ { tag: 'section.footnotes > ol', skip: true },
+ ];
},
renderHTML({ HTMLAttributes }) {
diff --git a/app/assets/javascripts/content_editor/extensions/sourcemap.js b/app/assets/javascripts/content_editor/extensions/sourcemap.js
index 94236e2e70e..87118074462 100644
--- a/app/assets/javascripts/content_editor/extensions/sourcemap.js
+++ b/app/assets/javascripts/content_editor/extensions/sourcemap.js
@@ -4,6 +4,8 @@ import Bold from './bold';
import BulletList from './bullet_list';
import Code from './code';
import CodeBlockHighlight from './code_block_highlight';
+import FootnoteReference from './footnote_reference';
+import FootnoteDefinition from './footnote_definition';
import Heading from './heading';
import HardBreak from './hard_break';
import HorizontalRule from './horizontal_rule';
@@ -13,6 +15,13 @@ import Link from './link';
import ListItem from './list_item';
import OrderedList from './ordered_list';
import Paragraph from './paragraph';
+import Strike from './strike';
+import TaskList from './task_list';
+import TaskItem from './task_item';
+import Table from './table';
+import TableCell from './table_cell';
+import TableHeader from './table_header';
+import TableRow from './table_row';
export default Extension.create({
addGlobalAttributes() {
@@ -24,6 +33,8 @@ export default Extension.create({
BulletList.name,
Code.name,
CodeBlockHighlight.name,
+ FootnoteReference.name,
+ FootnoteDefinition.name,
HardBreak.name,
Heading.name,
HorizontalRule.name,
@@ -33,6 +44,13 @@ export default Extension.create({
ListItem.name,
OrderedList.name,
Paragraph.name,
+ Strike.name,
+ TaskList.name,
+ TaskItem.name,
+ Table.name,
+ TableCell.name,
+ TableHeader.name,
+ TableRow.name,
],
attributes: {
sourceMarkdown: {
diff --git a/app/assets/javascripts/content_editor/services/asset_resolver.js b/app/assets/javascripts/content_editor/services/asset_resolver.js
index 942457b9664..c0bcddbe58d 100644
--- a/app/assets/javascripts/content_editor/services/asset_resolver.js
+++ b/app/assets/javascripts/content_editor/services/asset_resolver.js
@@ -1,13 +1,24 @@
import { memoize } from 'lodash';
+const parser = new DOMParser();
+
export default ({ renderMarkdown }) => ({
resolveUrl: memoize(async (canonicalSrc) => {
const html = await renderMarkdown(`[link](${canonicalSrc})`);
if (!html) return canonicalSrc;
- const parser = new DOMParser();
const { body } = parser.parseFromString(html, 'text/html');
-
return body.querySelector('a').getAttribute('href');
}),
+
+ renderDiagram: memoize(async (code, language) => {
+ const backticks = '`'.repeat(4);
+ const html = await renderMarkdown(`${backticks}${language}\n${code}\n${backticks}`);
+
+ const { body } = parser.parseFromString(html, 'text/html');
+ const img = body.querySelector('img');
+ if (!img) return '';
+
+ return img.dataset.src || img.getAttribute('src');
+ }),
});
diff --git a/app/assets/javascripts/content_editor/services/code_block_language_loader.js b/app/assets/javascripts/content_editor/services/code_block_language_loader.js
index 1afaf4bfef6..b7cf1bb087c 100644
--- a/app/assets/javascripts/content_editor/services/code_block_language_loader.js
+++ b/app/assets/javascripts/content_editor/services/code_block_language_loader.js
@@ -8,7 +8,7 @@ const codeBlockLanguageLoader = {
allLanguages: CODE_BLOCK_LANGUAGES,
- findLanguageBySyntax(value) {
+ findOrCreateLanguageBySyntax(value, isDiagram) {
const lowercaseValue = value?.toLowerCase() || 'plaintext';
return (
this.allLanguages.find(
@@ -16,7 +16,9 @@ const codeBlockLanguageLoader = {
syntax === lowercaseValue || variants?.toLowerCase().split(', ').includes(lowercaseValue),
) || {
syntax: lowercaseValue,
- label: sprintf(__(`Custom (%{language})`), { language: lowercaseValue }),
+ label: sprintf(isDiagram ? __(`Diagram (%{language})`) : __(`Custom (%{language})`), {
+ language: lowercaseValue,
+ }),
}
);
},
@@ -38,7 +40,7 @@ const codeBlockLanguageLoader = {
},
loadLanguageFromInputRule(match) {
- const { syntax } = this.findLanguageBySyntax(match[1]);
+ const { syntax } = this.findOrCreateLanguageBySyntax(match[1]);
this.loadLanguage(syntax);
diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js
index 52dacb84153..06757e7a280 100644
--- a/app/assets/javascripts/content_editor/services/content_editor.js
+++ b/app/assets/javascripts/content_editor/services/content_editor.js
@@ -52,6 +52,10 @@ export class ContentEditor {
return this._assetResolver.resolveUrl(canonicalSrc);
}
+ renderDiagram(code, language) {
+ return this._assetResolver.renderDiagram(code, language);
+ }
+
async setSerializedContent(serializedContent) {
const { _tiptapEditor: editor, _eventHub: eventHub } = this;
const { doc, tr } = editor.state;
diff --git a/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js
index b6a3e0bc26a..2c462cdde91 100644
--- a/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js
+++ b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js
@@ -20,9 +20,9 @@
*/
import { Mark } from 'prosemirror-model';
-import { visitParents } from 'unist-util-visit-parents';
+import { visitParents, SKIP } from 'unist-util-visit-parents';
import { toString } from 'hast-util-to-string';
-import { isFunction } from 'lodash';
+import { isFunction, isString, noop } from 'lodash';
/**
* Merges two ProseMirror text nodes if both text nodes
@@ -63,10 +63,12 @@ function maybeMerge(a, b) {
function createSourceMapAttributes(hastNode, source) {
const { position } = hastNode;
- return {
- sourceMapKey: `${position.start.offset}:${position.end.offset}`,
- sourceMarkdown: source.substring(position.start.offset, position.end.offset),
- };
+ return position && position.end
+ ? {
+ sourceMapKey: `${position.start.offset}:${position.end.offset}`,
+ sourceMarkdown: source.substring(position.start.offset, position.end.offset),
+ }
+ : {};
}
/**
@@ -141,6 +143,20 @@ class HastToProseMirrorConverterState {
return this.stack.length === 0;
}
+ findInStack(fn) {
+ const last = this.stack.length - 1;
+
+ for (let i = last; i >= 0; i -= 1) {
+ const item = this.stack[i];
+
+ if (fn(item) === true) {
+ return item;
+ }
+ }
+
+ return null;
+ }
+
/**
* Creates a text node and adds it to
* the top node in the stack.
@@ -249,33 +265,38 @@ class HastToProseMirrorConverterState {
* @returns An object that contains ProseMirror node factories
*/
const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source) => {
- const handlers = {
- root: (state, hastNode) => state.openNode(schema.topNodeType, hastNode, {}),
- text: (state, hastNode) => {
- const { factorySpec } = state.top;
-
- if (/^\s+$/.test(hastNode.value)) {
- return;
- }
+ const factories = {
+ root: {
+ selector: 'root',
+ wrapInParagraph: true,
+ handle: (state, hastNode) => state.openNode(schema.topNodeType, hastNode, {}, {}),
+ },
+ text: {
+ selector: 'text',
+ handle: (state, hastNode) => {
+ const found = state.findInStack((node) => isFunction(node.factorySpec.processText));
+ const { value: text } = hastNode;
+
+ if (/^\s+$/.test(text)) {
+ return;
+ }
- if (factorySpec.wrapTextInParagraph === true) {
- state.openNode(schema.nodeType('paragraph'));
- state.addText(schema, hastNode.value);
- state.closeNode();
- } else {
- state.addText(schema, hastNode.value);
- }
+ state.addText(schema, found ? found.factorySpec.processText(text) : text);
+ },
},
};
-
- for (const [hastNodeTagName, factorySpec] of Object.entries(proseMirrorFactorySpecs)) {
- if (factorySpec.block) {
- handlers[hastNodeTagName] = (state, hastNode, parent, ancestors) => {
- const nodeType = schema.nodeType(
- isFunction(factorySpec.block)
- ? factorySpec.block(hastNode, parent, ancestors)
- : factorySpec.block,
- );
+ for (const [proseMirrorName, factorySpec] of Object.entries(proseMirrorFactorySpecs)) {
+ const factory = {
+ selector: factorySpec.selector,
+ skipChildren: factorySpec.skipChildren,
+ processText: factorySpec.processText,
+ parent: factorySpec.parent,
+ wrapInParagraph: factorySpec.wrapInParagraph,
+ };
+
+ if (factorySpec.type === 'block') {
+ factory.handle = (state, hastNode, parent) => {
+ const nodeType = schema.nodeType(proseMirrorName);
state.closeUntil(parent);
state.openNode(
@@ -297,9 +318,9 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source)
state.closeNode();
}
};
- } else if (factorySpec.inline) {
- const nodeType = schema.nodeType(factorySpec.inline);
- handlers[hastNodeTagName] = (state, hastNode, parent) => {
+ } else if (factorySpec.type === 'inline') {
+ const nodeType = schema.nodeType(proseMirrorName);
+ factory.handle = (state, hastNode, parent) => {
state.closeUntil(parent);
state.openNode(
nodeType,
@@ -310,23 +331,115 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source)
// Inline nodes do not have children therefore they are immediately closed
state.closeNode();
};
- } else if (factorySpec.mark) {
- const markType = schema.marks[factorySpec.mark];
- handlers[hastNodeTagName] = (state, hastNode, parent) => {
+ } else if (factorySpec.type === 'mark') {
+ const markType = schema.marks[proseMirrorName];
+ factory.handle = (state, hastNode, parent) => {
state.openMark(markType, getAttrs(factorySpec, hastNode, parent, source));
if (factorySpec.inlineContent) {
state.addText(schema, hastNode.value);
}
};
+ } else if (factorySpec.type === 'ignore') {
+ factory.handle = noop;
} else {
- throw new RangeError(`Unrecognized node factory spec ${JSON.stringify(factorySpec)}`);
+ throw new RangeError(
+ `Unrecognized ProseMirror object type ${JSON.stringify(factorySpec.type)}`,
+ );
}
+
+ factories[proseMirrorName] = factory;
}
- return handlers;
+ return factories;
};
+const findFactory = (hastNode, ancestors, factories) =>
+ Object.entries(factories).find(([, factorySpec]) => {
+ const { selector } = factorySpec;
+
+ return isFunction(selector)
+ ? selector(hastNode, ancestors)
+ : [hastNode.tagName, hastNode.type].includes(selector);
+ })?.[1];
+
+const findParent = (ancestors, parent) => {
+ if (isString(parent)) {
+ return ancestors.reverse().find((ancestor) => ancestor.tagName === parent);
+ }
+
+ return ancestors[ancestors.length - 1];
+};
+
+const calcTextNodePosition = (textNode) => {
+ const { position, value, type } = textNode;
+
+ if (type !== 'text' || (!position.start && !position.end) || (position.start && position.end)) {
+ return textNode.position;
+ }
+
+ const span = value.length - 1;
+
+ if (position.start && !position.end) {
+ const { start } = position;
+
+ return {
+ start,
+ end: {
+ row: start.row,
+ column: start.column + span,
+ offset: start.offset + span,
+ },
+ };
+ }
+
+ const { end } = position;
+
+ return {
+ start: {
+ row: end.row,
+ column: end.column - span,
+ offset: end.offset - span,
+ },
+ end,
+ };
+};
+
+const removeEmptyTextNodes = (nodes) =>
+ nodes.filter(
+ (node) => node.type !== 'text' || (node.type === 'text' && !/^\s+$/.test(node.value)),
+ );
+
+const wrapInlineElements = (nodes, wrappableTags) =>
+ nodes.reduce((children, child) => {
+ const previous = children[children.length - 1];
+
+ if (child.type !== 'text' && !wrappableTags.includes(child.tagName)) {
+ return [...children, child];
+ }
+
+ const wrapperExists = previous?.properties.wrapper;
+
+ if (wrapperExists) {
+ const wrapper = previous;
+
+ wrapper.position.end = child.position.end;
+ wrapper.children.push(child);
+
+ return children;
+ }
+
+ const wrapper = {
+ type: 'element',
+ tagName: 'p',
+ position: calcTextNodePosition(child),
+ children: [child],
+ properties: { wrapper: true },
+ };
+
+ return [...children, wrapper];
+ }, []);
+
/**
* Converts a Hast AST to a ProseMirror document based on a series
* of specifications that describe how to map all the nodes of the former
@@ -339,8 +452,9 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source)
* The object should have the following shape:
*
* {
- * [hastNode.tagName]: {
- * [block|node|mark]: [ProseMirror.Node.name],
+ * [ProseMirrorNodeOrMarkName]: {
+ * type: 'block' | 'inline' | 'mark',
+ * selector: String | hastNode -> Boolean,
* ...configurationOptions
* }
* }
@@ -348,57 +462,21 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source)
* Where each property in the object represents a HAST node with a given tag name, for example:
*
* {
- * h1: {},
- * h2: {},
- * table: {},
- * strong: {},
- * // etc
- * }
- *
- * You can specify the type of ProseMirror object adding one the following
- * properties:
- *
- * 1. "block": A ProseMirror node that contains one or more children.
- * 2. "inline": A ProseMirror node that doesn’t contain any children although
- * it can have inline content like a code block or a reference.
- * 3. "mark": A ProseMirror mark.
- *
- * The value of that property should be the name of the ProseMirror node or mark, i.e:
- *
- * {
- * h1: {
- * block: 'heading',
+ * horizontalRule: {
+ * type: 'block',
+ * selector: 'hr',
* },
- * h2: {
- * block: 'heading',
+ * heading: {
+ * type: 'block',
+ * selector: (hastNode) => ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(hastNode),
* },
- * img: {
- * node: 'image',
+ * bold: {
+ * type: 'mark'
+ * selector: (hastNode) => ['b', 'strong'].includes(hastNode),
* },
- * strong: {
- * mark: 'bold',
- * }
- * }
+ * // etc
+ * }
*
- * You can compute a ProseMirror’s node or mark name based on the HAST node
- * by passing a function instead of a String. The converter invokes the function
- * and provides a HAST node object:
- *
- * {
- * list: {
- * block: (hastNode) => {
- * let type = 'bulletList';
-
- * if (hastNode.children.some(isTaskItem)) {
- * type = 'taskList';
- * } else if (hastNode.ordered) {
- * type = 'orderedList';
- * }
-
- * return type;
- * }
- * }
- * }
*
* Configuration options
* ----------------------
@@ -406,6 +484,28 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source)
* You can customize the conversion process for every node or mark
* setting the following properties in the specification object:
*
+ * **type**
+ *
+ * The `type` property should have one of following three values:
+ *
+ * 1. "block": A ProseMirror node that contains one or more children.
+ * 2. "inline": A ProseMirror node that doesn’t contain any children although
+ * it can have inline content like an image or a mention object.
+ * 3. "mark": A ProseMirror mark.
+ * 4. "ignore": A hast node that should be ignored and won’t be mapped to a
+ * ProseMirror node.
+ *
+ * **selector**
+ *
+ * The `selector` property matches a HastNode to a ProseMirror node or
+ * Mark. If you assign a string value to this property, the converter
+ * will match the first hast node with a `tagName` or `type` property
+ * that equals the string value.
+ *
+ * If you assign a function, the converter will invoke the function with
+ * the hast node and its ancestors. The function should return `true`
+ * if the hastNode matches the custom criteria implemented in the function
+ *
* **getAttrs**
*
* Computes a ProseMirror node or mark attributes. The converter will invoke
@@ -415,12 +515,19 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source)
* 2. hasParents: All the hast node’s ancestors up to the root node
* 3. source: Markdown source file’s content
*
- * **wrapTextInParagraph**
+ * **wrapInParagraph**
*
- * This property only applies to block nodes. If a block node contains text,
- * it will wrap that text in a paragraph. This is useful for ProseMirror block
+ * This property only applies to block nodes. If a block node contains inline
+ * elements like text, images, links, etc, the converter will wrap those inline
+ * elements in a paragraph. This is useful for ProseMirror block
* nodes that don’t allow text directly such as list items and tables.
*
+ * **processText**
+ *
+ * This property only applies to block nodes. If a block node contains text,
+ * it allows applying a processing function to that text. This is useful when
+ * you can transform the text node, i.e trim(), substring(), etc.
+ *
* **skipChildren**
*
* Skips a hast node’s children while traversing the tree.
@@ -434,6 +541,13 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source)
* Use this property along skipChildren to provide custom processing of child nodes
* for a block node.
*
+ * **parent**
+ *
+ * Specifies what is the node’s parent. This is useful when the node’s parent is not
+ * its direct ancestor in Abstract Syntax Tree. For example, imagine that you want
+ * to make <tr> elements a direct children of tables and skip `<thead>` and `<tbody>`
+ * altogether.
+ *
* @param {model.Document_Schema} params.schema A ProseMirror schema that specifies the shape
* of the ProseMirror document.
* @param {Object} params.factorySpec A factory specification as described above
@@ -442,17 +556,20 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source)
*
* @returns A ProseMirror document
*/
-export const createProseMirrorDocFromMdastTree = ({ schema, factorySpecs, tree, source }) => {
+export const createProseMirrorDocFromMdastTree = ({
+ schema,
+ factorySpecs,
+ wrappableTags,
+ tree,
+ source,
+}) => {
const proseMirrorNodeFactories = createProseMirrorNodeFactories(schema, factorySpecs, source);
const state = new HastToProseMirrorConverterState();
visitParents(tree, (hastNode, ancestors) => {
- const parent = ancestors[ancestors.length - 1];
- const skipChildren = factorySpecs[hastNode.tagName]?.skipChildren;
-
- const handler = proseMirrorNodeFactories[hastNode.tagName || hastNode.type];
+ const factory = findFactory(hastNode, ancestors, proseMirrorNodeFactories);
- if (!handler) {
+ if (!factory) {
throw new Error(
`Hast node of type "${
hastNode.tagName || hastNode.type
@@ -460,9 +577,25 @@ export const createProseMirrorDocFromMdastTree = ({ schema, factorySpecs, tree,
);
}
- handler(state, hastNode, parent, ancestors);
+ const parent = findParent(ancestors, factory.parent);
+
+ if (factory.wrapInParagraph) {
+ /**
+ * Modifying parameters is a bad practice. For performance reasons,
+ * the author of the unist-util-visit-parents function recommends
+ * modifying nodes in place to avoid traversing the Abstract Syntax
+ * Tree more than once
+ */
+ // eslint-disable-next-line no-param-reassign
+ hastNode.children = wrapInlineElements(
+ removeEmptyTextNodes(hastNode.children),
+ wrappableTags,
+ );
+ }
+
+ factory.handle(state, hastNode, parent);
- return skipChildren === true ? 'skip' : true;
+ return factory.skipChildren === true ? SKIP : true;
});
let doc;
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index d665f24bba1..2d33a16f1a5 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -17,7 +17,6 @@ import Diagram from '../extensions/diagram';
import Emoji from '../extensions/emoji';
import Figure from '../extensions/figure';
import FigureCaption from '../extensions/figure_caption';
-import FootnotesSection from '../extensions/footnotes_section';
import FootnoteDefinition from '../extensions/footnote_definition';
import FootnoteReference from '../extensions/footnote_reference';
import Frontmatter from '../extensions/frontmatter';
@@ -60,11 +59,13 @@ import {
renderPlayable,
renderHTMLNode,
renderContent,
+ renderBulletList,
preserveUnchanged,
bold,
italic,
link,
code,
+ strike,
} from './serialization_helpers';
const defaultSerializerConfig = {
@@ -89,12 +90,7 @@ const defaultSerializerConfig = {
close: (...args) => `${defaultMarkdownSerializer.marks.code.close(...args)}$`,
escape: false,
},
- [Strike.name]: {
- open: '~~',
- close: '~~',
- mixable: true,
- expelEnclosingWhitespace: true,
- },
+ [Strike.name]: strike,
...HTMLMarks.reduce(
(acc, { name }) => ({
...acc,
@@ -124,7 +120,7 @@ const defaultSerializerConfig = {
state.wrapBlock('> ', null, node, () => state.renderContent(node));
}
}),
- [BulletList.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.bullet_list),
+ [BulletList.name]: preserveUnchanged(renderBulletList),
[CodeBlockHighlight.name]: preserveUnchanged(renderCodeBlock),
[Diagram.name]: renderCodeBlock,
[Division.name]: (state, node) => {
@@ -157,15 +153,14 @@ const defaultSerializerConfig = {
state.write(`:${name}:`);
},
- [FootnoteDefinition.name]: (state, node) => {
+ [FootnoteDefinition.name]: preserveUnchanged((state, node) => {
+ state.write(`[^${node.attrs.identifier}]: `);
state.renderInline(node);
- },
- [FootnoteReference.name]: (state, node) => {
- state.write(`[^${node.attrs.footnoteNumber}]`);
- },
- [FootnotesSection.name]: (state, node) => {
- state.renderList(node, '', (index) => `[^${index + 1}]: `);
- },
+ state.ensureNewLine();
+ }),
+ [FootnoteReference.name]: preserveUnchanged((state, node) => {
+ state.write(`[^${node.attrs.identifier}]`);
+ }),
[Frontmatter.name]: (state, node) => {
const { language } = node.attrs;
const syntax = {
@@ -196,18 +191,18 @@ const defaultSerializerConfig = {
state.write('[[_TOC_]]');
state.closeBlock(node);
},
- [Table.name]: renderTable,
+ [Table.name]: preserveUnchanged(renderTable),
[TableCell.name]: renderTableCell,
[TableHeader.name]: renderTableCell,
[TableRow.name]: renderTableRow,
- [TaskItem.name]: (state, node) => {
+ [TaskItem.name]: preserveUnchanged((state, node) => {
state.write(`[${node.attrs.checked ? 'x' : ' '}] `);
state.renderContent(node);
- },
- [TaskList.name]: (state, node) => {
+ }),
+ [TaskList.name]: preserveUnchanged((state, node) => {
if (node.attrs.numeric) renderOrderedList(state, node);
- else defaultMarkdownSerializer.nodes.bullet_list(state, node);
- },
+ else renderBulletList(state, node);
+ }),
[Text.name]: defaultMarkdownSerializer.nodes.text,
[Video.name]: renderPlayable,
[WordBreak.name]: (state) => state.write('<wbr>'),
diff --git a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js
index 770de1df0d0..da10c684b0b 100644
--- a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js
+++ b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js
@@ -2,39 +2,51 @@ import { isString } from 'lodash';
import { render } from '~/lib/gfm';
import { createProseMirrorDocFromMdastTree } from './hast_to_prosemirror_converter';
+const wrappableTags = ['img', 'br', 'code', 'i', 'em', 'b', 'strong', 'a', 'strike', 's', 'del'];
+
+const isTaskItem = (hastNode) => {
+ const { className } = hastNode.properties;
+
+ return (
+ hastNode.tagName === 'li' && Array.isArray(className) && className.includes('task-list-item')
+ );
+};
+
+const getTableCellAttrs = (hastNode) => ({
+ colspan: parseInt(hastNode.properties.colSpan, 10) || 1,
+ rowspan: parseInt(hastNode.properties.rowSpan, 10) || 1,
+});
+
const factorySpecs = {
- blockquote: { block: 'blockquote' },
- p: { block: 'paragraph' },
- li: { block: 'listItem', wrapTextInParagraph: true },
- ul: { block: 'bulletList' },
- ol: { block: 'orderedList' },
- h1: {
- block: 'heading',
- getAttrs: () => ({ level: 1 }),
- },
- h2: {
- block: 'heading',
- getAttrs: () => ({ level: 2 }),
- },
- h3: {
- block: 'heading',
- getAttrs: () => ({ level: 3 }),
- },
- h4: {
- block: 'heading',
- getAttrs: () => ({ level: 4 }),
- },
- h5: {
- block: 'heading',
- getAttrs: () => ({ level: 5 }),
- },
- h6: {
- block: 'heading',
- getAttrs: () => ({ level: 6 }),
- },
- pre: {
- block: 'codeBlock',
+ blockquote: { type: 'block', selector: 'blockquote' },
+ paragraph: { type: 'block', selector: 'p' },
+ listItem: {
+ type: 'block',
+ wrapInParagraph: true,
+ selector: (hastNode) => hastNode.tagName === 'li' && !hastNode.properties.className,
+ processText: (text) => text.trimRight(),
+ },
+ orderedList: {
+ type: 'block',
+ selector: (hastNode) => hastNode.tagName === 'ol' && !hastNode.properties.className,
+ },
+ bulletList: {
+ type: 'block',
+ selector: (hastNode) => hastNode.tagName === 'ul' && !hastNode.properties.className,
+ },
+ heading: {
+ type: 'block',
+ selector: (hastNode) => ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(hastNode.tagName),
+ getAttrs: (hastNode) => {
+ const level = parseInt(/(\d)$/.exec(hastNode.tagName)?.[1], 10) || 1;
+
+ return { level };
+ },
+ },
+ codeBlock: {
+ type: 'block',
skipChildren: true,
+ selector: 'pre',
getContent: ({ hastNodeText }) => hastNodeText.replace(/\n$/, ''),
getAttrs: (hastNode) => {
const languageClass = hastNode.children[0]?.properties.className?.[0];
@@ -43,28 +55,111 @@ const factorySpecs = {
return { language };
},
},
- hr: { inline: 'horizontalRule' },
- img: {
- inline: 'image',
+ horizontalRule: {
+ type: 'block',
+ selector: 'hr',
+ },
+ taskList: {
+ type: 'block',
+ selector: (hastNode) => {
+ const { className } = hastNode.properties;
+
+ return (
+ ['ul', 'ol'].includes(hastNode.tagName) &&
+ Array.isArray(className) &&
+ className.includes('contains-task-list')
+ );
+ },
+ getAttrs: (hastNode) => ({
+ numeric: hastNode.tagName === 'ol',
+ }),
+ },
+ taskItem: {
+ type: 'block',
+ wrapInParagraph: true,
+ selector: isTaskItem,
+ getAttrs: (hastNode) => ({
+ checked: hastNode.children[0].properties.checked,
+ }),
+ processText: (text) => text.trimLeft(),
+ },
+ taskItemCheckbox: {
+ type: 'ignore',
+ selector: (hastNode, ancestors) =>
+ hastNode.tagName === 'input' && isTaskItem(ancestors[ancestors.length - 1]),
+ },
+ table: {
+ type: 'block',
+ selector: 'table',
+ },
+ tableRow: {
+ type: 'block',
+ selector: 'tr',
+ parent: 'table',
+ },
+ tableHeader: {
+ type: 'block',
+ selector: 'th',
+ getAttrs: getTableCellAttrs,
+ wrapInParagraph: true,
+ },
+ tableCell: {
+ type: 'block',
+ selector: 'td',
+ getAttrs: getTableCellAttrs,
+ wrapInParagraph: true,
+ },
+ ignoredTableNodes: {
+ type: 'ignore',
+ selector: (hastNode) => ['thead', 'tbody', 'tfoot'].includes(hastNode.tagName),
+ },
+ footnoteDefinition: {
+ type: 'block',
+ selector: 'footnotedefinition',
+ getAttrs: (hastNode) => hastNode.properties,
+ },
+ image: {
+ type: 'inline',
+ selector: 'img',
getAttrs: (hastNode) => ({
src: hastNode.properties.src,
title: hastNode.properties.title,
alt: hastNode.properties.alt,
}),
},
- br: { inline: 'hardBreak' },
- code: { mark: 'code' },
- em: { mark: 'italic' },
- i: { mark: 'italic' },
- strong: { mark: 'bold' },
- b: { mark: 'bold' },
- a: {
- mark: 'link',
+ hardBreak: {
+ type: 'inline',
+ selector: 'br',
+ },
+ footnoteReference: {
+ type: 'inline',
+ selector: 'footnotereference',
+ getAttrs: (hastNode) => hastNode.properties,
+ },
+ code: {
+ type: 'mark',
+ selector: 'code',
+ },
+ italic: {
+ type: 'mark',
+ selector: (hastNode) => ['em', 'i'].includes(hastNode.tagName),
+ },
+ bold: {
+ type: 'mark',
+ selector: (hastNode) => ['strong', 'b'].includes(hastNode.tagName),
+ },
+ link: {
+ type: 'mark',
+ selector: 'a',
getAttrs: (hastNode) => ({
href: hastNode.properties.href,
title: hastNode.properties.title,
}),
},
+ strike: {
+ type: 'mark',
+ selector: (hastNode) => ['strike', 's', 'del'].includes(hastNode.tagName),
+ },
};
export default () => {
@@ -77,6 +172,7 @@ export default () => {
schema,
factorySpecs,
tree,
+ wrappableTags,
source: markdown,
}),
});
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index 089d30edec7..88f5192af77 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -1,4 +1,4 @@
-import { uniq, isString } from 'lodash';
+import { uniq, isString, omit } from 'lodash';
const defaultAttrs = {
td: { colspan: 1, rowspan: 1, colwidth: null },
@@ -12,22 +12,6 @@ const ignoreAttrs = {
const tableMap = new WeakMap();
-// Source taken from
-// prosemirror-markdown/src/to_markdown.js
-export function isPlainURL(link, parent, index, side) {
- if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) return false;
- const content = parent.child(index + (side < 0 ? -1 : 0));
- if (
- !content.isText ||
- content.text !== link.attrs.href ||
- content.marks[content.marks.length - 1] !== link
- )
- return false;
- if (index === (side < 0 ? 1 : parent.childCount - 1)) return true;
- const next = parent.child(index + (side < 0 ? -2 : 1));
- return !link.isInSet(next.marks);
-}
-
function containsOnlyText(node) {
if (node.childCount === 1) {
const child = node.child(0);
@@ -219,7 +203,7 @@ function renderTableRowAsHTML(state, node) {
node.forEach((cell, _, i) => {
const tag = cell.type.name === 'tableHeader' ? 'th' : 'td';
- renderTagOpen(state, tag, cell.attrs);
+ renderTagOpen(state, tag, omit(cell.attrs, 'sourceMapKey', 'sourceMarkdown'));
if (!containsParagraphWithOnlyText(cell)) {
state.closeBlock(node);
@@ -272,19 +256,6 @@ export function renderHTMLNode(tagName, forceRenderContentInline = false) {
};
}
-export function renderOrderedList(state, node) {
- const { parens } = node.attrs;
- const start = node.attrs.start || 1;
- const maxW = String(start + node.childCount - 1).length;
- const space = state.repeat(' ', maxW + 2);
- const delimiter = parens ? ')' : '.';
-
- state.renderList(node, space, (i) => {
- const nStr = String(start + i);
- return `${state.repeat(' ', maxW - nStr.length) + nStr}${delimiter} `;
- });
-}
-
export function renderTableCell(state, node) {
if (!isInBlockTable(node) || containsParagraphWithOnlyText(node)) {
state.renderInline(node.child(0));
@@ -364,7 +335,72 @@ export function preserveUnchanged(render) {
};
}
-const generateBoldTags = (open = true) => {
+/**
+ * We extracted this function from
+ * https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/to_markdown.ts#L350.
+ *
+ * We need to overwrite this function because we don’t want to wrap the list item nodes
+ * with the bullet delimiter when the list item node hasn’t changed
+ */
+const renderList = (state, node, delim, firstDelim) => {
+ if (state.closed && state.closed.type === node.type) state.flushClose(3);
+ else if (state.inTightList) state.flushClose(1);
+
+ const isTight =
+ typeof node.attrs.tight !== 'undefined' ? node.attrs.tight : state.options.tightLists;
+ const prevTight = state.inTightList;
+
+ state.inTightList = isTight;
+
+ node.forEach((child, _, i) => {
+ const same = state.options.changeTracker.get(child);
+
+ if (i && isTight) {
+ state.flushClose(1);
+ }
+
+ if (same) {
+ // Avoid wrapping list item when node hasn’t changed
+ state.render(child, node, i);
+ } else {
+ state.wrapBlock(delim, firstDelim(i), node, () => state.render(child, node, i));
+ }
+ });
+
+ state.inTightList = prevTight;
+};
+
+export const renderBulletList = (state, node) => {
+ const { sourceMarkdown, bullet: bulletAttr } = node.attrs;
+ const bullet = /^(\*|\+|-)\s/.exec(sourceMarkdown)?.[1] || bulletAttr || '*';
+
+ renderList(state, node, ' ', () => `${bullet} `);
+};
+
+export function renderOrderedList(state, node) {
+ const { sourceMarkdown } = node.attrs;
+ let start;
+ let delimiter;
+
+ if (sourceMarkdown) {
+ const match = /^(\d+)(\)|\.)/.exec(sourceMarkdown);
+ start = parseInt(match[1], 10) || 1;
+ [, , delimiter] = match;
+ } else {
+ start = node.attrs.start || 1;
+ delimiter = node.attrs.parens ? ')' : '.';
+ }
+
+ const maxW = String(start + node.childCount - 1).length;
+ const space = state.repeat(' ', maxW + 2);
+
+ renderList(state, node, space, (i) => {
+ const nStr = String(start + i);
+ return `${state.repeat(' ', maxW - nStr.length) + nStr}${delimiter} `;
+ });
+}
+
+const generateBoldTags = (wrapTagName = openTag) => {
return (_, mark) => {
const type = /^(\*\*|__|<strong|<b).*/.exec(mark.attrs.sourceMarkdown)?.[1];
@@ -375,7 +411,7 @@ const generateBoldTags = (open = true) => {
// eslint-disable-next-line @gitlab/require-i18n-strings
case '<strong':
case '<b':
- return (open ? openTag : closeTag)(type.substring(1));
+ return wrapTagName(type.substring(1));
default:
return '**';
}
@@ -384,12 +420,12 @@ const generateBoldTags = (open = true) => {
export const bold = {
open: generateBoldTags(),
- close: generateBoldTags(false),
+ close: generateBoldTags(closeTag),
mixable: true,
expelEnclosingWhitespace: true,
};
-const generateItalicTag = (open = true) => {
+const generateItalicTag = (wrapTagName = openTag) => {
return (_, mark) => {
const type = /^(\*|_|<em|<i).*/.exec(mark.attrs.sourceMarkdown)?.[1];
@@ -400,7 +436,7 @@ const generateItalicTag = (open = true) => {
// eslint-disable-next-line @gitlab/require-i18n-strings
case '<em':
case '<i':
- return (open ? openTag : closeTag)(type.substring(1));
+ return wrapTagName(type.substring(1));
default:
return '_';
}
@@ -409,17 +445,17 @@ const generateItalicTag = (open = true) => {
export const italic = {
open: generateItalicTag(),
- close: generateItalicTag(false),
+ close: generateItalicTag(closeTag),
mixable: true,
expelEnclosingWhitespace: true,
};
-const generateCodeTag = (open = true) => {
+const generateCodeTag = (wrapTagName = openTag) => {
return (_, mark) => {
const type = /^(`|<code).*/.exec(mark.attrs.sourceMarkdown)?.[1];
if (type === '<code') {
- return (open ? openTag : closeTag)(type.substring(1));
+ return wrapTagName(type.substring(1));
}
return '`';
@@ -428,7 +464,7 @@ const generateCodeTag = (open = true) => {
export const code = {
open: generateCodeTag(),
- close: generateCodeTag(false),
+ close: generateCodeTag(closeTag),
mixable: true,
expelEnclosingWhitespace: true,
};
@@ -446,10 +482,79 @@ const linkType = (sourceMarkdown) => {
return LINK_HTML;
};
+const removeUrlProtocol = (url) => url.replace(/^\w+:\/?\/?/, '');
+
+const normalizeUrl = (url) => decodeURIComponent(removeUrlProtocol(url));
+
+/**
+ * Validates that the provided URL is well-formed
+ *
+ * @param {String} url
+ * @returns Returns true when the browser’s URL constructor
+ * can successfully parse the URL string
+ */
+const isValidUrl = (url) => {
+ try {
+ return new URL(url) && true;
+ } catch {
+ return false;
+ }
+};
+
+const findChildWithMark = (mark, parent) => {
+ let child;
+ let offset;
+ let index;
+
+ parent.forEach((_child, _offset, _index) => {
+ if (mark.isInSet(_child.marks)) {
+ child = _child;
+ offset = _offset;
+ index = _index;
+ }
+ });
+
+ return child ? { child, offset, index } : null;
+};
+
+/**
+ * This function detects whether a link should be serialized
+ * as an autolink.
+ *
+ * See https://github.github.com/gfm/#autolinks-extension-
+ * to understand the parsing rules of autolinks.
+ * */
+const isAutoLink = (linkMark, parent) => {
+ const { title, href } = linkMark.attrs;
+
+ if (title || !/^\w+:/.test(href)) {
+ return false;
+ }
+
+ const { child } = findChildWithMark(linkMark, parent);
+
+ if (
+ !child ||
+ !child.isText ||
+ !isValidUrl(href) ||
+ normalizeUrl(child.text) !== normalizeUrl(href)
+ ) {
+ return false;
+ }
+
+ return true;
+};
+
+/**
+ * Returns true if the user used brackets to the define
+ * the autolink in the original markdown source
+ */
+const isBracketAutoLink = (sourceMarkdown) => /^<.+?>$/.test(sourceMarkdown);
+
export const link = {
- open(state, mark, parent, index) {
- if (isPlainURL(mark, parent, index, 1)) {
- return '<';
+ open(state, mark, parent) {
+ if (isAutoLink(mark, parent)) {
+ return isBracketAutoLink(mark.attrs.sourceMarkdown) ? '<' : '';
}
const { canonicalSrc, href, title, sourceMarkdown } = mark.attrs;
@@ -466,9 +571,9 @@ export const link = {
return openTag('a', attrs);
},
- close(state, mark, parent, index) {
- if (isPlainURL(mark, parent, index, -1)) {
- return '>';
+ close(state, mark, parent) {
+ if (isAutoLink(mark, parent)) {
+ return isBracketAutoLink(mark.attrs.sourceMarkdown) ? '>' : '';
}
const { canonicalSrc, href, title, sourceMarkdown } = mark.attrs;
@@ -480,3 +585,28 @@ export const link = {
return `](${state.esc(canonicalSrc || href)}${title ? ` ${state.quote(title)}` : ''})`;
},
};
+
+const generateStrikeTag = (wrapTagName = openTag) => {
+ return (_, mark) => {
+ const type = /^(~~|<del|<strike|<s).*/.exec(mark.attrs.sourceMarkdown)?.[1];
+
+ switch (type) {
+ case '~~':
+ return type;
+ /* eslint-disable @gitlab/require-i18n-strings */
+ case '<del':
+ case '<strike':
+ case '<s':
+ return wrapTagName(type.substring(1));
+ default:
+ return '~~';
+ }
+ };
+};
+
+export const strike = {
+ open: generateStrikeTag(),
+ close: generateStrikeTag(closeTag),
+ mixable: true,
+ expelEnclosingWhitespace: true,
+};
diff --git a/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue b/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue
index 3158ae9b126..ccd22085470 100644
--- a/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue
+++ b/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue
@@ -22,7 +22,7 @@ export default {
type: Boolean,
required: true,
},
- editProjectServicePath: {
+ editIntegrationPath: {
type: String,
required: true,
},
@@ -79,7 +79,7 @@ export default {
<gl-button variant="success" category="primary" :disabled="!formIsValid" @click="submit">
{{ saveButtonText }}
</gl-button>
- <gl-button class="float-right" :href="editProjectServicePath">{{ __('Cancel') }}</gl-button>
+ <gl-button class="float-right" :href="editIntegrationPath">{{ __('Cancel') }}</gl-button>
<delete-custom-metric-modal
v-if="metricPersisted"
:delete-metric-url="customMetricsPath"
diff --git a/app/assets/javascripts/custom_metrics/index.js b/app/assets/javascripts/custom_metrics/index.js
index 4c279daf5f0..bf572217f5e 100644
--- a/app/assets/javascripts/custom_metrics/index.js
+++ b/app/assets/javascripts/custom_metrics/index.js
@@ -13,7 +13,7 @@ export default () => {
const domEl = document.querySelector(this.$options.el);
const {
customMetricsPath,
- editProjectServicePath,
+ editIntegrationPath,
validateQueryPath,
title,
query,
@@ -30,7 +30,7 @@ export default () => {
props: {
customMetricsPath,
metricPersisted,
- editProjectServicePath,
+ editIntegrationPath,
validateQueryPath,
formData: {
title,
diff --git a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue b/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
index af7334ecf2e..72a7659aac0 100644
--- a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
+++ b/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
@@ -1,10 +1,5 @@
<script>
-import {
- GlPath,
- GlPopover,
- GlDeprecatedSkeletonLoading as GlSkeletonLoading,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlPath, GlPopover, GlSkeletonLoader, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import Tracking from '~/tracking';
import { OVERVIEW_STAGE_ID } from '../constants';
import FormattedStageCount from './formatted_stage_count.vue';
@@ -13,7 +8,7 @@ export default {
name: 'PathNavigation',
components: {
GlPath,
- GlSkeletonLoading,
+ GlSkeletonLoader,
GlPopover,
FormattedStageCount,
},
@@ -57,7 +52,7 @@ export default {
};
</script>
<template>
- <gl-skeleton-loading v-if="loading" :lines="2" />
+ <gl-skeleton-loader v-if="loading" :width="235" :lines="2" />
<gl-path v-else :key="selectedStage.id" :items="stages" @selected="onSelectStage">
<template #default="{ pathItem, pathId }">
<gl-popover
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_table.vue b/app/assets/javascripts/cycle_analytics/components/stage_table.vue
index e4236968efc..85a40b89b77 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_table.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_table.vue
@@ -13,6 +13,7 @@ import { __ } from '~/locale';
import Tracking from '~/tracking';
import {
NOT_ENOUGH_DATA_ERROR,
+ FIELD_KEY_TITLE,
PAGINATION_SORT_FIELD_END_EVENT,
PAGINATION_SORT_FIELD_DURATION,
PAGINATION_SORT_DIRECTION_ASC,
@@ -22,7 +23,8 @@ import TotalTime from './total_time.vue';
const DEFAULT_WORKFLOW_TITLE_PROPERTIES = {
thClass: 'gl-w-half',
- key: PAGINATION_SORT_FIELD_END_EVENT,
+ key: FIELD_KEY_TITLE,
+ sortable: false,
};
const WORKFLOW_COLUMN_TITLES = {
@@ -132,14 +134,16 @@ export default {
return [
this.workflowTitle,
{
+ key: PAGINATION_SORT_FIELD_END_EVENT,
+ label: __('Last event'),
+ sortable: this.sortable,
+ },
+ {
key: PAGINATION_SORT_FIELD_DURATION,
- label: __('Time'),
- thClass: 'gl-w-half',
+ label: __('Duration'),
+ sortable: this.sortable,
},
- ].map((field) => ({
- ...field,
- sortable: this.sortable,
- }));
+ ];
},
prevPage() {
return Math.max(this.pagination.page - 1, 0);
@@ -182,7 +186,7 @@ export default {
</script>
<template>
<div data-testid="vsa-stage-table">
- <gl-loading-icon v-if="isLoading" class="gl-mt-4" size="md" />
+ <gl-loading-icon v-if="isLoading" class="gl-mt-4" size="lg" />
<gl-empty-state
v-else-if="isEmptyStage"
:title="emptyStateTitleText"
@@ -201,7 +205,7 @@ export default {
:empty-text="emptyStateMessage"
@sort-changed="onSort"
>
- <template v-if="stageCount" #head(end_event)="data">
+ <template v-if="stageCount" #head(title)="data">
<span>{{ data.label }}</span
><gl-badge class="gl-ml-2" size="sm"
><formatted-stage-count :stage-count="stageCount"
@@ -210,7 +214,10 @@ export default {
<template #head(duration)="data">
<span data-testid="vsa-stage-header-duration">{{ data.label }}</span>
</template>
- <template #cell(end_event)="{ item }">
+ <template #head(end_event)="data">
+ <span data-testid="vsa-stage-header-last-event">{{ data.label }}</span>
+ </template>
+ <template #cell(title)="{ item }">
<div data-testid="vsa-stage-event">
<div v-if="item.id" data-testid="vsa-stage-content">
<p class="gl-m-0">
@@ -282,6 +289,9 @@ export default {
<template #cell(duration)="{ item }">
<total-time :time="item.totalTime" data-testid="vsa-stage-event-time" />
</template>
+ <template #cell(end_event)="{ item }">
+ <span data-testid="vsa-stage-last-event">{{ item.endEventTimestamp }}</span>
+ </template>
</gl-table>
<gl-pagination
v-if="pagination && !isLoading && !isEmptyStage"
diff --git a/app/assets/javascripts/cycle_analytics/constants.js b/app/assets/javascripts/cycle_analytics/constants.js
index f0b2bd9dc5b..2758d686fb1 100644
--- a/app/assets/javascripts/cycle_analytics/constants.js
+++ b/app/assets/javascripts/cycle_analytics/constants.js
@@ -22,6 +22,7 @@ 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 FIELD_KEY_TITLE = 'title';
export const I18N_VSA_ERROR_STAGES = __(
'There was an error fetching value stream analytics stages.',
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/render.js b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
index 37287b9d981..f10c2d82b61 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/render.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
@@ -107,10 +107,10 @@ function createLink(data, selected, options, index) {
}
if (options.trackSuggestionClickedLabel) {
- link.setAttribute('data-track-action', 'click_text');
- link.setAttribute('data-track-label', options.trackSuggestionClickedLabel);
- link.setAttribute('data-track-value', index);
- link.setAttribute('data-track-property', slugify(data.category || 'no-category'));
+ link.dataset.trackAction = 'click_text';
+ link.dataset.trackLabel = options.trackSuggestionClickedLabel;
+ link.dataset.trackValue = index;
+ link.dataset.trackProperty = slugify(data.category || 'no-category');
}
link.classList.toggle('is-active', selected);
diff --git a/app/assets/javascripts/design_management/components/delete_button.vue b/app/assets/javascripts/design_management/components/delete_button.vue
index ae2ce7c3e5e..dec1038d2e3 100644
--- a/app/assets/javascripts/design_management/components/delete_button.vue
+++ b/app/assets/javascripts/design_management/components/delete_button.vue
@@ -26,7 +26,7 @@ export default {
buttonVariant: {
type: String,
required: false,
- default: 'info',
+ default: 'default',
},
buttonCategory: {
type: String,
diff --git a/app/assets/javascripts/design_management/components/design_presentation.vue b/app/assets/javascripts/design_management/components/design_presentation.vue
index 5116bacefa5..6bdd8568625 100644
--- a/app/assets/javascripts/design_management/components/design_presentation.vue
+++ b/app/assets/javascripts/design_management/components/design_presentation.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab/ui';
import { throttle } from 'lodash';
import { isLoggedIn } from '~/lib/utils/common_utils';
import DesignOverlay from './design_overlay.vue';
@@ -10,6 +11,7 @@ export default {
components: {
DesignImage,
DesignOverlay,
+ GlLoadingIcon,
},
props: {
image: {
@@ -40,6 +42,10 @@ export default {
type: Boolean,
required: true,
},
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
@@ -299,7 +305,12 @@ export default {
@touchend="onPresentationMouseup"
@touchcancel="onPresentationMouseup"
>
- <div class="gl-h-full gl-w-full gl-display-flex gl-align-items-center gl-relative">
+ <gl-loading-icon
+ v-if="isLoading"
+ size="xl"
+ class="gl-display-flex gl-h-full gl-align-items-center"
+ />
+ <div v-else class="gl-h-full gl-w-full gl-display-flex gl-align-items-center gl-relative">
<design-image
v-if="image"
:image="image"
diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue
index 81d0b6d0df4..8a6dd17a25b 100644
--- a/app/assets/javascripts/design_management/components/design_sidebar.vue
+++ b/app/assets/javascripts/design_management/components/design_sidebar.vue
@@ -1,5 +1,5 @@
<script>
-import { GlCollapse, GlButton, GlPopover } from '@gitlab/ui';
+import { GlCollapse, GlButton, GlPopover, GlSkeletonLoader } from '@gitlab/ui';
import { getCookie, setCookie, parseBoolean, isLoggedIn } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
@@ -20,6 +20,7 @@ export default {
GlCollapse,
GlButton,
GlPopover,
+ GlSkeletonLoader,
DesignTodoButton,
},
mixins: [glFeatureFlagsMixin()],
@@ -50,6 +51,10 @@ export default {
type: String,
required: true,
},
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
@@ -65,11 +70,11 @@ export default {
issue() {
return {
...this.design.issue,
- webPath: this.design.issue.webPath.substr(1),
+ webPath: this.design.issue?.webPath.substr(1),
};
},
discussionParticipants() {
- return extractParticipants(this.issue.participants.nodes);
+ return extractParticipants(this.issue.participants?.nodes || []);
},
resolvedDiscussions() {
return this.discussions.filter((discussion) => discussion.resolved);
@@ -142,91 +147,94 @@ export default {
:show-participant-label="false"
class="gl-mb-4"
/>
- <h2
- v-if="isLoggedIn && unresolvedDiscussions.length === 0"
- class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4"
- data-testid="new-discussion-disclaimer"
- >
- {{ s__("DesignManagement|Click the image where you'd like to start a new discussion") }}
- </h2>
- <design-note-signed-out
- v-if="!isLoggedIn"
- class="gl-mb-4"
- :register-path="registerPath"
- :sign-in-path="signInPath"
- :is-add-discussion="true"
- />
- <design-discussion
- v-for="discussion in unresolvedDiscussions"
- :key="discussion.id"
- :discussion="discussion"
- :design-id="$route.params.id"
- :noteable-id="design.id"
- :markdown-preview-path="markdownPreviewPath"
- :register-path="registerPath"
- :sign-in-path="signInPath"
- :resolved-discussions-expanded="resolvedDiscussionsExpanded"
- :discussion-with-open-form="discussionWithOpenForm"
- data-testid="unresolved-discussion"
- @create-note-error="$emit('onDesignDiscussionError', $event)"
- @update-note-error="$emit('updateNoteError', $event)"
- @resolve-discussion-error="$emit('resolveDiscussionError', $event)"
- @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
- @open-form="updateDiscussionWithOpenForm"
- />
- <template v-if="resolvedDiscussions.length > 0">
- <gl-button
- id="resolved-comments"
- ref="resolvedComments"
- data-testid="resolved-comments"
- :icon="resolvedCommentsToggleIcon"
- variant="link"
- class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-mb-4"
- @click="$emit('toggleResolvedComments')"
- >{{ $options.resolveCommentsToggleText }} ({{ resolvedDiscussions.length }})
- </gl-button>
- <gl-popover
- v-if="!isResolvedCommentsPopoverHidden"
- :show="!isResolvedCommentsPopoverHidden"
- target="resolved-comments"
- container="popovercontainer"
- placement="top"
- :title="s__('DesignManagement|Resolved Comments')"
+ <gl-skeleton-loader v-if="isLoading" />
+ <template v-else>
+ <h2
+ v-if="isLoggedIn && unresolvedDiscussions.length === 0"
+ class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4"
+ data-testid="new-discussion-disclaimer"
>
- <p>
- {{
- s__(
- 'DesignManagement|Comments you resolve can be viewed and unresolved by going to the "Resolved Comments" section below',
- )
- }}
- </p>
- <a
- href="https://docs.gitlab.com/ee/user/project/issues/design_management.html#resolve-design-threads"
- rel="noopener noreferrer"
- target="_blank"
- >{{ s__('DesignManagement|Learn more about resolving comments') }}</a
+ {{ s__("DesignManagement|Click the image where you'd like to start a new discussion") }}
+ </h2>
+ <design-note-signed-out
+ v-if="!isLoggedIn"
+ class="gl-mb-4"
+ :register-path="registerPath"
+ :sign-in-path="signInPath"
+ :is-add-discussion="true"
+ />
+ <design-discussion
+ v-for="discussion in unresolvedDiscussions"
+ :key="discussion.id"
+ :discussion="discussion"
+ :design-id="$route.params.id"
+ :noteable-id="design.id"
+ :markdown-preview-path="markdownPreviewPath"
+ :register-path="registerPath"
+ :sign-in-path="signInPath"
+ :resolved-discussions-expanded="resolvedDiscussionsExpanded"
+ :discussion-with-open-form="discussionWithOpenForm"
+ data-testid="unresolved-discussion"
+ @create-note-error="$emit('onDesignDiscussionError', $event)"
+ @update-note-error="$emit('updateNoteError', $event)"
+ @resolve-discussion-error="$emit('resolveDiscussionError', $event)"
+ @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
+ @open-form="updateDiscussionWithOpenForm"
+ />
+ <template v-if="resolvedDiscussions.length > 0">
+ <gl-button
+ id="resolved-comments"
+ ref="resolvedComments"
+ data-testid="resolved-comments"
+ :icon="resolvedCommentsToggleIcon"
+ variant="link"
+ class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-mb-4"
+ @click="$emit('toggleResolvedComments')"
+ >{{ $options.resolveCommentsToggleText }} ({{ resolvedDiscussions.length }})
+ </gl-button>
+ <gl-popover
+ v-if="!isResolvedCommentsPopoverHidden"
+ :show="!isResolvedCommentsPopoverHidden"
+ target="resolved-comments"
+ container="popovercontainer"
+ placement="top"
+ :title="s__('DesignManagement|Resolved Comments')"
>
- </gl-popover>
- <gl-collapse :visible="resolvedDiscussionsExpanded" class="gl-mt-3">
- <design-discussion
- v-for="discussion in resolvedDiscussions"
- :key="discussion.id"
- :discussion="discussion"
- :design-id="$route.params.id"
- :noteable-id="design.id"
- :markdown-preview-path="markdownPreviewPath"
- :register-path="registerPath"
- :sign-in-path="signInPath"
- :resolved-discussions-expanded="resolvedDiscussionsExpanded"
- :discussion-with-open-form="discussionWithOpenForm"
- data-testid="resolved-discussion"
- @error="$emit('onDesignDiscussionError', $event)"
- @updateNoteError="$emit('updateNoteError', $event)"
- @open-form="updateDiscussionWithOpenForm"
- @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
- />
- </gl-collapse>
+ <p>
+ {{
+ s__(
+ 'DesignManagement|Comments you resolve can be viewed and unresolved by going to the "Resolved Comments" section below',
+ )
+ }}
+ </p>
+ <a
+ href="https://docs.gitlab.com/ee/user/project/issues/design_management.html#resolve-design-threads"
+ rel="noopener noreferrer"
+ target="_blank"
+ >{{ s__('DesignManagement|Learn more about resolving comments') }}</a
+ >
+ </gl-popover>
+ <gl-collapse :visible="resolvedDiscussionsExpanded" class="gl-mt-3">
+ <design-discussion
+ v-for="discussion in resolvedDiscussions"
+ :key="discussion.id"
+ :discussion="discussion"
+ :design-id="$route.params.id"
+ :noteable-id="design.id"
+ :markdown-preview-path="markdownPreviewPath"
+ :register-path="registerPath"
+ :sign-in-path="signInPath"
+ :resolved-discussions-expanded="resolvedDiscussionsExpanded"
+ :discussion-with-open-form="discussionWithOpenForm"
+ data-testid="resolved-discussion"
+ @error="$emit('onDesignDiscussionError', $event)"
+ @updateNoteError="$emit('updateNoteError', $event)"
+ @open-form="updateDiscussionWithOpenForm"
+ @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
+ />
+ </gl-collapse>
+ </template>
+ <slot name="reply-form"></slot>
</template>
- <slot name="reply-form"></slot>
</div>
</template>
diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue
index b6163491abc..3092b8554ac 100644
--- a/app/assets/javascripts/design_management/components/list/item.vue
+++ b/app/assets/javascripts/design_management/components/list/item.vue
@@ -145,7 +145,7 @@ export default {
</span>
</div>
<gl-intersection-observer @appear="onAppear">
- <gl-loading-icon v-if="showLoadingSpinner" size="md" />
+ <gl-loading-icon v-if="showLoadingSpinner" size="lg" />
<gl-icon
v-else-if="showImageErrorIcon"
name="media-broken"
diff --git a/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue
index 3ebcde817f9..0bbbc795fff 100644
--- a/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue
+++ b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue
@@ -87,7 +87,7 @@ export default {
:disabled="!previousDesign"
:title="$options.i18n.previousButton"
:aria-label="$options.i18n.previousButton"
- icon="angle-left"
+ icon="chevron-lg-left"
class="js-previous-design"
@click="navigateToDesign(previousDesign)"
/>
@@ -96,7 +96,7 @@ export default {
:disabled="!nextDesign"
:title="$options.i18n.nextButton"
:aria-label="$options.i18n.nextButton"
- icon="angle-right"
+ icon="chevron-lg-right"
class="js-next-design"
@click="navigateToDesign(nextDesign)"
/>
diff --git a/app/assets/javascripts/design_management/components/toolbar/index.vue b/app/assets/javascripts/design_management/components/toolbar/index.vue
index b84fe45b77e..6d571365306 100644
--- a/app/assets/javascripts/design_management/components/toolbar/index.vue
+++ b/app/assets/javascripts/design_management/components/toolbar/index.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlIcon, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui';
import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
import { __, s__, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@@ -14,6 +14,7 @@ export default {
components: {
GlButton,
GlIcon,
+ GlSkeletonLoader,
DesignNavigation,
DeleteButton,
},
@@ -61,6 +62,10 @@ export default {
type: String,
required: true,
},
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
@@ -113,7 +118,8 @@ export default {
<gl-icon name="close" />
</router-link>
<div class="gl-overflow-hidden gl-display-flex gl-align-items-center">
- <h2 class="gl-m-0 str-truncated-100 gl-font-base">{{ filename }}</h2>
+ <gl-skeleton-loader v-if="isLoading" :lines="1" />
+ <h2 v-else class="gl-m-0 str-truncated-100 gl-font-base">{{ filename }}</h2>
<small v-if="updatedAt" class="gl-text-gray-500">{{ updatedText }}</small>
</div>
</div>
@@ -130,7 +136,7 @@ export default {
v-gl-tooltip.bottom
class="gl-ml-3"
:is-deleting="isDeleting"
- button-variant="warning"
+ button-variant="default"
button-icon="archive"
button-category="secondary"
:title="s__('DesignManagement|Archive design')"
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index 2b395921ee1..1825ce7f092 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import { GlAlert } from '@gitlab/ui';
import { isNull } from 'lodash';
import Mousetrap from 'mousetrap';
import { ApolloMutation } from 'vue-apollo';
@@ -56,7 +56,6 @@ export default {
DesignScaler,
DesignDestroyer,
Toolbar,
- GlLoadingIcon,
GlAlert,
DesignSidebar,
},
@@ -118,10 +117,8 @@ export default {
},
},
computed: {
- isFirstLoading() {
- // We only want to show spinner on initial design load (when opened from a deep link to design)
- // If we already have cached a design, loading shouldn't be indicated to user
- return this.$apollo.queries.design.loading && !this.design.filename;
+ isLoading() {
+ return this.$apollo.queries.design.loading;
},
discussions() {
if (!this.design.discussions) {
@@ -343,88 +340,88 @@ export default {
<div
class="design-detail js-design-detail fixed-top gl-w-full gl-bottom-0 gl-display-flex gl-justify-content-center gl-flex-direction-column gl-lg-flex-direction-row"
>
- <gl-loading-icon v-if="isFirstLoading" size="xl" class="gl-align-self-center" />
- <template v-else>
- <div
- class="gl-display-flex gl-overflow-hidden gl-flex-grow-1 gl-flex-direction-column gl-relative"
+ <div
+ class="gl-display-flex gl-overflow-hidden gl-flex-grow-1 gl-flex-direction-column gl-relative"
+ >
+ <design-destroyer
+ :filenames="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
+ design.filename,
+ ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ :project-path="projectPath"
+ :iid="issueIid"
+ @done="$router.push({ name: $options.DESIGNS_ROUTE_NAME })"
+ @error="onDesignDeleteError"
>
- <design-destroyer
- :filenames="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
- design.filename,
- ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
- :project-path="projectPath"
- :iid="issueIid"
- @done="$router.push({ name: $options.DESIGNS_ROUTE_NAME })"
- @error="onDesignDeleteError"
- >
- <template #default="{ mutate, loading }">
- <toolbar
- :id="id"
- :is-deleting="loading"
- :is-latest-version="isLatestVersion"
- v-bind="design"
- @delete="mutate"
- />
- </template>
- </design-destroyer>
+ <template #default="{ mutate, loading }">
+ <toolbar
+ :id="id"
+ :is-deleting="loading"
+ :is-latest-version="isLatestVersion"
+ :is-loading="isLoading"
+ v-bind="design"
+ @delete="mutate"
+ />
+ </template>
+ </design-destroyer>
- <div v-if="errorMessage" class="gl-p-5">
- <gl-alert variant="danger" @dismiss="errorMessage = null">
- {{ errorMessage }}
- </gl-alert>
- </div>
- <design-presentation
- :image="design.image"
- :image-name="design.filename"
- :discussions="discussions"
- :is-annotating="isAnnotating"
- :scale="scale"
- :resolved-discussions-expanded="resolvedDiscussionsExpanded"
- @openCommentForm="openCommentForm"
- @closeCommentForm="closeCommentForm"
- @moveNote="onMoveNote"
- @setMaxScale="setMaxScale"
- />
-
- <div
- class="design-scaler-wrapper gl-absolute gl-mb-6 gl-display-flex gl-justify-content-center gl-align-items-center"
- >
- <design-scaler :max-scale="maxScale" @scale="scale = $event" />
- </div>
+ <div v-if="errorMessage" class="gl-p-5">
+ <gl-alert variant="danger" @dismiss="errorMessage = null">
+ {{ errorMessage }}
+ </gl-alert>
</div>
- <design-sidebar
- :design="design"
+ <design-presentation
+ :image="design.image"
+ :image-name="design.filename"
+ :discussions="discussions"
+ :is-annotating="isAnnotating"
+ :scale="scale"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
- :markdown-preview-path="markdownPreviewPath"
- @onDesignDiscussionError="onDesignDiscussionError"
- @onCreateImageDiffNoteError="onCreateImageDiffNoteError"
- @updateNoteError="onUpdateNoteError"
- @resolveDiscussionError="onResolveDiscussionError"
- @toggleResolvedComments="toggleResolvedComments"
- @todoError="onTodoError"
+ :is-loading="isLoading"
+ @openCommentForm="openCommentForm"
+ @closeCommentForm="closeCommentForm"
+ @moveNote="onMoveNote"
+ @setMaxScale="setMaxScale"
+ />
+
+ <div
+ class="design-scaler-wrapper gl-absolute gl-mb-6 gl-display-flex gl-justify-content-center gl-align-items-center"
>
- <template #reply-form>
- <apollo-mutation
- v-if="isAnnotating"
- #default="{ mutate, loading }"
- :mutation="$options.createImageDiffNoteMutation"
- :variables="{
- input: mutationPayload,
- }"
- :update="addImageDiffNoteToStore"
- @done="closeCommentForm"
- @error="onCreateImageDiffNoteError"
- >
- <design-reply-form
- ref="newDiscussionForm"
- v-model="comment"
- :is-saving="loading"
- :markdown-preview-path="markdownPreviewPath"
- @submit-form="mutate"
- @cancel-form="closeCommentForm"
- /> </apollo-mutation
- ></template>
- </design-sidebar>
- </template>
+ <design-scaler :max-scale="maxScale" @scale="scale = $event" />
+ </div>
+ </div>
+ <design-sidebar
+ :design="design"
+ :resolved-discussions-expanded="resolvedDiscussionsExpanded"
+ :markdown-preview-path="markdownPreviewPath"
+ :is-loading="isLoading"
+ @onDesignDiscussionError="onDesignDiscussionError"
+ @onCreateImageDiffNoteError="onCreateImageDiffNoteError"
+ @updateNoteError="onUpdateNoteError"
+ @resolveDiscussionError="onResolveDiscussionError"
+ @toggleResolvedComments="toggleResolvedComments"
+ @todoError="onTodoError"
+ >
+ <template #reply-form>
+ <apollo-mutation
+ v-if="isAnnotating"
+ #default="{ mutate, loading }"
+ :mutation="$options.createImageDiffNoteMutation"
+ :variables="{
+ input: mutationPayload,
+ }"
+ :update="addImageDiffNoteToStore"
+ @done="closeCommentForm"
+ @error="onCreateImageDiffNoteError"
+ >
+ <design-reply-form
+ ref="newDiscussionForm"
+ v-model="comment"
+ :is-saving="loading"
+ :markdown-preview-path="markdownPreviewPath"
+ @submit-form="mutate"
+ @cancel-form="closeCommentForm"
+ /> </apollo-mutation
+ ></template>
+ </design-sidebar>
</div>
</template>
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index 42d5d8fb359..f81d4f6662f 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -1,5 +1,6 @@
<script>
import { GlLoadingIcon, GlButton, GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import VueDraggable from 'vuedraggable';
import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
@@ -97,6 +98,9 @@ export default {
isSaving() {
return this.filesToBeSaved.length > 0;
},
+ isMobile() {
+ return GlBreakpointInstance.getBreakpointSize() === 'xs';
+ },
canCreateDesign() {
return this.permissions.createDesign;
},
@@ -354,7 +358,7 @@ export default {
>
<header
v-if="showToolbar"
- class="row-content-block gl-border-t-0 gl-py-3 gl-display-flex"
+ class="gl-display-flex gl-my-0 gl-text-gray-900"
data-testid="design-toolbar-wrapper"
>
<div
@@ -370,7 +374,7 @@ export default {
>
<gl-button
v-if="isLatestVersion"
- variant="link"
+ category="tertiary"
size="small"
class="gl-mr-3"
data-testid="select-all-designs-button"
@@ -407,7 +411,7 @@ export default {
</div>
</header>
<div class="gl-mt-6">
- <gl-loading-icon v-if="isLoading" size="md" />
+ <gl-loading-icon v-if="isLoading" size="lg" />
<gl-alert v-else-if="error" variant="danger" :dismissible="false">
{{ __('An error occurred while loading designs. Please try again.') }}
</gl-alert>
@@ -429,7 +433,7 @@ export default {
<vue-draggable
v-else
:value="designs"
- :disabled="!isLatestVersion || isReorderingInProgress"
+ :disabled="!isLatestVersion || isReorderingInProgress || isMobile"
v-bind="$options.dragOptions"
tag="ol"
draggable=".js-design-tile"
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index a12829f8420..9f3fb715150 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -26,7 +26,8 @@ export default class Diff {
FilesCommentButton.init($diffFile);
const firstFile = $('.files').first().get(0);
- const canCreateNote = firstFile && firstFile.hasAttribute('data-can-create-note');
+ const canCreateNote =
+ firstFile && Object.prototype.hasOwnProperty.call(firstFile.dataset, 'canCreateNote');
$diffFile.each((index, file) => initImageDiffHelper.initImageDiff(file, canCreateNote));
if (!isBound) {
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index c3436159cea..530f3a3a7f7 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -384,14 +384,26 @@ export default {
this.unwatchDiscussions = this.$watch(
() => `${this.diffFiles.length}:${this.$store.state.notes.discussions.length}`,
- () => this.setDiscussions(),
+ () => {
+ this.setDiscussions();
+
+ if (
+ this.$store.state.notes.doneFetchingBatchDiscussions &&
+ window.gon?.features?.paginatedMrDiscussions
+ ) {
+ this.unwatchDiscussions();
+ }
+ },
);
this.unwatchRetrievingBatches = this.$watch(
() => `${this.retrievingBatches}:${this.$store.state.notes.discussions.length}`,
() => {
if (!this.retrievingBatches && this.$store.state.notes.discussions.length) {
- this.unwatchDiscussions();
+ if (!window.gon?.features?.paginatedMrDiscussions) {
+ this.unwatchDiscussions();
+ }
+
this.unwatchRetrievingBatches();
}
},
diff --git a/app/assets/javascripts/diffs/components/collapsed_files_warning.vue b/app/assets/javascripts/diffs/components/collapsed_files_warning.vue
index b7eea32e699..ebb6ec1e7c8 100644
--- a/app/assets/javascripts/diffs/components/collapsed_files_warning.vue
+++ b/app/assets/javascripts/diffs/components/collapsed_files_warning.vue
@@ -55,7 +55,7 @@ export default {
{{ __('For a faster browsing experience, some files are collapsed by default.') }}
</p>
<template #actions>
- <gl-button category="secondary" variant="warning" class="gl-alert-action" @click="expand">
+ <gl-button class="gl-alert-action" @click="expand">
{{ __('Expand all files') }}
</gl-button>
</template>
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 42f4ea8eb58..54b648e8d03 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -7,8 +7,6 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import initUserPopovers from '../../user_popovers';
-
/**
* CommitItem
*
@@ -82,11 +80,6 @@ export default {
return this.commit.description_html.replace(/^&#x000A;/, '');
},
},
- created() {
- this.$nextTick(() => {
- initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
- });
- },
safeHtmlConfig: {
ADD_TAGS: ['gl-emoji'],
},
@@ -128,7 +121,7 @@ export default {
<div class="d-flex float-left align-items-center align-self-start">
<input
v-if="isSelectable"
- class="mr-2"
+ class="gl-mr-3"
type="checkbox"
:checked="checked"
@change="$emit('handleCheckboxChange', $event.target.checked)"
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index 1eba12a3ae9..bfe35e9346d 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -120,7 +120,7 @@ export default {
:help-page-path="helpPagePath"
:inline="isInlineView"
/>
- <gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" />
+ <gl-loading-icon v-if="diffFile.renderingLines" size="lg" class="mt-3" />
</template>
<not-diffable-viewer v-else-if="notDiffable" />
<no-preview-viewer v-else-if="noPreview" />
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index 4e7dc578193..fc5766a23ef 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -3,7 +3,6 @@ import { GlTooltipDirective, GlSafeHtmlDirective, GlIcon, GlLoadingIcon } from '
import { mapActions } from 'vuex';
import createFlash from '~/flash';
import { s__, sprintf } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { UNFOLD_COUNT, INLINE_DIFF_LINES_KEY } from '../constants';
import * as utils from '../store/utils';
@@ -24,7 +23,6 @@ export default {
GlTooltip: GlTooltipDirective,
SafeHtml: GlSafeHtmlDirective,
},
- mixins: [glFeatureFlagsMixin()],
props: {
file: {
type: Object,
@@ -93,25 +91,16 @@ export default {
nextLineNumbers = {},
) {
this.loadMoreLines({ endpoint, params, lineNumbers, fileHash, isExpandDown, nextLineNumbers })
- .then(() => {
- this.isRequesting = false;
- })
.catch(() => {
createFlash({
message: s__('Diffs|Something went wrong while fetching diff lines.'),
});
- this.isRequesting = false;
})
.finally(() => {
this.loading = { up: false, down: false, all: false };
});
},
handleExpandLines(type = EXPAND_ALL) {
- if (this.isRequesting) {
- return;
- }
-
- this.isRequesting = true;
const endpoint = this.file.context_lines_path;
const oldLineNumber = this.line.meta_data.old_pos || 0;
const newLineNumber = this.line.meta_data.new_pos || 0;
@@ -228,10 +217,7 @@ export default {
</script>
<template>
- <div
- v-if="glFeatures.updatedDiffExpansionButtons"
- class="diff-grid-row diff-grid-row-full diff-tr line_holder match expansion"
- >
+ <div class="diff-grid-row diff-grid-row-full diff-tr line_holder match expansion">
<div :class="{ parallel: !inline }" class="diff-grid-left diff-grid-2-col left-side">
<div
class="diff-td diff-line-num gl-text-center! gl-p-0! gl-w-full! gl-display-flex gl-flex-direction-column"
@@ -240,6 +226,7 @@ export default {
v-if="showExpandDown"
v-gl-tooltip.left
:title="s__('Diffs|Next 20 lines')"
+ :disabled="loading.down"
type="button"
class="js-unfold-down gl-rounded-0 gl-border-0 diff-line-expand-button"
@click="handleExpandLines($options.EXPAND_DOWN)"
@@ -251,6 +238,7 @@ export default {
v-if="lineCountBetween !== -1 && lineCountBetween < 20"
v-gl-tooltip.left
:title="s__('Diffs|Expand all lines')"
+ :disabled="loading.all"
type="button"
class="js-unfold-all gl-rounded-0 gl-border-0 diff-line-expand-button"
@click="handleExpandLines()"
@@ -262,6 +250,7 @@ export default {
v-if="showExpandUp"
v-gl-tooltip.left
:title="s__('Diffs|Previous 20 lines')"
+ :disabled="loading.up"
type="button"
class="js-unfold gl-rounded-0 gl-border-0 diff-line-expand-button"
@click="handleExpandLines($options.EXPAND_UP)"
@@ -276,32 +265,4 @@ export default {
></div>
</div>
</div>
- <div v-else class="content js-line-expansion-content">
- <button
- type="button"
- :disabled="!canExpandDown"
- class="js-unfold-down gl-mx-2 gl-py-4 gl-cursor-pointer"
- @click="handleExpandLines($options.EXPAND_DOWN)"
- >
- <gl-icon :size="12" name="expand-down" />
- <span>{{ $options.i18n.showMore }}</span>
- </button>
- <button
- type="button"
- class="js-unfold-all gl-mx-2 gl-py-4 gl-cursor-pointer"
- @click="handleExpandLines()"
- >
- <gl-icon :size="12" name="expand" />
- <span>{{ $options.i18n.showAll }}</span>
- </button>
- <button
- type="button"
- :disabled="!canExpandUp"
- class="js-unfold gl-mx-2 gl-py-4 gl-cursor-pointer"
- @click="handleExpandLines($options.EXPAND_UP)"
- >
- <gl-icon :size="12" name="expand-up" />
- <span>{{ $options.i18n.showMore }}</span>
- </button>
- </div>
</template>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 0b82be7140c..aec608007d5 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -456,12 +456,7 @@ export default {
<p class="gl-mb-5">
{{ $options.i18n.autoCollapsed }}
</p>
- <gl-button
- data-testid="expand-button"
- category="secondary"
- variant="warning"
- @click.prevent="handleToggle"
- >
+ <gl-button data-testid="expand-button" @click.prevent="handleToggle">
{{ $options.i18n.expand }}
</gl-button>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index a75262ee303..07316f9433a 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -19,8 +19,6 @@ import { scrollToElement } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import FileIcon from '~/vue_shared/components/file_icon.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { DIFF_FILE_AUTOMATIC_COLLAPSE } from '../constants';
import { DIFF_FILE_HEADER } from '../i18n';
@@ -33,7 +31,6 @@ export default {
components: {
ClipboardButton,
GlIcon,
- FileIcon,
DiffStats,
GlBadge,
GlButton,
@@ -48,7 +45,7 @@ export default {
GlTooltip: GlTooltipDirective,
SafeHtml: GlSafeHtmlDirective,
},
- mixins: [glFeatureFlagsMixin(), IdState({ idProp: (vm) => vm.diffFile.file_hash })],
+ mixins: [IdState({ idProp: (vm) => vm.diffFile.file_hash })],
i18n: {
...DIFF_FILE_HEADER,
compareButtonLabel: __('Compare submodule commit revisions'),
@@ -301,14 +298,6 @@ export default {
:href="titleLink"
@click="handleFileNameClick"
>
- <file-icon
- v-if="!glFeatures.removeDiffHeaderIcons"
- :file-name="filePath"
- :size="16"
- aria-hidden="true"
- css-classes="gl-mr-2"
- :submodule="diffFile.submodule"
- />
<span v-if="isFileRenamed">
<strong
v-gl-tooltip
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index a2f0e2c2653..ebc68bafb9a 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -3,6 +3,7 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import { s__, __ } from '~/locale';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MultilineCommentForm from '~/notes/components/multiline_comment_form.vue';
import { commentLineOptions, formatLineRange } from '~/notes/components/multiline_comment_utils';
@@ -175,7 +176,10 @@ export default {
'saveDiffDiscussion',
'setSuggestPopoverDismissed',
]),
- async handleCancelCommentForm(shouldConfirm, isDirty) {
+ handleCancelCommentForm: ignoreWhilePending(async function handleCancelCommentForm(
+ shouldConfirm,
+ isDirty,
+ ) {
if (shouldConfirm && isDirty) {
const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
@@ -195,7 +199,7 @@ export default {
this.$nextTick(() => {
this.resetAutoSave();
});
- },
+ }),
handleSaveNote(note) {
return this.saveDiffDiscussion({ note, formData: this.formData }).then(() =>
this.handleCancelCommentForm(),
diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue
index 529f8e0a2f9..d740d5adcb6 100644
--- a/app/assets/javascripts/diffs/components/diff_view.vue
+++ b/app/assets/javascripts/diffs/components/diff_view.vue
@@ -6,7 +6,6 @@ import DraftNote from '~/batch_comments/components/draft_note.vue';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
import { hide } from '~/tooltips';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { pickDirection } from '../utils/diff_line';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
@@ -23,11 +22,7 @@ export default {
directives: {
SafeHtml,
},
- mixins: [
- draftCommentsMixin,
- glFeatureFlagsMixin(),
- IdState({ idProp: (vm) => vm.diffFile.file_hash }),
- ],
+ mixins: [draftCommentsMixin, IdState({ idProp: (vm) => vm.diffFile.file_hash })],
props: {
diffFile: {
type: Object,
@@ -171,7 +166,6 @@ export default {
<template v-for="(line, index) in diffLines">
<template v-if="line.isMatchLineLeft || line.isMatchLineRight">
<diff-expansion-cell
- v-if="glFeatures.updatedDiffExpansionButtons"
:key="`expand-${index}`"
:file="diffFile"
:line="line.left"
@@ -180,41 +174,6 @@ export default {
:inline="inline"
:line-count-between="getCountBetweenIndex(index)"
/>
- <template v-else>
- <div :key="`expand-${index}`" class="diff-tr line_expansion old-line_expansion match">
- <div class="diff-td text-center gl-font-regular">
- <diff-expansion-cell
- :file="diffFile"
- :context-lines-path="diffFile.context_lines_path"
- :line="line.left"
- :is-top="index === 0"
- :is-bottom="index + 1 === diffLinesLength"
- :inline="inline"
- />
- </div>
- </div>
- <div
- v-if="line.left.rich_text"
- :key="`expand-definition-${index}`"
- class="diff-grid-row diff-tr line_holder match"
- >
- <div class="diff-grid-left diff-grid-3-col left-side">
- <div class="diff-td diff-line-num"></div>
- <div v-if="inline" class="diff-td diff-line-num"></div>
- <div
- v-safe-html="line.left.rich_text"
- class="diff-td line_content left-side gl-white-space-normal!"
- ></div>
- </div>
- <div v-if="!inline" class="diff-grid-right diff-grid-3-col right-side">
- <div class="diff-td diff-line-num"></div>
- <div
- v-safe-html="line.left.rich_text"
- class="diff-td line_content right-side gl-white-space-normal!"
- ></div>
- </div>
- </div>
- </template>
</template>
<diff-row
v-if="!line.isMatchLineLeft && !line.isMatchLineRight"
diff --git a/app/assets/javascripts/diffs/components/merge_conflict_warning.vue b/app/assets/javascripts/diffs/components/merge_conflict_warning.vue
index 6e1e6f5c2d0..c37a1d75650 100644
--- a/app/assets/javascripts/diffs/components/merge_conflict_warning.vue
+++ b/app/assets/javascripts/diffs/components/merge_conflict_warning.vue
@@ -44,8 +44,8 @@ export default {
<gl-button
v-if="resolutionPath"
:href="resolutionPath"
- variant="info"
- class="gl-mr-5 gl-alert-action"
+ variant="confirm"
+ class="gl-mr-3 gl-alert-action"
>
{{ __('Resolve conflicts') }}
</gl-button>
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 92f3cf83740..cf86ebea4a9 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -126,14 +126,6 @@ export function findDiffFile(files, match, matchKey = 'file_hash') {
return files.find((file) => file[matchKey] === match);
}
-export const getReversePosition = (linePosition) => {
- if (linePosition === LINE_POSITION_RIGHT) {
- return LINE_POSITION_LEFT;
- }
-
- return LINE_POSITION_RIGHT;
-};
-
export function getFormData(params) {
const {
commit,
diff --git a/app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js b/app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js
index b41eae88c54..b33dcba2b7d 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js
@@ -17,10 +17,14 @@ export class CiSchemaExtension {
const absoluteSchemaUrl = new URL(ciSchemaPath, gon.gitlab_url).href;
const modelFileName = instance.getModel().uri.path.split('/').pop();
- registerSchema({
- uri: absoluteSchemaUrl,
- fileMatch: [modelFileName],
- });
+ registerSchema(
+ {
+ uri: absoluteSchemaUrl,
+ fileMatch: [modelFileName],
+ },
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ { customTags: ['!reference sequence'] },
+ );
},
};
}
diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
index 11cc85c659d..e4ad0bf8e76 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
@@ -36,6 +36,8 @@ const setupDomElement = ({ injectToEl = null } = {}) => {
return previewEl;
};
+let dimResize = false;
+
export class EditorMarkdownPreviewExtension {
static get extensionName() {
return 'EditorMarkdownPreview';
@@ -50,6 +52,7 @@ export class EditorMarkdownPreviewExtension {
},
shown: false,
modelChangeListener: undefined,
+ layoutChangeListener: undefined,
path: setupOptions.previewMarkdownPath,
actionShowPreviewCondition: instance.createContextKey('toggleLivePreview', true),
};
@@ -59,6 +62,14 @@ export class EditorMarkdownPreviewExtension {
if (instance.toolbar) {
this.setupToolbar(instance);
}
+
+ this.preview.layoutChangeListener = instance.onDidLayoutChange(() => {
+ if (instance.markdownPreview?.shown && !dimResize) {
+ const { width } = instance.getLayoutInfo();
+ const newWidth = width * EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH;
+ EditorMarkdownPreviewExtension.resizePreviewLayout(instance, newWidth);
+ }
+ });
}
onBeforeUnuse(instance) {
@@ -70,6 +81,9 @@ export class EditorMarkdownPreviewExtension {
}
cleanup(instance) {
+ if (this.preview.layoutChangeListener) {
+ this.preview.layoutChangeListener.dispose();
+ }
if (this.preview.modelChangeListener) {
this.preview.modelChangeListener.dispose();
}
@@ -82,6 +96,15 @@ export class EditorMarkdownPreviewExtension {
this.preview.shown = false;
}
+ static resizePreviewLayout(instance, width) {
+ const { height } = instance.getLayoutInfo();
+ dimResize = true;
+ instance.layout({ width, height });
+ window.requestAnimationFrame(() => {
+ dimResize = false;
+ });
+ }
+
setupToolbar(instance) {
this.toolbarButtons = [
{
@@ -99,11 +122,11 @@ export class EditorMarkdownPreviewExtension {
}
togglePreviewLayout(instance) {
- const { width, height } = instance.getLayoutInfo();
+ const { width } = instance.getLayoutInfo();
const newWidth = this.preview.shown
? width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH
: width * EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH;
- instance.layout({ width: newWidth, height });
+ EditorMarkdownPreviewExtension.resizePreviewLayout(instance, newWidth);
}
togglePreviewPanel(instance) {
diff --git a/app/assets/javascripts/editor/extensions/source_editor_webide_ext.js b/app/assets/javascripts/editor/extensions/source_editor_webide_ext.js
index 4e8c11bac54..6270517b3f3 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_webide_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_webide_ext.js
@@ -7,7 +7,6 @@
* @property {Object} options The Monaco editor options
*/
-import { debounce } from 'lodash';
import { KeyCode, KeyMod, Range } from 'monaco-editor';
import { EDITOR_TYPE_DIFF } from '~/editor/constants';
import Disposable from '~/ide/lib/common/disposable';
@@ -59,13 +58,10 @@ const renderSideBySide = (domElement) => {
return domElement.offsetWidth >= 700;
};
-const updateInstanceDimensions = (instance) => {
- instance.layout();
- if (isDiffEditorType(instance)) {
- instance.updateOptions({
- renderSideBySide: renderSideBySide(instance.getDomNode()),
- });
- }
+const updateDiffInstanceRendering = (instance) => {
+ instance.updateOptions({
+ renderSideBySide: renderSideBySide(instance.getDomNode()),
+ });
};
export class EditorWebIdeExtension {
@@ -85,15 +81,14 @@ export class EditorWebIdeExtension {
this.options = setupOptions.options;
this.disposable = new Disposable();
- this.debouncedUpdate = debounce(() => {
- updateInstanceDimensions(instance);
- }, UPDATE_DIMENSIONS_DELAY);
-
addActions(instance, setupOptions.store);
- }
- onUse(instance) {
- window.addEventListener('resize', this.debouncedUpdate, false);
+ if (isDiffEditorType(instance)) {
+ updateDiffInstanceRendering(instance);
+ instance.getModifiedEditor().onDidLayoutChange(() => {
+ updateDiffInstanceRendering(instance);
+ });
+ }
instance.onDidDispose(() => {
this.onUnuse();
@@ -101,8 +96,6 @@ export class EditorWebIdeExtension {
}
onUnuse() {
- window.removeEventListener('resize', this.debouncedUpdate);
-
// catch any potential errors with disposing the error
// this is mainly for tests caused by elements not existing
try {
@@ -149,7 +142,6 @@ export class EditorWebIdeExtension {
modified: model.getModel(),
});
},
- updateDimensions: (instance) => updateInstanceDimensions(instance),
setPos: (instance, { lineNumber, column }) => {
instance.revealPositionInCenter({
lineNumber,
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 1352211b927..c8015f884b7 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://gitlab.com/.gitlab-ci.yml",
"title": "Gitlab CI configuration",
- "description": "Gitlab has a built-in solution for doing CI called Gitlab CI. It is configured by supplying a file called `.gitlab-ci.yml`, which will list all the jobs that are going to run for the project. A full list of all options can be found at https://docs.gitlab.com/ee/ci/yaml/. You can read more about Gitlab CI at https://docs.gitlab.com/ee/ci/README.html.",
+ "markdownDescription": "Gitlab has a built-in solution for doing CI called Gitlab CI. It is configured by supplying a file called `.gitlab-ci.yml`, which will list all the jobs that are going to run for the project. A full list of all options can be found [here](https://docs.gitlab.com/ee/ci/yaml). [Learn More](https://docs.gitlab.com/ee/ci/index.html).",
"type": "object",
"properties": {
"$schema": {
@@ -15,6 +15,7 @@
"after_script": { "$ref": "#/definitions/after_script" },
"variables": { "$ref": "#/definitions/globalVariables" },
"cache": { "$ref": "#/definitions/cache" },
+ "!reference": {"$ref" : "#/definitions/!reference"},
"default": {
"type": "object",
"properties": {
@@ -27,13 +28,14 @@
"retry": { "$ref": "#/definitions/retry" },
"services": { "$ref": "#/definitions/services" },
"tags": { "$ref": "#/definitions/tags" },
- "timeout": { "$ref": "#/definitions/timeout" }
+ "timeout": { "$ref": "#/definitions/timeout" },
+ "!reference": {"$ref" : "#/definitions/!reference"}
},
"additionalProperties": false
},
"stages": {
"type": "array",
- "description": "Groups jobs into stages. All jobs in one stage must complete before next stage is executed. Defaults to ['build', 'test', 'deploy'].",
+ "markdownDescription": "Groups jobs into stages. All jobs in one stage must complete before next stage is executed. Defaults to ['build', 'test', 'deploy']. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#stages).",
"default": ["build", "test", "deploy"],
"items": {
"type": "string"
@@ -42,7 +44,7 @@
"minItems": 1
},
"include": {
- "description": "Can be `IncludeItem` or `IncludeItem[]`. Each `IncludeItem` will be a string, or an object with properties for the method if including external YAML file. The external content will be fetched, included and evaluated along the `.gitlab-ci.yml`.",
+ "markdownDescription": "Can be `IncludeItem` or `IncludeItem[]`. Each `IncludeItem` will be a string, or an object with properties for the method if including external YAML file. The external content will be fetched, included and evaluated along the `.gitlab-ci.yml`. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#include).",
"oneOf": [
{ "$ref": "#/definitions/include_item" },
{
@@ -53,7 +55,7 @@
},
"pages": {
"$ref": "#/definitions/job",
- "description": "A special job used to upload static sites to Gitlab pages. Requires a `public/` directory with `artifacts.path` pointing to it."
+ "markdownDescription": "A special job used to upload static sites to Gitlab pages. Requires a `public/` directory with `artifacts.path` pointing to it. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#pages)."
},
"workflow": {
"type": "object",
@@ -61,7 +63,10 @@
"rules": {
"type": "array",
"items": {
- "type": "object",
+ "anyOf": [
+ {"type": "object"},
+ {"type": "array", "minLength": 1, "items": { "type": "string" }}
+ ],
"properties": {
"if": { "$ref": "#/definitions/if" },
"changes": { "$ref": "#/definitions/changes" },
@@ -93,12 +98,12 @@
"definitions": {
"artifacts": {
"type": "object",
- "description": "Used to specify a list of files and directories that should be attached to the job if it succeeds. Artifacts are sent to Gitlab where they can be downloaded.",
+ "markdownDescription": "Used to specify a list of files and directories that should be attached to the job if it succeeds. Artifacts are sent to Gitlab where they can be downloaded. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#artifacts).",
"additionalProperties": false,
"properties": {
"paths": {
"type": "array",
- "description": "A list of paths to files/folders that should be included in the artifact.",
+ "markdownDescription": "A list of paths to files/folders that should be included in the artifact. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#artifactspaths).",
"items": {
"type": "string"
},
@@ -106,7 +111,7 @@
},
"exclude": {
"type": "array",
- "description": "A list of paths to files/folders that should be excluded in the artifact.",
+ "markdownDescription": "A list of paths to files/folders that should be excluded in the artifact. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#artifactsexclude).",
"items": {
"type": "string"
},
@@ -114,19 +119,19 @@
},
"expose_as": {
"type": "string",
- "description": "Can be used to expose job artifacts in the merge request UI. GitLab will add a link <expose_as> to the relevant merge request that points to the artifact."
+ "markdownDescription": "Can be used to expose job artifacts in the merge request UI. GitLab will add a link <expose_as> to the relevant merge request that points to the artifact. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#artifactsexpose_as)."
},
"name": {
"type": "string",
- "description": "Name for the archive created on job success. Can use variables in the name, e.g. '$CI_JOB_NAME'"
+ "markdownDescription": "Name for the archive created on job success. Can use variables in the name, e.g. '$CI_JOB_NAME' [Learn More](https://docs.gitlab.com/ee/ci/yaml/#artifactsname)."
},
"untracked": {
"type": "boolean",
- "description": "Whether to add all untracked files (along with 'artifacts.paths') to the artifact.",
+ "markdownDescription": "Whether to add all untracked files (along with 'artifacts.paths') to the artifact. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#artifactsuntracked).",
"default": false
},
"when": {
- "description": "Configure when artifacts are uploaded depended on job status.",
+ "markdownDescription": "Configure when artifacts are uploaded depended on job status. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#artifactswhen).",
"default": "on_success",
"oneOf": [
{
@@ -145,12 +150,12 @@
},
"expire_in": {
"type": "string",
- "description": "How long artifacts should be kept. They are saved 30 days by default. Artifacts that have expired are removed periodically via cron job. Supports a wide variety of formats, e.g. '1 week', '3 mins 4 sec', '2 hrs 20 min', '2h20min', '6 mos 1 day', '47 yrs 6 mos and 4d', '3 weeks and 2 days'.",
+ "markdownDescription": "How long artifacts should be kept. They are saved 30 days by default. Artifacts that have expired are removed periodically via cron job. Supports a wide variety of formats, e.g. '1 week', '3 mins 4 sec', '2 hrs 20 min', '2h20min', '6 mos 1 day', '47 yrs 6 mos and 4d', '3 weeks and 2 days'. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#artifactsexpire_in).",
"default": "30 days"
},
"reports": {
"type": "object",
- "description": "Reports will be uploaded as artifacts, and often displayed in the Gitlab UI, such as in Merge Requests.",
+ "markdownDescription": "Reports will be uploaded as artifacts, and often displayed in the Gitlab UI, such as in Merge Requests. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#artifactsreports).",
"additionalProperties": false,
"properties": {
"junit": {
@@ -341,6 +346,13 @@
}
]
},
+ "!reference": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
"image": {
"oneOf": [
{
@@ -362,16 +374,43 @@
"type": "array",
"description": "Command or script that should be executed as the container's entrypoint. It will be translated to Docker's --entrypoint option while creating the container. The syntax is similar to Dockerfile's ENTRYPOINT directive, where each shell token is a separate string in the array.",
"minItems": 1
+ },
+ "pull_policy": {
+ "markdownDescription": "Specifies how to pull the image in Runner. It can be one of `always`, `never` or `if-not-present`. The default value is `always`. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#imagepull_policy).",
+ "default": "always",
+ "oneOf": [
+ {
+ "type": "string",
+ "enum": [
+ "always",
+ "never",
+ "if-not-present"
+ ]
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "always",
+ "never",
+ "if-not-present"
+ ]
+ },
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ ]
}
},
"required": ["name"]
}
],
- "description": "Specifies the docker image to use for the job or globally for all jobs. Job configuration takes precedence over global setting. Requires a certain kind of Gitlab runner executor."
+ "markdownDescription": "Specifies the docker image to use for the job or globally for all jobs. Job configuration takes precedence over global setting. Requires a certain kind of Gitlab runner executor. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#image)."
},
"services": {
"type": "array",
- "description": "Similar to `image` property, but will link the specified services to the `image` container.",
+ "markdownDescription": "Similar to `image` property, but will link the specified services to the `image` container. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#services).",
"items": {
"oneOf": [
{
@@ -418,7 +457,7 @@
},
"secrets": {
"type": "object",
- "description": "Defines secrets to be injected as environment variables",
+ "markdownDescription": "Defines secrets to be injected as environment variables. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#secrets).",
"additionalProperties": {
"type": "object",
"description": "Environment variable name",
@@ -453,7 +492,7 @@
},
"before_script": {
"type": "array",
- "description": "Defines scripts that should run *before* the job. Can be set globally or per job.",
+ "markdownDescription": "Defines scripts that should run *before* the job. Can be set globally or per job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#before_script).",
"items": {
"anyOf": [
{
@@ -470,7 +509,7 @@
},
"after_script": {
"type": "array",
- "description": "Defines scripts that should run *after* the job. Can be set globally or per job.",
+ "markdownDescription": "Defines scripts that should run *after* the job. Can be set globally or per job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#after_script).",
"items": {
"anyOf": [
{
@@ -487,27 +526,41 @@
},
"rules": {
"type": "array",
- "description": "Rules allows for an array of individual rule objects to be evaluated in order, until one matches and dynamically provides attributes to the job.",
+ "markdownDescription": "Rules allows for an array of individual rule objects to be evaluated in order, until one matches and dynamically provides attributes to the job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#rules).",
"items": {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "if": { "$ref": "#/definitions/if" },
- "changes": { "$ref": "#/definitions/changes" },
- "exists": { "$ref": "#/definitions/exists" },
- "variables": { "$ref": "#/definitions/variables" },
- "when": { "$ref": "#/definitions/when" },
- "start_in": { "$ref": "#/definitions/start_in" },
- "allow_failure": { "$ref": "#/definitions/allow_failure" }
- }
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "if": { "$ref": "#/definitions/if" },
+ "changes": { "$ref": "#/definitions/changes" },
+ "exists": { "$ref": "#/definitions/exists" },
+ "variables": { "$ref": "#/definitions/variables" },
+ "when": { "$ref": "#/definitions/when" },
+ "start_in": { "$ref": "#/definitions/start_in" },
+ "allow_failure": { "$ref": "#/definitions/allow_failure" }
+ }
+ },
+ {"type": "string", "minLength": 1},
+ {"type": "array", "minLength": 1, "items": { "type": "string" }}
+ ]
}
},
"globalVariables": {
- "description": "Defines environment variables globally. Job level property overrides global variables. If a job sets `variables: {}`, all global variables are turned off. You can use the value and description keywords to define variables that are prefilled when running a pipeline manually.",
- "type": "object",
+ "markdownDescription": "Defines environment variables globally. Job level property overrides global variables. If a job sets `variables: {}`, all global variables are turned off. You can use the value and description keywords to define variables that are prefilled when running a pipeline manually. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variables).",
+ "anyOf": [
+ {"type": "object"},
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ],
"additionalProperties": {
"anyOf": [
- {"type": ["string", "integer"]},
+ {"type": ["string", "integer", "array"]},
{
"type": "object",
"properties": {
@@ -523,41 +576,51 @@
},
"if": {
"type": "string",
- "description": "Expression to evaluate whether additional attributes should be provided to the job"
+ "markdownDescription": "Expression to evaluate whether additional attributes should be provided to the job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#rulesif)."
},
"changes": {
"type": "array",
- "description": "Additional attributes will be provided to job if any of the provided paths matches a modified file",
+ "markdownDescription": "Additional attributes will be provided to job if any of the provided paths matches a modified file. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#ruleschanges).",
"items": {
"type": "string"
}
},
"exists": {
"type": "array",
- "description": "Additional attributes will be provided to job if any of the provided paths matches an existing file in the repository",
+ "markdownDescription": "Additional attributes will be provided to job if any of the provided paths matches an existing file in the repository. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#rulesexists).",
"items": {
"type": "string"
}
},
"variables": {
- "type": "object",
- "description": "Defines environment variables for specific jobs. Job level property overrides global variables. If a job sets `variables: {}`, all global variables are turned off.",
- "additionalProperties": {
- "type": ["string", "integer"]
- }
+ "markdownDescription": "Defines environment variables for specific jobs. Job level property overrides global variables. If a job sets `variables: {}`, all global variables are turned off. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#rulesvariables).",
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": {
+ "type": ["string", "integer", "array"]
+ }
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
},
"timeout": {
"type": "string",
- "description": "Allows you to configure a timeout for a specific job (e.g. `1 minute`, `1h 30m 12s`). Read more: https://docs.gitlab.com/ee/ci/yaml/README.html#timeout",
+ "markdownDescription": "Allows you to configure a timeout for a specific job (e.g. `1 minute`, `1h 30m 12s`). [Learn More](https://docs.gitlab.com/ee/ci/yaml/index.html#timeout).",
"minLength": 1
},
"start_in": {
"type": "string",
- "description": "Used in conjunction with 'when: delayed' to set how long to delay before starting a job. e.g. '5', 5 seconds, 30 minutes, 1 week, etc. Read more: https://docs.gitlab.com/ee/ci/jobs/job_control.html#run-a-job-after-a-delay",
+ "markdownDescription": "Used in conjunction with 'when: delayed' to set how long to delay before starting a job. e.g. '5', 5 seconds, 30 minutes, 1 week, etc. [Learn More](https://docs.gitlab.com/ee/ci/jobs/job_control.html#run-a-job-after-a-delay).",
"minLength": 1
},
"allow_failure": {
- "description": "Allow job to fail. A failed job does not cause the pipeline to fail.",
+ "markdownDescription": "Allow job to fail. A failed job does not cause the pipeline to fail. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#allow_failure).",
"oneOf": [
{
"description": "Setting this option to true will allow the job to fail while still letting the pipeline pass.",
@@ -594,7 +657,7 @@
]
},
"when": {
- "description": "Describes the conditions for when to run the job. Defaults to 'on_success'.",
+ "markdownDescription": "Describes the conditions for when to run the job. Defaults to 'on_success'.",
"default": "on_success",
"oneOf": [
{
@@ -611,11 +674,11 @@
},
{
"enum": ["manual"],
- "description": "Execute the job manually from Gitlab UI or API. Read more: https://docs.gitlab.com/ee/ci/yaml/#when-manual"
+ "markdownDescription": "Execute the job manually from Gitlab UI or API. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#when)."
},
{
"enum": ["delayed"],
- "description": "Execute a job after the time limit in 'start_in' expires. Read more: https://docs.gitlab.com/ee/ci/yaml/#when-delayed"
+ "markdownDescription": "Execute a job after the time limit in 'start_in' expires. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#when)."
},
{
"enum": ["never"],
@@ -626,7 +689,7 @@
"cache": {
"properties": {
"when": {
- "description": "Defines when to save the cache, based on the status of the job.",
+ "markdownDescription": "Defines when to save the cache, based on the status of the job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachewhen).",
"default": "on_success",
"oneOf": [
{
@@ -778,7 +841,7 @@
},
"variables": {
"type": "array",
- "description": "Filter job by checking comparing values of environment variables. Read more about variable expressions: https://docs.gitlab.com/ee/ci/variables/README.html#variables-expressions",
+ "markdownDescription": "Filter job by checking comparing values of CI/CD variables. [Learn More](https://docs.gitlab.com/ee/ci/jobs/job_control.html#cicd-variable-expressions).",
"items": {
"type": "string"
}
@@ -795,7 +858,7 @@
]
},
"retry": {
- "description": "Retry a job if it fails. Can be a simple integer or object definition.",
+ "markdownDescription": "Retry a job if it fails. Can be a simple integer or object definition. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#retry).",
"oneOf": [
{ "$ref": "#/definitions/retry_max" },
{
@@ -804,7 +867,7 @@
"properties": {
"max": { "$ref": "#/definitions/retry_max" },
"when": {
- "description": "Either a single or array of error types to trigger job retry.",
+ "markdownDescription": "Either a single or array of error types to trigger job retry. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#retrywhen).",
"oneOf": [
{ "$ref": "#/definitions/retry_errors" },
{
@@ -884,19 +947,12 @@
},
"interruptible": {
"type": "boolean",
- "description": "Interruptible is used to indicate that a job should be canceled if made redundant by a newer pipeline run.",
+ "markdownDescription": "Interruptible is used to indicate that a job should be canceled if made redundant by a newer pipeline run. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#interruptible).",
"default": false
},
"job": {
"allOf": [
- { "$ref": "#/definitions/job_template" },
- {
- "anyOf": [
- { "required": ["script"] },
- { "required": ["extends"] },
- { "required": ["trigger"] }
- ]
- }
+ { "$ref": "#/definitions/job_template" }
]
},
"job_template": {
@@ -912,7 +968,7 @@
"cache": { "$ref": "#/definitions/cache" },
"secrets": { "$ref": "#/definitions/secrets" },
"script": {
- "description": "Shell scripts executed by the Runner. The only required property of jobs. Be careful with special characters (e.g. `:`, `{`, `}`, `&`) and use single or double quotes to avoid issues.",
+ "markdownDescription": "Shell scripts executed by the Runner. The only required property of jobs. Be careful with special characters (e.g. `:`, `{`, `}`, `&`) and use single or double quotes to avoid issues. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#script)",
"oneOf": [
{
"type": "string",
@@ -1241,11 +1297,11 @@
"description": "Limit job concurrency. Can be used to ensure that the Runner will not run certain jobs simultaneously."
},
"trigger": {
- "description": "Trigger allows you to define downstream pipeline trigger. When a job created from trigger definition is started by GitLab, a downstream pipeline gets created. Read more: https://docs.gitlab.com/ee/ci/yaml/README.html#trigger",
+ "markdownDescription": "Trigger allows you to define downstream pipeline trigger. When a job created from trigger definition is started by GitLab, a downstream pipeline gets created. [Learn More](https://docs.gitlab.com/ee/ci/yaml/index.html#trigger).",
"oneOf": [
{
"type": "object",
- "description": "Trigger a multi-project pipeline. Read more: https://docs.gitlab.com/ee/ci/pipelines/multi_project_pipelines.html#specify-a-downstream-pipeline-branch",
+ "markdownDescription": "Trigger a multi-project pipeline. [Learn More](https://docs.gitlab.com/ee/ci/pipelines/multi_project_pipelines.html#specify-a-downstream-pipeline-branch).",
"additionalProperties": false,
"properties": {
"project": {
@@ -1287,7 +1343,7 @@
},
{
"type": "object",
- "description": "Trigger a child pipeline. Read more: https://docs.gitlab.com/ee/ci/pipelines/parent_child_pipelines.html",
+ "description": "Trigger a child pipeline. [Learn More](https://docs.gitlab.com/ee/ci/pipelines/parent_child_pipelines.html).",
"additionalProperties": false,
"properties": {
"include": {
@@ -1398,7 +1454,7 @@
}
},
{
- "description": "Path to the project, e.g. `group/project`, or `group/sub-group/project`. Read more: https://docs.gitlab.com/ee/ci/pipelines/multi_project_pipelines.html#define-multi-project-pipelines-in-your-gitlab-ciyml-file",
+ "markdownDescription": "Path to the project, e.g. `group/project`, or `group/sub-group/project`. [Learn More](https://docs.gitlab.com/ee/ci/pipelines/multi_project_pipelines.html#define-multi-project-pipelines-in-your-gitlab-ciyml-file).",
"type": "string",
"pattern": "\\S/\\S"
}
@@ -1406,10 +1462,10 @@
},
"inherit": {
"type": "object",
- "description": "Controls inheritance of globally-defined defaults and variables. Boolean values control inheritance of all default: or variables: keywords. To inherit only a subset of default: or variables: keywords, specify what you wish to inherit. Anything not listed is not inherited.",
+ "markdownDescription": "Controls inheritance of globally-defined defaults and variables. Boolean values control inheritance of all default: or variables: keywords. To inherit only a subset of default: or variables: keywords, specify what you wish to inherit. Anything not listed is not inherited. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#inherit).",
"properties": {
"default": {
- "description": "Whether to inherit all globally-defined defaults or not. Or subset of inherited defaults",
+ "markdownDescription": "Whether to inherit all globally-defined defaults or not. Or subset of inherited defaults. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#inheritdefault).",
"oneOf": [
{
"type": "boolean"
@@ -1435,7 +1491,7 @@
]
},
"variables": {
- "description": "Whether to inherit all globally-defined variables or not. Or subset of inherited variables",
+ "markdownDescription": "Whether to inherit all globally-defined variables or not. Or subset of inherited variables. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#inheritvariables).",
"oneOf": [
{ "type": "boolean" },
{
@@ -1470,7 +1526,7 @@
},
"tags": {
"type": "array",
- "description": "Used to select runners from the list of available runners. A runner must have all tags listed here to run the job.",
+ "markdownDescription": "Used to select runners from the list of available runners. A runner must have all tags listed here to run the job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#tags).",
"items": {
"type": "string"
}
diff --git a/app/assets/javascripts/editor/source_editor.js b/app/assets/javascripts/editor/source_editor.js
index fa749112ab5..d585dc009e6 100644
--- a/app/assets/javascripts/editor/source_editor.js
+++ b/app/assets/javascripts/editor/source_editor.js
@@ -75,6 +75,7 @@ export default class SourceEditor {
blobGlobalId,
instance,
isDiff,
+ language,
} = {}) {
if (!instance) {
return null;
@@ -82,7 +83,7 @@ export default class SourceEditor {
const uriFilePath = joinPaths(URI_PREFIX, blobGlobalId, blobPath);
const uri = Uri.file(uriFilePath);
const existingModel = monacoEditor.getModel(uri);
- const model = existingModel || monacoEditor.createModel(blobContent, undefined, uri);
+ const model = existingModel || monacoEditor.createModel(blobContent, language, uri);
if (!isDiff) {
instance.setModel(model);
return model;
@@ -132,6 +133,7 @@ export default class SourceEditor {
});
let model;
+ const language = instanceOptions.language || getBlobLanguage(blobPath);
if (instanceOptions.model !== null) {
model = SourceEditor.createEditorModel({
blobGlobalId,
@@ -140,6 +142,7 @@ export default class SourceEditor {
blobContent,
instance,
isDiff,
+ language,
});
}
diff --git a/app/assets/javascripts/editor/source_editor_extension.js b/app/assets/javascripts/editor/source_editor_extension.js
index 6d47e1e2248..7b73da4465f 100644
--- a/app/assets/javascripts/editor/source_editor_extension.js
+++ b/app/assets/javascripts/editor/source_editor_extension.js
@@ -12,6 +12,6 @@ export default class EditorExtension {
}
get api() {
- return this.obj.provides?.();
+ return this.obj.provides?.() || {};
}
}
diff --git a/app/assets/javascripts/emoji/constants.js b/app/assets/javascripts/emoji/constants.js
index a6eb4256561..7970a932095 100644
--- a/app/assets/javascripts/emoji/constants.js
+++ b/app/assets/javascripts/emoji/constants.js
@@ -19,3 +19,5 @@ export const CATEGORY_ROW_HEIGHT = 37;
export const CACHE_VERSION_KEY = 'gl-emoji-map-version';
export const CACHE_KEY = 'gl-emoji-map';
+
+export const NEUTRAL_INTENT_MULTIPLIER = 1;
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index 4fdcdcc1b04..b9392fabcbd 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -2,6 +2,7 @@ import { escape, minBy } from 'lodash';
import emojiRegexFactory from 'emoji-regex';
import emojiAliases from 'emojis/aliases.json';
import { setAttributes } from '~/lib/utils/dom_utils';
+import { getEmojiScoreWithIntent } from '~/emoji/utils';
import AccessorUtilities from '../lib/utils/accessor';
import axios from '../lib/utils/axios_utils';
import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
@@ -144,6 +145,11 @@ function getNameMatch(emoji, query) {
return null;
}
+// Sort emoji by emoji score falling back to a string comparison
+export function sortEmoji(a, b) {
+ return a.score - b.score || a.fieldValue.localeCompare(b.fieldValue);
+}
+
export function searchEmoji(query) {
const lowercaseQuery = query ? `${query}`.toLowerCase() : '';
@@ -156,16 +162,14 @@ export function searchEmoji(query) {
getDescriptionMatch(emoji, lowercaseQuery),
getAliasMatch(emoji, matchingAliases),
getNameMatch(emoji, lowercaseQuery),
- ].filter(Boolean);
+ ]
+ .filter(Boolean)
+ .map((x) => ({ ...x, score: getEmojiScoreWithIntent(x.emoji.name, x.score) }));
return minBy(matches, (x) => x.score);
})
- .filter(Boolean);
-}
-
-export function sortEmoji(items) {
- // Sort results by index of and string comparison
- return [...items].sort((a, b) => a.score - b.score || a.fieldValue.localeCompare(b.fieldValue));
+ .filter(Boolean)
+ .sort(sortEmoji);
}
export const CATEGORY_NAMES = Object.keys(CATEGORY_ICON_MAP);
diff --git a/app/assets/javascripts/emoji/utils.js b/app/assets/javascripts/emoji/utils.js
new file mode 100644
index 00000000000..eb3dcea73c0
--- /dev/null
+++ b/app/assets/javascripts/emoji/utils.js
@@ -0,0 +1,8 @@
+import emojiIntents from 'emojis/intents.json';
+import { NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants';
+
+export function getEmojiScoreWithIntent(emojiName, baseScore) {
+ const intentMultiplier = emojiIntents[emojiName] || NEUTRAL_INTENT_MULTIPLIER;
+
+ return 2 ** baseScore * intentMultiplier;
+}
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index cec53869aa8..b2844ed5ad6 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -33,7 +33,7 @@ export default {
<template>
<div class="environments-container">
- <gl-loading-icon v-if="isLoading" size="md" class="gl-mt-3" label="Loading environments" />
+ <gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3" label="Loading environments" />
<slot name="empty-state"></slot>
diff --git a/app/assets/javascripts/environments/components/deploy_board_wrapper.vue b/app/assets/javascripts/environments/components/deploy_board_wrapper.vue
index d9d77088ad3..d1132bc6e24 100644
--- a/app/assets/javascripts/environments/components/deploy_board_wrapper.vue
+++ b/app/assets/javascripts/environments/components/deploy_board_wrapper.vue
@@ -25,7 +25,7 @@ export default {
},
computed: {
icon() {
- return this.visible ? 'angle-down' : 'angle-right';
+ return this.visible ? 'chevron-lg-down' : 'chevron-lg-right';
},
label() {
return this.visible ? this.$options.i18n.collapse : this.$options.i18n.expand;
diff --git a/app/assets/javascripts/environments/components/environment_folder.vue b/app/assets/javascripts/environments/components/environment_folder.vue
index 788c3ba6fed..881f404340d 100644
--- a/app/assets/javascripts/environments/components/environment_folder.vue
+++ b/app/assets/javascripts/environments/components/environment_folder.vue
@@ -47,8 +47,8 @@ export default {
computed: {
icons() {
return this.visible
- ? { caret: 'angle-down', folder: 'folder-open' }
- : { caret: 'angle-right', folder: 'folder-o' };
+ ? { caret: 'chevron-lg-down', folder: 'folder-open' }
+ : { caret: 'chevron-lg-right', folder: 'folder-o' };
},
label() {
return this.visible ? this.$options.i18n.collapse : this.$options.i18n.expand;
diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue
index 1d1d8d61b66..1bac0ef1359 100644
--- a/app/assets/javascripts/environments/components/environment_form.vue
+++ b/app/assets/javascripts/environments/components/environment_form.vue
@@ -81,27 +81,24 @@ export default {
</script>
<template>
<div>
- <h3 class="page-title">
+ <h1 class="page-title gl-font-size-h-display">
{{ 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>
+ </h1>
+ <div class="row col-12">
+ <h4 class="gl-mt-0">
+ {{ $options.i18n.header }}
+ </h4>
+ <p class="gl-w-full">
+ <gl-sprintf :message="$options.i18n.helpMessage">
+ <template #link="{ content }">
+ <gl-link :href="$options.helpPagePath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
<gl-form
id="new_environment"
:aria-label="title"
- class="col-lg-9"
+ class="gl-w-full"
@submit.prevent="$emit('submit')"
>
<gl-form-group
@@ -144,7 +141,7 @@ export default {
/>
</gl-form-group>
- <div class="form-actions">
+ <div class="gl-mr-6">
<gl-button
:loading="loading"
type="submit"
diff --git a/app/assets/javascripts/environments/components/environments_detail_header.vue b/app/assets/javascripts/environments/components/environments_detail_header.vue
index d71b553a878..13b9cf14f52 100644
--- a/app/assets/javascripts/environments/components/environments_detail_header.vue
+++ b/app/assets/javascripts/environments/components/environments_detail_header.vue
@@ -94,9 +94,9 @@ export default {
<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">
+ <h1 class="page-title gl-font-size-h-display">
{{ environment.name }}
- </h3>
+ </h1>
<p v-if="shouldShowCancelAutoStopButton" class="gl-mb-0 gl-ml-3" data-testid="auto-stops-at">
<gl-sprintf :message="$options.i18n.autoStopAtText">
<template #autoStopAt>
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 7fcd6e5fff8..895a6cf2ccb 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -177,7 +177,7 @@ export default {
<template v-if="shouldRenderFolderContent(model)">
<div v-if="model.isLoadingFolderContent" :key="`loading-item-${i}`">
- <gl-loading-icon size="md" class="gl-mt-5" />
+ <gl-loading-icon size="lg" class="gl-mt-5" />
</div>
<template v-else>
diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue
index f5e9d612316..75bd473497b 100644
--- a/app/assets/javascripts/environments/components/new_environment_item.vue
+++ b/app/assets/javascripts/environments/components/new_environment_item.vue
@@ -83,7 +83,7 @@ export default {
},
computed: {
icon() {
- return this.visible ? 'angle-down' : 'angle-right';
+ return this.visible ? 'chevron-lg-down' : 'chevron-lg-right';
},
externalUrl() {
return this.environment.externalUrl;
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index 0a8abdc90c6..a602c92a840 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -301,7 +301,7 @@ export default {
<gl-button
class="ml-2"
category="secondary"
- variant="info"
+ variant="confirm"
:loading="updatingResolveStatus"
data-testid="update-resolve-status-btn"
@click="onResolveStatusUpdate"
@@ -313,7 +313,7 @@ export default {
class="ml-2"
data-testid="view_issue_button"
:href="error.gitlabIssuePath"
- variant="success"
+ variant="confirm"
>
{{ __('View issue') }}
</gl-button>
@@ -364,7 +364,6 @@ export default {
v-if="error.gitlabIssuePath"
data-qa-selector="view_issue_button"
:href="error.gitlabIssuePath"
- variant="success"
>{{ __('View issue') }}</gl-dropdown-item
>
<gl-dropdown-item
@@ -382,7 +381,7 @@ export default {
<h2 class="text-truncate">{{ error.title }}</h2>
</tooltip-on-truncate>
<template v-if="error.tags">
- <gl-badge v-if="error.tags.level" :variant="errorSeverityVariant" class="mr-2">
+ <gl-badge v-if="error.tags.level" :variant="errorSeverityVariant" class="gl-mr-3">
{{ errorLevel }}
</gl-badge>
<gl-badge v-if="error.tags.logger" variant="muted">{{ error.tags.logger }} </gl-badge>
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue b/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue
index 9438900c736..a5e712f4fc2 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue
@@ -73,7 +73,7 @@ export default {
<gl-button
:href="detailsLink"
category="primary"
- variant="info"
+ variant="confirm"
class="gl-display-block d-md-none gl-mb-4 mb-md-0"
>
{{ __('More details') }}
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 86102fd54b1..d29d5aa0671 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -369,7 +369,7 @@ export default {
</div>
<div v-if="loading" class="py-3">
- <gl-loading-icon size="md" />
+ <gl-loading-icon size="lg" />
</div>
<template v-else>
diff --git a/app/assets/javascripts/feature_flags/components/empty_state.vue b/app/assets/javascripts/feature_flags/components/empty_state.vue
index a6de4972bb1..a66215cdae6 100644
--- a/app/assets/javascripts/feature_flags/components/empty_state.vue
+++ b/app/assets/javascripts/feature_flags/components/empty_state.vue
@@ -67,7 +67,7 @@ export default {
{{ message }}
</gl-alert>
- <gl-loading-icon v-if="isLoading" :label="loadingLabel" size="md" class="gl-mt-4" />
+ <gl-loading-icon v-if="isLoading" :label="loadingLabel" size="lg" class="gl-mt-4" />
<gl-empty-state
v-else-if="errorState"
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags.vue b/app/assets/javascripts/feature_flags/components/feature_flags.vue
index 53909dcf42e..645c2456c6e 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags.vue
@@ -161,7 +161,7 @@ export default {
<gl-button
v-if="canUserConfigure"
v-gl-modal="'configure-feature-flags'"
- variant="info"
+ variant="confirm"
category="secondary"
data-qa-selector="configure_feature_flags_button"
data-testid="ff-configure-button"
@@ -184,7 +184,10 @@ export default {
class="gl-display-flex gl-align-items-baseline gl-flex-direction-row gl-justify-content-space-between gl-mt-6"
>
<div class="gl-display-flex gl-align-items-center">
- <h2 data-testid="feature-flags-tab-title" class="gl-font-size-h2 gl-my-0">
+ <h2
+ data-testid="feature-flags-tab-title"
+ class="page-title gl-font-size-h-display gl-my-0"
+ >
{{ s__('FeatureFlags|Feature Flags') }}
</h2>
<gl-badge v-if="count" class="gl-ml-4">{{ count }}</gl-badge>
@@ -197,7 +200,7 @@ export default {
:href="userListPath"
variant="confirm"
category="tertiary"
- class="gl-mb-0 gl-mr-4"
+ class="gl-mb-0 gl-mr-3"
data-testid="ff-user-list-button"
>
{{ s__('FeatureFlags|View user lists') }}
@@ -205,11 +208,11 @@ export default {
<gl-button
v-if="canUserConfigure"
v-gl-modal="'configure-feature-flags'"
- variant="info"
+ variant="confirm"
category="secondary"
data-qa-selector="configure_feature_flags_button"
data-testid="ff-configure-button"
- class="gl-mb-0 gl-mr-4"
+ class="gl-mb-0 gl-mr-3"
>
{{ s__('FeatureFlags|Configure') }}
</gl-button>
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
index f8a8bed2467..f0f42d19ea5 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
@@ -81,6 +81,20 @@ export default {
});
},
},
+ modal: {
+ actionPrimary: {
+ text: s__('FeatureFlags|Delete feature flag'),
+ attributes: {
+ variant: 'danger',
+ },
+ },
+ actionSecondary: {
+ text: __('Cancel'),
+ attributes: {
+ variant: 'default',
+ },
+ },
+ },
};
</script>
<template>
@@ -193,11 +207,11 @@ export default {
<gl-modal
:ref="modalId"
:title="modalTitle"
- :ok-title="s__('FeatureFlags|Delete feature flag')"
:modal-id="modalId"
title-tag="h4"
- ok-variant="danger"
- category="primary"
+ size="sm"
+ :action-primary="$options.modal.actionPrimary"
+ :action-secondary="$options.modal.actionSecondary"
@ok="onSubmit"
>
{{ deleteModalMessage }}
diff --git a/app/assets/javascripts/feature_flags/components/form.vue b/app/assets/javascripts/feature_flags/components/form.vue
index 26da0d56f9a..8b79c661b12 100644
--- a/app/assets/javascripts/feature_flags/components/form.vue
+++ b/app/assets/javascripts/feature_flags/components/form.vue
@@ -188,8 +188,8 @@ export default {
<div class="row">
<div class="col-md-12">
<h4>{{ s__('FeatureFlags|Strategies') }}</h4>
- <div class="flex align-items-baseline justify-content-between">
- <p class="mr-3">{{ $options.translations.newHelpText }}</p>
+ <div class="gl-display-flex gl-align-items-baseline gl-justify-content-space-between">
+ <p class="gl-mr-5">{{ $options.translations.newHelpText }}</p>
<gl-button variant="confirm" category="secondary" @click="addStrategy">
{{ s__('FeatureFlags|Add strategy') }}
</gl-button>
@@ -206,21 +206,21 @@ export default {
@delete="deleteStrategy(strategy)"
/>
</div>
- <div v-else class="flex justify-content-center border-top py-4 w-100">
+ <div v-else class="gl-display-flex gl-justify-content-center gl-border-t gl-py-6 w-100">
<span>{{ $options.translations.noStrategiesText }}</span>
</div>
</fieldset>
- <div class="form-actions">
+ <div class="gl-mr-6">
<gl-button
ref="submitButton"
type="button"
variant="confirm"
- class="js-ff-submit col-xs-12"
+ class="js-ff-submit gl-mr-2"
@click="handleSubmit"
>{{ submitText }}</gl-button
>
- <gl-button :href="cancelPath" class="js-ff-cancel col-xs-12 float-right">
+ <gl-button :href="cancelPath" class="js-ff-cancel">
{{ __('Cancel') }}
</gl-button>
</div>
diff --git a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
index 5575c6567b5..98982920121 100644
--- a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
+++ b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
@@ -72,7 +72,7 @@ export default {
<span class="d-md-none mr-1">
{{ $options.translations.addEnvironmentsLabel }}
</span>
- <gl-icon class="d-none d-md-inline-flex" name="plus" />
+ <gl-icon class="d-none d-md-inline-flex gl-mr-1" name="plus" />
</template>
<gl-search-box-by-type
ref="searchBox"
diff --git a/app/assets/javascripts/feature_flags/components/new_feature_flag.vue b/app/assets/javascripts/feature_flags/components/new_feature_flag.vue
index 865c1e677cd..bc05e88e643 100644
--- a/app/assets/javascripts/feature_flags/components/new_feature_flag.vue
+++ b/app/assets/javascripts/feature_flags/components/new_feature_flag.vue
@@ -24,7 +24,7 @@ export default {
</script>
<template>
<div>
- <h3 class="page-title">{{ s__('FeatureFlags|New feature flag') }}</h3>
+ <h1 class="page-title gl-font-size-h-display">{{ s__('FeatureFlags|New feature flag') }}</h1>
<gl-alert v-if="error.length" variant="warning" class="gl-mb-5" :dismissible="false">
<p v-for="(message, index) in error" :key="index" class="gl-mb-0">{{ message }}</p>
diff --git a/app/assets/javascripts/feature_flags/components/strategy.vue b/app/assets/javascripts/feature_flags/components/strategy.vue
index 3f515dcdf18..1d7a79f926a 100644
--- a/app/assets/javascripts/feature_flags/components/strategy.vue
+++ b/app/assets/javascripts/feature_flags/components/strategy.vue
@@ -176,7 +176,7 @@ export default {
}}</label>
<div class="gl-display-flex gl-flex-direction-column">
<div
- class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row align-items-start gl-md-align-items-center"
+ class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-md-align-items-center"
>
<new-environments-dropdown
:id="environmentsDropdownId"
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index b57db73a86e..3913e4e8d81 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -197,10 +197,10 @@ export default class AvailableDropdownMappings {
}
getGroupId() {
- return this.filteredSearchInput.getAttribute('data-group-id') || '';
+ return this.filteredSearchInput.dataset.groupId || '';
}
getProjectId() {
- return this.filteredSearchInput.getAttribute('data-project-id') || '';
+ return this.filteredSearchInput.dataset.projectId || '';
}
}
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 9d29782c9a7..10c3a6a36d5 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -25,9 +25,9 @@ export default class DropdownHint extends FilteredSearchDropdown {
const { selected } = e.detail;
if (selected.tagName === 'LI') {
- if (selected.hasAttribute('data-value')) {
+ if (Object.prototype.hasOwnProperty.call(selected.dataset, 'value')) {
this.dismissDropdown();
- } else if (selected.getAttribute('data-action') === 'submit') {
+ } else if (selected.dataset.action === 'submit') {
this.dismissDropdown();
this.dispatchFormSubmitEvent();
} else {
diff --git a/app/assets/javascripts/filtered_search/dropdown_operator.js b/app/assets/javascripts/filtered_search/dropdown_operator.js
index fb9f25a8c45..f3f159ab988 100644
--- a/app/assets/javascripts/filtered_search/dropdown_operator.js
+++ b/app/assets/javascripts/filtered_search/dropdown_operator.js
@@ -23,7 +23,7 @@ export default class DropdownOperator extends FilteredSearchDropdown {
const { selected } = e.detail;
if (selected.tagName === 'LI') {
- if (selected.hasAttribute('data-value')) {
+ if (Object.prototype.hasOwnProperty.call(selected.dataset, 'value')) {
const name = FilteredSearchVisualTokens.getLastTokenPartial();
const operator = selected.dataset.value;
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index 9a23ff25eac..26507a85fa8 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -31,11 +31,11 @@ export default class DropdownUser extends DropdownAjaxFilter {
}
getGroupId() {
- return this.input.getAttribute('data-group-id');
+ return this.input.dataset.groupId;
}
getProjectId() {
- return this.input.getAttribute('data-project-id');
+ return this.input.dataset.projectId;
}
projectOrGroupId() {
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index c98d1f8e064..22e1604871a 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -87,6 +87,7 @@ export default class DropdownUtils {
}
static setDataValueIfSelected(filter, operator, selected) {
+ // eslint-disable-next-line unicorn/prefer-dom-node-dataset
const dataValue = selected.getAttribute('data-value');
if (dataValue) {
@@ -96,6 +97,7 @@ export default class DropdownUtils {
tokenValue: dataValue,
clicked: true,
options: {
+ // eslint-disable-next-line unicorn/prefer-dom-node-dataset
capitalizeTokenValue: selected.hasAttribute('data-capitalize'),
},
});
diff --git a/app/assets/javascripts/filtered_search/droplab/drop_down.js b/app/assets/javascripts/filtered_search/droplab/drop_down.js
index 05b741af191..398a7b26677 100644
--- a/app/assets/javascripts/filtered_search/droplab/drop_down.js
+++ b/app/assets/javascripts/filtered_search/droplab/drop_down.js
@@ -165,8 +165,8 @@ class DropDown {
images.forEach((image) => {
const img = image;
- img.src = img.getAttribute('data-src');
- img.removeAttribute('data-src');
+ img.src = img.dataset.src;
+ delete img.dataset.src;
});
}
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 07f2c75f00a..ac2cf27e873 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -814,7 +814,7 @@ export default class FilteredSearchManager {
getUsernameParams() {
const usernamesById = {};
try {
- const attribute = this.filteredSearchInput.getAttribute('data-username-params');
+ const attribute = this.filteredSearchInput.dataset.usernameParams;
JSON.parse(attribute).forEach((user) => {
usernamesById[user.id] = user.username;
});
diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
index 6216ab5401d..359a276aa74 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
@@ -1,5 +1,3 @@
-import { __ } from '~/locale';
-
export default class FilteredSearchTokenKeys {
constructor(tokenKeys = [], alternativeTokenKeys = [], conditions = []) {
this.tokenKeys = tokenKeys;
@@ -76,24 +74,6 @@ export default class FilteredSearchTokenKeys {
);
}
- addExtraTokensForIssues() {
- const confidentialToken = {
- formattedKey: __('Confidential'),
- key: 'confidential',
- type: 'string',
- param: '',
- symbol: '',
- icon: 'eye-slash',
- tag: __('Yes or No'),
- lowercaseValueOnSubmit: true,
- uppercaseTokenName: false,
- capitalizeTokenValue: true,
- };
-
- this.tokenKeys.push(confidentialToken);
- this.tokenKeysWithAlternative.push(confidentialToken);
- }
-
removeTokensForKeys(...keys) {
const keysSet = new Set(keys);
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
index 1700437aa84..6b1676eca8a 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -1,5 +1,6 @@
<script>
import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
+import { snakeCase } from 'lodash';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
import { mapVuexModuleState } from '~/lib/utils/vuex_module_mappers';
@@ -56,6 +57,9 @@ export default {
highlightedItemName() {
return highlight(this.itemName, this.matcher);
},
+ itemTrackingLabel() {
+ return `${this.dropdownType}_dropdown_frequent_items_list_item_${snakeCase(this.itemName)}`;
+ },
},
};
</script>
@@ -66,7 +70,7 @@ export default {
category="tertiary"
:href="webUrl"
class="gl-text-left gl-justify-content-start!"
- @click="track('click_link', { label: `${dropdownType}_dropdown_frequent_items_list_item` })"
+ @click="track('click_link', { label: itemTrackingLabel })"
>
<project-avatar
class="gl-float-left gl-mr-3"
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 146255df31f..d4dafbdc94f 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -897,7 +897,7 @@ GfmAutoComplete.Emoji = {
return Emoji.searchEmoji(query);
},
sorter(items) {
- return Emoji.sortEmoji(items);
+ return items.sort(Emoji.sortEmoji);
},
};
// Team Members
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 3a04779e48d..2b157fac878 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -60,7 +60,10 @@ export default class GLForm {
this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
this.formDropzone = dropzoneInput(this.form, { parallelUploads: 1 });
- autosize(this.textarea);
+
+ if (this.form.is(':not(.js-no-autosize)')) {
+ autosize(this.textarea);
+ }
}
// form and textarea event listeners
this.addEventListeners();
diff --git a/app/assets/javascripts/google_cloud/components/gcp_regions_list.vue b/app/assets/javascripts/google_cloud/components/gcp_regions_list.vue
index 1cc5a85198a..5d403d5cd65 100644
--- a/app/assets/javascripts/google_cloud/components/gcp_regions_list.vue
+++ b/app/assets/javascripts/google_cloud/components/gcp_regions_list.vue
@@ -48,7 +48,7 @@ export default {
<gl-table :items="list" :fields="$options.tableFields" />
- <gl-button :href="createUrl" category="primary" variant="info">
+ <gl-button :href="createUrl" category="primary" variant="confirm">
{{ $options.i18n.configureRegions }}
</gl-button>
</div>
diff --git a/app/assets/javascripts/google_cloud/components/revoke_oauth.vue b/app/assets/javascripts/google_cloud/components/revoke_oauth.vue
index 07d966894f6..c07702ff42b 100644
--- a/app/assets/javascripts/google_cloud/components/revoke_oauth.vue
+++ b/app/assets/javascripts/google_cloud/components/revoke_oauth.vue
@@ -30,7 +30,7 @@ export default {
<p>{{ $options.i18n.description }}</p>
<gl-form :action="url" method="post">
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
- <gl-button category="secondary" variant="warning" type="submit">
+ <gl-button category="secondary" variant="danger" type="submit">
{{ $options.i18n.title }}
</gl-button>
</gl-form>
diff --git a/app/assets/javascripts/google_cloud/components/service_accounts_list.vue b/app/assets/javascripts/google_cloud/components/service_accounts_list.vue
index 37b716d7be5..4b580c594f5 100644
--- a/app/assets/javascripts/google_cloud/components/service_accounts_list.vue
+++ b/app/assets/javascripts/google_cloud/components/service_accounts_list.vue
@@ -67,7 +67,7 @@ export default {
</template>
</gl-table>
- <gl-button :href="createUrl" category="primary" variant="info">
+ <gl-button :href="createUrl" category="primary" variant="confirm">
{{ $options.i18n.createServiceAccount }}
</gl-button>
diff --git a/app/assets/javascripts/google_tag_manager/index.js b/app/assets/javascripts/google_tag_manager/index.js
index a44a5b30e1e..2969121bf06 100644
--- a/app/assets/javascripts/google_tag_manager/index.js
+++ b/app/assets/javascripts/google_tag_manager/index.js
@@ -19,6 +19,7 @@ const PRODUCT_INFO = {
variant: 'SaaS',
},
};
+const EMPTY_NAMESPACE_ID_VALUE = 'not available';
const generateProductInfo = (sku, quantity) => {
const product = PRODUCT_INFO[sku];
@@ -200,6 +201,10 @@ export const trackCheckout = (selectedPlan, quantity) => {
pushEnhancedEcommerceEvent('EECCheckout', eventData);
};
+export const getNamespaceId = () => {
+ return window.gl.snowplowStandardContext?.data?.namespace_id || EMPTY_NAMESPACE_ID_VALUE;
+};
+
export const trackTransaction = (transactionDetails) => {
if (!isSupported()) {
return;
@@ -208,6 +213,7 @@ export const trackTransaction = (transactionDetails) => {
const transactionId = uuidv4();
const { paymentOption, revenue, tax, selectedPlan, quantity } = transactionDetails;
const product = generateProductInfo(selectedPlan, quantity);
+ const namespaceId = getNamespaceId();
if (Object.keys(product).length === 0) {
return;
@@ -224,7 +230,7 @@ export const trackTransaction = (transactionDetails) => {
revenue: revenue.toString(),
tax: tax.toString(),
},
- products: [product],
+ products: [{ ...product, dimension36: namespaceId }],
},
},
};
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 824997f8e33..fb771d7ec8a 100644
--- a/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql
@@ -11,4 +11,7 @@ fragment TimelogFragment on Timelog {
body
}
summary
+ userPermissions {
+ adminTimelog
+ }
}
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 7ca3f20ec1c..50b40526ee0 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -129,5 +129,9 @@
"VulnerabilityLocationGeneric",
"VulnerabilityLocationSast",
"VulnerabilityLocationSecretDetection"
+ ],
+ "WorkItemWidget": [
+ "WorkItemWidgetDescription",
+ "WorkItemWidgetHierarchy"
]
}
diff --git a/app/assets/javascripts/group_settings/components/shared_runners_form.vue b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
index 3365f4aa76c..06aea26830d 100644
--- a/app/assets/javascripts/group_settings/components/shared_runners_form.vue
+++ b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
@@ -1,8 +1,7 @@
<script>
import { GlToggle, GlAlert } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
-import { ERROR_MESSAGE } from '../constants';
+import { I18N_UPDATE_ERROR_MESSAGE, I18N_REFRESH_MESSAGE } from '../constants';
export default {
components: {
@@ -62,8 +61,8 @@ export default {
})
.catch((error) => {
const message = [
- error.response?.data?.error || __('An error occurred while updating configuration.'),
- ERROR_MESSAGE,
+ error.response?.data?.error || I18N_UPDATE_ERROR_MESSAGE,
+ I18N_REFRESH_MESSAGE,
].join(' ');
this.error = message;
diff --git a/app/assets/javascripts/group_settings/constants.js b/app/assets/javascripts/group_settings/constants.js
index ab5c0db45ba..1b44161903d 100644
--- a/app/assets/javascripts/group_settings/constants.js
+++ b/app/assets/javascripts/group_settings/constants.js
@@ -1,3 +1,4 @@
import { __ } from '~/locale';
-export const ERROR_MESSAGE = __('Refresh the page and try again.');
+export const I18N_UPDATE_ERROR_MESSAGE = __('An error occurred while updating configuration.');
+export const I18N_REFRESH_MESSAGE = __('Refresh the page and try again.');
diff --git a/app/assets/javascripts/group_settings/stale_runner_cleanup.js b/app/assets/javascripts/group_settings/stale_runner_cleanup.js
new file mode 100644
index 00000000000..3a4c171915f
--- /dev/null
+++ b/app/assets/javascripts/group_settings/stale_runner_cleanup.js
@@ -0,0 +1,3 @@
+export default () => {
+ // Overridden in EE
+};
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index e3147065d5c..cd5521c599e 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -1,19 +1,26 @@
<script>
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import createFlash from '~/flash';
-import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { mergeUrlParams, getParameterByName } from '~/lib/utils/url_utility';
+import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
import { COMMON_STR, CONTENT_LIST_CLASS } from '../constants';
import eventHub from '../event_hub';
-import groupsComponent from './groups.vue';
+import GroupsComponent from './groups.vue';
+import EmptyState from './empty_state.vue';
export default {
components: {
- groupsComponent,
+ GroupsComponent,
GlModal,
GlLoadingIcon,
+ EmptyState,
+ },
+ inject: {
+ renderEmptyState: {
+ default: false,
+ },
},
props: {
action: {
@@ -47,13 +54,14 @@ export default {
searchEmptyMessage: '',
targetGroup: null,
targetParentGroup: null,
+ showEmptyState: false,
};
},
computed: {
primaryProps() {
return {
text: __('Leave group'),
- attributes: [{ variant: 'warning' }, { category: 'primary' }],
+ attributes: [{ variant: 'danger' }, { category: 'primary' }],
};
},
cancelProps() {
@@ -75,6 +83,9 @@ export default {
pageInfo() {
return this.store.getPaginationInfo();
},
+ filterGroupsBy() {
+ return getParameterByName('filter') || null;
+ },
},
created() {
this.searchEmptyMessage = this.hideProjects
@@ -128,19 +139,18 @@ export default {
const page = getParameterByName('page') || null;
const sortBy = getParameterByName('sort') || null;
const archived = getParameterByName('archived') || null;
- const filterGroupsBy = getParameterByName('filter') || null;
this.isLoading = true;
return this.fetchGroups({
page,
- filterGroupsBy,
+ filterGroupsBy: this.filterGroupsBy,
sortBy,
archived,
updatePagination: true,
}).then((res) => {
this.isLoading = false;
- this.updateGroups(res, Boolean(filterGroupsBy));
+ this.updateGroups(res, Boolean(this.filterGroupsBy));
});
},
fetchPage({ page, filterGroupsBy, sortBy, archived }) {
@@ -212,7 +222,7 @@ export default {
this.targetGroup.isBeingRemoved = false;
});
},
- showEmptyState() {
+ showLegacyEmptyState() {
const { containerEl } = this;
const contentListEl = containerEl.querySelector(CONTENT_LIST_CLASS);
const emptyStateEl = containerEl.querySelector('.empty-state');
@@ -230,7 +240,12 @@ export default {
},
updateGroups(groups, fromSearch) {
const hasGroups = groups && groups.length > 0;
- this.isSearchEmpty = !hasGroups;
+
+ if (this.renderEmptyState) {
+ this.isSearchEmpty = this.filterGroupsBy !== null && !hasGroups;
+ } else {
+ this.isSearchEmpty = !hasGroups;
+ }
if (fromSearch) {
this.store.setSearchedGroups(groups);
@@ -239,7 +254,11 @@ export default {
}
if (this.action && !hasGroups && !fromSearch) {
- this.showEmptyState();
+ if (this.renderEmptyState) {
+ this.showEmptyState = true;
+ } else {
+ this.showLegacyEmptyState();
+ }
}
},
},
@@ -251,7 +270,7 @@ export default {
<gl-loading-icon
v-if="isLoading"
:label="s__('GroupsTree|Loading groups')"
- size="md"
+ size="lg"
class="loading-animation prepend-top-20"
/>
<groups-component
@@ -262,6 +281,7 @@ export default {
:page-info="pageInfo"
:action="action"
/>
+ <empty-state v-if="showEmptyState" />
<gl-modal
modal-id="leave-group-modal"
:visible="isModalVisible"
diff --git a/app/assets/javascripts/groups/components/empty_state.vue b/app/assets/javascripts/groups/components/empty_state.vue
new file mode 100644
index 00000000000..4219b52737d
--- /dev/null
+++ b/app/assets/javascripts/groups/components/empty_state.vue
@@ -0,0 +1,91 @@
+<script>
+import { GlLink, GlEmptyState } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+
+export default {
+ components: { GlLink, GlEmptyState },
+ i18n: {
+ withLinks: {
+ subgroup: {
+ title: s__('GroupsEmptyState|Create new subgroup'),
+ description: s__(
+ 'GroupsEmptyState|Groups are the best way to manage multiple projects and members.',
+ ),
+ },
+ project: {
+ title: s__('GroupsEmptyState|Create new project'),
+ description: s__(
+ 'GroupsEmptyState|Projects are where you can store your code, access issues, wiki, and other features of Gitlab.',
+ ),
+ },
+ },
+ withoutLinks: {
+ title: s__('GroupsEmptyState|No subgroups or projects.'),
+ description: s__(
+ 'GroupsEmptyState|You do not have necessary permissions to create a subgroup or project in this group. Please contact an owner of this group to create a new subgroup or project.',
+ ),
+ },
+ },
+ linkClasses: [
+ 'gl-border',
+ 'gl-text-decoration-none!',
+ 'gl-rounded-base',
+ 'gl-p-7',
+ 'gl-display-flex',
+ 'gl-h-full',
+ 'gl-align-items-center',
+ 'gl-text-purple-600',
+ 'gl-hover-bg-gray-50',
+ ],
+ inject: [
+ 'newSubgroupPath',
+ 'newProjectPath',
+ 'newSubgroupIllustration',
+ 'newProjectIllustration',
+ 'emptySubgroupIllustration',
+ 'canCreateSubgroups',
+ 'canCreateProjects',
+ ],
+};
+</script>
+
+<template>
+ <div v-if="canCreateSubgroups || canCreateProjects" class="gl-mt-5">
+ <div class="gl-display-flex gl-mx-n3 gl-my-n3 gl-flex-wrap">
+ <div v-if="canCreateSubgroups" class="gl-p-3 gl-w-full gl-sm-w-half">
+ <gl-link :href="newSubgroupPath" :class="$options.linkClasses">
+ <div class="svg-content gl-w-15 gl-flex-shrink-0 gl-mr-5">
+ <img :src="newSubgroupIllustration" :alt="$options.i18n.withLinks.subgroup.title" />
+ </div>
+ <div>
+ <h4 class="gl-reset-color">{{ $options.i18n.withLinks.subgroup.title }}</h4>
+ <p class="gl-text-body">
+ {{ $options.i18n.withLinks.subgroup.description }}
+ </p>
+ </div>
+ </gl-link>
+ </div>
+ <div v-if="canCreateProjects" class="gl-p-3 gl-w-full gl-sm-w-half">
+ <gl-link :href="newProjectPath" :class="$options.linkClasses">
+ <div class="svg-content gl-w-13 gl-flex-shrink-0 gl-mr-5">
+ <img :src="newProjectIllustration" :alt="$options.i18n.withLinks.project.title" />
+ </div>
+ <div>
+ <h4 class="gl-reset-color">{{ $options.i18n.withLinks.project.title }}</h4>
+ <p class="gl-text-body">
+ {{ $options.i18n.withLinks.project.description }}
+ </p>
+ </div>
+ </gl-link>
+ </div>
+ </div>
+ </div>
+ <gl-empty-state
+ v-else
+ class="gl-mt-5"
+ :title="$options.i18n.withoutLinks.title"
+ :svg-path="emptySubgroupIllustration"
+ :description="$options.i18n.withoutLinks.description"
+ />
+</template>
diff --git a/app/assets/javascripts/groups/components/group_folder.vue b/app/assets/javascripts/groups/components/group_folder.vue
index 042d818338a..96162c32d52 100644
--- a/app/assets/javascripts/groups/components/group_folder.vue
+++ b/app/assets/javascripts/groups/components/group_folder.vue
@@ -39,7 +39,7 @@ export default {
</script>
<template>
- <ul class="groups-list group-list-tree">
+ <ul class="groups-list group-list-tree gl-display-flex gl-flex-direction-column gl-m-0">
<group-item
v-for="(group, index) in groups"
:key="index"
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 4f21f68fa65..2241d57f96f 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -157,7 +157,9 @@ export default {
</a>
<div class="group-text-container d-flex flex-fill align-items-center">
<div class="group-text flex-grow-1 flex-shrink-1">
- <div class="d-flex align-items-center flex-wrap title namespace-title gl-mr-3">
+ <div
+ class="gl-display-flex gl-align-items-center gl-flex-wrap title namespace-title gl-font-weight-bold gl-mr-3"
+ >
<a
v-gl-tooltip.bottom
data-testid="group-name"
diff --git a/app/assets/javascripts/groups/components/group_name_and_path.vue b/app/assets/javascripts/groups/components/group_name_and_path.vue
new file mode 100644
index 00000000000..f9bd8701199
--- /dev/null
+++ b/app/assets/javascripts/groups/components/group_name_and_path.vue
@@ -0,0 +1,279 @@
+<script>
+import {
+ GlFormGroup,
+ GlFormInput,
+ GlFormInputGroup,
+ GlInputGroupText,
+ GlLink,
+ GlAlert,
+} from '@gitlab/ui';
+import { debounce } from 'lodash';
+
+import { s__, __ } from '~/locale';
+import { getGroupPathAvailability } from '~/rest_api';
+import { createAlert } from '~/flash';
+import { slugify } from '~/lib/utils/text_utility';
+import axios from '~/lib/utils/axios_utils';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+const DEBOUNCE_DURATION = 1000;
+
+export default {
+ i18n: {
+ inputs: {
+ name: {
+ label: s__('Groups|Group name'),
+ placeholder: __('My awesome group'),
+ description: s__(
+ 'Groups|Must start with letter, digit, emoji, or underscore. Can also contain periods, dashes, spaces, and parentheses.',
+ ),
+ invalidFeedback: s__('Groups|Enter a descriptive name for your group.'),
+ },
+ path: {
+ label: s__('Groups|Group URL'),
+ placeholder: __('my-awesome-group'),
+ invalidFeedbackInvalidPattern: s__(
+ 'GroupSettings|Choose a group path that does not start with a dash or end with a period. It can also contain alphanumeric characters and underscores.',
+ ),
+ invalidFeedbackPathUnavailable: s__(
+ 'Groups|Group path is unavailable. Path has been replaced with a suggested available path.',
+ ),
+ validFeedback: s__('Groups|Group path is available.'),
+ },
+ groupId: {
+ label: s__('Groups|Group ID'),
+ },
+ },
+ apiLoadingMessage: s__('Groups|Checking group URL availability...'),
+ apiErrorMessage: __(
+ 'An error occurred while checking group path. Please refresh and try again.',
+ ),
+ changingUrlWarningMessage: s__('Groups|Changing group URL can have unintended side effects.'),
+ learnMore: s__('Groups|Learn more'),
+ },
+ nameInputSize: { md: 'lg' },
+ changingGroupPathHelpPagePath: helpPagePath('user/group/index', {
+ anchor: 'change-a-groups-path',
+ }),
+ mattermostDataBindName: 'create_chat_team',
+ components: {
+ GlFormGroup,
+ GlFormInput,
+ GlFormInputGroup,
+ GlInputGroupText,
+ GlLink,
+ GlAlert,
+ },
+ inject: ['fields', 'basePath', 'mattermostEnabled'],
+ data() {
+ return {
+ name: this.fields.name.value,
+ path: this.fields.path.value,
+ hasPathBeenManuallySet: false,
+ apiSuggestedPath: '',
+ apiLoading: false,
+ nameFeedbackState: null,
+ pathFeedbackState: null,
+ pathInvalidFeedback: null,
+ activeApiRequestAbortController: null,
+ };
+ },
+ computed: {
+ computedPath() {
+ return this.apiSuggestedPath || this.path;
+ },
+ pathDescription() {
+ return this.apiLoading ? this.$options.i18n.apiLoadingMessage : '';
+ },
+ isEditingGroup() {
+ return this.fields.groupId.value !== '';
+ },
+ },
+ watch: {
+ name: [
+ function updatePath(newName) {
+ if (this.isEditingGroup || this.hasPathBeenManuallySet) return;
+
+ this.nameFeedbackState = null;
+ this.pathFeedbackState = null;
+ this.apiSuggestedPath = '';
+ this.path = slugify(newName);
+ },
+ debounce(async function updatePathWithSuggestions() {
+ if (this.isEditingGroup || this.hasPathBeenManuallySet) return;
+
+ try {
+ const { suggests } = await this.checkPathAvailability();
+
+ const [suggestedPath] = suggests;
+
+ this.apiSuggestedPath = suggestedPath;
+ } catch (error) {
+ // Do nothing, error handled in `checkPathAvailability`
+ }
+ }, DEBOUNCE_DURATION),
+ ],
+ },
+ methods: {
+ async checkPathAvailability() {
+ if (!this.path) return Promise.reject();
+
+ this.apiLoading = true;
+
+ if (this.activeApiRequestAbortController !== null) {
+ this.activeApiRequestAbortController.abort();
+ }
+
+ this.activeApiRequestAbortController = new AbortController();
+
+ try {
+ const {
+ data: { exists, suggests },
+ } = await getGroupPathAvailability(this.path, this.fields.parentId?.value, {
+ signal: this.activeApiRequestAbortController.signal,
+ });
+
+ if (exists) {
+ if (suggests.length) {
+ return Promise.resolve({ exists, suggests });
+ }
+
+ createAlert({
+ message: this.$options.i18n.apiErrorMessage,
+ });
+
+ return Promise.reject();
+ }
+
+ return Promise.resolve({ exists, suggests });
+ } catch (error) {
+ if (!axios.isCancel(error)) {
+ createAlert({
+ message: this.$options.i18n.apiErrorMessage,
+ });
+ }
+
+ return Promise.reject();
+ } finally {
+ this.apiLoading = false;
+ }
+ },
+ handlePathInput(value) {
+ this.pathFeedbackState = null;
+ this.apiSuggestedPath = '';
+ this.hasPathBeenManuallySet = true;
+ this.path = value;
+ this.debouncedValidatePath();
+ },
+ debouncedValidatePath: debounce(async function validatePath() {
+ if (this.isEditingGroup && this.path === this.fields.path.value) return;
+
+ try {
+ const {
+ exists,
+ suggests: [suggestedPath],
+ } = await this.checkPathAvailability();
+
+ if (exists) {
+ this.apiSuggestedPath = suggestedPath;
+ this.pathInvalidFeedback = this.$options.i18n.inputs.path.invalidFeedbackPathUnavailable;
+ this.pathFeedbackState = false;
+ } else {
+ this.pathFeedbackState = true;
+ }
+ } catch (error) {
+ // Do nothing, error handled in `checkPathAvailability`
+ }
+ }, DEBOUNCE_DURATION),
+ handleInvalidName(event) {
+ event.preventDefault();
+
+ this.nameFeedbackState = false;
+ },
+ handleInvalidPath(event) {
+ event.preventDefault();
+
+ this.pathInvalidFeedback = this.$options.i18n.inputs.path.invalidFeedbackInvalidPattern;
+ this.pathFeedbackState = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <input
+ :id="fields.parentId.id"
+ type="hidden"
+ :name="fields.parentId.name"
+ :value="fields.parentId.value"
+ />
+ <gl-form-group
+ :label="$options.i18n.inputs.name.label"
+ :description="$options.i18n.inputs.name.description"
+ :label-for="fields.name.id"
+ :invalid-feedback="$options.i18n.inputs.name.invalidFeedback"
+ :state="nameFeedbackState"
+ >
+ <gl-form-input
+ :id="fields.name.id"
+ v-model="name"
+ class="gl-field-error-ignore"
+ required
+ :name="fields.name.name"
+ :placeholder="$options.i18n.inputs.name.placeholder"
+ data-qa-selector="group_name_field"
+ :size="$options.nameInputSize"
+ :state="nameFeedbackState"
+ @invalid="handleInvalidName"
+ />
+ </gl-form-group>
+ <gl-form-group
+ :label="$options.i18n.inputs.path.label"
+ :label-for="fields.path.id"
+ :description="pathDescription"
+ :state="pathFeedbackState"
+ :valid-feedback="$options.i18n.inputs.path.validFeedback"
+ :invalid-feedback="pathInvalidFeedback"
+ >
+ <gl-form-input-group>
+ <template #prepend>
+ <gl-input-group-text class="group-root-path">{{ basePath }}</gl-input-group-text>
+ </template>
+ <gl-form-input
+ :id="fields.path.id"
+ class="gl-field-error-ignore"
+ :name="fields.path.name"
+ :value="computedPath"
+ :placeholder="$options.i18n.inputs.path.placeholder"
+ :maxlength="fields.path.maxLength"
+ :pattern="fields.path.pattern"
+ :state="pathFeedbackState"
+ :size="$options.nameInputSize"
+ required
+ data-qa-selector="group_path_field"
+ :data-bind-in="mattermostEnabled ? $options.mattermostDataBindName : null"
+ @input="handlePathInput"
+ @invalid="handleInvalidPath"
+ />
+ </gl-form-input-group>
+ </gl-form-group>
+ <template v-if="isEditingGroup">
+ <gl-alert class="gl-mb-5" :dismissible="false" variant="warning">
+ {{ $options.i18n.changingUrlWarningMessage }}
+ <gl-link :href="$options.changingGroupPathHelpPagePath"
+ >{{ $options.i18n.learnMore }}
+ </gl-link>
+ </gl-alert>
+ <gl-form-group :label="$options.i18n.inputs.groupId.label" :label-for="fields.groupId.id">
+ <gl-form-input
+ :id="fields.groupId.id"
+ :value="fields.groupId.value"
+ :name="fields.groupId.name"
+ size="sm"
+ readonly
+ />
+ </gl-form-group>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue
index 313c8dadd1f..5706df0dd1b 100644
--- a/app/assets/javascripts/groups/components/groups.vue
+++ b/app/assets/javascripts/groups/components/groups.vue
@@ -43,7 +43,12 @@ export default {
<template>
<div class="groups-list-tree-container qa-groups-list-tree-container">
- <div v-if="searchEmpty" class="has-no-search-results">{{ searchEmptyMessage }}</div>
+ <div
+ v-if="searchEmpty"
+ class="has-no-search-results gl-font-style-italic gl-text-center gl-text-gray-600 gl-p-5"
+ >
+ {{ searchEmptyMessage }}
+ </div>
<template v-else>
<group-folder :groups="groups" :action="action" />
<pagination-links
diff --git a/app/assets/javascripts/groups/components/item_caret.vue b/app/assets/javascripts/groups/components/item_caret.vue
index a51edd385dd..ef82e6d693a 100644
--- a/app/assets/javascripts/groups/components/item_caret.vue
+++ b/app/assets/javascripts/groups/components/item_caret.vue
@@ -14,14 +14,14 @@ export default {
},
computed: {
iconClass() {
- return this.isGroupOpen ? 'angle-down' : 'angle-right';
+ return this.isGroupOpen ? 'chevron-down' : 'chevron-right';
},
},
};
</script>
<template>
- <span class="folder-caret gl-mr-2">
+ <span class="folder-caret gl-display-inline-block gl-text-secondary gl-w-5 gl-mr-2">
<gl-icon :size="12" :name="iconClass" />
</span>
</template>
diff --git a/app/assets/javascripts/groups/components/item_type_icon.vue b/app/assets/javascripts/groups/components/item_type_icon.vue
index 7821e604700..da4173993c5 100644
--- a/app/assets/javascripts/groups/components/item_type_icon.vue
+++ b/app/assets/javascripts/groups/components/item_type_icon.vue
@@ -24,5 +24,7 @@ export default {
</script>
<template>
- <span class="item-type-icon"> <gl-icon :name="iconClass" /> </span>
+ <span class="item-type-icon gl-display-inline-block gl-text-secondary">
+ <gl-icon :name="iconClass" />
+ </span>
</template>
diff --git a/app/assets/javascripts/groups/create_edit_form.js b/app/assets/javascripts/groups/create_edit_form.js
new file mode 100644
index 00000000000..8ca0e6077e9
--- /dev/null
+++ b/app/assets/javascripts/groups/create_edit_form.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import { parseRailsFormFields } from '~/lib/utils/forms';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import GroupNameAndPath from './components/group_name_and_path.vue';
+
+export const initGroupNameAndPath = () => {
+ const elements = document.querySelectorAll('.js-group-name-and-path');
+
+ if (!elements.length) {
+ return;
+ }
+
+ elements.forEach((element) => {
+ const fields = parseRailsFormFields(element);
+ const { basePath, mattermostEnabled } = element.dataset;
+
+ return new Vue({
+ el: element,
+ provide: {
+ fields,
+ basePath,
+ mattermostEnabled: parseBoolean(mattermostEnabled),
+ },
+ render(h) {
+ return h(GroupNameAndPath);
+ },
+ });
+ });
+};
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
index c34810954a3..dfcee80aec7 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -44,6 +44,31 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
components: {
groupsApp,
},
+ provide() {
+ const {
+ dataset: {
+ newSubgroupPath,
+ newProjectPath,
+ newSubgroupIllustration,
+ newProjectIllustration,
+ emptySubgroupIllustration,
+ renderEmptyState,
+ canCreateSubgroups,
+ canCreateProjects,
+ },
+ } = this.$options.el;
+
+ return {
+ newSubgroupPath,
+ newProjectPath,
+ newSubgroupIllustration,
+ newProjectIllustration,
+ emptySubgroupIllustration,
+ renderEmptyState: parseBoolean(renderEmptyState),
+ canCreateSubgroups: parseBoolean(canCreateSubgroups),
+ canCreateProjects: parseBoolean(canCreateProjects),
+ };
+ },
data() {
const { dataset } = dataEl || this.$options.el;
const hideProjects = parseBoolean(dataset.hideProjects);
diff --git a/app/assets/javascripts/groups/settings/api/access_dropdown_api.js b/app/assets/javascripts/groups/settings/api/access_dropdown_api.js
new file mode 100644
index 00000000000..5560d10d179
--- /dev/null
+++ b/app/assets/javascripts/groups/settings/api/access_dropdown_api.js
@@ -0,0 +1,16 @@
+import axios from '~/lib/utils/axios_utils';
+import { joinPaths } from '~/lib/utils/url_utility';
+
+const GROUP_SUBGROUPS_PATH = '/-/autocomplete/group_subgroups.json';
+
+const buildUrl = (urlRoot, url) => {
+ return joinPaths(urlRoot, url);
+};
+
+export const getSubGroups = () => {
+ return axios.get(buildUrl(gon.relative_url_root || '', GROUP_SUBGROUPS_PATH), {
+ params: {
+ group_id: gon.current_group_id,
+ },
+ });
+};
diff --git a/app/assets/javascripts/groups/settings/components/access_dropdown.vue b/app/assets/javascripts/groups/settings/components/access_dropdown.vue
new file mode 100644
index 00000000000..b8a269de98a
--- /dev/null
+++ b/app/assets/javascripts/groups/settings/components/access_dropdown.vue
@@ -0,0 +1,194 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
+import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } from 'lodash';
+import createFlash from '~/flash';
+import { __, s__, n__ } from '~/locale';
+import { getSubGroups } from '../api/access_dropdown_api';
+import { LEVEL_TYPES } from '../constants';
+
+export const i18n = {
+ selectUsers: s__('ProtectedEnvironment|Select groups'),
+ groupsSectionHeader: s__('AccessDropdown|Groups'),
+};
+
+export default {
+ i18n,
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlSearchBoxByType,
+ },
+ props: {
+ hasLicense: {
+ required: false,
+ type: Boolean,
+ default: true,
+ },
+ label: {
+ type: String,
+ required: false,
+ default: i18n.selectUsers,
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ preselectedItems: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ initialLoading: false,
+ query: '',
+ groups: [],
+ selected: {
+ [LEVEL_TYPES.GROUP]: [],
+ },
+ };
+ },
+ computed: {
+ preselected() {
+ return groupBy(this.preselectedItems, 'type');
+ },
+ toggleLabel() {
+ const counts = Object.fromEntries(
+ Object.entries(this.selected).map(([key, value]) => [key, value.length]),
+ );
+
+ const labelPieces = [];
+
+ if (counts[LEVEL_TYPES.GROUP] > 0) {
+ labelPieces.push(n__('1 group', '%d groups', counts[LEVEL_TYPES.GROUP]));
+ }
+
+ return labelPieces.join(', ') || this.label;
+ },
+ toggleClass() {
+ return this.toggleLabel === this.label ? 'gl-text-gray-500!' : '';
+ },
+ selection() {
+ return [...this.getDataForSave(LEVEL_TYPES.GROUP, 'group_id')];
+ },
+ },
+ watch: {
+ query: debounce(function debouncedSearch() {
+ return this.getData();
+ }, 500),
+ },
+ created() {
+ this.getData({ initial: true });
+ },
+ methods: {
+ focusInput() {
+ this.$refs.search.focusInput();
+ },
+ getData({ initial = false } = {}) {
+ this.initialLoading = initial;
+ this.loading = true;
+
+ if (this.hasLicense) {
+ Promise.all([this.groups.length ? Promise.resolve({ data: this.groups }) : getSubGroups()])
+ .then(([groupsResponse]) => {
+ this.consolidateData(groupsResponse.data);
+ this.setSelected({ initial });
+ })
+ .catch(() => createFlash({ message: __('Failed to load groups.') }))
+ .finally(() => {
+ this.initialLoading = false;
+ this.loading = false;
+ });
+ }
+ },
+ consolidateData(groupsResponse = []) {
+ if (this.hasLicense) {
+ this.groups = groupsResponse.map((group) => ({ ...group, type: LEVEL_TYPES.GROUP }));
+ }
+ },
+ setSelected({ initial } = {}) {
+ if (initial) {
+ const selectedGroups = intersectionWith(
+ this.groups,
+ this.preselectedItems,
+ (group, selected) => {
+ return selected.type === LEVEL_TYPES.GROUP && group.id === selected.group_id;
+ },
+ );
+ this.selected[LEVEL_TYPES.GROUP] = selectedGroups;
+ }
+ },
+ getDataForSave(accessType, key) {
+ const selected = this.selected[accessType].map(({ id }) => ({ [key]: id }));
+ const preselected = this.preselected[accessType];
+ const added = differenceBy(selected, preselected, key);
+ const preserved = intersectionBy(preselected, selected, key).map(({ id, [key]: keyId }) => ({
+ id,
+ [key]: keyId,
+ }));
+ const removed = differenceBy(preselected, selected, key).map(({ id, [key]: keyId }) => ({
+ id,
+ [key]: keyId,
+ _destroy: true,
+ }));
+ return [...added, ...removed, ...preserved];
+ },
+ onItemClick(item) {
+ this.toggleSelection(this.selected[item.type], item);
+ this.emitUpdate();
+ },
+ toggleSelection(arr, item) {
+ const itemIndex = arr.findIndex(({ id }) => id === item.id);
+ if (itemIndex > -1) {
+ arr.splice(itemIndex, 1);
+ } else arr.push(item);
+ },
+ isSelected(item) {
+ return this.selected[item.type].some((selected) => selected.id === item.id);
+ },
+ emitUpdate() {
+ this.$emit('select', this.selection);
+ },
+ onHide() {
+ this.$emit('hidden', this.selection);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown
+ :disabled="disabled || initialLoading"
+ :text="toggleLabel"
+ class="gl-min-w-20"
+ :toggle-class="toggleClass"
+ aria-labelledby="allowed-users-label"
+ @shown="focusInput"
+ @hidden="onHide"
+ >
+ <template #header>
+ <gl-search-box-by-type ref="search" v-model.trim="query" :is-loading="loading" />
+ </template>
+ <template v-if="groups.length">
+ <gl-dropdown-section-header>{{
+ $options.i18n.groupsSectionHeader
+ }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="group in groups"
+ :key="`${group.id}${group.name}`"
+ fingerprint
+ data-testid="group-dropdown-item"
+ :avatar-url="group.avatar_url"
+ is-check-item
+ :is-checked="isSelected(group)"
+ @click.native.capture.stop="onItemClick(group)"
+ >
+ {{ group.name }}
+ </gl-dropdown-item>
+ </template>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/groups/settings/constants.js b/app/assets/javascripts/groups/settings/constants.js
new file mode 100644
index 00000000000..c91c2a20529
--- /dev/null
+++ b/app/assets/javascripts/groups/settings/constants.js
@@ -0,0 +1,3 @@
+export const LEVEL_TYPES = {
+ GROUP: 'group',
+};
diff --git a/app/assets/javascripts/groups/settings/init_access_dropdown.js b/app/assets/javascripts/groups/settings/init_access_dropdown.js
new file mode 100644
index 00000000000..24419280fc0
--- /dev/null
+++ b/app/assets/javascripts/groups/settings/init_access_dropdown.js
@@ -0,0 +1,36 @@
+import * as Sentry from '@sentry/browser';
+import Vue from 'vue';
+import AccessDropdown from './components/access_dropdown.vue';
+
+export const initAccessDropdown = (el) => {
+ if (!el) {
+ return false;
+ }
+
+ const { label, disabled, preselectedItems } = el.dataset;
+ let preselected = [];
+ try {
+ preselected = JSON.parse(preselectedItems);
+ } catch (e) {
+ Sentry.captureException(e);
+ }
+
+ return new Vue({
+ el,
+ render(createElement) {
+ const vm = this;
+ return createElement(AccessDropdown, {
+ props: {
+ preselectedItems: preselected,
+ label,
+ disabled,
+ },
+ on: {
+ select(selected) {
+ vm.$emit('select', selected);
+ },
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
index 75f02af28c4..7112c43bab8 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
@@ -18,6 +18,20 @@ export default {
required: true,
},
},
+ modal: {
+ actionPrimary: {
+ text: __('Discard changes'),
+ attributes: {
+ variant: 'danger',
+ },
+ },
+ actionCancel: {
+ text: __('Cancel'),
+ attributes: {
+ variant: 'default',
+ },
+ },
+ },
computed: {
discardModalId() {
return `discard-file-${this.activeFile.path}`;
@@ -43,9 +57,9 @@ export default {
</script>
<template>
- <div class="d-flex ide-commit-editor-header align-items-center">
- <file-icon :file-name="activeFile.name" :size="16" class="mr-2" />
- <strong class="mr-2">
+ <div class="gl-display-flex ide-commit-editor-header gl-align-items-center">
+ <file-icon :file-name="activeFile.name" :size="16" class="gl-mr-3" />
+ <strong class="gl-mr-3">
<template v-if="activeFile.prevPath && activeFile.prevPath !== activeFile.path">
{{ activeFile.prevPath }} &#x2192;
</template>
@@ -66,12 +80,11 @@ export default {
</div>
<gl-modal
ref="discardModal"
- ok-variant="danger"
- cancel-variant="light"
- :ok-title="__('Discard changes')"
:modal-id="discardModalId"
:title="discardModalTitle"
- @ok="discardChanges(activeFile.path)"
+ :action-primary="$options.modal.actionPrimary"
+ :action-cancel="$options.modal.actionCancel"
+ @primary="discardChanges(activeFile.path)"
>
{{ __("You will lose all changes you've made to this file. This action cannot be undone.") }}
</gl-modal>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index cb906374fe1..05a254d3fbf 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -154,7 +154,7 @@ export default {
<gl-button
:disabled="commitButtonDisabled"
category="primary"
- variant="info"
+ variant="confirm"
block
class="qa-begin-commit-button"
data-testid="begin-commit-button"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index 829686ef051..91d78a7c28c 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -38,6 +38,20 @@ export default {
default: __('No changes'),
},
},
+ modal: {
+ actionPrimary: {
+ text: __('Discard all changes'),
+ attributes: {
+ variant: 'danger',
+ },
+ },
+ actionCancel: {
+ text: __('Cancel'),
+ attributes: {
+ variant: 'default',
+ },
+ },
+ },
computed: {
titleText() {
if (!this.title) return __('Changes');
@@ -66,10 +80,10 @@ export default {
<template>
<div class="ide-commit-list-container">
- <header class="multi-file-commit-panel-header d-flex mb-0">
- <div class="d-flex align-items-center flex-fill">
+ <header class="multi-file-commit-panel-header gl-display-flex gl-mb-0">
+ <div class="gl-display-flex gl-align-items-center flex-fill">
<strong> {{ titleText }} </strong>
- <div class="d-flex ml-auto">
+ <div class="gl-display-flex gl-ml-auto">
<gl-button
v-if="!stagedList"
v-gl-tooltip
@@ -100,17 +114,17 @@ export default {
/>
</li>
</ul>
- <p v-else class="multi-file-commit-list form-text text-muted text-center">
+ <p v-else class="multi-file-commit-list form-text gl-text-gray-600 gl-text-center">
{{ emptyStateText }}
</p>
<gl-modal
v-if="!stagedList"
ref="discardAllModal"
- ok-variant="danger"
modal-id="discard-all-changes"
- :ok-title="__('Discard all changes')"
:title="__('Discard all changes?')"
- @ok="unstageAndDiscardAllChanges"
+ :action-primary="$options.modal.actionPrimary"
+ :action-cancel="$options.modal.actionCancel"
+ @primary="unstageAndDiscardAllChanges"
>
{{ $options.discardModalText }}
</gl-modal>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue
index 43bf2e1a90c..0a8fec49aac 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlTooltipDirective, GlFormCheckbox } from '@gitlab/ui';
import { createNamespacedHelpers } from 'vuex';
import { s__ } from '~/locale';
@@ -8,19 +8,20 @@ const { mapActions: mapCommitActions, mapGetters: mapCommitGetters } = createNam
);
export default {
+ components: { GlFormCheckbox },
directives: {
GlTooltip: GlTooltipDirective,
},
+ i18n: {
+ newMrText: s__('IDE|Start a new merge request'),
+ tooltipText: s__(
+ 'IDE|This option is disabled because you are not allowed to create merge requests in this project.',
+ ),
+ },
computed: {
...mapCommitGetters(['shouldHideNewMrOption', 'shouldDisableNewMrOption', 'shouldCreateMR']),
tooltipText() {
- if (this.shouldDisableNewMrOption) {
- return s__(
- 'IDE|This option is disabled because you are not allowed to create merge requests in this project.',
- );
- }
-
- return '';
+ return this.shouldDisableNewMrOption ? this.$options.i18n.tooltipText : null;
},
},
methods: {
@@ -30,22 +31,23 @@ export default {
</script>
<template>
- <fieldset v-if="!shouldHideNewMrOption">
- <hr class="my-2" />
- <label
- v-gl-tooltip="tooltipText"
- class="mb-0 js-ide-commit-new-mr"
- :class="{ 'is-disabled': shouldDisableNewMrOption }"
+ <fieldset
+ v-if="!shouldHideNewMrOption"
+ v-gl-tooltip="tooltipText"
+ data-testid="new-merge-request-fieldset"
+ class="js-ide-commit-new-mr"
+ :class="{ 'is-disabled': shouldDisableNewMrOption }"
+ >
+ <hr class="gl-mt-3 gl-mb-4" />
+
+ <gl-form-checkbox
+ :disabled="shouldDisableNewMrOption"
+ :checked="shouldCreateMR"
+ @change="toggleShouldCreateMR"
>
- <input
- :disabled="shouldDisableNewMrOption"
- :checked="shouldCreateMR"
- type="checkbox"
- @change="toggleShouldCreateMR"
- />
- <span class="gl-ml-3 ide-option-label">
- {{ __('Start a new merge request') }}
+ <span class="ide-option-label">
+ {{ $options.i18n.newMrText }}
</span>
- </label>
+ </gl-form-checkbox>
</fieldset>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
index 870355e884e..bd5d28dbb56 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
@@ -1,8 +1,20 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlTooltipDirective,
+ GlFormRadio,
+ GlFormRadioGroup,
+ GlFormGroup,
+ GlFormInput,
+} from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
export default {
+ components: {
+ GlFormRadio,
+ GlFormRadioGroup,
+ GlFormGroup,
+ GlFormInput,
+ },
directives: {
GlTooltip: GlTooltipDirective,
},
@@ -51,35 +63,42 @@ export default {
</script>
<template>
- <fieldset>
- <label
+ <fieldset class="gl-mb-2">
+ <gl-form-radio-group
v-gl-tooltip="tooltipTitle"
+ :checked="commitAction"
:class="{
'is-disabled': disabled,
}"
>
- <input
+ <gl-form-radio
:value="value"
- :checked="commitAction === value"
:disabled="disabled"
- type="radio"
name="commit-action"
data-qa-selector="commit_type_radio"
- @change="updateCommitAction($event.target.value)"
- />
- <span class="gl-ml-3">
- <span v-if="label" class="ide-option-label"> {{ label }} </span> <slot v-else></slot>
- </span>
- </label>
- <div v-if="commitAction === value && showInput" class="ide-commit-new-branch">
- <input
+ @change="updateCommitAction(value)"
+ >
+ <span v-if="label" class="ide-option-label">
+ {{ label }}
+ </span>
+ <slot v-else></slot>
+ </gl-form-radio>
+ </gl-form-radio-group>
+
+ <gl-form-group
+ v-if="commitAction === value && showInput"
+ :label="placeholderBranchName"
+ :label-sr-only="true"
+ class="gl-ml-6 gl-mb-0"
+ >
+ <gl-form-input
:placeholder="placeholderBranchName"
:value="newBranchName"
+ :disabled="disabled"
data-testid="ide-new-branch-name"
- type="text"
- class="form-control monospace"
- @input="updateBranchName($event.target.value)"
+ class="gl-font-monospace"
+ @input="updateBranchName($event)"
/>
- </div>
+ </gl-form-group>
</fieldset>
</template>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 45bbf93ebc9..d589f56dd7c 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -176,7 +176,7 @@ export default {
{{ __('New file') }}
</gl-button>
</template>
- <gl-loading-icon v-else-if="!currentTree || currentTree.loading" size="md" />
+ <gl-loading-icon v-else-if="!currentTree || currentTree.loading" size="lg" />
<p v-else>
{{
__(
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index c3d6494692a..f32d35bf774 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlSkeletonLoader } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
import { SIDEBAR_INIT_WIDTH, leftSidebarViews } from '../constants';
import ActivityBar from './activity_bar.vue';
@@ -10,7 +10,7 @@ import ResizablePanel from './resizable_panel.vue';
export default {
components: {
- GlSkeletonLoading,
+ GlSkeletonLoader,
ResizablePanel,
ActivityBar,
IdeTree,
@@ -38,7 +38,7 @@ export default {
<template v-if="loading">
<div class="multi-file-commit-panel-inner" data-testid="ide-side-bar-inner">
<div v-for="n in 3" :key="n" class="multi-file-loading-container">
- <gl-skeleton-loading />
+ <gl-skeleton-loader />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 93ff7e8566f..e0b7ac9b1e1 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -32,8 +32,11 @@ export default {
...mapState('pipelines', ['latestPipeline']),
},
watch: {
- lastCommit() {
- this.initPipelinePolling();
+ lastCommit: {
+ handler() {
+ this.initPipelinePolling();
+ },
+ immediate: true,
},
},
mounted() {
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index 0fc7337ad26..c9bf84be6ac 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlSkeletonLoader } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import { WEBIDE_MARK_FILE_CLICKED } from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
@@ -10,7 +10,7 @@ import NavDropdown from './nav_dropdown.vue';
export default {
name: 'IdeTreeList',
components: {
- GlSkeletonLoading,
+ GlSkeletonLoader,
NavDropdown,
FileTree,
},
@@ -55,7 +55,7 @@ export default {
<div class="ide-file-list qa-file-list">
<template v-if="showLoading">
<div v-for="n in 3" :key="n" class="multi-file-loading-container">
- <gl-skeleton-loading />
+ <gl-skeleton-loader />
</div>
</template>
<template v-else>
diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue
index 8fd1973267c..c184e25f67f 100644
--- a/app/assets/javascripts/ide/components/jobs/detail/description.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue
@@ -23,7 +23,7 @@ export default {
<template>
<div class="d-flex align-items-center">
- <ci-icon :status="job.status" :borderless="true" :size="24" class="d-flex" />
+ <ci-icon is-borderless :status="job.status" :size="24" class="d-flex" />
<span class="gl-ml-3">
{{ job.name }}
<a
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
index 7797850f097..2284ffb8480 100644
--- a/app/assets/javascripts/ide/components/jobs/stage.vue
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -27,7 +27,7 @@ export default {
},
computed: {
collapseIcon() {
- return this.stage.isCollapsed ? 'angle-left' : 'angle-down';
+ return this.stage.isCollapsed ? 'chevron-lg-left' : 'chevron-lg-down';
},
showLoadingIcon() {
return this.stage.isLoading && !this.stage.jobs.length;
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index f14d86114b8..d71ac766933 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -65,6 +65,8 @@ export default {
modelManager: new ModelManager(),
isEditorLoading: true,
unwatchCiYaml: null,
+ SELivepreviewExtension: null,
+ MarkdownLivePreview: null,
};
},
computed: {
@@ -192,23 +194,6 @@ export default {
this.createEditorInstance();
}
},
- panelResizing() {
- if (!this.panelResizing) {
- this.refreshEditorDimensions();
- }
- },
- showTabs() {
- this.$nextTick(() => this.refreshEditorDimensions());
- },
- rightPaneIsOpen() {
- this.refreshEditorDimensions();
- },
- showEditor(val) {
- if (val) {
- // We need to wait for the editor to actually be rendered.
- this.$nextTick(() => this.refreshEditorDimensions());
- }
- },
showContentViewer(val) {
if (!val) return;
@@ -324,17 +309,33 @@ export default {
},
]);
- if (
- this.fileType === MARKDOWN_FILE_TYPE &&
- this.editor?.getEditorType() === EDITOR_TYPE_CODE &&
- this.previewMarkdownPath
- ) {
+ this.$nextTick(() => {
+ this.setupEditor();
+ });
+ }
+ },
+
+ setupEditor() {
+ if (!this.file || !this.editor || this.file.loading) return;
+
+ const useLivePreviewExtension = () => {
+ this.SELivepreviewExtension = this.editor.use({
+ definition: this.MarkdownLivePreview,
+ setupOptions: { previewMarkdownPath: this.previewMarkdownPath },
+ });
+ };
+ if (
+ this.fileType === MARKDOWN_FILE_TYPE &&
+ this.editor?.getEditorType() === EDITOR_TYPE_CODE &&
+ this.previewMarkdownPath
+ ) {
+ if (this.MarkdownLivePreview) {
+ useLivePreviewExtension();
+ } else {
import('~/editor/extensions/source_editor_markdown_livepreview_ext')
- .then(({ EditorMarkdownPreviewExtension: MarkdownLivePreview }) => {
- this.editor.use({
- definition: MarkdownLivePreview,
- setupOptions: { previewMarkdownPath: this.previewMarkdownPath },
- });
+ .then(({ EditorMarkdownPreviewExtension }) => {
+ this.MarkdownLivePreview = EditorMarkdownPreviewExtension;
+ useLivePreviewExtension();
})
.catch((e) =>
createFlash({
@@ -342,15 +343,9 @@ export default {
}),
);
}
-
- this.$nextTick(() => {
- this.setupEditor();
- });
+ } else if (this.SELivepreviewExtension) {
+ this.editor.unuse(this.SELivepreviewExtension);
}
- },
-
- setupEditor() {
- if (!this.file || !this.editor || this.file.loading) return;
const head = this.getStagedFile(this.file.path);
@@ -396,10 +391,6 @@ export default {
fileLanguage: this.model.language,
});
- this.$nextTick(() => {
- this.editor.updateDimensions();
- });
-
this.$emit('editorSetup');
if (performance.getEntriesByName(WEBIDE_MARK_FILE_CLICKED).length) {
eventHub.$emit(WEBIDE_MEASURE_FILE_AFTER_INTERACTION);
@@ -415,11 +406,6 @@ export default {
});
}
},
- refreshEditorDimensions() {
- if (this.showEditor && this.editor) {
- this.editor.updateDimensions();
- }
- },
fetchEditorconfigRules() {
return getRulesWithTraversal(this.file.path, (path) => {
const entry = this.entries[path];
diff --git a/app/assets/javascripts/ide/components/terminal/empty_state.vue b/app/assets/javascripts/ide/components/terminal/empty_state.vue
index f4dd83b16c7..623ba719b28 100644
--- a/app/assets/javascripts/ide/components/terminal/empty_state.vue
+++ b/app/assets/javascripts/ide/components/terminal/empty_state.vue
@@ -55,7 +55,7 @@ export default {
<gl-button
:disabled="!isValid"
category="primary"
- variant="info"
+ variant="confirm"
data-qa-selector="start_web_terminal_button"
@click="onStart"
>
diff --git a/app/assets/javascripts/ide/components/terminal/session.vue b/app/assets/javascripts/ide/components/terminal/session.vue
index 3a4128b6207..384e27844c6 100644
--- a/app/assets/javascripts/ide/components/terminal/session.vue
+++ b/app/assets/javascripts/ide/components/terminal/session.vue
@@ -16,7 +16,7 @@ export default {
if (isEndingStatus(this.session.status)) {
return {
action: () => this.restartSession(),
- variant: 'info',
+ variant: 'confirm',
category: 'primary',
text: __('Restart Terminal'),
};
diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js
index 52da9942efe..525afcb2083 100644
--- a/app/assets/javascripts/ide/lib/editor_options.js
+++ b/app/assets/javascripts/ide/lib/editor_options.js
@@ -8,6 +8,7 @@ export const defaultEditorOptions = {
},
wordWrap: 'on',
glyphMargin: true,
+ automaticLayout: true,
};
export const defaultDiffOptions = {
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index ec3630cc5eb..a7e6506b045 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -81,7 +81,7 @@ export function registerLanguages(def, ...defs) {
languages.setLanguageConfiguration(languageId, def.conf);
}
-export function registerSchema(schema) {
+export function registerSchema(schema, options = {}) {
const defaults = [languages.json.jsonDefaults, languages.yaml.yamlDefaults];
defaults.forEach((d) =>
d.setDiagnosticsOptions({
@@ -90,6 +90,7 @@ export function registerSchema(schema) {
hover: true,
completion: true,
schemas: [schema],
+ ...options,
}),
);
}
diff --git a/app/assets/javascripts/image_diff/helpers/dom_helper.js b/app/assets/javascripts/image_diff/helpers/dom_helper.js
index 3468a629f5a..180e927a3e7 100644
--- a/app/assets/javascripts/image_diff/helpers/dom_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/dom_helper.js
@@ -6,7 +6,7 @@ export function setPositionDataAttribute(el, options) {
const positionObject = { ...JSON.parse(position), x, y, width, height };
- el.setAttribute('data-position', JSON.stringify(positionObject));
+ el.dataset.position = JSON.stringify(positionObject);
}
export function updateDiscussionAvatarBadgeNumber(discussionEl, newBadgeNumber) {
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 ce401862cc1..6b96fa7c45c 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
@@ -15,9 +15,11 @@ import { debounce } from 'lodash';
import createFlash from '~/flash';
import { s__, __, n__, sprintf } from '~/locale';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
import { getGroupPathAvailability } from '~/rest_api';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { helpPagePath } from '~/helpers/help_page_helper';
import { STATUSES } from '../../constants';
import ImportStatusCell from '../../components/import_status.vue';
@@ -56,6 +58,7 @@ export default {
ImportStatusCell,
ImportActionsCell,
PaginationBar,
+ HelpPopover,
},
props: {
@@ -190,9 +193,9 @@ export default {
statusMessage() {
return this.filter.length === 0
- ? s__('BulkImport|Showing %{start}-%{end} of %{total} from %{link}')
+ ? s__('BulkImport|Showing %{start}-%{end} of %{total} that you own from %{link}')
: s__(
- 'BulkImport|Showing %{start}-%{end} of %{total} matching filter "%{filter}" from %{link}',
+ 'BulkImport|Showing %{start}-%{end} of %{total} that you own matching filter "%{filter}" from %{link}',
);
},
@@ -484,6 +487,9 @@ export default {
gitlabLogo: window.gon.gitlab_logo,
PAGE_SIZES,
+ permissionsHelpPath: helpPagePath('user/permissions', { anchor: 'group-members-permissions' }),
+ popoverOptions: { title: __('What is listed here?') },
+ i18n,
};
</script>
@@ -533,8 +539,8 @@ export default {
<div
class="gl-py-5 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex"
>
- <span>
- <gl-sprintf v-if="!$apollo.loading && hasGroups" :message="statusMessage">
+ <span v-if="!$apollo.loading && hasGroups">
+ <gl-sprintf :message="statusMessage">
<template #start>
<strong>{{ paginationInfo.start }}</strong>
</template>
@@ -548,12 +554,26 @@ export default {
<strong>{{ filter }}</strong>
</template>
<template #link>
- <gl-link :href="sourceUrl" target="_blank">
- {{ sourceUrl }}<gl-icon name="external-link" class="vertical-align-middle" />
- </gl-link>
+ {{ sourceUrl }}
</template>
</gl-sprintf>
+ <help-popover :options="$options.popoverOptions">
+ <gl-sprintf
+ :message="
+ s__(
+ 'BulkImport|Only groups that you have the %{role} role for are listed as groups you can import.',
+ )
+ "
+ >
+ <template #role>
+ <gl-link class="gl-font-sm" :href="$options.permissionsHelpPath" target="_blank">{{
+ $options.i18n.OWNER
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </help-popover>
</span>
+
<gl-search-box-by-click
class="gl-ml-auto"
:placeholder="s__('BulkImport|Filter by source group')"
@@ -561,18 +581,26 @@ export default {
@clear="filter = ''"
/>
</div>
- <gl-loading-icon v-if="$apollo.loading" size="md" class="gl-mt-5" />
+ <gl-loading-icon v-if="$apollo.loading" size="lg" class="gl-mt-5" />
<template v-else>
<gl-empty-state
v-if="hasEmptyFilter"
:title="__('Sorry, your filter produced no results')"
:description="__('To widen your search, change or remove filters above.')"
/>
- <gl-empty-state
- v-else-if="!hasGroups"
- :title="s__('BulkImport|You have no groups to import')"
- :description="__('Check your source instance permissions.')"
- />
+ <gl-empty-state v-else-if="!hasGroups" :title="$options.i18n.NO_GROUPS_FOUND">
+ <template #description>
+ <gl-sprintf
+ :message="__('You don\'t have the %{role} role for any groups in this instance.')"
+ >
+ <template #role>
+ <gl-link :href="$options.permissionsHelpPath" target="_blank">{{
+ $options.i18n.OWNER
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-empty-state>
<template v-else>
<div
class="gl-bg-gray-10 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-px-4 gl-display-flex gl-align-items-center import-table-bar"
diff --git a/app/assets/javascripts/import_entities/import_groups/constants.js b/app/assets/javascripts/import_entities/import_groups/constants.js
index 32137308684..7e532dfec05 100644
--- a/app/assets/javascripts/import_entities/import_groups/constants.js
+++ b/app/assets/javascripts/import_entities/import_groups/constants.js
@@ -12,6 +12,9 @@ export const i18n = {
ERROR_IMPORT: s__('BulkImport|Importing the group failed.'),
ERROR_IMPORT_COMPLETED: s__('BulkImport|Import is finished. Pick another name for re-import'),
+ NO_GROUPS_FOUND: s__('BulkImport|No groups found'),
+ OWNER: __('Owner'),
+
features: {
projectMigration: __('projects'),
},
diff --git a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
index 0307607321e..848c7361601 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
@@ -181,7 +181,7 @@ export default {
:key="pagePaginationStateKey"
@appear="fetchRepos"
/>
- <gl-loading-icon v-if="isLoading" class="gl-mt-7" size="md" />
+ <gl-loading-icon v-if="isLoading" class="gl-mt-7" size="lg" />
<div v-if="!isLoading && repositories.length === 0" class="gl-text-center">
<strong>{{ emptyStateText }}</strong>
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 922e870caa7..dbd2225167a 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -144,7 +144,6 @@ export default {
'assigneeUsernameQuery',
'slaFeatureAvailable',
'canCreateIncident',
- 'incidentEscalationsAvailable',
],
apollo: {
incidents: {
@@ -238,7 +237,6 @@ export default {
const isHidden = {
published: !this.publishedAvailable,
incidentSla: !this.slaFeatureAvailable,
- escalationStatus: !this.incidentEscalationsAvailable,
};
return this.$options.fields.filter(({ key }) => !isHidden[key]);
@@ -421,7 +419,7 @@ export default {
</div>
</template>
- <template v-if="incidentEscalationsAvailable" #cell(escalationStatus)="{ item }">
+ <template #cell(escalationStatus)="{ item }">
<tooltip-on-truncate
:title="getEscalationStatus(item.escalationStatus)"
data-testid="incident-escalation-status"
diff --git a/app/assets/javascripts/incidents/list.js b/app/assets/javascripts/incidents/list.js
index c0f16a43d5c..1d40f1093a4 100644
--- a/app/assets/javascripts/incidents/list.js
+++ b/app/assets/javascripts/incidents/list.js
@@ -46,7 +46,6 @@ export default () => {
assigneeUsernameQuery,
slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
canCreateIncident: parseBoolean(canCreateIncident),
- incidentEscalationsAvailable: parseBoolean(gon?.features?.incidentEscalations),
},
apolloProvider,
render(createElement) {
diff --git a/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue b/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue
index 866d2ff399e..e8c9aa53a7c 100644
--- a/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue
+++ b/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue
@@ -11,6 +11,7 @@ import {
GlModal,
GlModalDirective,
} from '@gitlab/ui';
+import { __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { I18N_PAGERDUTY_SETTINGS_FORM, CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK } from '../constants';
@@ -42,6 +43,21 @@ export default {
};
},
i18n: I18N_PAGERDUTY_SETTINGS_FORM,
+ modal: {
+ id: 'resetWebhookModal',
+ actionPrimary: {
+ text: I18N_PAGERDUTY_SETTINGS_FORM.webhookUrl.resetWebhookUrl,
+ attributes: {
+ variant: 'danger',
+ },
+ },
+ actionCancel: {
+ text: __('Cancel'),
+ attributes: {
+ variant: 'default',
+ },
+ },
+ },
CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK,
computed: {
formData() {
@@ -152,11 +168,11 @@ export default {
{{ $options.i18n.webhookUrl.resetWebhookUrl }}
</gl-button>
<gl-modal
- modal-id="resetWebhookModal"
+ :modal-id="$options.modal.id"
:title="$options.i18n.webhookUrl.resetWebhookUrl"
- :ok-title="$options.i18n.webhookUrl.resetWebhookUrl"
- ok-variant="danger"
- @ok="resetWebhookUrl"
+ :action-primary="$options.modal.actionPrimary"
+ :action-cancel="$options.modal.actionCancel"
+ @primary="resetWebhookUrl"
>
{{ $options.i18n.webhookUrl.restKeyInfo }}
</gl-modal>
diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js
index b9975eed716..e4f6e931ec0 100644
--- a/app/assets/javascripts/integrations/constants.js
+++ b/app/assets/javascripts/integrations/constants.js
@@ -27,15 +27,51 @@ export const settingsTabTitle = __('Settings');
export const overridesTabTitle = s__('Integrations|Projects using custom settings');
export const integrationFormSections = {
+ CONFIGURATION: 'configuration',
CONNECTION: 'connection',
JIRA_TRIGGER: 'jira_trigger',
JIRA_ISSUES: 'jira_issues',
+ TRIGGER: 'trigger',
};
export const integrationFormSectionComponents = {
+ [integrationFormSections.CONFIGURATION]: 'IntegrationSectionConfiguration',
[integrationFormSections.CONNECTION]: 'IntegrationSectionConnection',
[integrationFormSections.JIRA_TRIGGER]: 'IntegrationSectionJiraTrigger',
[integrationFormSections.JIRA_ISSUES]: 'IntegrationSectionJiraIssues',
+ [integrationFormSections.TRIGGER]: 'IntegrationSectionTrigger',
+};
+
+export const integrationTriggerEvents = {
+ PUSH: 'push_events',
+ ISSUE: 'issues_events',
+ CONFIDENTIAL_ISSUE: 'confidential_issues_events',
+ MERGE_REQUEST: 'merge_requests_events',
+ NOTE: 'note_events',
+ CONFIDENTIAL_NOTE: 'confidential_note_events',
+ TAG_PUSH: 'tag_push_events',
+ PIPELINE: 'pipeline_events',
+ WIKI_PAGE: 'wiki_page_events',
+};
+
+export const integrationTriggerEventTitles = {
+ [integrationTriggerEvents.PUSH]: s__('IntegrationEvents|A push is made to the repository'),
+ [integrationTriggerEvents.ISSUE]: s__(
+ 'IntegrationEvents|An issue is created, updated, or closed',
+ ),
+ [integrationTriggerEvents.CONFIDENTIAL_ISSUE]: s__(
+ 'IntegrationEvents|A confidential issue is created, updated, or closed',
+ ),
+ [integrationTriggerEvents.MERGE_REQUEST]: s__(
+ 'IntegrationEvents|A merge request is created, updated, or merged',
+ ),
+ [integrationTriggerEvents.NOTE]: s__('IntegrationEvents|A comment is added on an issue'),
+ [integrationTriggerEvents.CONFIDENTIAL_NOTE]: s__(
+ 'IntegrationEvents|A comment is added on a confidential issue',
+ ),
+ [integrationTriggerEvents.TAG_PUSH]: s__('IntegrationEvents|A tag is pushed to the repository'),
+ [integrationTriggerEvents.PIPELINE]: s__('IntegrationEvents|A pipeline status changes'),
+ [integrationTriggerEvents.WIKI_PAGE]: s__('IntegrationEvents|A wiki page is created or updated'),
};
export const billingPlans = {
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index 9f43360fb73..9307d7c2d3d 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -37,6 +37,10 @@ export default {
DynamicField,
ConfirmationModal,
ResetConfirmationModal,
+ IntegrationSectionConfiguration: () =>
+ import(
+ /* webpackChunkName: 'integrationSectionConfiguration' */ '~/integrations/edit/components/sections/configuration.vue'
+ ),
IntegrationSectionConnection: () =>
import(
/* webpackChunkName: 'integrationSectionConnection' */ '~/integrations/edit/components/sections/connection.vue'
@@ -49,6 +53,10 @@ export default {
import(
/* webpackChunkName: 'integrationSectionJiraTrigger' */ '~/integrations/edit/components/sections/jira_trigger.vue'
),
+ IntegrationSectionTrigger: () =>
+ import(
+ /* webpackChunkName: 'integrationSectionTrigger' */ '~/integrations/edit/components/sections/trigger.vue'
+ ),
GlBadge,
GlButton,
GlForm,
@@ -193,7 +201,7 @@ export default {
<gl-form
ref="integrationForm"
method="post"
- class="gl-mb-3 gl-show-field-errors integration-settings-form"
+ class="gl-mt-6 gl-mb-3 gl-show-field-errors integration-settings-form"
:action="propsSource.formPath"
:novalidate="!integrationActive"
>
diff --git a/app/assets/javascripts/integrations/edit/components/jira_upgrade_cta.vue b/app/assets/javascripts/integrations/edit/components/jira_upgrade_cta.vue
deleted file mode 100644
index 9164e484440..00000000000
--- a/app/assets/javascripts/integrations/edit/components/jira_upgrade_cta.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<script>
-import { GlButton, GlCard } from '@gitlab/ui';
-import { s__, __ } from '~/locale';
-
-export default {
- components: {
- GlButton,
- GlCard,
- },
- props: {
- upgradePlanPath: {
- type: String,
- required: false,
- default: '',
- },
- showPremiumMessage: {
- type: Boolean,
- required: false,
- default: false,
- },
- showUltimateMessage: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- computed: {
- title() {
- return this.showUltimateMessage
- ? this.$options.i18n.titleUltimate
- : this.$options.i18n.titlePremium;
- },
- },
- i18n: {
- titleUltimate: s__('JiraService|This is an Ultimate feature'),
- titlePremium: s__('JiraService|This is a Premium feature'),
- content: s__('JiraService|Upgrade your plan to enable this feature of the Jira Integration.'),
- upgrade: __('Upgrade your plan'),
- },
-};
-</script>
-
-<template>
- <gl-card>
- <strong>{{ title }}</strong>
- <p>{{ $options.i18n.content }}</p>
- <gl-button v-if="upgradePlanPath" category="primary" variant="info" :href="upgradePlanPath">
- {{ $options.i18n.upgrade }}
- </gl-button>
- </gl-card>
-</template>
diff --git a/app/assets/javascripts/integrations/edit/components/sections/configuration.vue b/app/assets/javascripts/integrations/edit/components/sections/configuration.vue
new file mode 100644
index 00000000000..9e1ad24ae9f
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/components/sections/configuration.vue
@@ -0,0 +1,38 @@
+<script>
+import { mapGetters } from 'vuex';
+
+import DynamicField from '../dynamic_field.vue';
+
+export default {
+ name: 'IntegrationSectionConfiguration',
+ components: {
+ DynamicField,
+ },
+ props: {
+ fields: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ isValidated: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapGetters(['currentKey']),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <dynamic-field
+ v-for="field in fields"
+ :key="`${currentKey}-${field.name}`"
+ v-bind="field"
+ :is-validated="isValidated"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/integrations/edit/components/sections/trigger.vue b/app/assets/javascripts/integrations/edit/components/sections/trigger.vue
new file mode 100644
index 00000000000..9af5070d4cf
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/components/sections/trigger.vue
@@ -0,0 +1,26 @@
+<script>
+import { mapGetters } from 'vuex';
+
+import TriggerField from '../trigger_field.vue';
+
+export default {
+ name: 'IntegrationSectionTrigger',
+ components: {
+ TriggerField,
+ },
+ computed: {
+ ...mapGetters(['currentKey', 'propsSource']),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <trigger-field
+ v-for="event in propsSource.triggerEvents"
+ :key="`${currentKey}-trigger-fields-${event.name}`"
+ :event="event"
+ class="gl-mb-3"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/integrations/edit/components/trigger_field.vue b/app/assets/javascripts/integrations/edit/components/trigger_field.vue
new file mode 100644
index 00000000000..dc5ae2f3a3d
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/components/trigger_field.vue
@@ -0,0 +1,46 @@
+<script>
+import { GlFormCheckbox } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
+
+import { integrationTriggerEventTitles } from '~/integrations/constants';
+
+export default {
+ name: 'TriggerField',
+ components: {
+ GlFormCheckbox,
+ },
+ props: {
+ event: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ value: false,
+ };
+ },
+ computed: {
+ ...mapGetters(['isInheriting']),
+ name() {
+ return `service[${this.event.name}]`;
+ },
+ title() {
+ return integrationTriggerEventTitles[this.event.name];
+ },
+ },
+ mounted() {
+ this.value = this.event.value || false;
+ },
+};
+</script>
+
+<template>
+ <div>
+ <input :name="name" type="hidden" :value="value" />
+ <gl-form-checkbox v-model="value" :disabled="isInheriting">
+ {{ title }}
+ </gl-form-checkbox>
+ </div>
+</template>
diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js
index 92e6ca509c3..2360588ab39 100644
--- a/app/assets/javascripts/integrations/edit/index.js
+++ b/app/assets/javascripts/integrations/edit/index.js
@@ -21,7 +21,6 @@ function parseDatasetToProps(data) {
type,
commentDetail,
projectKey,
- upgradePlanPath,
learnMorePath,
aboutPricingUrl,
triggerEvents,
@@ -80,12 +79,11 @@ function parseDatasetToProps(data) {
initialEnableJiraVulnerabilities: enableJiraVulnerabilities,
initialVulnerabilitiesIssuetype: vulnerabilitiesIssuetype,
initialProjectKey: projectKey,
- upgradePlanPath,
},
learnMorePath,
aboutPricingUrl,
triggerEvents: JSON.parse(triggerEvents),
- sections: JSON.parse(sections, { deep: true }),
+ sections: JSON.parse(sections),
fields: convertObjectPropsToCamelCase(JSON.parse(fields), { deep: true }),
inheritFromId: parseInt(inheritFromId, 10),
integrationLevel,
diff --git a/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue b/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
index f2d3e6489ee..1255ed01f6d 100644
--- a/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
+++ b/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
@@ -136,7 +136,7 @@ export default {
</template>
<template #table-busy>
- <gl-loading-icon size="md" class="gl-my-2" />
+ <gl-loading-icon size="lg" class="gl-my-2" />
</template>
</gl-table>
<div class="gl-display-flex gl-justify-content-center gl-mt-5">
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 7857b9d86d2..d597c7e53bb 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -14,6 +14,7 @@ import ExperimentTracking from '~/experimentation/experiment_tracking';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { getParameterValues } from '~/lib/utils/url_utility';
import {
+ CLOSE_TO_LIMIT_COUNT,
USERS_FILTER_ALL,
INVITE_MEMBERS_FOR_TASK,
MEMBER_MODAL_LABELS,
@@ -151,6 +152,16 @@ export default {
isOnLearnGitlab() {
return this.source === LEARN_GITLAB;
},
+ closeToLimit() {
+ if (this.usersLimitDataset.freeUsersLimit && this.usersLimitDataset.membersCount) {
+ return (
+ this.usersLimitDataset.membersCount >=
+ this.usersLimitDataset.freeUsersLimit - CLOSE_TO_LIMIT_COUNT
+ );
+ }
+
+ return false;
+ },
reachedLimit() {
if (this.usersLimitDataset.freeUsersLimit && this.usersLimitDataset.membersCount) {
return this.usersLimitDataset.membersCount >= this.usersLimitDataset.freeUsersLimit;
@@ -297,6 +308,7 @@ export default {
:is-loading="isLoading"
:new-users-to-invite="newUsersToInvite"
:root-group-id="rootId"
+ :close-to-limit="closeToLimit"
:reached-limit="reachedLimit"
:users-limit-dataset="usersLimitDataset"
@reset="resetFields"
@@ -314,6 +326,7 @@ export default {
<template #user-limit-notification>
<user-limit-notification
+ :close-to-limit="closeToLimit"
:reached-limit="reachedLimit"
:users-limit-dataset="usersLimitDataset"
/>
diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
index 79b192e2495..42645110e48 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
@@ -2,7 +2,11 @@
import { GlButton, GlLink, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
-import { TRIGGER_ELEMENT_BUTTON, TRIGGER_ELEMENT_SIDE_NAV } from '../constants';
+import {
+ TRIGGER_ELEMENT_BUTTON,
+ TRIGGER_ELEMENT_SIDE_NAV,
+ TRIGGER_DEFAULT_QA_SELECTOR,
+} from '../constants';
export default {
components: { GlButton, GlLink, GlIcon },
@@ -46,12 +50,17 @@ export default {
required: false,
default: '',
},
+ qaSelector: {
+ type: String,
+ required: false,
+ default: TRIGGER_DEFAULT_QA_SELECTOR,
+ },
},
computed: {
componentAttributes() {
const baseAttributes = {
class: this.classes,
- 'data-qa-selector': 'invite_members_button',
+ 'data-qa-selector': this.qaSelector,
'data-test-id': 'invite-members-button',
};
diff --git a/app/assets/javascripts/invite_members/components/invite_modal_base.vue b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
index 33d37b809c2..90d266c3155 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -131,6 +131,11 @@ export default {
required: false,
default: false,
},
+ closeToLimit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
reachedLimit: {
type: Boolean,
required: false,
@@ -183,6 +188,17 @@ export default {
actionCancel() {
if (this.reachedLimit && this.usersLimitDataset.userNamespace) return undefined;
+ if (this.closeToLimit && this.usersLimitDataset.userNamespace) {
+ return {
+ text: INVITE_BUTTON_TEXT_DISABLED,
+ attributes: {
+ href: this.usersLimitDataset.membersPath,
+ category: 'secondary',
+ variant: 'confirm',
+ },
+ };
+ }
+
return {
text: this.reachedLimit ? CANCEL_BUTTON_TEXT_DISABLED : this.cancelButtonText,
...(this.reachedLimit && { attributes: { href: this.usersLimitDataset.purchasePath } }),
diff --git a/app/assets/javascripts/invite_members/components/user_limit_notification.vue b/app/assets/javascripts/invite_members/components/user_limit_notification.vue
index ea5f4317d86..ae5c3c11386 100644
--- a/app/assets/javascripts/invite_members/components/user_limit_notification.vue
+++ b/app/assets/javascripts/invite_members/components/user_limit_notification.vue
@@ -8,15 +8,20 @@ import {
REACHED_LIMIT_MESSAGE,
REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
CLOSE_TO_LIMIT_MESSAGE,
+ CLOSE_TO_LIMIT_MESSAGE_PERSONAL_NAMESPACE,
+ DANGER_ALERT_TITLE_PERSONAL_NAMESPACE,
+ WARNING_ALERT_TITLE_PERSONAL_NAMESPACE,
} from '../constants';
-const CLOSE_TO_LIMIT_COUNT = 2;
-
export default {
name: 'UserLimitNotification',
components: { GlAlert, GlSprintf, GlLink },
inject: ['name'],
props: {
+ closeToLimit: {
+ type: Boolean,
+ required: true,
+ },
reachedLimit: {
type: Boolean,
required: true,
@@ -40,14 +45,14 @@ export default {
purchasePath() {
return this.usersLimitDataset.purchasePath;
},
- closeToLimit() {
- if (this.freeUsersLimit && this.membersCount) {
- return this.membersCount >= this.freeUsersLimit - CLOSE_TO_LIMIT_COUNT;
+ warningAlertTitle() {
+ if (this.usersLimitDataset.userNamespace) {
+ return sprintf(WARNING_ALERT_TITLE_PERSONAL_NAMESPACE, {
+ count: this.freeUsersLimit - this.membersCount,
+ members: this.pluralMembers(this.freeUsersLimit - this.membersCount),
+ });
}
- return false;
- },
- warningAlertTitle() {
return sprintf(WARNING_ALERT_TITLE, {
count: this.freeUsersLimit - this.membersCount,
members: this.pluralMembers(this.freeUsersLimit - this.membersCount),
@@ -55,6 +60,13 @@ export default {
});
},
dangerAlertTitle() {
+ if (this.usersLimitDataset.userNamespace) {
+ return sprintf(DANGER_ALERT_TITLE_PERSONAL_NAMESPACE, {
+ count: this.freeUsersLimit,
+ members: this.pluralMembers(this.freeUsersLimit),
+ });
+ }
+
return sprintf(DANGER_ALERT_TITLE, {
count: this.freeUsersLimit,
members: this.pluralMembers(this.freeUsersLimit),
@@ -79,6 +91,10 @@ export default {
return this.reachedLimitMessage;
}
+ if (this.usersLimitDataset.userNamespace) {
+ return this.$options.i18n.closeToLimitMessagePersonalNamespace;
+ }
+
return this.$options.i18n.closeToLimitMessage;
},
},
@@ -91,6 +107,7 @@ export default {
reachedLimitMessage: REACHED_LIMIT_MESSAGE,
reachedLimitUpgradeSuggestionMessage: REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
closeToLimitMessage: CLOSE_TO_LIMIT_MESSAGE,
+ closeToLimitMessagePersonalNamespace: CLOSE_TO_LIMIT_MESSAGE_PERSONAL_NAMESPACE,
},
};
</script>
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index 928f79f1c8d..beb8f5b5aab 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -1,7 +1,7 @@
import { s__ } from '~/locale';
+export const CLOSE_TO_LIMIT_COUNT = 2;
export const SEARCH_DELAY = 200;
-
export const INVITE_MEMBERS_FOR_TASK = {
minimum_access_level: 30,
name: 'invite_members_for_task',
@@ -18,6 +18,7 @@ export const USERS_FILTER_ALL = 'all';
export const USERS_FILTER_SAML_PROVIDER_ID = 'saml_provider_id';
export const TRIGGER_ELEMENT_BUTTON = 'button';
export const TRIGGER_ELEMENT_SIDE_NAV = 'side-nav';
+export const TRIGGER_DEFAULT_QA_SELECTOR = 'invite_members_button';
export const MEMBERS_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite members');
export const MEMBERS_MODAL_CELEBRATE_TITLE = s__(
'InviteMembersModal|GitLab is better with colleagues!',
@@ -131,10 +132,17 @@ export const ON_SUBMIT_TRACK_LABEL = 'manage_members_clicked';
export const WARNING_ALERT_TITLE = s__(
'InviteMembersModal|You only have space for %{count} more %{members} in %{name}',
);
+export const WARNING_ALERT_TITLE_PERSONAL_NAMESPACE = s__(
+ 'InviteMembersModal|You only have space for %{count} more %{members} in your personal projects',
+);
export const DANGER_ALERT_TITLE = s__(
"InviteMembersModal|You've reached your %{count} %{members} limit for %{name}",
);
+export const DANGER_ALERT_TITLE_PERSONAL_NAMESPACE = s__(
+ "InviteMembersModal|You've reached your %{count} %{members} limit for your personal projects",
+);
+
export const REACHED_LIMIT_MESSAGE = s__(
'InviteMembersModal|You cannot add more members, but you can remove members who no longer need access.',
);
@@ -148,3 +156,6 @@ export const REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE = REACHED_LIMIT_MESSAGE.co
export const CLOSE_TO_LIMIT_MESSAGE = s__(
'InviteMembersModal|To get more members an owner of this namespace can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier.',
);
+export const CLOSE_TO_LIMIT_MESSAGE_PERSONAL_NAMESPACE = s__(
+ 'InviteMembersModal|To make more space, you can remove members who no longer need access.',
+);
diff --git a/app/assets/javascripts/issuable/components/csv_export_modal.vue b/app/assets/javascripts/issuable/components/csv_export_modal.vue
index b0af3612e05..736da92fa9f 100644
--- a/app/assets/javascripts/issuable/components/csv_export_modal.vue
+++ b/app/assets/javascripts/issuable/components/csv_export_modal.vue
@@ -1,16 +1,18 @@
<script>
-import { GlButton, GlModal, GlSprintf, GlIcon } from '@gitlab/ui';
+import { GlModal, GlSprintf, GlIcon } from '@gitlab/ui';
import { __, n__ } from '~/locale';
import { ISSUABLE_TYPE } from '../constants';
export default {
+ actionCancel: {
+ text: __('Cancel'),
+ },
i18n: {
exportText: __(
'The CSV export will be created in the background. Once finished, it will be sent to %{email} in an attachment.',
),
},
components: {
- GlButton,
GlModal,
GlSprintf,
GlIcon,
@@ -38,6 +40,19 @@ export default {
},
},
computed: {
+ actionPrimary() {
+ return {
+ text: this.exportText,
+ attributes: {
+ href: this.exportCsvPath,
+ variant: 'confirm',
+ 'data-method': 'post',
+ 'data-qa-selector': `export_${this.issuableType}_button`,
+ 'data-track-action': 'click_button',
+ 'data-track-label': `export_${this.issuableType}_csv`,
+ },
+ };
+ },
isIssue() {
return this.issuableType === ISSUABLE_TYPE.issues;
},
@@ -56,6 +71,8 @@ export default {
<template>
<gl-modal
:modal-id="modalId"
+ :action-primary="actionPrimary"
+ :action-cancel="$options.actionCancel"
body-class="gl-p-0!"
:title="exportText"
data-qa-selector="export_issuable_modal"
@@ -73,18 +90,5 @@ export default {
</template>
</gl-sprintf>
</div>
- <template #modal-footer>
- <gl-button
- category="primary"
- variant="confirm"
- :href="exportCsvPath"
- data-method="post"
- :data-qa-selector="`export_${issuableType}_button`"
- data-track-action="click_button"
- :data-track-label="`export_${issuableType}_csv`"
- >
- {{ exportText }}
- </gl-button>
- </template>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/issuable/components/csv_import_modal.vue b/app/assets/javascripts/issuable/components/csv_import_modal.vue
index 7e2cbf03801..72293343c48 100644
--- a/app/assets/javascripts/issuable/components/csv_import_modal.vue
+++ b/app/assets/javascripts/issuable/components/csv_import_modal.vue
@@ -18,6 +18,9 @@ export default {
actionPrimary: {
text: __('Import issues'),
},
+ actionCancel: {
+ text: __('Cancel'),
+ },
components: {
GlModal,
GlFormGroup,
@@ -55,6 +58,7 @@ export default {
:modal-id="modalId"
:title="$options.i18n.importIssuesText"
:action-primary="$options.actionPrimary"
+ :action-cancel="$options.actionCancel"
@primary="submitForm"
>
<form ref="form" :action="importCsvIssuesPath" enctype="multipart/form-data" method="post">
diff --git a/app/assets/javascripts/issuable/components/issuable_header_warnings.vue b/app/assets/javascripts/issuable/components/issuable_header_warnings.vue
index 06d1a2ee233..543dca0afe1 100644
--- a/app/assets/javascripts/issuable/components/issuable_header_warnings.vue
+++ b/app/assets/javascripts/issuable/components/issuable_header_warnings.vue
@@ -27,7 +27,7 @@ export default {
return this.getNoteableData.confidential;
},
isMergeRequest() {
- return this.getNoteableData.targetType === 'merge_request' && this.glFeatures.updatedMrHeader;
+ return this.getNoteableData.targetType === 'merge_request';
},
warningIconsMeta() {
return [
diff --git a/app/assets/javascripts/issuable/components/related_issuable_item.vue b/app/assets/javascripts/issuable/components/related_issuable_item.vue
index dfe18567608..e6379b35f7a 100644
--- a/app/assets/javascripts/issuable/components/related_issuable_item.vue
+++ b/app/assets/javascripts/issuable/components/related_issuable_item.vue
@@ -71,8 +71,9 @@ export default {
:class="{
'issuable-info-container': !canReorder,
'card-body': canReorder,
+ 'gl-pr-2': canRemove,
}"
- class="item-body d-flex align-items-center py-2 px-3"
+ class="item-body d-flex align-items-center gl-py-3 gl-px-5"
>
<div
class="item-contents gl-display-flex gl-align-items-center gl-flex-wrap gl-flex-grow-1 flex-xl-nowrap gl-min-h-7"
@@ -170,7 +171,7 @@ export default {
<issue-assignees
v-if="assignees.length !== 0"
:assignees="assignees"
- class="item-assignees d-flex align-items-center align-self-end flex-shrink-0 d-md-none ml-2"
+ class="item-assignees d-flex align-items-center align-self-end flex-shrink-0 d-md-none gl-ml-3"
/>
</div>
</div>
diff --git a/app/assets/javascripts/issuable/components/status_box.vue b/app/assets/javascripts/issuable/components/status_box.vue
index 498dc859186..d72ee5c6757 100644
--- a/app/assets/javascripts/issuable/components/status_box.vue
+++ b/app/assets/javascripts/issuable/components/status_box.vue
@@ -63,6 +63,8 @@ export default {
},
},
data() {
+ if (!this.iid) return { state: this.initialState };
+
if (this.initialState) {
badgeState.state = this.initialState;
}
@@ -74,8 +76,7 @@ export default {
return [
CLASSES[this.state],
{
- 'gl-vertical-align-bottom':
- this.issuableType === IssuableType.MergeRequest && this.glFeatures.updatedMrHeader,
+ 'gl-vertical-align-bottom': this.issuableType === IssuableType.MergeRequest,
},
];
},
diff --git a/app/assets/javascripts/issuable/issuable_form.js b/app/assets/javascripts/issuable/issuable_form.js
index 8e76a33c7dd..38453072af8 100644
--- a/app/assets/javascripts/issuable/issuable_form.js
+++ b/app/assets/javascripts/issuable/issuable_form.js
@@ -46,6 +46,9 @@ function getFallbackKey() {
export default class IssuableForm {
constructor(form) {
+ if (form.length === 0) {
+ return;
+ }
this.form = form;
this.toggleWip = this.toggleWip.bind(this);
this.renderWipExplanation = this.renderWipExplanation.bind(this);
diff --git a/app/assets/javascripts/issuable/popover/components/issue_popover.vue b/app/assets/javascripts/issuable/popover/components/issue_popover.vue
new file mode 100644
index 00000000000..0cafaa1e500
--- /dev/null
+++ b/app/assets/javascripts/issuable/popover/components/issue_popover.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlPopover, GlSkeletonLoader } from '@gitlab/ui';
+import StatusBox from '~/issuable/components/status_box.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import query from '../queries/issue.query.graphql';
+
+export default {
+ components: {
+ GlPopover,
+ GlSkeletonLoader,
+ StatusBox,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ target: {
+ type: HTMLAnchorElement,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ iid: {
+ type: String,
+ required: true,
+ },
+ cachedTitle: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ issue: {},
+ };
+ },
+ computed: {
+ formattedTime() {
+ return this.timeFormatted(this.issue.createdAt);
+ },
+ title() {
+ return this.issue?.title || this.cachedTitle;
+ },
+ showDetails() {
+ return Object.keys(this.issue).length > 0;
+ },
+ },
+ apollo: {
+ issue: {
+ query,
+ update: (data) => data.project.issue,
+ variables() {
+ const { projectPath, iid } = this;
+
+ return {
+ projectPath,
+ iid,
+ };
+ },
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-popover :target="target" boundary="viewport" placement="top" show>
+ <gl-skeleton-loader v-if="$apollo.queries.issue.loading" :height="15">
+ <rect width="250" height="15" rx="4" />
+ </gl-skeleton-loader>
+ <div v-else-if="showDetails" class="gl-display-flex gl-align-items-center">
+ <status-box issuable-type="issue" :initial-state="issue.state" />
+ <span class="gl-text-secondary">
+ {{ __('Opened') }} <time :datetime="issue.createdAt">{{ formattedTime }}</time>
+ </span>
+ </div>
+ <h5 v-if="!$apollo.queries.issue.loading" class="gl-my-3">{{ title }}</h5>
+ <!-- eslint-disable @gitlab/vue-require-i18n-strings -->
+ <div class="gl-text-secondary">
+ {{ `${projectPath}#${iid}` }}
+ </div>
+ <!-- eslint-enable @gitlab/vue-require-i18n-strings -->
+ </gl-popover>
+</template>
diff --git a/app/assets/javascripts/mr_popover/components/mr_popover.vue b/app/assets/javascripts/issuable/popover/components/mr_popover.vue
index fef75b6d5d0..92994809362 100644
--- a/app/assets/javascripts/mr_popover/components/mr_popover.vue
+++ b/app/assets/javascripts/issuable/popover/components/mr_popover.vue
@@ -1,6 +1,6 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
-import { GlPopover, GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlBadge, GlPopover, GlSkeletonLoader } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { mrStates, humanMRStates } from '../constants';
@@ -10,8 +10,9 @@ export default {
// name: 'MRPopover' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
name: 'MRPopover', // eslint-disable-line @gitlab/require-i18n-strings
components: {
+ GlBadge,
GlPopover,
- GlSkeletonLoading,
+ GlSkeletonLoader,
CiIcon,
},
mixins: [timeagoMixin],
@@ -24,11 +25,11 @@ export default {
type: String,
required: true,
},
- mergeRequestIID: {
+ iid: {
type: String,
required: true,
},
- mergeRequestTitle: {
+ cachedTitle: {
type: String,
required: true,
},
@@ -45,14 +46,14 @@ export default {
formattedTime() {
return this.timeFormatted(this.mergeRequest.createdAt);
},
- statusBoxClass() {
+ badgeVariant() {
switch (this.mergeRequest.state) {
case mrStates.merged:
- return 'status-box-mr-merged';
+ return 'info';
case mrStates.closed:
- return 'status-box-closed';
+ return 'danger';
default:
- return 'status-box-open';
+ return 'success';
}
},
stateHumanName() {
@@ -66,7 +67,7 @@ export default {
}
},
title() {
- return this.mergeRequest?.title || this.mergeRequestTitle;
+ return this.mergeRequest?.title || this.cachedTitle;
},
showDetails() {
return Object.keys(this.mergeRequest).length > 0;
@@ -77,11 +78,11 @@ export default {
query,
update: (data) => data.project.mergeRequest,
variables() {
- const { projectPath, mergeRequestIID } = this;
+ const { projectPath, iid } = this;
return {
projectPath,
- mergeRequestIID,
+ iid,
};
},
},
@@ -92,14 +93,14 @@ export default {
<template>
<gl-popover :target="target" boundary="viewport" placement="top" show>
<div class="mr-popover">
- <div v-if="$apollo.queries.mergeRequest.loading">
- <gl-skeleton-loading :lines="1" class="animation-container-small mt-1" />
- </div>
+ <gl-skeleton-loader v-if="$apollo.queries.mergeRequest.loading" :height="15">
+ <rect width="250" height="15" rx="4" />
+ </gl-skeleton-loader>
<div v-else-if="showDetails" class="d-flex align-items-center justify-content-between">
<div class="d-inline-flex align-items-center">
- <div :class="`issuable-status-box status-box ${statusBoxClass}`">
+ <gl-badge class="gl-mr-3" :variant="badgeVariant">
{{ stateHumanName }}
- </div>
+ </gl-badge>
<span class="gl-text-secondary">Opened <time v-text="formattedTime"></time></span>
</div>
<ci-icon v-if="detailedStatus" :status="detailedStatus" />
@@ -107,7 +108,7 @@ export default {
<h5 v-if="!$apollo.queries.mergeRequest.loading" class="my-2">{{ title }}</h5>
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
<div class="gl-text-secondary">
- {{ `${projectPath}!${mergeRequestIID}` }}
+ {{ `${projectPath}!${iid}` }}
</div>
<!-- eslint-enable @gitlab/vue-require-i18n-strings -->
</div>
diff --git a/app/assets/javascripts/mr_popover/constants.js b/app/assets/javascripts/issuable/popover/constants.js
index 352bc635293..352bc635293 100644
--- a/app/assets/javascripts/mr_popover/constants.js
+++ b/app/assets/javascripts/issuable/popover/constants.js
diff --git a/app/assets/javascripts/issuable/popover/index.js b/app/assets/javascripts/issuable/popover/index.js
new file mode 100644
index 00000000000..de3c8160b7a
--- /dev/null
+++ b/app/assets/javascripts/issuable/popover/index.js
@@ -0,0 +1,85 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import IssuePopover from './components/issue_popover.vue';
+import MRPopover from './components/mr_popover.vue';
+
+const componentsByReferenceType = {
+ issue: IssuePopover,
+ merge_request: MRPopover,
+};
+
+let renderFn;
+
+const handleIssuablePopoverMouseOut = ({ target }) => {
+ target.removeEventListener('mouseleave', handleIssuablePopoverMouseOut);
+
+ if (renderFn) {
+ clearTimeout(renderFn);
+ }
+};
+
+const popoverMountedAttr = 'data-popover-mounted';
+
+/**
+ * Adds a MergeRequestPopover component to the body, hands over as much data as the target element has in data attributes.
+ * loads based on data-project-path and data-iid more data about an MR from the API and sets it on the popover
+ */
+const handleIssuablePopoverMount = ({
+ apolloProvider,
+ projectPath,
+ title,
+ iid,
+ referenceType,
+ target,
+}) => {
+ // Add listener to actually remove it again
+ target.addEventListener('mouseleave', handleIssuablePopoverMouseOut);
+
+ renderFn = setTimeout(() => {
+ const PopoverComponent = Vue.extend(componentsByReferenceType[referenceType]);
+ new PopoverComponent({
+ propsData: {
+ target,
+ projectPath,
+ iid,
+ cachedTitle: title,
+ },
+ apolloProvider,
+ }).$mount();
+
+ target.setAttribute(popoverMountedAttr, true);
+ }, 200); // 200ms delay so not every mouseover triggers Popover + API Call
+};
+
+export default (elements) => {
+ if (elements.length > 0) {
+ Vue.use(VueApollo);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+ const listenerAddedAttr = 'data-popover-listener-added';
+
+ elements.forEach((el) => {
+ const { projectPath, iid, referenceType } = el.dataset;
+ const title = el.dataset.mrTitle || el.title;
+
+ if (!el.getAttribute(listenerAddedAttr) && projectPath && title && iid && referenceType) {
+ el.addEventListener('mouseenter', ({ target }) => {
+ if (!el.getAttribute(popoverMountedAttr)) {
+ handleIssuablePopoverMount({
+ apolloProvider,
+ projectPath,
+ title,
+ iid,
+ referenceType,
+ target,
+ });
+ }
+ });
+ el.setAttribute(listenerAddedAttr, true);
+ }
+ });
+ }
+};
diff --git a/app/assets/javascripts/issuable/popover/queries/issue.query.graphql b/app/assets/javascripts/issuable/popover/queries/issue.query.graphql
new file mode 100644
index 00000000000..47a62e2b6ea
--- /dev/null
+++ b/app/assets/javascripts/issuable/popover/queries/issue.query.graphql
@@ -0,0 +1,11 @@
+query issue($projectPath: ID!, $iid: String!) {
+ project(fullPath: $projectPath) {
+ id
+ issue(iid: $iid) {
+ id
+ title
+ createdAt
+ state
+ }
+ }
+}
diff --git a/app/assets/javascripts/mr_popover/queries/merge_request.query.graphql b/app/assets/javascripts/issuable/popover/queries/merge_request.query.graphql
index b3e5d89d495..7cd344c1d5e 100644
--- a/app/assets/javascripts/mr_popover/queries/merge_request.query.graphql
+++ b/app/assets/javascripts/issuable/popover/queries/merge_request.query.graphql
@@ -1,7 +1,7 @@
-query mergeRequest($projectPath: ID!, $mergeRequestIID: String!) {
+query mergeRequest($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
id
- mergeRequest(iid: $mergeRequestIID) {
+ mergeRequest(iid: $iid) {
id
title
createdAt
diff --git a/app/assets/javascripts/issues/create_merge_request_dropdown.js b/app/assets/javascripts/issues/create_merge_request_dropdown.js
index 8294c018117..edf3789e6dc 100644
--- a/app/assets/javascripts/issues/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/issues/create_merge_request_dropdown.js
@@ -11,6 +11,7 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
import { mergeUrlParams } from '~/lib/utils/url_utility';
+import api from '~/api';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = { ...ISetter };
@@ -81,10 +82,7 @@ export default class CreateMergeRequestDropdown {
this.init();
if (isConfidentialIssue()) {
- this.createMergeRequestButton.setAttribute(
- 'data-dropdown-trigger',
- '#create-merge-request-dropdown',
- );
+ this.createMergeRequestButton.dataset.dropdownTrigger = '#create-merge-request-dropdown';
initConfidentialMergeRequest();
}
}
@@ -149,7 +147,7 @@ export default class CreateMergeRequestDropdown {
});
}
- createBranch() {
+ createBranch(navigateToBranch = true) {
this.isCreatingBranch = true;
return axios
@@ -158,7 +156,10 @@ export default class CreateMergeRequestDropdown {
})
.then(({ data }) => {
this.branchCreated = true;
- window.location.href = data.url;
+
+ if (navigateToBranch) {
+ window.location.href = data.url;
+ }
})
.catch(() =>
createFlash({
@@ -170,23 +171,25 @@ export default class CreateMergeRequestDropdown {
createMergeRequest() {
return new Promise(() => {
this.isCreatingMergeRequest = true;
- return this.createBranch().then(() => {
- let path = canCreateConfidentialMergeRequest()
- ? this.createMrPath.replace(
- this.projectPath,
- confidentialMergeRequestState.selectedProject.pathWithNamespace,
- )
- : this.createMrPath;
- path = mergeUrlParams(
- {
- 'merge_request[target_branch]': this.refInput.value,
- 'merge_request[source_branch]': this.branchInput.value,
- },
- path,
- );
-
- window.location.href = path;
- });
+ return this.createBranch(false)
+ .then(() => api.trackRedisHllUserEvent('i_code_review_user_create_mr_from_issue'))
+ .then(() => {
+ let path = canCreateConfidentialMergeRequest()
+ ? this.createMrPath.replace(
+ this.projectPath,
+ confidentialMergeRequestState.selectedProject.pathWithNamespace,
+ )
+ : this.createMrPath;
+ path = mergeUrlParams(
+ {
+ 'merge_request[target_branch]': this.refInput.value,
+ 'merge_request[source_branch]': this.branchInput.value,
+ },
+ path,
+ );
+
+ window.location.href = path;
+ });
});
}
diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js
index bcd729785b3..67c6c723dcc 100644
--- a/app/assets/javascripts/issues/index.js
+++ b/app/assets/javascripts/issues/index.js
@@ -9,6 +9,7 @@ import { IssueType } from '~/issues/constants';
import Issue from '~/issues/issue';
import { initTitleSuggestions, initTypePopover } from '~/issues/new';
import { initRelatedMergeRequests } from '~/issues/related_merge_requests';
+import initRelatedIssues from '~/related_issues';
import {
initHeaderActions,
initIncidentApp,
@@ -56,8 +57,9 @@ export function initShow() {
const { issueType, ...issuableData } = parseIssuableData(el);
if (issueType === IssueType.Incident) {
- initIncidentApp(issuableData);
+ initIncidentApp({ ...issuableData, issuableId: el.dataset.issuableId });
initHeaderActions(store, IssueType.Incident);
+ initRelatedIssues(IssueType.Incident);
} else {
initIssueApp(issuableData, store);
initHeaderActions(store);
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 b81ab103271..fa56c0183b2 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -13,8 +13,6 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
-import getIssuesWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_without_crm.query.graphql';
-import getIssuesCountsWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_counts_without_crm.query.graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -23,6 +21,7 @@ import CsvImportExportButtons from '~/issuable/components/csv_import_export_butt
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import { IssuableStatus } from '~/issues/constants';
import axios from '~/lib/utils/axios_utils';
+import { isPositiveInteger } from '~/lib/utils/number_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { getParameterByName, joinPaths } from '~/lib/utils/url_utility';
import {
@@ -31,20 +30,28 @@ import {
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
TOKEN_TITLE_CONFIDENTIAL,
+ TOKEN_TITLE_CONTACT,
TOKEN_TITLE_LABEL,
TOKEN_TITLE_MILESTONE,
TOKEN_TITLE_MY_REACTION,
+ TOKEN_TITLE_ORGANIZATION,
TOKEN_TITLE_RELEASE,
TOKEN_TITLE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
-import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
+import {
+ IssuableListTabs,
+ IssuableStates,
+ IssuableTypes,
+} from '~/vue_shared/issuable/list/constants';
import {
CREATED_DESC,
i18n,
ISSUE_REFERENCE,
MAX_LIST_SIZE,
PAGE_SIZE,
+ PARAM_FIRST_PAGE_SIZE,
+ PARAM_LAST_PAGE_SIZE,
PARAM_PAGE_AFTER,
PARAM_PAGE_BEFORE,
PARAM_SORT,
@@ -53,9 +60,11 @@ import {
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_CONTACT,
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_ORGANIZATION,
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
UPDATED_DESC,
@@ -93,6 +102,7 @@ const ReleaseToken = () =>
export default {
i18n,
IssuableListTabs,
+ IssuableTypes: [IssuableTypes.Issue, IssuableTypes.Incident, IssuableTypes.TestCase],
components: {
CsvImportExportButtons,
GlButton,
@@ -112,6 +122,9 @@ export default {
'autocompleteAwardEmojisPath',
'calendarPath',
'canBulkUpdate',
+ 'canCreateProjects',
+ 'canReadCrmContact',
+ 'canReadCrmOrganization',
'emptyStateSvgPath',
'exportCsvPath',
'fullPath',
@@ -120,6 +133,7 @@ export default {
'hasBlockedIssuesFeature',
'hasIssueWeightsFeature',
'hasMultipleIssueAssigneesFeature',
+ 'hasScopedLabelsFeature',
'initialEmail',
'initialSort',
'isAnonymousSearchDisabled',
@@ -129,6 +143,7 @@ export default {
'isSignedIn',
'jiraIntegrationPath',
'newIssuePath',
+ 'newProjectPath',
'releasesPath',
'rssPath',
'showNewIssueLink',
@@ -157,11 +172,11 @@ export default {
},
apollo: {
issues: {
- query() {
- return this.hasCrmParameter ? getIssuesQuery : getIssuesWithoutCrmQuery;
- },
+ query: getIssuesQuery,
variables() {
- return this.queryVariables;
+ const { types } = this.queryVariables;
+
+ return { ...this.queryVariables, types: types ? [types] : this.$options.IssuableTypes };
},
update(data) {
return data[this.namespace]?.issues.nodes ?? [];
@@ -183,11 +198,11 @@ export default {
debounce: 200,
},
issuesCounts: {
- query() {
- return this.hasCrmParameter ? getIssuesCountsQuery : getIssuesCountsWithoutCrmQuery;
- },
+ query: getIssuesCountsQuery,
variables() {
- return this.queryVariables;
+ const { types } = this.queryVariables;
+
+ return { ...this.queryVariables, types: types ? [types] : this.$options.IssuableTypes };
},
update(data) {
return data[this.namespace] ?? {};
@@ -363,6 +378,28 @@ export default {
});
}
+ if (this.canReadCrmContact) {
+ tokens.push({
+ type: TOKEN_TYPE_CONTACT,
+ title: TOKEN_TITLE_CONTACT,
+ icon: 'user',
+ token: GlFilteredSearchToken,
+ operators: OPERATOR_IS_ONLY,
+ unique: true,
+ });
+ }
+
+ if (this.canReadCrmOrganization) {
+ tokens.push({
+ type: TOKEN_TYPE_ORGANIZATION,
+ title: TOKEN_TITLE_ORGANIZATION,
+ icon: 'users',
+ token: GlFilteredSearchToken,
+ operators: OPERATOR_IS_ONLY,
+ unique: true,
+ });
+ }
+
if (this.eeSearchTokens.length) {
tokens.push(...this.eeSearchTokens);
}
@@ -390,20 +427,16 @@ export default {
},
urlParams() {
return {
- page_after: this.pageParams.afterCursor,
- page_before: this.pageParams.beforeCursor,
search: this.searchQuery,
sort: urlSortParams[this.sortKey],
state: this.state,
...this.urlFilterParams,
+ first_page_size: this.pageParams.firstPageSize,
+ last_page_size: this.pageParams.lastPageSize,
+ page_after: this.pageParams.afterCursor,
+ page_before: this.pageParams.beforeCursor,
};
},
- hasCrmParameter() {
- return (
- window.location.search.includes('crm_contact_id=') ||
- window.location.search.includes('crm_organization_id=')
- );
- },
},
watch: {
$route(newValue, oldValue) {
@@ -632,6 +665,8 @@ export default {
this.showBulkEditSidebar = showBulkEditSidebar;
},
updateData(sortValue) {
+ const firstPageSize = getParameterByName(PARAM_FIRST_PAGE_SIZE);
+ const lastPageSize = getParameterByName(PARAM_LAST_PAGE_SIZE);
const pageAfter = getParameterByName(PARAM_PAGE_AFTER);
const pageBefore = getParameterByName(PARAM_PAGE_BEFORE);
const state = getParameterByName(PARAM_STATE);
@@ -660,7 +695,13 @@ export default {
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
this.filterTokens = isSearchDisabled ? [] : getFilterTokens(window.location.search);
- this.pageParams = getInitialPageParams(sortKey, pageAfter, pageBefore);
+ this.pageParams = getInitialPageParams(
+ sortKey,
+ isPositiveInteger(firstPageSize) ? parseInt(firstPageSize, 10) : undefined,
+ isPositiveInteger(lastPageSize) ? parseInt(lastPageSize, 10) : undefined,
+ pageAfter,
+ pageBefore,
+ );
this.sortKey = sortKey;
this.state = state || IssuableStates.Opened;
},
@@ -676,6 +717,7 @@ export default {
recent-searches-storage-key="issues"
:search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
+ :has-scoped-labels-feature="hasScopedLabelsFeature"
:initial-filter-value="filterTokens"
:sort-options="sortOptions"
:initial-sort-by="sortKey"
@@ -815,12 +857,17 @@ export default {
</issuable-list>
<template v-else-if="isSignedIn">
- <gl-empty-state
- :description="$options.i18n.noIssuesSignedInDescription"
- :title="$options.i18n.noIssuesSignedInTitle"
- :svg-path="emptyStateSvgPath"
- >
+ <gl-empty-state :title="$options.i18n.noIssuesSignedInTitle" :svg-path="emptyStateSvgPath">
+ <template #description>
+ <p>{{ $options.i18n.noIssuesSignedInDescription }}</p>
+ <p v-if="canCreateProjects">
+ <strong>{{ $options.i18n.noGroupIssuesSignedInDescription }}</strong>
+ </p>
+ </template>
<template #actions>
+ <gl-button v-if="canCreateProjects" :href="newProjectPath" variant="confirm">
+ {{ $options.i18n.newProjectLabel }}
+ </gl-button>
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
{{ $options.i18n.newIssueLabel }}
</gl-button>
@@ -830,7 +877,7 @@ export default {
:export-csv-path="exportCsvPathWithQuery"
:issuable-count="currentTabCount"
/>
- <new-issue-dropdown v-if="showNewIssueDropdown" />
+ <new-issue-dropdown v-if="showNewIssueDropdown" class="gl-align-self-center" />
</template>
</gl-empty-state>
<hr />
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 0795df10a7c..74f801f685c 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -29,7 +29,11 @@ export const i18n = {
jiraIntegrationSecondaryMessage: s__('JiraService|This feature requires a Premium plan.'),
jiraIntegrationTitle: s__('JiraService|Using Jira for issue tracking?'),
newIssueLabel: __('New issue'),
+ newProjectLabel: __('New project'),
noClosedIssuesTitle: __('There are no closed issues'),
+ noGroupIssuesSignedInDescription: __(
+ 'Issues exist in projects, so to create an issue, first create a project.',
+ ),
noOpenIssuesDescription: __('To keep this project going, create a new issue'),
noOpenIssuesTitle: __('There are no open issues'),
noIssuesSignedInDescription: __(
@@ -57,6 +61,8 @@ export const MAX_LIST_SIZE = 10;
export const PAGE_SIZE = 20;
export const PAGE_SIZE_MANUAL = 100;
export const PARAM_ASSIGNEE_ID = 'assignee_id';
+export const PARAM_FIRST_PAGE_SIZE = 'first_page_size';
+export const PARAM_LAST_PAGE_SIZE = 'last_page_size';
export const PARAM_PAGE_AFTER = 'page_after';
export const PARAM_PAGE_BEFORE = 'page_before';
export const PARAM_SORT = 'sort';
diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js
index f5cb160e344..93333c31b34 100644
--- a/app/assets/javascripts/issues/list/index.js
+++ b/app/assets/javascripts/issues/list/index.js
@@ -80,8 +80,11 @@ export function mountIssuesListApp() {
autocompleteAwardEmojisPath,
calendarPath,
canBulkUpdate,
+ canCreateProjects,
canEdit,
canImportIssues,
+ canReadCrmContact,
+ canReadCrmOrganization,
email,
emailsHelpPagePath,
emptyStateSvgPath,
@@ -95,6 +98,7 @@ export function mountIssuesListApp() {
hasIssueWeightsFeature,
hasIterationsFeature,
hasMultipleIssueAssigneesFeature,
+ hasScopedLabelsFeature,
importCsvIssuesPath,
initialEmail,
initialSort,
@@ -107,6 +111,7 @@ export function mountIssuesListApp() {
markdownHelpPath,
maxAttachmentSize,
newIssuePath,
+ newProjectPath,
projectImportJiraPath,
quickActionsHelpPath,
releasesPath,
@@ -131,6 +136,9 @@ export function mountIssuesListApp() {
autocompleteAwardEmojisPath,
calendarPath,
canBulkUpdate: parseBoolean(canBulkUpdate),
+ canCreateProjects: parseBoolean(canCreateProjects),
+ canReadCrmContact: parseBoolean(canReadCrmContact),
+ canReadCrmOrganization: parseBoolean(canReadCrmOrganization),
emptyStateSvgPath,
fullPath,
groupPath,
@@ -141,6 +149,7 @@ export function mountIssuesListApp() {
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasIterationsFeature: parseBoolean(hasIterationsFeature),
hasMultipleIssueAssigneesFeature: parseBoolean(hasMultipleIssueAssigneesFeature),
+ hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature),
initialSort,
isAnonymousSearchDisabled: parseBoolean(isAnonymousSearchDisabled),
isIssueRepositioningDisabled: parseBoolean(isIssueRepositioningDisabled),
@@ -149,6 +158,7 @@ export function mountIssuesListApp() {
isSignedIn: parseBoolean(isSignedIn),
jiraIntegrationPath,
newIssuePath,
+ newProjectPath,
releasesPath,
rssPath,
showNewIssueLink: parseBoolean(showNewIssueLink),
diff --git a/app/assets/javascripts/issues/list/queries/get_issues_counts_without_crm.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues_counts_without_crm.query.graphql
deleted file mode 100644
index ab91aab1218..00000000000
--- a/app/assets/javascripts/issues/list/queries/get_issues_counts_without_crm.query.graphql
+++ /dev/null
@@ -1,136 +0,0 @@
-query getIssuesCountWithoutCrm(
- $isProject: Boolean = false
- $fullPath: ID!
- $iid: String
- $search: String
- $assigneeId: String
- $assigneeUsernames: [String!]
- $authorUsername: String
- $confidential: Boolean
- $labelName: [String]
- $milestoneTitle: [String]
- $milestoneWildcardId: MilestoneWildcardId
- $myReactionEmoji: String
- $releaseTag: [String!]
- $releaseTagWildcardId: ReleaseTagWildcardId
- $types: [IssueType!]
- $not: NegatedIssueFilterInput
-) {
- group(fullPath: $fullPath) @skip(if: $isProject) {
- id
- openedIssues: issues(
- includeSubgroups: true
- state: opened
- iid: $iid
- search: $search
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- confidential: $confidential
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- myReactionEmoji: $myReactionEmoji
- types: $types
- not: $not
- ) {
- count
- }
- closedIssues: issues(
- includeSubgroups: true
- state: closed
- iid: $iid
- search: $search
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- confidential: $confidential
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- myReactionEmoji: $myReactionEmoji
- types: $types
- not: $not
- ) {
- count
- }
- allIssues: issues(
- includeSubgroups: true
- state: all
- iid: $iid
- search: $search
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- confidential: $confidential
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- myReactionEmoji: $myReactionEmoji
- types: $types
- not: $not
- ) {
- count
- }
- }
- project(fullPath: $fullPath) @include(if: $isProject) {
- id
- openedIssues: issues(
- state: opened
- iid: $iid
- search: $search
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- confidential: $confidential
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- myReactionEmoji: $myReactionEmoji
- releaseTag: $releaseTag
- releaseTagWildcardId: $releaseTagWildcardId
- types: $types
- not: $not
- ) {
- count
- }
- closedIssues: issues(
- state: closed
- iid: $iid
- search: $search
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- confidential: $confidential
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- myReactionEmoji: $myReactionEmoji
- releaseTag: $releaseTag
- releaseTagWildcardId: $releaseTagWildcardId
- types: $types
- not: $not
- ) {
- count
- }
- allIssues: issues(
- state: all
- iid: $iid
- search: $search
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- confidential: $confidential
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- myReactionEmoji: $myReactionEmoji
- releaseTag: $releaseTag
- releaseTagWildcardId: $releaseTagWildcardId
- types: $types
- not: $not
- ) {
- count
- }
- }
-}
diff --git a/app/assets/javascripts/issues/list/queries/get_issues_without_crm.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues_without_crm.query.graphql
deleted file mode 100644
index 4a8b1dfd618..00000000000
--- a/app/assets/javascripts/issues/list/queries/get_issues_without_crm.query.graphql
+++ /dev/null
@@ -1,94 +0,0 @@
-#import "~/graphql_shared/fragments/page_info.fragment.graphql"
-#import "./issue.fragment.graphql"
-
-query getIssuesWithoutCrm(
- $hideUsers: Boolean = false
- $isProject: Boolean = false
- $isSignedIn: Boolean = false
- $fullPath: ID!
- $iid: String
- $search: String
- $sort: IssueSort
- $state: IssuableState
- $assigneeId: String
- $assigneeUsernames: [String!]
- $authorUsername: String
- $confidential: Boolean
- $labelName: [String]
- $milestoneTitle: [String]
- $milestoneWildcardId: MilestoneWildcardId
- $myReactionEmoji: String
- $releaseTag: [String!]
- $releaseTagWildcardId: ReleaseTagWildcardId
- $types: [IssueType!]
- $not: NegatedIssueFilterInput
- $beforeCursor: String
- $afterCursor: String
- $firstPageSize: Int
- $lastPageSize: Int
-) {
- group(fullPath: $fullPath) @skip(if: $isProject) {
- id
- issues(
- includeSubgroups: true
- iid: $iid
- search: $search
- sort: $sort
- state: $state
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- confidential: $confidential
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- myReactionEmoji: $myReactionEmoji
- types: $types
- not: $not
- before: $beforeCursor
- after: $afterCursor
- first: $firstPageSize
- last: $lastPageSize
- ) {
- pageInfo {
- ...PageInfo
- }
- nodes {
- ...IssueFragment
- reference(full: true)
- }
- }
- }
- project(fullPath: $fullPath) @include(if: $isProject) {
- id
- issues(
- iid: $iid
- search: $search
- sort: $sort
- state: $state
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- confidential: $confidential
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- myReactionEmoji: $myReactionEmoji
- releaseTag: $releaseTag
- releaseTagWildcardId: $releaseTagWildcardId
- types: $types
- not: $not
- before: $beforeCursor
- after: $afterCursor
- first: $firstPageSize
- last: $lastPageSize
- ) {
- pageInfo {
- ...PageInfo
- }
- nodes {
- ...IssueFragment
- }
- }
- }
-}
diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js
index 3ca93069628..dfdc6e27f0d 100644
--- a/app/assets/javascripts/issues/list/utils.js
+++ b/app/assets/javascripts/issues/list/utils.js
@@ -46,8 +46,15 @@ import {
WEIGHT_DESC,
} from './constants';
-export const getInitialPageParams = (sortKey, afterCursor, beforeCursor) => ({
- firstPageSize: sortKey === RELATIVE_POSITION_ASC ? PAGE_SIZE_MANUAL : PAGE_SIZE,
+export const getInitialPageParams = (
+ sortKey,
+ firstPageSize = sortKey === RELATIVE_POSITION_ASC ? PAGE_SIZE_MANUAL : PAGE_SIZE,
+ lastPageSize,
+ afterCursor,
+ beforeCursor,
+) => ({
+ firstPageSize: lastPageSize ? undefined : firstPageSize,
+ lastPageSize,
afterCursor,
beforeCursor,
});
diff --git a/app/assets/javascripts/issues/new/components/title_suggestions.vue b/app/assets/javascripts/issues/new/components/title_suggestions.vue
index 0a9cdb12519..4fd018ab4ce 100644
--- a/app/assets/javascripts/issues/new/components/title_suggestions.vue
+++ b/app/assets/javascripts/issues/new/components/title_suggestions.vue
@@ -66,8 +66,8 @@ export default {
</script>
<template>
- <div v-show="showSuggestions" class="form-group row">
- <div v-once class="col-form-label col-sm-2 pt-0">
+ <div v-show="showSuggestions" class="form-group">
+ <div v-once class="gl-pb-3">
{{ __('Similar issues') }}
<gl-icon
v-gl-tooltip.bottom
@@ -77,18 +77,16 @@ export default {
class="text-secondary gl-cursor-help"
/>
</div>
- <div class="col-sm-10">
- <ul class="list-unstyled m-0">
- <li
- v-for="(suggestion, index) in issues"
- :key="suggestion.id"
- :class="{
- 'gl-mb-3': index !== issues.length - 1,
- }"
- >
- <title-suggestions-item :suggestion="suggestion" />
- </li>
- </ul>
- </div>
+ <ul class="gl-list-style-none gl-m-0 gl-p-0">
+ <li
+ v-for="(suggestion, index) in issues"
+ :key="suggestion.id"
+ :class="{
+ 'gl-mb-3': index !== issues.length - 1,
+ }"
+ >
+ <title-suggestions-item :suggestion="suggestion" />
+ </li>
+ </ul>
</div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index daa1632c4aa..892c631f8ea 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -23,6 +23,7 @@ import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
+import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import animateMixin from '../mixins/animate';
import { convertDescriptionWithNewSort } from '../utils';
@@ -135,9 +136,6 @@ export default {
this.$nextTick(() => {
this.renderGFM();
- if (this.workItemsEnabled) {
- this.renderTaskActions();
- }
});
},
taskStatus() {
@@ -148,10 +146,6 @@ export default {
this.renderGFM();
this.updateTaskStatusText();
- if (this.workItemsEnabled) {
- this.renderTaskActions();
- }
-
if (this.workItemId) {
const taskLink = this.$el.querySelector(
`.gfm-issue[data-issue="${getIdFromGraphQLId(this.workItemId)}"]`,
@@ -178,15 +172,20 @@ export default {
onError: this.taskListUpdateError.bind(this),
});
- if (this.issuableType === IssuableType.Issue) {
- this.renderSortableLists();
+ this.removeAllPointerEventListeners();
+
+ this.renderSortableLists();
+
+ if (this.workItemsEnabled) {
+ this.renderTaskActions();
}
}
},
renderSortableLists() {
- this.removeAllPointerEventListeners();
-
- const lists = document.querySelectorAll('.description ul, .description ol');
+ // We exclude GLFM table of contents which have a `section-nav` class on the root `ul`.
+ const lists = document.querySelectorAll(
+ '.description .md > ul:not(.section-nav), .description .md > ul:not(.section-nav) ul, .description ol',
+ );
lists.forEach((list) => {
if (list.children.length <= 1) {
return;
@@ -194,7 +193,7 @@ export default {
Array.from(list.children).forEach((listItem) => {
listItem.prepend(this.createDragIconElement());
- this.addPointerEventListeners(listItem);
+ this.addPointerEventListeners(listItem, '.drag-icon');
});
Sortable.create(
@@ -216,20 +215,20 @@ export default {
</svg>`;
return container.firstChild;
},
- addPointerEventListeners(listItem) {
+ addPointerEventListeners(listItem, iconSelector) {
const pointeroverListener = (event) => {
- const dragIcon = event.target.closest('li').querySelector('.drag-icon');
- if (!dragIcon || isDragging() || this.isUpdating) {
+ const icon = event.target.closest('li').querySelector(iconSelector);
+ if (!icon || isDragging() || this.isUpdating) {
return;
}
- dragIcon.style.visibility = 'visible';
+ icon.style.visibility = 'visible';
};
const pointeroutListener = (event) => {
- const dragIcon = event.target.closest('li').querySelector('.drag-icon');
- if (!dragIcon) {
+ const icon = event.target.closest('li').querySelector(iconSelector);
+ if (!icon) {
return;
}
- dragIcon.style.visibility = 'hidden';
+ icon.style.visibility = 'hidden';
};
// We use pointerover/pointerout instead of CSS so that when we hover over a
@@ -238,10 +237,16 @@ export default {
listItem.addEventListener('pointerout', pointeroutListener);
this.pointerEventListeners = this.pointerEventListeners || new Map();
- this.pointerEventListeners.set(listItem, [
+ const events = [
{ type: 'pointerover', listener: pointeroverListener },
{ type: 'pointerout', listener: pointeroutListener },
- ]);
+ ];
+ if (this.pointerEventListeners.has(listItem)) {
+ const concatenatedEvents = this.pointerEventListeners.get(listItem).concat(events);
+ this.pointerEventListeners.set(listItem, concatenatedEvents);
+ } else {
+ this.pointerEventListeners.set(listItem, events);
+ }
},
removeAllPointerEventListeners() {
this.pointerEventListeners?.forEach((events, listItem) => {
@@ -311,13 +316,14 @@ export default {
this.workItemId = workItemId;
this.updateWorkItemIdUrlQuery(issue);
this.track('viewed_work_item_from_modal', {
- category: 'workItems:show',
+ category: TRACKING_CATEGORY_SHOW,
label: 'work_item_view',
property: `type_${referenceType}`,
});
});
return;
}
+ this.addPointerEventListeners(item, '.js-add-task');
const button = document.createElement('button');
button.classList.add(
'btn',
@@ -325,6 +331,7 @@ export default {
'btn-md',
'gl-button',
'btn-default-tertiary',
+ 'gl-visibility-hidden',
'gl-p-0!',
'gl-mt-n1',
'gl-ml-3',
@@ -339,7 +346,7 @@ export default {
`;
button.setAttribute('aria-label', s__('WorkItem|Convert to work item'));
button.addEventListener('click', () => this.openCreateTaskModal(button));
- item.append(button);
+ this.insertButtonNextToTaskText(item, button);
});
},
addHoverListeners(taskLink, id) {
@@ -355,9 +362,24 @@ export default {
}
});
},
+ insertButtonNextToTaskText(listItem, button) {
+ const paragraph = Array.from(listItem.children).find((element) => element.tagName === 'P');
+ const lastChild = listItem.lastElementChild;
+ if (paragraph) {
+ // If there's a `p` element, then it's a multi-paragraph task item
+ // and the task text exists within the `p` element as the last child
+ paragraph.append(button);
+ } else if (lastChild.tagName === 'OL' || lastChild.tagName === 'UL') {
+ // Otherwise, the task item can have a child list which exists directly after the task text
+ lastChild.insertAdjacentElement('beforebegin', button);
+ } else {
+ // Otherwise, the task item is a simple one where the task text exists as the last child
+ listItem.append(button);
+ }
+ },
setActiveTask(el) {
const { parentElement } = el;
- const lineNumbers = parentElement.getAttribute('data-sourcepos').match(/\b\d+(?=:)/g);
+ const lineNumbers = parentElement.dataset.sourcepos.match(/\b\d+(?=:)/g);
this.activeTask = {
title: parentElement.innerText,
lineNumberStart: lineNumbers[0],
@@ -431,13 +453,7 @@ export default {
>
</textarea>
- <gl-modal
- ref="modal"
- modal-id="create-task-modal"
- :title="s__('WorkItem|New Task')"
- hide-footer
- body-class="gl-p-0!"
- >
+ <gl-modal ref="modal" size="lg" modal-id="create-task-modal" hide-footer body-class="gl-p-0!">
<create-work-item
is-modal
:initial-title="activeTask.title"
diff --git a/app/assets/javascripts/issues/show/components/edit_actions.vue b/app/assets/javascripts/issues/show/components/edit_actions.vue
index 4daf6f2b61b..9b31014c1ba 100644
--- a/app/assets/javascripts/issues/show/components/edit_actions.vue
+++ b/app/assets/javascripts/issues/show/components/edit_actions.vue
@@ -77,7 +77,7 @@ export default {
return this.formState.title.trim() !== '';
},
shouldShowDeleteButton() {
- return this.canDestroy && this.showDeleteButton;
+ return this.canDestroy && this.showDeleteButton && this.typeToShow;
},
typeToShow() {
const { issueState, issuableType } = this;
diff --git a/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql
new file mode 100644
index 00000000000..7e049d98c1a
--- /dev/null
+++ b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql
@@ -0,0 +1,21 @@
+query GetTimelineEvents($fullPath: ID!, $incidentId: IssueID!) {
+ project(fullPath: $fullPath) {
+ id
+ incidentManagementTimelineEvents(incidentId: $incidentId) {
+ nodes {
+ id
+ author {
+ id
+ name
+ username
+ }
+ note
+ noteHtml
+ action
+ occurredAt
+ createdAt
+ updatedAt
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
index ea0e15adfed..6fdce6045f2 100644
--- a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
@@ -9,6 +9,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DescriptionComponent from '../description.vue';
import getAlert from './graphql/queries/get_alert.graphql';
import HighlightBar from './highlight_bar.vue';
+import TimelineTab from './timeline_events_tab.vue';
export default {
components: {
@@ -17,8 +18,7 @@ export default {
GlTab,
GlTabs,
HighlightBar,
- TimelineTab: () =>
- import('ee_component/issues/show/components/incidents/timeline_events_tab.vue'),
+ TimelineTab,
IncidentMetricTab: () =>
import('ee_component/issues/show/components/incidents/incident_metric_tab.vue'),
},
@@ -53,7 +53,7 @@ export default {
return this.$apollo.queries.alert.loading;
},
incidentTabEnabled() {
- return this.glFeatures.incidentTimelineEvents && this.glFeatures.incidentTimeline;
+ return this.glFeatures.incidentTimeline;
},
},
mounted() {
@@ -65,17 +65,26 @@ export default {
Tracking.event(category, action);
},
handleTabChange(tabIndex) {
+ /**
+ * TODO: Implement a solution that does not violate Vue principles in using
+ * DOM manipulation directly (#361618)
+ */
const parent = document.querySelector('.js-issue-details');
if (parent !== null) {
const itemsToHide = parent.querySelectorAll('.js-issue-widgets');
const lineSeparator = parent.querySelector('.js-detail-page-description');
+ const editButton = document.querySelector('.js-issuable-edit');
+ const isSummaryTab = tabIndex === 0;
- lineSeparator.classList.toggle('gl-border-b-0', tabIndex > 0);
+ lineSeparator.classList.toggle('gl-border-b-0', !isSummaryTab);
itemsToHide.forEach(function hide(item) {
- item.classList.toggle('gl-display-none', tabIndex > 0);
+ item.classList.toggle('gl-display-none', !isSummaryTab);
});
+
+ editButton.classList.toggle('gl-display-none', !isSummaryTab);
+ editButton.classList.toggle('gl-sm-display-inline-flex!', isSummaryTab);
}
},
},
@@ -103,7 +112,7 @@ export default {
>
<alert-details-table :alert="alert" :loading="loading" />
</gl-tab>
- <timeline-tab v-if="incidentTabEnabled" data-testid="timeline-events-tab" />
+ <timeline-tab v-if="incidentTabEnabled" />
</gl-tabs>
</div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue
new file mode 100644
index 00000000000..a6e58ee0bdc
--- /dev/null
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue
@@ -0,0 +1,73 @@
+<script>
+import { formatDate } from '~/lib/utils/datetime_utility';
+import IncidentTimelineEventListItem from './timeline_events_list_item.vue';
+
+export default {
+ name: 'IncidentTimelineEventList',
+ components: {
+ IncidentTimelineEventListItem,
+ },
+ props: {
+ timelineEventLoading: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ timelineEvents: {
+ type: Array,
+ required: true,
+ default: () => [],
+ },
+ },
+ computed: {
+ dateGroupedEvents() {
+ const groupedEvents = new Map();
+
+ this.timelineEvents.forEach((event) => {
+ const date = formatDate(event.occurredAt, 'isoDate', true);
+
+ if (groupedEvents.has(date)) {
+ groupedEvents.get(date).push(event);
+ } else {
+ groupedEvents.set(date, [event]);
+ }
+ });
+
+ return groupedEvents;
+ },
+ },
+ methods: {
+ isLastItem(groups, groupIndex, events, eventIndex) {
+ if (groupIndex < groups.size - 1) {
+ return false;
+ }
+ return eventIndex === events.length - 1;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="issuable-discussion incident-timeline-events">
+ <div
+ v-for="([eventDate, events], groupIndex) in dateGroupedEvents"
+ :key="eventDate"
+ data-testid="timeline-group"
+ >
+ <div class="gl-pb-3 gl-border-gray-50 gl-border-1 gl-border-b-solid">
+ <strong class="gl-font-size-h2" data-testid="event-date">{{ eventDate }}</strong>
+ </div>
+ <ul class="notes main-notes-list gl-pl-n3">
+ <incident-timeline-event-list-item
+ v-for="(event, eventIndex) in events"
+ :key="event.id"
+ :action="event.action"
+ :occurred-at="event.occurredAt"
+ :note-html="event.noteHtml"
+ :is-last-item="isLastItem(dateGroupedEvents, groupIndex, events, eventIndex)"
+ data-testid="timeline-event"
+ />
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_list_item.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_list_item.vue
new file mode 100644
index 00000000000..fef9bf713b7
--- /dev/null
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_list_item.vue
@@ -0,0 +1,71 @@
+<script>
+import { GlIcon, GlSafeHtmlDirective, GlSprintf } from '@gitlab/ui';
+import { formatDate } from '~/lib/utils/datetime_utility';
+import { __ } from '~/locale';
+import { getEventIcon } from './utils';
+
+export default {
+ name: 'IncidentTimelineEventListItem',
+ i18n: {
+ timeUTC: __('%{time} UTC'),
+ },
+ components: {
+ GlIcon,
+ GlSprintf,
+ },
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
+ props: {
+ isLastItem: {
+ type: Boolean,
+ required: true,
+ },
+ occurredAt: {
+ type: String,
+ required: true,
+ },
+ action: {
+ type: String,
+ required: true,
+ },
+ noteHtml: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ time() {
+ return formatDate(this.occurredAt, 'HH:MM', true);
+ },
+ },
+ methods: {
+ getEventIcon,
+ },
+};
+</script>
+<template>
+ <li
+ class="timeline-entry timeline-entry-vertical-line note system-note note-wrapper gl-my-2! gl-pr-0!"
+ >
+ <div class="gl-display-flex gl-align-items-center">
+ <div
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-bg-white gl-text-gray-200 gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-full gl-mt-n2 gl-mr-3 gl-w-8 gl-h-8 gl-p-3 gl-z-index-1"
+ >
+ <gl-icon :name="getEventIcon(action)" class="note-icon" />
+ </div>
+ <div
+ class="timeline-event-note gl-w-full"
+ :class="{ 'gl-pb-3 gl-border-gray-50 gl-border-1 gl-border-b-solid': !isLastItem }"
+ data-testid="event-text-container"
+ >
+ <strong class="gl-font-lg" data-testid="event-time">
+ <gl-sprintf :message="$options.i18n.timeUTC">
+ <template #time>{{ time }}</template>
+ </gl-sprintf>
+ </strong>
+ <div v-safe-html="noteHtml"></div>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue
new file mode 100644
index 00000000000..400e1f0b725
--- /dev/null
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue
@@ -0,0 +1,70 @@
+<script>
+import { GlEmptyState, GlLoadingIcon, GlTab } from '@gitlab/ui';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_ISSUE } from '~/graphql_shared/constants';
+import { fetchPolicies } from '~/lib/graphql';
+import getTimelineEvents from './graphql/queries/get_timeline_events.query.graphql';
+import { displayAndLogError } from './utils';
+
+import IncidentTimelineEventsList from './timeline_events_list.vue';
+
+export default {
+ components: {
+ GlEmptyState,
+ GlLoadingIcon,
+ GlTab,
+ IncidentTimelineEventsList,
+ },
+ inject: ['fullPath', 'issuableId'],
+ data() {
+ return {
+ timelineEvents: [],
+ };
+ },
+ apollo: {
+ timelineEvents: {
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ query: getTimelineEvents,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ incidentId: convertToGraphQLId(TYPE_ISSUE, this.issuableId),
+ };
+ },
+ update(data) {
+ return data.project.incidentManagementTimelineEvents.nodes;
+ },
+ error(error) {
+ displayAndLogError(error);
+ },
+ },
+ },
+ computed: {
+ timelineEventLoading() {
+ return this.$apollo.queries.timelineEvents.loading;
+ },
+ hasTimelineEvents() {
+ return Boolean(this.timelineEvents.length);
+ },
+ showEmptyState() {
+ return !this.timelineEventLoading && !this.hasTimelineEvents;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-tab :title="s__('Incident|Timeline')">
+ <gl-loading-icon v-if="timelineEventLoading" size="lg" color="dark" class="gl-mt-5" />
+ <gl-empty-state
+ v-else-if="showEmptyState"
+ :compact="true"
+ :description="s__('Incident|No timeline items have been added yet.')"
+ />
+ <incident-timeline-events-list
+ v-if="hasTimelineEvents"
+ :timeline-event-loading="timelineEventLoading"
+ :timeline-events="timelineEvents"
+ />
+ </gl-tab>
+</template>
diff --git a/app/assets/javascripts/issues/show/components/incidents/utils.js b/app/assets/javascripts/issues/show/components/incidents/utils.js
new file mode 100644
index 00000000000..8b5a2ec4031
--- /dev/null
+++ b/app/assets/javascripts/issues/show/components/incidents/utils.js
@@ -0,0 +1,18 @@
+import { createAlert } from '~/flash';
+import { s__ } from '~/locale';
+
+export const displayAndLogError = (error) =>
+ createAlert({
+ message: s__('Incident|Something went wrong while fetching incident timeline events.'),
+ captureError: true,
+ error,
+ });
+
+const EVENT_ICONS = {
+ comment: 'comment',
+ default: 'comment',
+};
+
+export const getEventIcon = (actionName) => {
+ return EVENT_ICONS[actionName] ?? EVENT_ICONS.default;
+};
diff --git a/app/assets/javascripts/issues/show/components/title.vue b/app/assets/javascripts/issues/show/components/title.vue
index 1982147e454..7f67b31b122 100644
--- a/app/assets/javascripts/issues/show/components/title.vue
+++ b/app/assets/javascripts/issues/show/components/title.vue
@@ -74,7 +74,7 @@ export default {
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation,
}"
- class="title qa-title"
+ class="title qa-title gl-font-size-h-display"
dir="auto"
></h1>
<gl-button
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index 6b0b26ef2e3..5bdad010af7 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -33,6 +33,7 @@ export function initIncidentApp(issueData = {}) {
canCreateIncident,
canUpdate,
iid,
+ issuableId,
projectNamespace,
projectPath,
projectId,
@@ -53,6 +54,7 @@ export function initIncidentApp(issueData = {}) {
canUpdate,
fullPath,
iid,
+ issuableId,
projectId,
slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
uploadMetricsFeatureAvailable: parseBoolean(uploadMetricsFeatureAvailable),
@@ -83,7 +85,7 @@ export function initIssueApp(issueData, store) {
bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
- const { canCreateIncident, ...issueProps } = issueData;
+ const { canCreateIncident, hasIssueWeightsFeature, ...issueProps } = issueData;
return new Vue({
el,
@@ -93,6 +95,7 @@ export function initIssueApp(issueData, store) {
provide: {
canCreateIncident,
fullPath,
+ hasIssueWeightsFeature,
},
computed: {
...mapGetters(['getNoteableData']),
diff --git a/app/assets/javascripts/jira_connect/branches/pages/index.vue b/app/assets/javascripts/jira_connect/branches/pages/index.vue
index d72dec6cdee..953e823ec96 100644
--- a/app/assets/javascripts/jira_connect/branches/pages/index.vue
+++ b/app/assets/javascripts/jira_connect/branches/pages/index.vue
@@ -46,7 +46,7 @@ export default {
<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>
+ <h1 data-testid="page-title" class="page-title gl-font-size-h-display">{{ pageTitle }}</h1>
</div>
<new-branch-form v-if="showForm" @success="onNewBranchFormSuccess" />
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue
index 7f035dddafe..a9ec7bd971e 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue
@@ -104,7 +104,7 @@ export default {
@input="onGroupSearch"
/>
- <gl-loading-icon v-if="isLoadingInitial" size="md" />
+ <gl-loading-icon v-if="isLoadingInitial" size="lg" />
<div v-else-if="groups.length === 0" class="gl-text-center">
<h5>{{ s__('Integrations|No available namespaces.') }}</h5>
<p class="gl-mt-5">
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
index 22422872183..66aea60c5b5 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
@@ -60,6 +60,12 @@ export default {
isBrowserSupported() {
return !this.isOauthEnabled || AccessorUtilities.canUseCrypto();
},
+ gitlabUrl() {
+ return gon.gitlab_url;
+ },
+ gitlabLogo() {
+ return gon.gitlab_logo;
+ },
},
created() {
this.setInitialAlert();
@@ -99,43 +105,55 @@ export default {
</script>
<template>
- <browser-support-alert v-if="!isBrowserSupported" class="gl-mb-7" />
- <div v-else data-testid="jira-connect-app">
- <compatibility-alert class="gl-mb-7" />
-
- <gl-alert
- v-if="shouldShowAlert"
- :variant="alert.variant"
- :title="alert.title"
- class="gl-mb-5"
- data-testid="jira-connect-persisted-alert"
- @dismiss="setAlert"
+ <div>
+ <header
+ class="jira-connect-header gl-display-flex gl-align-items-center gl-justify-content-center gl-px-5 gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-bg-white"
>
- <gl-sprintf v-if="alert.linkUrl" :message="alert.message">
- <template #link="{ content }">
- <gl-link :href="alert.linkUrl" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
+ <gl-link :href="gitlabUrl" target="_blank">
+ <img :src="gitlabLogo" class="gl-h-6" :alt="__('GitLab')" />
+ </gl-link>
+ <user-link
+ :user-signed-in="userSignedIn"
+ :has-subscriptions="hasSubscriptions"
+ :user="currentUser"
+ class="gl-fixed gl-right-4"
+ />
+ </header>
- <template v-else>
- {{ alert.message }}
- </template>
- </gl-alert>
+ <main class="jira-connect-app gl-px-5 gl-pt-7 gl-mx-auto">
+ <browser-support-alert v-if="!isBrowserSupported" class="gl-mb-7" />
+ <div v-else data-testid="jira-connect-app">
+ <compatibility-alert class="gl-mb-7" />
- <user-link
- :user-signed-in="userSignedIn"
- :has-subscriptions="hasSubscriptions"
- :user="currentUser"
- />
+ <gl-alert
+ v-if="shouldShowAlert"
+ :variant="alert.variant"
+ :title="alert.title"
+ class="gl-mb-5"
+ data-testid="jira-connect-persisted-alert"
+ @dismiss="setAlert"
+ >
+ <gl-sprintf v-if="alert.linkUrl" :message="alert.message">
+ <template #link="{ content }">
+ <gl-link :href="alert.linkUrl" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
- <div class="gl-layout-w-limited gl-mx-auto gl-px-5 gl-mb-7">
- <sign-in-page
- v-if="!userSignedIn"
- :has-subscriptions="hasSubscriptions"
- @sign-in-oauth="onSignInOauth"
- @error="onSignInError"
- />
- <subscriptions-page v-else :has-subscriptions="hasSubscriptions" />
- </div>
+ <template v-else>
+ {{ alert.message }}
+ </template>
+ </gl-alert>
+
+ <div class="gl-layout-w-limited gl-mx-auto gl-px-5 gl-mb-7">
+ <sign-in-page
+ v-if="!userSignedIn"
+ :has-subscriptions="hasSubscriptions"
+ @sign-in-oauth="onSignInOauth"
+ @error="onSignInError"
+ />
+ <subscriptions-page v-else :has-subscriptions="hasSubscriptions" />
+ </div>
+ </div>
+ </main>
</div>
</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue
index b9e8bab019f..ad3e70bcb5f 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue
@@ -112,7 +112,7 @@ export default {
</script>
<template>
<gl-button
- category="primary"
+ v-bind="$attrs"
variant="info"
:loading="loading"
:disabled="!canUseCrypto"
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue b/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue
index 5e2c83aff65..b253f888d22 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue
@@ -1,5 +1,6 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __ } from '~/locale';
import { getGitlabSignInURL } from '~/jira_connect/subscriptions/utils';
@@ -7,7 +8,9 @@ export default {
components: {
GlLink,
GlSprintf,
+ SignInOauthButton: () => import('./sign_in_oauth_button.vue'),
},
+ mixins: [glFeatureFlagMixin()],
inject: {
usersPath: {
default: '',
@@ -51,6 +54,9 @@ export default {
? this.$options.i18n.signedInAsUserText
: this.$options.i18n.signedInText;
},
+ isOauthEnabled() {
+ return this.glFeatures.jiraConnectOauth;
+ },
},
async created() {
this.signInURL = await getGitlabSignInURL(this.usersPath);
@@ -63,7 +69,7 @@ export default {
};
</script>
<template>
- <div class="jira-connect-user gl-font-base">
+ <div class="gl-font-base">
<gl-sprintf v-if="userSignedIn" :message="signedInText">
<template #user_link>
<gl-link data-testid="gitlab-user-link" :href="gitlabUserLink" target="_blank">
@@ -72,13 +78,14 @@ export default {
</template>
</gl-sprintf>
- <gl-link
- v-else-if="hasSubscriptions"
- data-testid="sign-in-link"
- :href="signInURL"
- target="_blank"
- >
- {{ $options.i18n.signInText }}
- </gl-link>
+ <template v-else-if="hasSubscriptions">
+ <sign-in-oauth-button v-if="isOauthEnabled" category="tertiary">
+ {{ $options.i18n.signInText }}
+ </sign-in-oauth-button>
+
+ <gl-link v-else data-testid="sign-in-link" :href="signInURL" target="_blank">
+ {{ $options.i18n.signInText }}
+ </gl-link>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions_page.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions_page.vue
index b1c1ae73e14..d7213f683d8 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions_page.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions_page.vue
@@ -29,7 +29,7 @@ export default {
<div>
<h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
- <gl-loading-icon v-if="subscriptionsLoading" size="md" />
+ <gl-loading-icon v-if="subscriptionsLoading" size="lg" />
<div v-else-if="hasSubscriptions && !subscriptionsError">
<div class="gl-display-flex gl-justify-content-end gl-mb-3">
<add-namespace-button />
diff --git a/app/assets/javascripts/jira_import/components/jira_import_app.vue b/app/assets/javascripts/jira_import/components/jira_import_app.vue
index 0f690d17da9..5e388900d2a 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_app.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_app.vue
@@ -95,7 +95,7 @@ export default {
:illustration="setupIllustration"
:jira-integration-path="jiraIntegrationPath"
/>
- <gl-loading-icon v-else-if="$apollo.loading" size="md" class="mt-3" />
+ <gl-loading-icon v-else-if="$apollo.loading" size="lg" class="mt-3" />
<jira-import-progress
v-else-if="jiraImportDetails.isInProgress"
:illustration="inProgressIllustration"
diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue
index 8a36a4d2466..f8ca62da1a5 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_form.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue
@@ -254,7 +254,7 @@ export default {
</gl-sprintf>
</gl-alert>
- <h3 class="page-title">{{ __('New Jira import') }}</h3>
+ <h1 class="page-title gl-font-size-h-display">{{ __('New Jira import') }}</h1>
<hr />
@@ -331,7 +331,7 @@ export default {
</template>
</gl-table-lite>
- <gl-loading-icon v-if="isInitialLoadingState" size="md" />
+ <gl-loading-icon v-if="isInitialLoadingState" size="lg" />
<gl-button
v-if="hasMoreUsers"
diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue
index d0594d1ad27..097ab3b4cf6 100644
--- a/app/assets/javascripts/jobs/components/job_container_item.vue
+++ b/app/assets/javascripts/jobs/components/job_container_item.vue
@@ -64,9 +64,10 @@ export default {
v-if="isActive"
name="arrow-right"
class="icon-arrow-right gl-absolute gl-display-block"
+ :size="14"
/>
- <ci-icon :status="job.status" />
+ <ci-icon :status="job.status" class="gl-mr-2" :size="14" />
<span class="gl-text-truncate gl-w-full">{{ jobName }}</span>
diff --git a/app/assets/javascripts/jobs/components/log/line_header.vue b/app/assets/javascripts/jobs/components/log/line_header.vue
index c72d488f844..de774e8408b 100644
--- a/app/assets/javascripts/jobs/components/log/line_header.vue
+++ b/app/assets/javascripts/jobs/components/log/line_header.vue
@@ -30,7 +30,7 @@ export default {
},
computed: {
iconName() {
- return this.isClosed ? 'angle-right' : 'angle-down';
+ return this.isClosed ? 'chevron-lg-right' : 'chevron-lg-down';
},
},
methods: {
diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/manual_variables_form.vue
index 7a52a1b0d6b..07ef4f054b4 100644
--- a/app/assets/javascripts/jobs/components/manual_variables_form.vue
+++ b/app/assets/javascripts/jobs/components/manual_variables_form.vue
@@ -178,7 +178,7 @@ export default {
<div class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-button
class="gl-mt-5"
- variant="info"
+ variant="confirm"
category="primary"
:aria-label="__('Trigger manual job')"
:disabled="triggerBtnDisabled"
diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue
index f9cde61e917..d7a26d22406 100644
--- a/app/assets/javascripts/jobs/components/stuck_block.vue
+++ b/app/assets/javascripts/jobs/components/stuck_block.vue
@@ -1,5 +1,5 @@
<script>
-import { GlAlert, GlBadge, GlLink } from '@gitlab/ui';
+import { GlAlert, GlBadge, GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
/**
* Renders Stuck Runners block for job's view.
@@ -9,6 +9,7 @@ export default {
GlAlert,
GlBadge,
GlLink,
+ GlSprintf,
},
props: {
hasOfflineRunnersForProject: {
@@ -29,11 +30,15 @@ export default {
hasNoRunnersWithCorrespondingTags() {
return this.tags.length > 0;
},
+ protectedBranchSettingsDocsLink() {
+ return 'https://docs.gitlab.com/runner/security/index.html#reduce-the-security-risk-of-using-privileged-containers';
+ },
stuckData() {
if (this.hasNoRunnersWithCorrespondingTags) {
return {
- text: s__(`Job|This job is stuck because you don't have
- any active runners online or available with any of these tags assigned to them:`),
+ text: s__(
+ `Job|This job is stuck because of one of the following problems. There are no active runners online, no runners for the %{linkStart}protected branch%{linkEnd}, or no runners that match all of the job's tags:`,
+ ),
dataTestId: 'job-stuck-with-tags',
showTags: true,
};
@@ -59,7 +64,17 @@ export default {
<template>
<gl-alert variant="warning" :dismissible="false">
<p class="gl-mb-0" :data-testid="stuckData.dataTestId">
- {{ stuckData.text }}
+ <gl-sprintf :message="stuckData.text">
+ <template #link="{ content }">
+ <a
+ class="gl-display-inline-block"
+ :href="protectedBranchSettingsDocsLink"
+ target="_blank"
+ >
+ {{ content }}
+ </a>
+ </template>
+ </gl-sprintf>
<template v-if="stuckData.showTags">
<gl-badge v-for="tag in tags" :key="tag" variant="info">
{{ tag }}
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
index 3ea50dfb7a3..1ac1a2d68e2 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
@@ -213,7 +213,7 @@ export default {
<gl-intersection-observer v-if="hasNextPage" @appear="fetchMoreJobs">
<gl-loading-icon
v-if="showLoadingSpinner"
- size="md"
+ size="lg"
:aria-label="$options.i18n.loadingAriaLabel"
/>
</gl-intersection-observer>
diff --git a/app/assets/javascripts/labels/components/promote_label_modal.vue b/app/assets/javascripts/labels/components/promote_label_modal.vue
index e708cd32fff..8598500c842 100644
--- a/app/assets/javascripts/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/labels/components/promote_label_modal.vue
@@ -9,7 +9,7 @@ import eventHub from '../event_hub';
export default {
primaryProps: {
text: s__('Labels|Promote Label'),
- attributes: [{ variant: 'warning' }, { category: 'primary' }],
+ attributes: [{ variant: 'confirm' }],
},
cancelProps: {
text: __('Cancel'),
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index 2b4dd205cf1..ba801082377 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -127,7 +127,7 @@ export default class LazyLoader {
// Loading Images which are in the current viewport or close to them
this.lazyImages = this.lazyImages.filter((selectedImage) => {
- if (selectedImage.getAttribute('data-src')) {
+ if (selectedImage.dataset.src) {
const imgBoundRect = selectedImage.getBoundingClientRect();
const imgTop = scrollTop + imgBoundRect.top;
const imgBound = imgTop + imgBoundRect.height;
@@ -156,16 +156,17 @@ export default class LazyLoader {
}
static loadImage(img) {
- if (img.getAttribute('data-src')) {
+ if (img.dataset.src) {
img.setAttribute('loading', 'lazy');
- let imgUrl = img.getAttribute('data-src');
+ let imgUrl = img.dataset.src;
// Only adding width + height for avatars for now
if (imgUrl.indexOf('/avatar/') > -1 && imgUrl.indexOf('?') === -1) {
const targetWidth = img.getAttribute('width') || img.width;
imgUrl += `?width=${targetWidth}`;
}
img.setAttribute('src', imgUrl);
- img.removeAttribute('data-src');
+ // eslint-disable-next-line no-param-reassign
+ delete img.dataset.src;
img.classList.remove('lazy');
img.classList.add('js-lazy-loaded');
img.classList.add('qa-js-lazy-loaded');
diff --git a/app/assets/javascripts/lib/gfm/index.js b/app/assets/javascripts/lib/gfm/index.js
index 4e704eb69b2..b4f941294de 100644
--- a/app/assets/javascripts/lib/gfm/index.js
+++ b/app/assets/javascripts/lib/gfm/index.js
@@ -1,10 +1,33 @@
import { unified } from 'unified';
import remarkParse from 'remark-parse';
-import remarkRehype from 'remark-rehype';
+import remarkGfm from 'remark-gfm';
+import remarkRehype, { all } from 'remark-rehype';
import rehypeRaw from 'rehype-raw';
const createParser = () => {
- return unified().use(remarkParse).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw);
+ return unified()
+ .use(remarkParse)
+ .use(remarkGfm)
+ .use(remarkRehype, {
+ allowDangerousHtml: true,
+ handlers: {
+ footnoteReference: (h, node) =>
+ h(
+ node.position,
+ 'footnoteReference',
+ { identifier: node.identifier, label: node.label },
+ [],
+ ),
+ footnoteDefinition: (h, node) =>
+ h(
+ node.position,
+ 'footnoteDefinition',
+ { identifier: node.identifier, label: node.label },
+ all(h, node),
+ ),
+ },
+ })
+ .use(rehypeRaw);
};
const compilerFactory = (renderer) =>
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 451950346b0..cfcce234bfb 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -105,7 +105,7 @@ export default (resolvers = {}, config = {}) => {
const {
baseUrl,
batchMax = 10,
- cacheConfig,
+ cacheConfig = { typePolicies: {}, possibleTypes: {} },
fetchPolicy = fetchPolicies.CACHE_FIRST,
typeDefs,
path = '/api/graphql',
@@ -166,6 +166,7 @@ export default (resolvers = {}, config = {}) => {
PerformanceBarService.interceptor({
config: {
url: httpResponse.url,
+ operationName: operation.operationName,
},
headers: {
'x-request-id': httpResponse.headers.get('x-request-id'),
@@ -221,9 +222,15 @@ export default (resolvers = {}, config = {}) => {
typeDefs,
link: appLink,
cache: new InMemoryCache({
- typePolicies,
- possibleTypes,
...cacheConfig,
+ typePolicies: {
+ ...typePolicies,
+ ...cacheConfig.typePolicies,
+ },
+ possibleTypes: {
+ ...possibleTypes,
+ ...cacheConfig.possibleTypes,
+ },
}),
resolvers,
defaultOptions: {
diff --git a/app/assets/javascripts/lib/utils/color_utils.js b/app/assets/javascripts/lib/utils/color_utils.js
index 66d52051905..3d8df4fde05 100644
--- a/app/assets/javascripts/lib/utils/color_utils.js
+++ b/app/assets/javascripts/lib/utils/color_utils.js
@@ -67,7 +67,7 @@ export function darkModeEnabled() {
const ideDarkThemes = ['dark', 'solarized-dark', 'monokai'];
// eslint-disable-next-line @gitlab/require-i18n-strings
- const isWebIde = document.body.dataset.page.startsWith('ide:');
+ const isWebIde = document.body.dataset.page?.startsWith('ide:');
if (isWebIde) {
return ideDarkThemes.includes(window.gon?.user_color_scheme);
diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
index 173116062c9..2dc479db80a 100644
--- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
+++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
@@ -56,7 +56,7 @@ export function confirmAction(
export function confirmViaGlModal(message, element) {
const primaryBtnConfig = {};
- const confirmBtnVariant = element.getAttribute('data-confirm-btn-variant');
+ const { confirmBtnVariant } = element.dataset;
if (confirmBtnVariant) {
primaryBtnConfig.primaryBtnVariant = confirmBtnVariant;
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index 4262329aae7..bca6978c206 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -67,17 +67,6 @@ export const parseBooleanDataAttributes = ({ dataset }, names) =>
export const isElementVisible = (element) =>
Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
-/**
- * The opposite of `isElementVisible`.
- * Returns whether or not the provided element is currently hidden.
- * This function operates identically to jQuery's `:hidden` pseudo-selector.
- * Documentation for this selector: https://api.jquery.com/hidden-selector/
- * Implementation of this selector: https://github.com/jquery/jquery/blob/d0ce00cdfa680f1f0c38460bc51ea14079ae8b07/src/css/hiddenVisibleSelectors.js#L6
- * @param {HTMLElement} element The element to test
- * @returns {Boolean} `true` if the element is currently hidden, otherwise false
- */
-export const isElementHidden = (element) => !isElementVisible(element);
-
export const getParents = (element) => {
const parents = [];
let parent = element.parentNode;
diff --git a/app/assets/javascripts/lib/utils/forms.js b/app/assets/javascripts/lib/utils/forms.js
index b58aef15dda..1d8c6ee23fc 100644
--- a/app/assets/javascripts/lib/utils/forms.js
+++ b/app/assets/javascripts/lib/utils/forms.js
@@ -119,12 +119,14 @@ export const parseRailsFormFields = (mountEl) => {
}
const fieldNameCamelCase = convertToCamelCase(fieldName);
- const { id, placeholder, name, value, type, checked } = input;
+ const { id, placeholder, name, value, type, checked, maxLength, pattern } = input;
const attributes = {
name,
id,
value,
...(placeholder && { placeholder }),
+ ...(input.hasAttribute('maxlength') && { maxLength }),
+ ...(pattern && { pattern }),
};
// Store radio buttons and checkboxes as an array so they can be
diff --git a/app/assets/javascripts/lib/utils/rails_ujs.js b/app/assets/javascripts/lib/utils/rails_ujs.js
index b4f425da871..48f1b32526f 100644
--- a/app/assets/javascripts/lib/utils/rails_ujs.js
+++ b/app/assets/javascripts/lib/utils/rails_ujs.js
@@ -37,9 +37,7 @@ function monkeyPatchConfirmModal() {
Rails.confirm = confirmViaModal;
}
-if (gon?.features?.bootstrapConfirmationModals) {
- monkeyPatchConfirmModal();
-}
+monkeyPatchConfirmModal();
export const initRails = () => {
// eslint-disable-next-line no-underscore-dangle
diff --git a/app/assets/javascripts/lib/utils/table_utility.js b/app/assets/javascripts/lib/utils/table_utility.js
index 6d66335b832..5d3aba9f4ed 100644
--- a/app/assets/javascripts/lib/utils/table_utility.js
+++ b/app/assets/javascripts/lib/utils/table_utility.js
@@ -2,6 +2,7 @@ import { convertToSnakeCase, convertToCamelCase } from '~/lib/utils/text_utility
import { DEFAULT_TH_CLASSES } from './constants';
/**
+ * Deprecated: use thWidthPercent instead
* Generates the table header classes to be used for GlTable fields.
*
* @param {Number} width - The column width as a percentage.
@@ -10,6 +11,15 @@ import { DEFAULT_TH_CLASSES } from './constants';
export const thWidthClass = (width) => `gl-w-${width}p ${DEFAULT_TH_CLASSES}`;
/**
+ * Generates the table header class for width to be used for GlTable fields.
+ *
+ * @param {Number} width - The column width as a percentage. Only accepts values
+ * as defined in https://gitlab.com/gitlab-org/gitlab-ui/blob/main/src/scss/utility-mixins/sizing.scss
+ * @returns {String} The class to be used in GlTable fields object.
+ */
+export const thWidthPercent = (width) => `gl-w-${width}p`;
+
+/**
* Converts a GlTable sort-changed event object into string format.
* This string can be used as a sort argument on GraphQL queries.
*
diff --git a/app/assets/javascripts/lib/utils/users_cache.js b/app/assets/javascripts/lib/utils/users_cache.js
index bd000bb26fe..670acbbabd7 100644
--- a/app/assets/javascripts/lib/utils/users_cache.js
+++ b/app/assets/javascripts/lib/utils/users_cache.js
@@ -29,8 +29,11 @@ class UsersCache extends Cache {
}
return getUser(userId).then(({ data }) => {
- this.internalStorage[userId] = data;
- return data;
+ this.internalStorage[userId] = {
+ ...this.get(userId),
+ ...data,
+ };
+ return this.internalStorage[userId];
});
// missing catch is intentional, error handling depends on use case
}
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index 403e216e70f..c570f8810a8 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -1,7 +1,5 @@
-import $ from 'jquery';
-
export default function initLogoAnimation() {
window.addEventListener('beforeunload', () => {
- $('.tanuki-logo').addClass('animate');
+ document.querySelector('.tanuki-logo').classList.add('animate');
});
}
diff --git a/app/assets/javascripts/logs/utils.js b/app/assets/javascripts/logs/utils.js
index 8e21863dd0c..74c2f8a68f8 100644
--- a/app/assets/javascripts/logs/utils.js
+++ b/app/assets/javascripts/logs/utils.js
@@ -1,25 +1,4 @@
import dateFormat from 'dateformat';
-import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import { dateFormatMask } from './constants';
-/**
- * Returns a time range (`start`, `end`) where `start` is the
- * current time minus a given number of seconds and `end`
- * is the current time (`now()`).
- *
- * @param {Number} seconds Seconds duration, defaults to 0.
- * @returns {Object} range Time range
- * @returns {String} range.start ISO String of current time minus given seconds
- * @returns {String} range.end ISO String of current time
- */
-export const getTimeRange = (seconds = 0) => {
- const end = Math.floor(Date.now() / 1000); // convert milliseconds to seconds
- const start = end - seconds;
-
- return {
- start: new Date(secondsToMilliseconds(start)).toISOString(),
- end: new Date(secondsToMilliseconds(end)).toISOString(),
- };
-};
-
export const formatDate = (timestamp) => dateFormat(timestamp, dateFormatMask);
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 2f3cdc525a7..e3e8efdd771 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -299,3 +299,10 @@ if (flashContainer && flashContainer.children.length) {
$('.gl-show-field-errors').each((i, form) => new GlFieldErrors(form));
requestIdleCallback(deferredInitialisation);
+
+// initialize hiding of tooltip after clicking on dropdown's links and buttons
+document
+ .querySelectorAll('a[data-toggle="dropdown"], button[data-toggle="dropdown"]')
+ .forEach((element) => {
+ element.addEventListener('click', () => tooltips.hide(element));
+ });
diff --git a/app/assets/javascripts/members/components/members_tabs.vue b/app/assets/javascripts/members/components/members_tabs.vue
index ee4743010cf..98995730df4 100644
--- a/app/assets/javascripts/members/components/members_tabs.vue
+++ b/app/assets/javascripts/members/components/members_tabs.vue
@@ -77,6 +77,10 @@ export default {
urlParams.push(state.filteredSearchBar.searchParam);
}
+ if (state?.filteredSearchBar?.tokens) {
+ urlParams.push(...state.filteredSearchBar.tokens);
+ }
+
return urlParams;
},
getTabCount({ namespace }) {
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 14d628e455c..460dc0041ab 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -4,7 +4,6 @@ import { mapState } from 'vuex';
import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue';
import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
-import initUserPopovers from '~/user_popovers';
import UserDate from '~/vue_shared/components/user_date.vue';
import {
FIELD_KEY_ACTIONS,
@@ -13,9 +12,9 @@ import {
TAB_QUERY_PARAM_VALUES,
MEMBER_STATE_AWAITING,
MEMBER_STATE_ACTIVE,
- USER_STATE_BLOCKED_PENDING_APPROVAL,
- BADGE_LABELS_AWAITING_USER_SIGNUP,
- BADGE_LABELS_PENDING_OWNER_APPROVAL,
+ USER_STATE_BLOCKED,
+ BADGE_LABELS_AWAITING_SIGNUP,
+ BADGE_LABELS_PENDING,
} from '../../constants';
import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
import RemoveMemberModal from '../modals/remove_member_modal.vue';
@@ -85,9 +84,6 @@ export default {
return this.tabQueryParamValue === TAB_QUERY_PARAM_VALUES.invite;
},
},
- mounted() {
- initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
- },
methods: {
hasActionButtons(member) {
return (
@@ -166,7 +162,7 @@ export default {
);
},
/**
- * Returns whether the user is awaiting root approval
+ * Returns whether the user is blocked awaiting root approval
*
* This checks User.state exposed via MemberEntity
*
@@ -174,11 +170,11 @@ export default {
* @see {@link ~/app/serializers/member_entity.rb}
* @returns {boolean}
*/
- isUserPendingRootApproval(memberInviteMetadata) {
- return memberInviteMetadata?.userState === USER_STATE_BLOCKED_PENDING_APPROVAL;
+ isUserBlocked(memberInviteMetadata) {
+ return memberInviteMetadata?.userState === USER_STATE_BLOCKED;
},
/**
- * Returns whether the member is awaiting owner approval
+ * Returns whether the member is awaiting state
*
* This checks Member.state exposed via MemberEntity
*
@@ -187,16 +183,13 @@ export default {
* @see {@link ~/app/serializers/member_entity.rb}
* @returns {boolean}
*/
- isMemberPendingOwnerApproval(memberState) {
+ isMemberAwaiting(memberState) {
return memberState === MEMBER_STATE_AWAITING;
},
isUserAwaiting(memberInviteMetadata, memberState) {
- return (
- this.isUserPendingRootApproval(memberInviteMetadata) ||
- this.isMemberPendingOwnerApproval(memberState)
- );
+ return this.isUserBlocked(memberInviteMetadata) || this.isMemberAwaiting(memberState);
},
- shouldAddPendingOwnerApprovalBadge(memberInviteMetadata, memberState) {
+ shouldAddPendingBadge(memberInviteMetadata, memberState) {
return (
this.isUserAwaiting(memberInviteMetadata, memberState) &&
!this.isNewUser(memberInviteMetadata)
@@ -213,11 +206,11 @@ export default {
*/
inviteBadge(memberInviteMetadata, memberState) {
if (this.isNewUser(memberInviteMetadata, memberState)) {
- return BADGE_LABELS_AWAITING_USER_SIGNUP;
+ return BADGE_LABELS_AWAITING_SIGNUP;
}
- if (this.shouldAddPendingOwnerApprovalBadge(memberInviteMetadata, memberState)) {
- return BADGE_LABELS_PENDING_OWNER_APPROVAL;
+ if (this.shouldAddPendingBadge(memberInviteMetadata, memberState)) {
+ return BADGE_LABELS_PENDING;
}
return '';
diff --git a/app/assets/javascripts/members/components/table/role_dropdown.vue b/app/assets/javascripts/members/components/table/role_dropdown.vue
index fa895cf24c4..6cd8bf57313 100644
--- a/app/assets/javascripts/members/components/table/role_dropdown.vue
+++ b/app/assets/javascripts/members/components/table/role_dropdown.vue
@@ -41,7 +41,7 @@ export default {
const dropdownToggle = this.$refs.glDropdown.$el.querySelector('.dropdown-toggle');
if (dropdownToggle) {
- dropdownToggle.setAttribute('data-qa-selector', 'access_level_dropdown');
+ dropdownToggle.dataset.qaSelector = 'access_level_dropdown';
}
},
methods: {
diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js
index c66a19c4765..8c40cc3f29d 100644
--- a/app/assets/javascripts/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
@@ -130,9 +130,15 @@ export const FILTERED_SEARCH_TOKEN_WITH_INHERITED_PERMISSIONS = {
],
};
+export const FILTERED_SEARCH_TOKEN_GROUPS_WITH_INHERITED_PERMISSIONS = {
+ ...FILTERED_SEARCH_TOKEN_WITH_INHERITED_PERMISSIONS,
+ type: 'groups_with_inherited_permissions',
+};
+
export const AVAILABLE_FILTERED_SEARCH_TOKENS = [
FILTERED_SEARCH_TOKEN_TWO_FACTOR,
FILTERED_SEARCH_TOKEN_WITH_INHERITED_PERMISSIONS,
+ FILTERED_SEARCH_TOKEN_GROUPS_WITH_INHERITED_PERMISSIONS,
];
export const AVATAR_SIZE = 48;
@@ -154,7 +160,7 @@ export const TAB_QUERY_PARAM_VALUES = {
* This user state value comes from the User model
* see the state machine in app/models/user.rb
*/
-export const USER_STATE_BLOCKED_PENDING_APPROVAL = 'blocked_pending_approval';
+export const USER_STATE_BLOCKED = 'blocked_pending_approval';
/**
* This and following member state constants' values
@@ -164,8 +170,8 @@ export const MEMBER_STATE_CREATED = 0;
export const MEMBER_STATE_AWAITING = 1;
export const MEMBER_STATE_ACTIVE = 2;
-export const BADGE_LABELS_AWAITING_USER_SIGNUP = __('Awaiting user signup');
-export const BADGE_LABELS_PENDING_OWNER_APPROVAL = __('Pending owner approval');
+export const BADGE_LABELS_AWAITING_SIGNUP = __('Awaiting user signup');
+export const BADGE_LABELS_PENDING = __('Pending owner action');
export const DAYS_TO_EXPIRE_SOON = 7;
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue b/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue
index fdcb99351a7..20ee9a17fa0 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue
@@ -31,7 +31,7 @@ export default {
},
inject: ['mergeRequestPath', 'sourceBranchPath', 'resolveConflictsPath'],
i18n: {
- commitStatSummary: __('Showing %{conflict} between %{sourceBranch} and %{targetBranch}'),
+ commitStatSummary: __('Showing %{conflict}'),
resolveInfo: __(
'You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}',
),
@@ -73,7 +73,7 @@ export default {
</script>
<template>
<div id="conflicts">
- <gl-loading-icon v-if="isLoading" size="md" data-testid="loading-spinner" />
+ <gl-loading-icon v-if="isLoading" size="lg" data-testid="loading-spinner" />
<div v-if="hasError" class="nothing-here-block">
{{ conflictsData.errorMessage }}
</div>
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 960b25bb552..8cdb9eb5fc4 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -95,7 +95,7 @@ MergeRequest.prototype.initMRBtnListeners = function () {
.then(({ data }) => {
draftToggle.removeAttribute('disabled');
eventHub.$emit('MRWidgetUpdateRequested');
- MergeRequest.toggleDraftStatus(data.title, wipEvent === 'unwip');
+ MergeRequest.toggleDraftStatus(data.title, wipEvent === 'ready');
})
.catch(() => {
createFlash({
@@ -156,11 +156,7 @@ MergeRequest.toggleDraftStatus = function (title, isReady) {
} else {
toast(__('Marked as draft. Can only be merged when marked as ready.'));
}
- const titleEl = document.querySelector(
- `.merge-request .detail-page-${
- window.gon?.features?.updatedMrHeader ? 'header' : 'description'
- } .title`,
- );
+ const titleEl = document.querySelector(`.merge-request .detail-page-header .title`);
if (titleEl) {
titleEl.textContent = title;
@@ -172,7 +168,7 @@ MergeRequest.toggleDraftStatus = function (title, isReady) {
draftToggles.forEach((el) => {
const draftToggle = el;
const url = setUrlParams(
- { 'merge_request[wip_event]': isReady ? 'wip' : 'unwip' },
+ { 'merge_request[wip_event]': isReady ? 'draft' : 'ready' },
draftToggle.href,
);
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index e02109d1fd1..94041d77bb0 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -177,6 +177,7 @@ export default class MergeRequestTabs {
this.peek = document.getElementById('js-peek');
this.sidebar = document.querySelector('.js-right-sidebar');
this.pageLayout = document.querySelector('.layout-page');
+ this.expandSidebar = document.querySelector('.js-expand-sidebar');
this.paddingTop = 16;
this.scrollPositions = {};
@@ -281,6 +282,8 @@ export default class MergeRequestTabs {
const tab = this.mergeRequestTabs.querySelector(`.${action}-tab`);
if (tab) tab.classList.add('active');
+ this.expandSidebar?.classList.toggle('gl-display-none!', action !== 'show');
+
if (action === 'commits') {
this.loadCommits(href);
// this.hideSidebar();
diff --git a/app/assets/javascripts/milestones/components/promote_milestone_modal.vue b/app/assets/javascripts/milestones/components/promote_milestone_modal.vue
index b41611001ab..cac6d722ced 100644
--- a/app/assets/javascripts/milestones/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/milestones/components/promote_milestone_modal.vue
@@ -80,7 +80,7 @@ export default {
},
primaryAction: {
text: s__('Milestones|Promote Milestone'),
- attributes: [{ variant: 'warning' }],
+ attributes: [{ variant: 'confirm' }],
},
cancelAction: {
text: __('Cancel'),
diff --git a/app/assets/javascripts/monitoring/components/create_dashboard_modal.vue b/app/assets/javascripts/monitoring/components/create_dashboard_modal.vue
index 288487d25a5..10178366db5 100644
--- a/app/assets/javascripts/monitoring/components/create_dashboard_modal.vue
+++ b/app/assets/javascripts/monitoring/components/create_dashboard_modal.vue
@@ -47,7 +47,7 @@ export default {
<gl-button category="secondary" @click="cancelHandler">{{ s__('Metrics|Cancel') }}</gl-button>
<gl-button
category="secondary"
- variant="info"
+ variant="confirm"
target="_blank"
:href="addDashboardDocumentationPath"
data-testid="create-dashboard-modal-docs-button"
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 6a85833db27..70e253508ce 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -132,16 +132,6 @@ export default {
required: false,
default: false,
},
- alertsEndpoint: {
- type: String,
- required: false,
- default: null,
- },
- prometheusAlertsAvailable: {
- type: Boolean,
- required: false,
- default: false,
- },
rearrangePanelsAvailable: {
type: Boolean,
required: false,
@@ -461,9 +451,7 @@ export default {
:settings-path="settingsPath"
:clipboard-text="generatePanelUrl(expandedPanel.group, expandedPanel.panel)"
:graph-data="expandedPanel.panel"
- :alerts-endpoint="alertsEndpoint"
:height="600"
- :prometheus-alerts-available="prometheusAlertsAvailable"
@timerangezoom="onTimeRangeZoom"
>
<template #top-left>
@@ -526,8 +514,6 @@ export default {
:settings-path="settingsPath"
:clipboard-text="generatePanelUrl(groupData.group, graphData)"
:graph-data="graphData"
- :alerts-endpoint="alertsEndpoint"
- :prometheus-alerts-available="prometheusAlertsAvailable"
@timerangezoom="onTimeRangeZoom"
@expand="onExpandPanel(groupData.group, graphData)"
/>
diff --git a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
index 8e5a0b5cda2..7f8fb3c223d 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
@@ -186,7 +186,7 @@ export default {
v-track-event="getAddMetricTrackingOptions()"
data-testid="add-metric-modal-submit-button"
:disabled="!customMetricsFormIsValid"
- variant="success"
+ variant="confirm"
@click="submitCustomMetricsForm"
>
{{ __('Save changes') }}
diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue
index f53f78a3f13..f18290e7048 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_header.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue
@@ -257,7 +257,7 @@ export default {
>
<gl-button
class="flex-grow-1 js-external-dashboard-link"
- variant="info"
+ variant="confirm"
category="primary"
:href="externalDashboardUrl"
target="_blank"
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
index e5f0206bb8b..8efea2bfc3e 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
@@ -106,7 +106,7 @@ export default {
<div class="gl-text-right">
<gl-button
ref="clipboardCopyBtn"
- variant="success"
+ variant="confirm"
category="secondary"
:data-clipboard-text="yml"
class="gl-xs-w-full gl-xs-mb-3"
@@ -116,7 +116,7 @@ export default {
</gl-button>
<gl-button
type="submit"
- variant="success"
+ variant="confirm"
:disabled="panelPreviewIsLoading"
class="js-no-auto-disable gl-xs-w-full"
>
@@ -162,7 +162,7 @@ export default {
ref="viewDocumentationBtn"
category="secondary"
class="gl-xs-w-full gl-xs-mb-3"
- variant="info"
+ variant="confirm"
target="_blank"
:href="addDashboardDocumentationPath"
>
@@ -170,7 +170,7 @@ export default {
</gl-button>
<gl-button
ref="openRepositoryBtn"
- variant="success"
+ variant="confirm"
:href="projectPath"
class="gl-xs-w-full"
>
diff --git a/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue b/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue
index fd07a41ec37..d1ce7bad39a 100644
--- a/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue
+++ b/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue
@@ -1,7 +1,7 @@
<script>
-import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui';
+import { GlAlert, GlModal } from '@gitlab/ui';
import { mapActions, mapGetters } from 'vuex';
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import DuplicateDashboardForm from './duplicate_dashboard_form.vue';
const events = {
@@ -9,7 +9,7 @@ const events = {
};
export default {
- components: { GlAlert, GlLoadingIcon, GlModal, DuplicateDashboardForm },
+ components: { GlAlert, GlModal, DuplicateDashboardForm },
props: {
defaultBranch: {
type: String,
@@ -32,6 +32,20 @@ export default {
okButtonText() {
return this.loading ? s__('Metrics|Duplicating...') : s__('Metrics|Duplicate');
},
+ actionPrimaryProps() {
+ return {
+ text: this.okButtonText,
+ attributes: {
+ loading: this.loading,
+ variant: 'confirm',
+ },
+ };
+ },
+ actionCancelProps() {
+ return {
+ text: __('Cancel'),
+ };
+ },
},
methods: {
...mapActions('monitoringDashboard', ['duplicateSystemDashboard']),
@@ -75,7 +89,8 @@ export default {
ref="duplicateDashboardModal"
:modal-id="modalId"
:title="s__('Metrics|Duplicate dashboard')"
- ok-variant="success"
+ :action-primary="actionPrimaryProps"
+ :action-cancel="actionCancelProps"
@ok="ok"
@hide="hide"
>
@@ -87,9 +102,5 @@ export default {
:default-branch="defaultBranch"
@change="formChange"
/>
- <template #modal-ok>
- <gl-loading-icon v-if="loading" size="sm" inline color="light" />
- {{ okButtonText }}
- </template>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue
index 5b73fb4e10d..74a806c50a9 100644
--- a/app/assets/javascripts/monitoring/components/graph_group.vue
+++ b/app/assets/javascripts/monitoring/components/graph_group.vue
@@ -37,7 +37,7 @@ export default {
},
computed: {
caretIcon() {
- return this.isCollapsed ? 'angle-right' : 'angle-down';
+ return this.isCollapsed ? 'chevron-lg-right' : 'chevron-lg-down';
},
},
watch: {
diff --git a/app/assets/javascripts/monitoring/services/alerts_service.js b/app/assets/javascripts/monitoring/services/alerts_service.js
deleted file mode 100644
index cb6dac7aa15..00000000000
--- a/app/assets/javascripts/monitoring/services/alerts_service.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import axios from '~/lib/utils/axios_utils';
-
-const mapAlert = ({ runbook_url, ...alert }) => {
- return { runbookUrl: runbook_url, ...alert };
-};
-
-export default class AlertsService {
- constructor({ alertsEndpoint }) {
- this.alertsEndpoint = alertsEndpoint;
- }
-
- getAlerts() {
- return axios.get(this.alertsEndpoint).then((resp) => mapAlert(resp.data));
- }
-
- createAlert({ prometheus_metric_id, operator, threshold, runbookUrl }) {
- return axios
- .post(this.alertsEndpoint, {
- prometheus_metric_id,
- operator,
- threshold,
- runbook_url: runbookUrl,
- })
- .then((resp) => mapAlert(resp.data));
- }
-
- // eslint-disable-next-line class-methods-use-this
- readAlert(alertPath) {
- return axios.get(alertPath).then((resp) => mapAlert(resp.data));
- }
-
- // eslint-disable-next-line class-methods-use-this
- updateAlert(alertPath, { operator, threshold, runbookUrl }) {
- return axios
- .put(alertPath, { operator, threshold, runbook_url: runbookUrl })
- .then((resp) => mapAlert(resp.data));
- }
-
- // eslint-disable-next-line class-methods-use-this
- deleteAlert(alertPath) {
- return axios.delete(alertPath).then((resp) => resp.data);
- }
-}
diff --git a/app/assets/javascripts/mr_popover/index.js b/app/assets/javascripts/mr_popover/index.js
deleted file mode 100644
index 714cf67e0bd..00000000000
--- a/app/assets/javascripts/mr_popover/index.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
-import MRPopover from './components/mr_popover.vue';
-
-let renderedPopover;
-let renderFn;
-
-const handleUserPopoverMouseOut = ({ target }) => {
- target.removeEventListener('mouseleave', handleUserPopoverMouseOut);
-
- if (renderFn) {
- clearTimeout(renderFn);
- }
- if (renderedPopover) {
- renderedPopover.$destroy();
- renderedPopover = null;
- }
-};
-
-/**
- * Adds a MergeRequestPopover component to the body, hands over as much data as the target element has in data attributes.
- * loads based on data-project-path and data-iid more data about an MR from the API and sets it on the popover
- */
-const handleMRPopoverMount = ({ apolloProvider, projectPath, mrTitle, iid }) => ({ target }) => {
- // Add listener to actually remove it again
- target.addEventListener('mouseleave', handleUserPopoverMouseOut);
-
- renderFn = setTimeout(() => {
- const MRPopoverComponent = Vue.extend(MRPopover);
- renderedPopover = new MRPopoverComponent({
- propsData: {
- target,
- projectPath,
- mergeRequestIID: iid,
- mergeRequestTitle: mrTitle,
- },
- apolloProvider,
- });
-
- renderedPopover.$mount();
- }, 200); // 200ms delay so not every mouseover triggers Popover + API Call
-};
-
-export default (elements) => {
- const mrLinks = elements || [...document.querySelectorAll('.gfm-merge_request')];
- if (mrLinks.length > 0) {
- Vue.use(VueApollo);
-
- const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
- });
- const listenerAddedAttr = 'data-mr-listener-added';
-
- mrLinks.forEach((el) => {
- const { projectPath, mrTitle, iid } = el.dataset;
-
- if (!el.getAttribute(listenerAddedAttr) && projectPath && mrTitle && iid) {
- el.addEventListener(
- 'mouseenter',
- handleMRPopoverMount({ apolloProvider, projectPath, mrTitle, iid }),
- );
- el.setAttribute(listenerAddedAttr, true);
- }
- });
- }
-};
diff --git a/app/assets/javascripts/nav/components/responsive_header.vue b/app/assets/javascripts/nav/components/responsive_header.vue
index 8a1d21993b7..e29b4a67383 100644
--- a/app/assets/javascripts/nav/components/responsive_header.vue
+++ b/app/assets/javascripts/nav/components/responsive_header.vue
@@ -14,7 +14,7 @@ export default {
return {
id: 'home',
view: 'home',
- icon: 'angle-left',
+ icon: 'chevron-lg-left',
};
},
},
diff --git a/app/assets/javascripts/notes/components/comment_field_layout.vue b/app/assets/javascripts/notes/components/comment_field_layout.vue
index 9638c20e28c..84bda1b0b5c 100644
--- a/app/assets/javascripts/notes/components/comment_field_layout.vue
+++ b/app/assets/javascripts/notes/components/comment_field_layout.vue
@@ -14,7 +14,7 @@ export default {
type: Object,
required: true,
},
- noteIsConfidential: {
+ isInternalNote: {
type: Boolean,
required: false,
default: false,
@@ -44,7 +44,7 @@ export default {
return this.noteableData.issue_email_participants?.map(({ email }) => email) || [];
},
showEmailParticipantsWarning() {
- return this.emailParticipants.length && !this.noteIsConfidential;
+ return this.emailParticipants.length && !this.isInternalNote;
},
},
};
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 8ef071034e5..e7ac27c5e3e 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -60,7 +60,7 @@ export default {
note: '',
noteType: constants.COMMENT,
errors: [],
- noteIsConfidential: false,
+ noteIsInternal: false,
isSubmitting: false,
};
},
@@ -91,13 +91,13 @@ export default {
},
commentButtonTitle() {
const { comment, internalComment, startThread, startInternalThread } = this.$options.i18n;
- if (this.noteIsConfidential) {
+ if (this.noteIsInternal) {
return this.noteType === constants.COMMENT ? internalComment : startInternalThread;
}
return this.noteType === constants.COMMENT ? comment : startThread;
},
textareaPlaceholder() {
- return this.noteIsConfidential
+ return this.noteIsInternal
? this.$options.i18n.bodyPlaceholderInternal
: this.$options.i18n.bodyPlaceholder;
},
@@ -110,7 +110,7 @@ export default {
canCreateNote() {
return this.getNoteableData.current_user.can_create_note;
},
- canSetConfidential() {
+ canSetInternalNote() {
return this.getNoteableData.current_user.can_update && (this.isIssue || this.isEpic);
},
issueActionButtonTitle() {
@@ -172,7 +172,7 @@ export default {
trackingLabel() {
return slugifyWithUnderscore(`${this.commentButtonTitle} button`);
},
- confidentialNotesEnabled() {
+ internalNotesEnabled() {
return Boolean(this.glFeatures.confidentialNotes);
},
disableSubmitButton() {
@@ -217,7 +217,11 @@ export default {
note: {
noteable_type: this.noteableType,
noteable_id: this.getNoteableData.id,
- confidential: this.noteIsConfidential,
+ // Internal notes were identified as `confidential`
+ // before we decided to treat them as _internal_
+ // so now until API is updated we need to use `confidential`
+ // in request payload.
+ confidential: this.noteIsInternal,
note: this.note,
},
merge_request_diff_head_sha: this.getNoteableData.diff_head_sha,
@@ -292,7 +296,7 @@ export default {
if (shouldClear) {
this.note = '';
- this.noteIsConfidential = false;
+ this.noteIsInternal = false;
this.resizeTextarea();
this.$refs.markdownField.previewMarkdown = false;
}
@@ -356,7 +360,7 @@ export default {
<comment-field-layout
:with-alert-container="true"
:noteable-data="getNoteableData"
- :note-is-confidential="noteIsConfidential"
+ :is-internal-note="noteIsInternal"
:noteable-type="noteableType"
>
<markdown-field
@@ -410,17 +414,17 @@ export default {
</template>
<template v-else>
<gl-form-checkbox
- v-if="confidentialNotesEnabled && canSetConfidential"
- v-model="noteIsConfidential"
- class="gl-mb-6"
- data-testid="confidential-note-checkbox"
+ v-if="internalNotesEnabled && canSetInternalNote"
+ v-model="noteIsInternal"
+ class="gl-mb-2"
+ data-testid="internal-note-checkbox"
>
- {{ $options.i18n.confidential }}
+ {{ $options.i18n.internal }}
<gl-icon
v-gl-tooltip:tooltipcontainer.bottom
name="question"
:size="16"
- :title="$options.i18n.confidentialVisibility"
+ :title="$options.i18n.internalVisibility"
class="gl-text-gray-500"
/>
</gl-form-checkbox>
@@ -429,7 +433,7 @@ export default {
class="gl-mr-3"
:disabled="disableSubmitButton"
:tracking-label="trackingLabel"
- :is-internal-note="noteIsConfidential"
+ :is-internal-note="noteIsInternal"
:noteable-display-name="noteableDisplayName"
:discussions-require-resolution="discussionsRequireResolution"
@click="handleSave"
diff --git a/app/assets/javascripts/notes/components/diff_discussion_header.vue b/app/assets/javascripts/notes/components/diff_discussion_header.vue
index 5210d2ca287..0e213028c7c 100644
--- a/app/assets/javascripts/notes/components/diff_discussion_header.vue
+++ b/app/assets/javascripts/notes/components/diff_discussion_header.vue
@@ -97,14 +97,17 @@ export default {
</script>
<template>
- <div class="discussion-header note-wrapper">
- <div v-once class="timeline-icon gl-align-self-start gl-flex-shrink-0 gl-flex-shrink gl-mr-4">
+ <div class="discussion-header gl-display-flex gl-align-items-center gl-p-5">
+ <div
+ v-once
+ class="timeline-icon gl-align-self-start gl-flex-shrink-0 gl-flex-shrink gl-ml-3 gl-mr-4"
+ >
<user-avatar-link
v-if="author"
:link-href="author.path"
:img-src="author.avatar_url"
:img-alt="author.name"
- :img-size="32"
+ :img-size="24"
:img-css-classes="'gl-mr-0!' /* NOTE: this is needed only while we migrate user-avatar-image to GlAvatar (https://gitlab.com/groups/gitlab-org/-/epics/7731) */"
/>
</div>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index e2b0c7fee32..3bdf8349a12 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,8 +1,5 @@
<script>
-import {
- GlDeprecatedSkeletonLoading as GlSkeletonLoading,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlSkeletonLoader, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
@@ -16,7 +13,7 @@ const FIRST_CHAR_REGEX = /^(\+|-| )/;
export default {
components: {
DiffFileHeader,
- GlSkeletonLoading,
+ GlSkeletonLoader,
DiffViewer,
ImageDiffOverlay,
},
@@ -107,7 +104,7 @@ export default {
<td v-if="error" class="js-error-lazy-load-diff diff-loading-error-block">
{{ __('Unable to load the diff') }}
<button
- class="btn-link btn-link-retry gl-p-0 js-toggle-lazy-diff-retry-button"
+ class="gl-button btn-link btn-link-retry gl-p-0 js-toggle-lazy-diff-retry-button gl-reset-font-size!"
@click="fetchDiff"
>
{{ __('Try again') }}
@@ -115,7 +112,7 @@ export default {
</td>
<td v-else class="line_content js-success-lazy-load">
<span></span>
- <gl-skeleton-loading />
+ <gl-skeleton-loader />
<span></span>
</td>
</tr>
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index f746f7ed0ed..eedcb0c09d4 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -75,25 +75,25 @@ export default {
<gl-button-group class="gl-ml-3">
<gl-button
v-gl-tooltip.hover
- :title="__('Jump to previous unresolved thread')"
- :aria-label="__('Jump to previous unresolved thread')"
+ :title="__('Go to previous unresolved thread')"
+ :aria-label="__('Go to previous unresolved thread')"
class="discussion-previous-btn gl-rounded-base! gl-px-2!"
data-track-action="click_button"
data-track-label="mr_previous_unresolved_thread"
data-track-property="click_previous_unresolved_thread_top"
- icon="angle-up"
+ icon="chevron-lg-up"
category="tertiary"
@click="jumpToPreviousDiscussion"
/>
<gl-button
v-gl-tooltip.hover
- :title="__('Jump to next unresolved thread')"
- :aria-label="__('Jump to next unresolved thread')"
+ :title="__('Go to next unresolved thread')"
+ :aria-label="__('Go to next unresolved thread')"
class="discussion-next-btn gl-rounded-base! gl-px-2!"
data-track-action="click_button"
data-track-label="mr_next_unresolved_thread"
data-track-property="click_next_unresolved_thread_top"
- icon="angle-down"
+ icon="chevron-lg-down"
category="tertiary"
@click="jumpToNextDiscussion"
/>
diff --git a/app/assets/javascripts/notes/components/email_participants_warning.vue b/app/assets/javascripts/notes/components/email_participants_warning.vue
index ecf42fce1d2..1875d48e7b2 100644
--- a/app/assets/javascripts/notes/components/email_participants_warning.vue
+++ b/app/assets/javascripts/notes/components/email_participants_warning.vue
@@ -58,7 +58,7 @@ export default {
<div class="issuable-note-warning" data-testid="email-participants-warning">
<gl-sprintf :message="message">
<template #andMore>
- <button type="button" class="btn-transparent btn-link" @click="showMoreParticipants">
+ <button type="button" class="gl-button btn-link" @click="showMoreParticipants">
{{ moreLabel }}
</button>
</template>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 1bd2f879e6c..10e3f57a56d 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -294,14 +294,20 @@ export default {
/>
<emoji-picker
v-if="canAwardEmoji"
- toggle-class="note-action-button note-emoji-button gl-text-gray-600 gl-m-3 gl-p-0! gl-shadow-none! gl-bg-transparent!"
+ toggle-class="note-action-button note-emoji-button btn-icon btn-default-tertiary"
data-testid="note-emoji-button"
@click="setAwardEmoji"
>
<template #button-content>
- <gl-icon class="link-highlight award-control-icon-neutral gl-m-0!" name="slight-smile" />
- <gl-icon class="link-highlight award-control-icon-positive gl-m-0!" name="smiley" />
- <gl-icon class="link-highlight award-control-icon-super-positive gl-m-0!" name="smile" />
+ <gl-icon class="award-control-icon-neutral gl-button-icon gl-icon" name="slight-smile" />
+ <gl-icon
+ class="award-control-icon-positive gl-button-icon gl-icon gl-left-3!"
+ name="smiley"
+ />
+ <gl-icon
+ class="award-control-icon-super-positive gl-button-icon gl-icon gl-left-3!"
+ name="smile"
+ />
</template>
</emoji-picker>
<reply-button
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 6c9bc4461c2..cc74c2ee605 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -8,6 +8,7 @@ import { __ } from '~/locale';
import '~/behaviors/markdown/render_gfm';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import autosave from '../mixins/autosave';
+import { INTERNAL_NOTE_CLASSES } from '../constants';
import noteAttachment from './note_attachment.vue';
import noteAwardsList from './note_awards_list.vue';
import noteEditedText from './note_edited_text.vue';
@@ -54,6 +55,11 @@ export default {
required: false,
default: '',
},
+ isInternalNote: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
...mapGetters(['getDiscussion', 'suggestionsCount', 'getSuggestionsFilePaths']),
@@ -95,6 +101,12 @@ export default {
return escape(suggestion);
},
+ internalNoteContainerClasses() {
+ if (this.isInternalNote && !this.isEditing) {
+ return INTERNAL_NOTE_CLASSES;
+ }
+ return '';
+ },
},
mounted() {
this.renderGFM();
@@ -160,53 +172,61 @@ export default {
</script>
<template>
- <div ref="note-body" :class="{ 'js-task-list-container': canEdit }" class="note-body">
- <suggestions
- v-if="hasSuggestion && !isEditing"
- :suggestions="note.suggestions"
- :suggestions-count="suggestionsCount"
- :batch-suggestions-info="batchSuggestionsInfo"
- :note-html="note.note_html"
- :line-type="lineType"
- :help-page-path="helpPagePath"
- :default-commit-message="commitMessage"
- :failed-to-load-metadata="failedToLoadMetadata"
- @apply="applySuggestion"
- @applyBatch="applySuggestionBatch"
- @addToBatch="addSuggestionToBatch"
- @removeFromBatch="removeSuggestionFromBatch"
- />
- <div v-else v-safe-html:[$options.safeHtmlConfig]="note.note_html" class="note-text md"></div>
- <note-form
- v-if="isEditing"
- ref="noteForm"
- :note-body="noteBody"
- :note-id="note.id"
- :line="line"
- :note="note"
- :save-button-title="saveButtonTitle"
- :help-page-path="helpPagePath"
- :discussion="discussion"
- :resolve-discussion="note.resolve_discussion"
- @handleFormUpdate="handleFormUpdate"
- @cancelForm="formCancelHandler"
- />
- <!-- eslint-disable vue/no-mutating-props -->
- <textarea
- v-if="canEdit"
- v-model="note.note"
- :data-update-url="note.path"
- class="hidden js-task-list-field"
- dir="auto"
- ></textarea>
- <!-- eslint-enable vue/no-mutating-props -->
- <note-edited-text
- v-if="note.last_edited_at"
- :edited-at="note.last_edited_at"
- :edited-by="note.last_edited_by"
- action-text="Edited"
- class="note_edited_ago"
- />
+ <div
+ ref="note-body"
+ :class="{
+ 'js-task-list-container': canEdit,
+ }"
+ class="note-body"
+ >
+ <div :class="internalNoteContainerClasses" data-testid="note-internal-container">
+ <suggestions
+ v-if="hasSuggestion && !isEditing"
+ :suggestions="note.suggestions"
+ :suggestions-count="suggestionsCount"
+ :batch-suggestions-info="batchSuggestionsInfo"
+ :note-html="note.note_html"
+ :line-type="lineType"
+ :help-page-path="helpPagePath"
+ :default-commit-message="commitMessage"
+ :failed-to-load-metadata="failedToLoadMetadata"
+ @apply="applySuggestion"
+ @applyBatch="applySuggestionBatch"
+ @addToBatch="addSuggestionToBatch"
+ @removeFromBatch="removeSuggestionFromBatch"
+ />
+ <div v-else v-safe-html:[$options.safeHtmlConfig]="note.note_html" class="note-text md"></div>
+ <note-form
+ v-if="isEditing"
+ ref="noteForm"
+ :note-body="noteBody"
+ :note-id="note.id"
+ :line="line"
+ :note="note"
+ :save-button-title="saveButtonTitle"
+ :help-page-path="helpPagePath"
+ :discussion="discussion"
+ :resolve-discussion="note.resolve_discussion"
+ @handleFormUpdate="handleFormUpdate"
+ @cancelForm="formCancelHandler"
+ />
+ <!-- eslint-disable vue/no-mutating-props -->
+ <textarea
+ v-if="canEdit"
+ v-model="note.note"
+ :data-update-url="note.path"
+ class="hidden js-task-list-field"
+ dir="auto"
+ ></textarea>
+ <!-- eslint-enable vue/no-mutating-props -->
+ <note-edited-text
+ v-if="note.last_edited_at"
+ :edited-at="note.last_edited_at"
+ :edited-by="note.last_edited_by"
+ action-text="Edited"
+ class="note_edited_ago"
+ />
+ </div>
<note-awards-list
v-if="note.award_emoji && note.award_emoji.length"
:note-id="note.id"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 5dd032abd72..a4cd20e6db8 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -329,7 +329,7 @@ export default {
<form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form">
<comment-field-layout
:noteable-data="getNoteableData"
- :note-is-confidential="discussion.confidential"
+ :is-internal-note="discussion.confidential"
>
<markdown-field
:markdown-preview-path="markdownPreviewPath"
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 1ad9d593ccc..9917249f0db 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -67,7 +67,7 @@ export default {
required: false,
default: true,
},
- isConfidential: {
+ isInternalNote: {
type: Boolean,
required: false,
default: false,
@@ -110,7 +110,7 @@ export default {
authorName() {
return this.author.name;
},
- noteConfidentialityTooltip() {
+ internalNoteTooltip() {
return s__('Notes|This internal note will always remain confidential');
},
},
@@ -231,12 +231,13 @@ export default {
<time-ago-tooltip v-else ref="noteTimestamp" :time="createdAt" tooltip-placement="bottom" />
</template>
<gl-badge
- v-if="isConfidential"
+ v-if="isInternalNote"
v-gl-tooltip:tooltipcontainer.bottom
data-testid="internalNoteIndicator"
variant="warning"
size="sm"
- :title="noteConfidentialityTooltip"
+ class="gl-mb-3 gl-ml-2"
+ :title="internalNoteTooltip"
>
{{ __('Internal note') }}
</gl-badge>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 0f5a517a4c5..c5d174ed890 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -108,7 +108,7 @@ export default {
return this.discussion.notes.slice(0, 1)[0];
},
saveButtonTitle() {
- return this.discussion.confidential ? __('Reply internally') : __('Comment');
+ return this.discussion.confidential ? __('Reply internally') : __('Reply');
},
shouldShowJumpToNextDiscussion() {
return this.showJumpToNextDiscussion(this.discussionsByDiffOrder ? 'diff' : 'discussion');
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index cda22b58c5b..af0c1e9619e 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -447,7 +447,7 @@ export default {
:author="author"
:created-at="note.created_at"
:note-id="note.id"
- :is-confidential="note.confidential"
+ :is-internal-note="note.confidential"
:noteable-type="noteableType"
>
<template #note-header-info>
@@ -493,9 +493,10 @@ export default {
<note-body
ref="noteBody"
:note="note"
+ :can-edit="note.current_user.can_edit"
+ :is-internal-note="note.confidential"
:line="line"
:file="diffFile"
- :can-edit="note.current_user.can_edit"
:is-editing="isEditing"
:help-page-path="helpPagePath"
@handleFormUpdate="formUpdateHandler"
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 7d8d23335e0..754c2917182 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -3,7 +3,6 @@ import { mapGetters, mapActions } from 'vuex';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
import createFlash from '~/flash';
import { __ } from '~/locale';
-import initUserPopovers from '~/user_popovers';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -169,7 +168,6 @@ export default {
updated() {
this.$nextTick(() => {
highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member'));
- initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
});
},
beforeDestroy() {
@@ -212,10 +210,6 @@ export default {
this.setFetchingState(true);
- if (this.glFeatures.paginatedNotes) {
- return this.initPolling();
- }
-
return this.fetchDiscussions(this.getFetchDiscussionsConfig())
.then(this.initPolling)
.then(() => {
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
index 65b3fd6f8b3..8cd4477a3bb 100644
--- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -72,7 +72,7 @@ export default {
{{ replies.length }} {{ n__('reply', 'replies', replies.length) }}
</gl-button>
{{ __('Last reply by') }}
- <a :href="lastReply.author.path" class="btn btn-link author-link gl-mx-2">
+ <a :href="lastReply.author.path" class="btn btn-link author-link gl-mx-2 gl-button">
{{ lastReply.author.name }}
</a>
<time-ago-tooltip :time="lastReply.created_at" tooltip-placement="bottom" />
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index cc14ea42a89..b8575016762 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -51,3 +51,5 @@ export const toggleStateErrorMessage = {
[REOPENED]: __('Something went wrong while closing the merge request. Please try again later.'),
},
};
+
+export const INTERNAL_NOTE_CLASSES = ['gl-bg-orange-50', 'gl-px-4', 'gl-py-2'];
diff --git a/app/assets/javascripts/notes/i18n.js b/app/assets/javascripts/notes/i18n.js
index 4c0ee81bec0..08792fd1a3f 100644
--- a/app/assets/javascripts/notes/i18n.js
+++ b/app/assets/javascripts/notes/i18n.js
@@ -14,8 +14,8 @@ export const COMMENT_FORM = {
epic: __('epic'),
bodyPlaceholder: __('Write a comment or drag your files here…'),
bodyPlaceholderInternal: __('Write an internal note or drag your files here…'),
- confidential: s__('Notes|Make this an internal note'),
- confidentialVisibility: s__(
+ internal: s__('Notes|Make this an internal note'),
+ internalVisibility: s__(
'Notes|Internal notes are only visible to the author, assignees, and members with the role of Reporter or higher',
),
discussionThatNeedsResolution: __(
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 0cfc17a6ae9..57bb9e295f9 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -19,7 +19,6 @@ import TaskList from '~/task_list';
import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import * as constants from '../constants';
-import eventHub from '../event_hub';
import * as types from './mutation_types';
import * as utils from './utils';
@@ -90,7 +89,10 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFi
? { params: { notes_filter: filter, persist_filter: persistFilter } }
: null;
- if (window.gon?.features?.paginatedIssueDiscussions) {
+ if (
+ window.gon?.features?.paginatedIssueDiscussions ||
+ window.gon?.features?.paginatedMrDiscussions
+ ) {
return dispatch('fetchDiscussionsBatch', { path, config, perPage: 20 });
}
@@ -128,6 +130,7 @@ export const fetchDiscussionsBatch = ({ commit, dispatch }, { path, config, curs
});
}
+ commit(types.SET_DONE_FETCHING_BATCH_DISCUSSIONS, true);
commit(types.SET_FETCHING_DISCUSSIONS, false);
dispatch('updateResolvableDiscussionsCounts');
@@ -495,13 +498,6 @@ const pollSuccessCallBack = async (resp, commit, state, getters, dispatch) => {
return null;
}
- if (window.gon?.features?.paginatedNotes && !resp.more && state.isFetching) {
- eventHub.$emit('fetchedNotesData');
- dispatch('setFetchingState', false);
- dispatch('setNotesFetchedState', true);
- dispatch('setLoadingState', false);
- }
-
if (resp.notes?.length) {
await dispatch('updateOrCreateNotes', resp.notes);
dispatch('startTaskList');
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index f154edd3434..f779aad5679 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -14,6 +14,7 @@ export default () => ({
currentDiscussionId: null,
batchSuggestionsInfo: [],
currentlyFetchingDiscussions: false,
+ doneFetchingBatchDiscussions: false,
/**
* selectedCommentPosition & selectedCommentPositionHover structures are the same as `position.line_range`:
* {
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index ebda08a3d62..e28a7bc5cdd 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -41,6 +41,7 @@ export const SET_SELECTED_COMMENT_POSITION = 'SET_SELECTED_COMMENT_POSITION';
export const SET_SELECTED_COMMENT_POSITION_HOVER = 'SET_SELECTED_COMMENT_POSITION_HOVER';
export const SET_FETCHING_DISCUSSIONS = 'SET_FETCHING_DISCUSSIONS';
export const SET_RESOLVING_DISCUSSION = 'SET_RESOLVING_DISCUSSION';
+export const SET_DONE_FETCHING_BATCH_DISCUSSIONS = 'SET_DONE_FETCHING_BATCH_DISCUSSIONS';
// Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 5cc2c673391..39d0a46d6d0 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -32,20 +32,6 @@ export default {
}
}
- if (window.gon?.features?.paginatedNotes && note.base_discussion) {
- if (discussion.diff_file) {
- discussion.file_hash = discussion.diff_file.file_hash;
-
- discussion.truncated_diff_lines = utils.prepareDiffLines(
- discussion.truncated_diff_lines || [],
- );
- }
-
- discussion.resolvable = note.resolvable;
- discussion.expanded = note.base_discussion.expanded;
- discussion.resolved = note.resolved;
- }
-
// note.base_discussion = undefined; // No point keeping a reference to this
delete note.base_discussion;
discussion.notes = [note];
@@ -436,4 +422,7 @@ export default {
[types.SET_FETCHING_DISCUSSIONS](state, value) {
state.currentlyFetchingDiscussions = value;
},
+ [types.SET_DONE_FETCHING_BATCH_DISCUSSIONS](state, value) {
+ state.doneFetchingBatchDiscussions = value;
+ },
};
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue
index 15d92ab0ef7..acf810257e6 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue
@@ -25,6 +25,7 @@ import {
NOT_AVAILABLE_TEXT,
NOT_AVAILABLE_SIZE,
MORE_ACTIONS_TEXT,
+ COPY_IMAGE_PATH_TITLE,
} from '../../constants/index';
export default {
@@ -72,6 +73,7 @@ export default {
CONFIGURATION_DETAILS_ROW_TEST,
MISSING_MANIFEST_WARNING_TOOLTIP,
MORE_ACTIONS_TEXT,
+ COPY_IMAGE_PATH_TITLE,
},
computed: {
formattedSize() {
@@ -130,6 +132,7 @@ export default {
<div
v-gl-tooltip="{ title: tag.name }"
data-testid="name"
+ data-qa-selector="tag_name_content"
class="gl-text-overflow-ellipsis gl-overflow-hidden gl-white-space-nowrap"
:class="mobileClasses"
>
@@ -138,7 +141,7 @@ export default {
<clipboard-button
v-if="tag.location"
- :title="tag.location"
+ :title="$options.i18n.COPY_IMAGE_PATH_TITLE"
:text="tag.location"
category="tertiary"
:disabled="disabled"
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue
index 3ae69731537..56da8e88b7a 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue
@@ -56,6 +56,9 @@ export default {
calculatedTimeTilNextRun() {
return timeTilRun(this.expirationPolicy?.next_run);
},
+ expireIconName() {
+ return this.failedDelete ? 'expire' : 'clock';
+ },
},
statusPopoverOptions: {
triggers: 'hover',
@@ -75,7 +78,7 @@ export default {
class="gl-display-inline-flex gl-align-items-center"
>
<div class="gl-display-inline-flex gl-align-items-center">
- <gl-icon name="expire" data-testid="main-icon" />
+ <gl-icon :name="expireIconName" data-testid="main-icon" />
</div>
<span class="gl-mx-2">
{{ statusText }}
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue
index d76a8245b63..e67d77210bb 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue
@@ -14,6 +14,7 @@ import {
IMAGE_FAILED_DELETED_STATUS,
IMAGE_MIGRATING_STATE,
ROOT_IMAGE_TEXT,
+ COPY_IMAGE_PATH_TITLE,
} from '../../constants/index';
import DeleteButton from '../delete_button.vue';
import CleanupStatus from './cleanup_status.vue';
@@ -52,6 +53,7 @@ export default {
i18n: {
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
+ COPY_IMAGE_PATH_TITLE,
},
computed: {
disabledDelete() {
@@ -115,7 +117,7 @@ export default {
v-if="item.location"
:disabled="deleting"
:text="item.location"
- :title="item.location"
+ :title="$options.i18n.COPY_IMAGE_PATH_TITLE"
category="tertiary"
/>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue
index 4ffd8390e4d..19d35a135fd 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue
@@ -107,7 +107,7 @@ export default {
<metadata-item
v-if="!hideExpirationPolicyData"
data-testid="expiration-policy"
- icon="expire"
+ icon="clock"
:text="expirationPolicyText"
size="xl"
/>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
index 2a58933cd64..98c24350f09 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
@@ -67,7 +67,7 @@ export const MISSING_MANIFEST_WARNING_TOOLTIP = s__(
export const UPDATED_AT = s__('ContainerRegistry|Last updated %{time}');
-export const NOT_AVAILABLE_TEXT = __('N/A');
+export const NOT_AVAILABLE_TEXT = __('Not applicable.');
export const NOT_AVAILABLE_SIZE = __('0 bytes');
export const CLEANUP_UNSCHEDULED_TEXT = s__('ContainerRegistry|Cleanup will run %{time}');
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js
index ceaf8a65a10..c6a7591e0d9 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js
@@ -41,6 +41,8 @@ export const EMPTY_RESULT_MESSAGE = s__(
'ContainerRegistry|To widen your search, change or remove the filters above.',
);
+export const COPY_IMAGE_PATH_TITLE = s__('ContainerRegistry|Copy image path');
+
// Parameters
export const IMAGE_DELETE_SCHEDULED_STATUS = 'DELETE_SCHEDULED';
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js
index 2519f6b74a2..b62c51bd208 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js
@@ -35,5 +35,5 @@ export const MISSING_MANIFEST_WARNING_TOOLTIP = s__(
'HarborRegistry|Invalid tag: missing manifest digest',
);
-export const NOT_AVAILABLE_TEXT = __('N/A');
+export const NOT_AVAILABLE_TEXT = __('Not applicable.');
export const NOT_AVAILABLE_SIZE = __('0 bytes');
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue
index 7aaef2ed57a..9c69059c968 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue
@@ -1,5 +1,6 @@
<script>
import { GlEmptyState, GlSprintf, GlLink, GlSkeletonLoader } from '@gitlab/ui';
+import { escape } from 'lodash';
import HarborListHeader from '~/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue';
@@ -80,7 +81,7 @@ export default {
this.sorting = sort;
const search = filters.find((i) => i.type === FILTERED_SEARCH_TERM);
- this.name = search?.value?.data;
+ this.name = escape(search?.value?.data);
this.fetchHarborImages();
},
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_files.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_files.vue
index ab4cfccd023..28bfb82093c 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_files.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_files.vue
@@ -100,7 +100,7 @@ export default {
<template #cell(name)="{ item, toggleDetails, detailsShowing }">
<gl-button
v-if="hasDetails(item)"
- :icon="detailsShowing ? 'angle-up' : 'angle-down'"
+ :icon="detailsShowing ? 'chevron-lg-up' : 'chevron-lg-down'"
:aria-label="detailsShowing ? __('Collapse') : __('Expand')"
category="tertiary"
size="small"
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue
index d3c38da1531..2046b717362 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue
@@ -31,7 +31,7 @@ export default {
<url-sync>
<template #default="{ updateQuery }">
<registry-search
- :filter="filter"
+ :filters="filter"
:sorting="sorting"
:tokens="[] /* eslint-disable-line @gitlab/vue-no-new-non-primitive-in-template */"
:sortable-fields="sortableFields"
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue
index a5f367bc1f6..a465fea0b74 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue
@@ -1,7 +1,7 @@
<script>
import { GlPagination, GlModal, GlSprintf } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
import PackagesListRow from '~/packages_and_registries/infrastructure_registry/shared/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
@@ -42,12 +42,22 @@ export default {
isListEmpty() {
return !this.list || this.list.length === 0;
},
- modalAction() {
- return s__('PackageRegistry|Delete package');
- },
deletePackageName() {
return this.itemToBeDeleted?.name ?? '';
},
+ deleteModalActionPrimaryProps() {
+ return {
+ text: this.$options.i18n.modalAction,
+ attributes: {
+ variant: 'danger',
+ },
+ };
+ },
+ deleteModalActionCancelProps() {
+ return {
+ text: __('Cancel'),
+ };
+ },
tracking() {
return {
category: TRACK_CATEGORY,
@@ -74,6 +84,7 @@ export default {
deleteModalContent: s__(
'PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?',
),
+ modalAction: s__('PackageRegistry|Delete package'),
},
};
</script>
@@ -110,12 +121,12 @@ export default {
ref="packageListDeleteModal"
size="sm"
modal-id="confirm-delete-pacakge"
- ok-variant="danger"
+ :action-primary="deleteModalActionPrimaryProps"
+ :action-cancel="deleteModalActionCancelProps"
@ok="deleteItemConfirmation"
@cancel="deleteItemCanceled"
>
- <template #modal-title>{{ modalAction }}</template>
- <template #modal-ok>{{ modalAction }}</template>
+ <template #modal-title>{{ $options.i18n.modalAction }}</template>
<gl-sprintf :message="$options.i18n.deleteModalContent">
<template #name>
<strong>{{ deletePackageName }}</strong>
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
index 74c0cb44c51..a3bbd569f41 100644
--- 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
@@ -1,30 +1,68 @@
<script>
+import { GlAlert } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { s__ } from '~/locale';
import Composer from '~/packages_and_registries/package_registry/components/details/metadata/composer.vue';
import Conan from '~/packages_and_registries/package_registry/components/details/metadata/conan.vue';
import Maven from '~/packages_and_registries/package_registry/components/details/metadata/maven.vue';
import Nuget from '~/packages_and_registries/package_registry/components/details/metadata/nuget.vue';
import Pypi from '~/packages_and_registries/package_registry/components/details/metadata/pypi.vue';
import {
+ FETCH_PACKAGE_METADATA_ERROR_MESSAGE,
PACKAGE_TYPE_COMPOSER,
PACKAGE_TYPE_CONAN,
PACKAGE_TYPE_MAVEN,
PACKAGE_TYPE_NUGET,
PACKAGE_TYPE_PYPI,
} from '~/packages_and_registries/package_registry/constants';
+import getPackageMetadataQuery from '../../graphql/queries/get_package_metadata.query.graphql';
+import AdditionalMetadataLoader from './additional_metadata_loader.vue';
export default {
components: {
Composer,
Conan,
+ GlAlert,
Maven,
Nuget,
Pypi,
+ AdditionalMetadataLoader,
},
props: {
- packageEntity: {
- type: Object,
+ packageId: {
+ type: String,
required: true,
},
+ packageType: {
+ type: String,
+ required: true,
+ },
+ },
+ apollo: {
+ packageMetadata: {
+ query: getPackageMetadataQuery,
+ context: {
+ isSingleRequest: true,
+ },
+ variables() {
+ return {
+ id: this.packageId,
+ };
+ },
+ update(data) {
+ return data.package?.metadata || null;
+ },
+ error(error) {
+ this.fetchPackageMetadataError = true;
+ Sentry.captureException(error);
+ },
+ },
+ },
+ data() {
+ return {
+ packageMetadata: null,
+ fetchPackageMetadataError: false,
+ };
},
computed: {
metadataComponent() {
@@ -34,22 +72,43 @@ export default {
[PACKAGE_TYPE_MAVEN]: Maven,
[PACKAGE_TYPE_NUGET]: Nuget,
[PACKAGE_TYPE_PYPI]: Pypi,
- }[this.packageEntity.packageType];
+ }[this.packageType];
},
showMetadata() {
- return this.metadataComponent && this.packageEntity.metadata;
+ return this.metadataComponent && this.packageMetadata;
+ },
+ isLoading() {
+ return this.$apollo.queries.packageMetadata.loading;
},
},
+ i18n: {
+ componentTitle: s__('PackageRegistry|Additional metadata'),
+ fetchPackageMetadataErrorMessage: FETCH_PACKAGE_METADATA_ERROR_MESSAGE,
+ },
};
</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">
+ <div>
+ <h3 v-if="isLoading || showMetadata" class="gl-font-lg" data-testid="title">
+ {{ $options.i18n.componentTitle }}
+ </h3>
+ <gl-alert
+ v-if="fetchPackageMetadataError"
+ variant="danger"
+ @dismiss="fetchPackageMetadataError = false"
+ >
+ {{ $options.i18n.fetchPackageMetadataErrorMessage }}
+ </gl-alert>
+ <additional-metadata-loader v-if="isLoading" />
+ <div
+ v-if="showMetadata"
+ class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base"
+ data-testid="main"
+ >
<component
:is="metadataComponent"
- :package-entity="packageEntity"
+ :package-metadata="packageMetadata"
data-testid="component-is"
/>
</div>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata_loader.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata_loader.vue
new file mode 100644
index 00000000000..628cf441831
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata_loader.vue
@@ -0,0 +1,30 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+ loader: {
+ width: 302,
+ height: 16,
+ repeat: 2,
+ },
+};
+</script>
+
+<template>
+ <div class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base">
+ <div
+ v-for="index in $options.loader.repeat"
+ :key="index"
+ class="gl-display-flex gl-align-items-center gl-p-4 gl-border-gray-100 gl-border-b-1"
+ >
+ <div class="gl-md-max-w-30p">
+ <gl-skeleton-loader :width="$options.loader.width" :height="$options.loader.height">
+ <rect :width="$options.loader.width" :height="$options.loader.height" rx="4" />
+ </gl-skeleton-loader>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/composer.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/composer.vue
index b6a36a0b00f..e3edaa3e45e 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/composer.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/composer.vue
@@ -18,7 +18,7 @@ export default {
ClipboardButton,
},
props: {
- packageEntity: {
+ packageMetadata: {
type: Object,
required: true,
},
@@ -31,10 +31,10 @@ export default {
<details-row icon="information-o" padding="gl-p-4" dashed data-testid="composer-target-sha">
<gl-sprintf :message="$options.i18n.targetSha">
<template #sha>
- <strong>{{ packageEntity.metadata.targetSha }}</strong>
+ <strong>{{ packageMetadata.targetSha }}</strong>
<clipboard-button
:title="$options.i18n.targetShaCopyButton"
- :text="packageEntity.metadata.targetSha"
+ :text="packageMetadata.targetSha"
category="tertiary"
css-class="gl-p-0!"
/>
@@ -44,10 +44,10 @@ export default {
<details-row icon="information-o" padding="gl-p-4" data-testid="composer-json">
<gl-sprintf :message="$options.i18n.composerJson">
<template #license>
- <strong>{{ packageEntity.metadata.composerJson.license }}</strong>
+ <strong>{{ packageMetadata.composerJson.license }}</strong>
</template>
<template #version>
- <strong>{{ packageEntity.metadata.composerJson.version }}</strong>
+ <strong>{{ packageMetadata.composerJson.version }}</strong>
</template>
</gl-sprintf>
</details-row>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/conan.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/conan.vue
index 10797d74acf..de7c1bc4cd3 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/conan.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/conan.vue
@@ -13,7 +13,7 @@ export default {
GlSprintf,
},
props: {
- packageEntity: {
+ packageMetadata: {
type: Object,
required: true,
},
@@ -25,7 +25,7 @@ export default {
<div>
<details-row icon="information-o" padding="gl-p-4" data-testid="conan-recipe">
<gl-sprintf :message="$options.i18n.recipeText">
- <template #recipe>{{ packageEntity.metadata.recipe }}</template>
+ <template #recipe>{{ packageMetadata.recipe }}</template>
</gl-sprintf>
</details-row>
</div>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/maven.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/maven.vue
index fd9fb49a9f2..7c3eb476a99 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/maven.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/maven.vue
@@ -14,7 +14,7 @@ export default {
GlSprintf,
},
props: {
- packageEntity: {
+ packageMetadata: {
type: Object,
required: true,
},
@@ -27,14 +27,14 @@ export default {
<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>
+ <strong>{{ packageMetadata.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>
+ <strong>{{ packageMetadata.appGroup }}</strong>
</template>
</gl-sprintf>
</details-row>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue
index 1360b03856f..1ddd419a639 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue
@@ -14,7 +14,7 @@ export default {
GlSprintf,
},
props: {
- packageEntity: {
+ packageMetadata: {
type: Object,
required: true,
},
@@ -25,7 +25,7 @@ export default {
<template>
<div>
<details-row
- v-if="packageEntity.metadata.projectUrl"
+ v-if="packageMetadata.projectUrl"
icon="project"
padding="gl-p-4"
dashed
@@ -33,22 +33,22 @@ export default {
>
<gl-sprintf :message="$options.i18n.sourceText">
<template #link>
- <gl-link :href="packageEntity.metadata.projectUrl" target="_blank">{{
- packageEntity.metadata.projectUrl
+ <gl-link :href="packageMetadata.projectUrl" target="_blank">{{
+ packageMetadata.projectUrl
}}</gl-link>
</template>
</gl-sprintf>
</details-row>
<details-row
- v-if="packageEntity.metadata.licenseUrl"
+ v-if="packageMetadata.licenseUrl"
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 :href="packageMetadata.licenseUrl" target="_blank">{{
+ packageMetadata.licenseUrl
}}</gl-link>
</template>
</gl-sprintf>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue
index 6534eef532c..ef35349c228 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue
@@ -13,7 +13,7 @@ export default {
GlSprintf,
},
props: {
- packageEntity: {
+ packageMetadata: {
type: Object,
required: true,
},
@@ -26,7 +26,7 @@ export default {
<details-row icon="information-o" padding="gl-p-4" data-testid="pypi-required-python">
<gl-sprintf :message="$options.i18n.requiredPython">
<template #pythonVersion>
- <strong>{{ packageEntity.metadata.requiredPython }}</strong>
+ <strong>{{ packageMetadata.requiredPython }}</strong>
</template>
</gl-sprintf>
</details-row>
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
index 3724e371e01..9e700a5236f 100644
--- 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
@@ -1,5 +1,5 @@
<script>
-import { GlLink, GlTable, GlDropdownItem, GlDropdown, GlIcon, GlButton } from '@gitlab/ui';
+import { GlLink, GlTableLite, GlDropdownItem, GlDropdown, GlIcon, GlButton } from '@gitlab/ui';
import { last } from 'lodash';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
@@ -12,7 +12,7 @@ export default {
name: 'PackageFiles',
components: {
GlLink,
- GlTable,
+ GlTableLite,
GlIcon,
GlDropdown,
GlDropdownItem,
@@ -94,7 +94,7 @@ export default {
<template>
<div>
<h3 class="gl-font-lg gl-mt-5">{{ __('Files') }}</h3>
- <gl-table
+ <gl-table-lite
:fields="filesTableHeaderFields"
:items="filesTableRows"
:tbody-tr-attr="{ 'data-testid': 'file-row' }"
@@ -102,7 +102,7 @@ export default {
<template #cell(name)="{ item, toggleDetails, detailsShowing }">
<gl-button
v-if="hasDetails(item)"
- :icon="detailsShowing ? 'angle-up' : 'angle-down'"
+ :icon="detailsShowing ? 'chevron-up' : 'chevron-down'"
:aria-label="detailsShowing ? __('Collapse') : __('Expand')"
category="tertiary"
size="small"
@@ -162,6 +162,6 @@ export default {
<file-sha v-if="item.fileSha1" data-testid="sha-1" title="SHA-1" :sha="item.fileSha1" />
</div>
</template>
- </gl-table>
+ </gl-table-lite>
</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
index af6bd7079ba..96b82a20364 100644
--- 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
@@ -1,5 +1,6 @@
<script>
-import { GlLink, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import { first } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { truncateSha } from '~/lib/utils/text_utility';
@@ -7,6 +8,12 @@ import { s__, n__ } from '~/locale';
import { HISTORY_PIPELINES_LIMIT } from '~/packages_and_registries/shared/constants';
import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import {
+ GRAPHQL_PACKAGE_PIPELINES_PAGE_SIZE,
+ FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE,
+} from '../../constants';
+import getPackagePipelinesQuery from '../../graphql/queries/get_package_pipelines.query.graphql';
+import PackageHistoryLoader from './package_history_loader.vue';
export default {
name: 'PackageHistory',
@@ -20,11 +27,14 @@ export default {
combinedUpdateText: s__(
'PackageRegistry|Package updated by commit %{link} on branch %{branch}, built by pipeline %{pipeline}, and published to the registry %{datetime}',
),
+ fetchPackagePipelinesErrorMessage: FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE,
},
components: {
+ GlAlert,
GlLink,
GlSprintf,
HistoryItem,
+ PackageHistoryLoader,
TimeAgoTooltip,
},
props: {
@@ -37,15 +47,28 @@ export default {
required: true,
},
},
+ apollo: {
+ pipelines: {
+ query: getPackagePipelinesQuery,
+ variables() {
+ return this.queryVariables;
+ },
+ update(data) {
+ return data.package?.pipelines?.nodes || [];
+ },
+ error(error) {
+ this.fetchPackagePipelinesError = true;
+ Sentry.captureException(error);
+ },
+ },
+ },
data() {
return {
- showDescription: false,
+ pipelines: [],
+ fetchPackagePipelinesError: false,
};
},
computed: {
- pipelines() {
- return this.packageEntity?.pipelines?.nodes || [];
- },
firstPipeline() {
return first(this.pipelines);
},
@@ -65,6 +88,15 @@ export default {
this.archivedLines,
);
},
+ isLoading() {
+ return this.$apollo.queries.pipelines.loading;
+ },
+ queryVariables() {
+ return {
+ id: this.packageEntity.id,
+ first: GRAPHQL_PACKAGE_PIPELINES_PAGE_SIZE,
+ };
+ },
},
methods: {
truncate(value) {
@@ -80,7 +112,15 @@ export default {
<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">
+ <gl-alert
+ v-if="fetchPackagePipelinesError"
+ variant="danger"
+ @dismiss="fetchPackagePipelinesError = false"
+ >
+ {{ $options.i18n.fetchPackagePipelinesErrorMessage }}
+ </gl-alert>
+ <package-history-loader v-if="isLoading" />
+ <ul v-else 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>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history_loader.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history_loader.vue
new file mode 100644
index 00000000000..950971c2f11
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history_loader.vue
@@ -0,0 +1,24 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+ loader: {
+ width: 580,
+ height: 80,
+ },
+};
+</script>
+
+<template>
+ <div class="gl-ml-5 gl-md-max-w-70p">
+ <gl-skeleton-loader :width="$options.loader.width" :height="$options.loader.height">
+ <rect x="49" y="9" width="531" height="16" rx="4" />
+ <circle cx="16" cy="16" r="16" />
+ <rect x="49" y="57" width="302" height="16" rx="4" />
+ <circle cx="16" cy="64" r="16" />
+ </gl-skeleton-loader>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
index 7a88e04d1f9..d28847c7900 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
@@ -105,7 +105,7 @@ export default {
<template #default="{ updateQuery }">
<registry-search
v-if="mountRegistrySearch"
- :filter="filters"
+ :filters="filters"
:sorting="sorting"
:tokens="$options.tokens"
:sortable-fields="sortableFields"
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
index 1aff23bc112..a6ac2eb1b2b 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert, GlModal, GlSprintf, GlKeysetPagination } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import {
@@ -73,6 +73,19 @@ export default {
}
},
},
+ deleteModalActionPrimaryProps() {
+ return {
+ text: this.$options.i18n.modalAction,
+ attributes: {
+ variant: 'danger',
+ },
+ };
+ },
+ deleteModalActionCancelProps() {
+ return {
+ text: __('Cancel'),
+ };
+ },
errorTitleAlert() {
return sprintf(
s__('PackageRegistry|There was an error publishing a %{packageName} package'),
@@ -161,12 +174,12 @@ export default {
v-model="showDeleteModal"
modal-id="confirm-delete-pacakge"
size="sm"
- ok-variant="danger"
+ :action-primary="deleteModalActionPrimaryProps"
+ :action-cancel="deleteModalActionCancelProps"
@ok="deleteItemConfirmation"
@cancel="deleteItemCanceled"
>
<template #modal-title>{{ $options.i18n.modalAction }}</template>
- <template #modal-ok>{{ $options.i18n.modalAction }}</template>
<gl-sprintf :message="$options.i18n.deleteModalContent">
<template #name>
<strong>{{ deletePackageName }}</strong>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
index c4d331fa384..3c090951b7d 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
@@ -72,6 +72,12 @@ export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__(
export const FETCH_PACKAGE_DETAILS_ERROR_MESSAGE = s__(
'PackageRegistry|Failed to load the package data',
);
+export const FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE = s__(
+ 'PackageRegistry|Something went wrong while fetching the package history.',
+);
+export const FETCH_PACKAGE_METADATA_ERROR_MESSAGE = s__(
+ 'PackageRegistry|Something went wrong while fetching the package metadata.',
+);
export const DELETE_PACKAGE_SUCCESS_MESSAGE = s__('PackageRegistry|Package deleted successfully');
export const PACKAGE_REGISTRY_TITLE = __('Package Registry');
@@ -149,3 +155,5 @@ export const CONAN_HELP_PATH = helpPagePath('user/packages/conan_repository/inde
export const NUGET_HELP_PATH = helpPagePath('user/packages/nuget_repository/index');
export const PYPI_HELP_PATH = helpPagePath('user/packages/pypi_repository/index');
export const COMPOSER_HELP_PATH = helpPagePath('user/packages/composer_repository/index');
+
+export const GRAPHQL_PACKAGE_PIPELINES_PAGE_SIZE = 10;
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
index 41b0c285fff..5574020c9e4 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
@@ -27,18 +27,10 @@ query getPackageDetails($id: PackagesPackageID!) {
name
}
}
- pipelines(first: 10) {
+ pipelines(first: 1) {
nodes {
ref
id
- sha
- createdAt
- commitPath
- path
- user {
- id
- name
- }
project {
id
name
@@ -91,37 +83,15 @@ query getPackageDetails($id: PackagesPackageID!) {
}
}
metadata {
- ... on ComposerMetadata {
- targetSha
- composerJson {
- license
- version
- }
- }
- ... on PypiMetadata {
- id
- requiredPython
- }
- ... on ConanMetadata {
- id
- packageChannel
- packageUsername
- recipe
- recipePath
- }
... on MavenMetadata {
id
appName
appGroup
appVersion
- path
}
-
... on NugetMetadata {
id
iconUrl
- licenseUrl
- projectUrl
}
}
}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql
new file mode 100644
index 00000000000..fc8b39b37ab
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql
@@ -0,0 +1,39 @@
+query getPackageMetadata($id: PackagesPackageID!) {
+ package(id: $id) {
+ id
+ packageType
+ metadata {
+ ... on ComposerMetadata {
+ targetSha
+ composerJson {
+ license
+ version
+ }
+ }
+ ... on PypiMetadata {
+ id
+ requiredPython
+ }
+ ... on ConanMetadata {
+ id
+ packageChannel
+ packageUsername
+ recipe
+ recipePath
+ }
+ ... on MavenMetadata {
+ id
+ appName
+ appGroup
+ appVersion
+ path
+ }
+ ... on NugetMetadata {
+ id
+ iconUrl
+ licenseUrl
+ projectUrl
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_pipelines.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_pipelines.query.graphql
new file mode 100644
index 00000000000..86e67320d63
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_pipelines.query.graphql
@@ -0,0 +1,24 @@
+query getPackagePipelines($id: PackagesPackageID!, $first: Int) {
+ package(id: $id) {
+ id
+ pipelines(first: $first) {
+ nodes {
+ ref
+ id
+ sha
+ createdAt
+ commitPath
+ path
+ user {
+ id
+ name
+ }
+ project {
+ id
+ name
+ webUrl
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
index 162b420a784..768c8d6478b 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
@@ -27,6 +27,9 @@ import DeletePackage from '~/packages_and_registries/package_registry/components
import {
PACKAGE_TYPE_NUGET,
PACKAGE_TYPE_COMPOSER,
+ PACKAGE_TYPE_CONAN,
+ PACKAGE_TYPE_MAVEN,
+ PACKAGE_TYPE_PYPI,
DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
@@ -122,6 +125,9 @@ export default {
packageFiles() {
return this.packageEntity.packageFiles?.nodes;
},
+ packageType() {
+ return this.packageEntity.packageType;
+ },
isLoading() {
return this.$apollo.queries.packageEntity.loading;
},
@@ -130,7 +136,7 @@ export default {
},
tracking() {
return {
- category: packageTypeToTrackCategory(this.packageEntity.packageType),
+ category: packageTypeToTrackCategory(this.packageType),
};
},
hasVersions() {
@@ -140,10 +146,19 @@ export default {
return this.packageEntity.dependencyLinks?.nodes || [];
},
showDependencies() {
- return this.packageEntity.packageType === PACKAGE_TYPE_NUGET;
+ return this.packageType === PACKAGE_TYPE_NUGET;
},
showFiles() {
- return this.packageEntity.packageType !== PACKAGE_TYPE_COMPOSER;
+ return this.packageType !== PACKAGE_TYPE_COMPOSER;
+ },
+ showMetadata() {
+ return [
+ PACKAGE_TYPE_COMPOSER,
+ PACKAGE_TYPE_CONAN,
+ PACKAGE_TYPE_MAVEN,
+ PACKAGE_TYPE_NUGET,
+ PACKAGE_TYPE_PYPI,
+ ].includes(this.packageType);
},
},
methods: {
@@ -262,7 +277,11 @@ export default {
<installation-commands :package-entity="packageEntity" />
- <additional-metadata :package-entity="packageEntity" />
+ <additional-metadata
+ v-if="showMetadata"
+ :package-id="packageEntity.id"
+ :package-type="packageType"
+ />
</div>
<package-files
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
index a5189201112..130d6977936 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
@@ -77,9 +77,6 @@ export default {
this.updateDependencyProxyImageTtlGroupPolicy(payload);
},
},
- helpText() {
- return this.enabled ? this.$options.i18n.enabledProxyHelpText : '';
- },
},
methods: {
mutationVariables(payload) {
@@ -144,11 +141,10 @@ export default {
v-model="enabled"
:disabled="isLoading"
:label="$options.i18n.enabledProxyLabel"
- :help="helpText"
data-qa-selector="dependency_proxy_setting_toggle"
data-testid="dependency-proxy-setting-toggle"
>
- <template #help>
+ <template v-if="enabled" #help>
<span class="gl-overflow-break-word gl-max-w-100vw gl-display-inline-block">
<gl-sprintf :message="$options.i18n.enabledProxyHelpText">
<template #link="{ content }">
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
new file mode 100644
index 00000000000..fdc7bd39780
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
@@ -0,0 +1,124 @@
+<script>
+import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import { isEqual, get, isEmpty } from 'lodash';
+import {
+ CONTAINER_CLEANUP_POLICY_TITLE,
+ CONTAINER_CLEANUP_POLICY_DESCRIPTION,
+ FETCH_SETTINGS_ERROR_MESSAGE,
+ UNAVAILABLE_FEATURE_TITLE,
+ UNAVAILABLE_FEATURE_INTRO_TEXT,
+ UNAVAILABLE_USER_FEATURE_TEXT,
+ UNAVAILABLE_ADMIN_FEATURE_TEXT,
+} from '~/packages_and_registries/settings/project/constants';
+import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+
+import ContainerExpirationPolicyForm from './container_expiration_policy_form.vue';
+
+export default {
+ components: {
+ SettingsBlock,
+ GlAlert,
+ GlSprintf,
+ GlLink,
+ ContainerExpirationPolicyForm,
+ },
+ inject: ['projectPath', 'isAdmin', 'adminSettingsPath', 'enableHistoricEntries', 'helpPagePath'],
+ i18n: {
+ CONTAINER_CLEANUP_POLICY_TITLE,
+ CONTAINER_CLEANUP_POLICY_DESCRIPTION,
+ UNAVAILABLE_FEATURE_TITLE,
+ UNAVAILABLE_FEATURE_INTRO_TEXT,
+ FETCH_SETTINGS_ERROR_MESSAGE,
+ },
+ apollo: {
+ containerExpirationPolicy: {
+ query: expirationPolicyQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ };
+ },
+ update: (data) => data.project?.containerExpirationPolicy,
+ result({ data }) {
+ this.workingCopy = { ...get(data, 'project.containerExpirationPolicy', {}) };
+ },
+ error(e) {
+ this.fetchSettingsError = e;
+ },
+ },
+ },
+ data() {
+ return {
+ fetchSettingsError: false,
+ containerExpirationPolicy: null,
+ workingCopy: {},
+ };
+ },
+ computed: {
+ isDisabled() {
+ return !(this.containerExpirationPolicy || this.enableHistoricEntries);
+ },
+ showDisabledFormMessage() {
+ return this.isDisabled && !this.fetchSettingsError;
+ },
+ unavailableFeatureMessage() {
+ return this.isAdmin ? UNAVAILABLE_ADMIN_FEATURE_TEXT : UNAVAILABLE_USER_FEATURE_TEXT;
+ },
+ isEdited() {
+ if (isEmpty(this.containerExpirationPolicy) && isEmpty(this.workingCopy)) {
+ return false;
+ }
+ return !isEqual(this.containerExpirationPolicy, this.workingCopy);
+ },
+ },
+ methods: {
+ restoreOriginal() {
+ this.workingCopy = { ...this.containerExpirationPolicy };
+ },
+ },
+};
+</script>
+
+<template>
+ <settings-block :collapsible="false">
+ <template #title> {{ $options.i18n.CONTAINER_CLEANUP_POLICY_TITLE }}</template>
+ <template #description>
+ <span>
+ <gl-sprintf :message="$options.i18n.CONTAINER_CLEANUP_POLICY_DESCRIPTION">
+ <template #link="{ content }">
+ <gl-link :href="helpPagePath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </template>
+ <template #default>
+ <container-expiration-policy-form
+ v-if="!isDisabled"
+ v-model="workingCopy"
+ :is-loading="$apollo.queries.containerExpirationPolicy.loading"
+ :is-edited="isEdited"
+ @reset="restoreOriginal"
+ />
+ <template v-else>
+ <gl-alert
+ v-if="showDisabledFormMessage"
+ :dismissible="false"
+ :title="$options.i18n.UNAVAILABLE_FEATURE_TITLE"
+ variant="tip"
+ >
+ {{ $options.i18n.UNAVAILABLE_FEATURE_INTRO_TEXT }}
+
+ <gl-sprintf :message="unavailableFeatureMessage">
+ <template #link="{ content }">
+ <gl-link :href="adminSettingsPath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+ <gl-alert v-else-if="fetchSettingsError" variant="warning" :dismissible="false">
+ <gl-sprintf :message="$options.i18n.FETCH_SETTINGS_ERROR_MESSAGE" />
+ </gl-alert>
+ </template>
+ </template>
+ </settings-block>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue
index ae2d5f4fbc5..ae2d5f4fbc5 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue
index d6d85189792..3fbbfd75ffb 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue
@@ -104,7 +104,7 @@ export default {
<span data-testid="description" class="gl-text-gray-400">
<gl-sprintf :message="description">
<template #link="{ content }">
- <gl-link :href="tagsRegexHelpPagePath" target="_blank">{{ content }}</gl-link>
+ <gl-link :href="tagsRegexHelpPagePath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
index 854c88b2ad3..95af19e6d85 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
@@ -1,128 +1,15 @@
<script>
-import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
-import { isEqual, get, isEmpty } from 'lodash';
-import {
- FETCH_SETTINGS_ERROR_MESSAGE,
- UNAVAILABLE_FEATURE_TITLE,
- UNAVAILABLE_FEATURE_INTRO_TEXT,
- UNAVAILABLE_USER_FEATURE_TEXT,
- UNAVAILABLE_ADMIN_FEATURE_TEXT,
-} from '~/packages_and_registries/settings/project/constants';
-import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
-import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
-
-import SettingsForm from './settings_form.vue';
+import ContainerExpirationPolicy from './container_expiration_policy.vue';
export default {
components: {
- SettingsBlock,
- SettingsForm,
- GlAlert,
- GlSprintf,
- GlLink,
- },
- inject: ['projectPath', 'isAdmin', 'adminSettingsPath', 'enableHistoricEntries', 'helpPagePath'],
- i18n: {
- UNAVAILABLE_FEATURE_TITLE,
- UNAVAILABLE_FEATURE_INTRO_TEXT,
- FETCH_SETTINGS_ERROR_MESSAGE,
- },
- apollo: {
- containerExpirationPolicy: {
- query: expirationPolicyQuery,
- variables() {
- return {
- projectPath: this.projectPath,
- };
- },
- update: (data) => data.project?.containerExpirationPolicy,
- result({ data }) {
- this.workingCopy = { ...get(data, 'project.containerExpirationPolicy', {}) };
- },
- error(e) {
- this.fetchSettingsError = e;
- },
- },
- },
- data() {
- return {
- fetchSettingsError: false,
- containerExpirationPolicy: null,
- workingCopy: {},
- };
- },
- computed: {
- isDisabled() {
- return !(this.containerExpirationPolicy || this.enableHistoricEntries);
- },
- showDisabledFormMessage() {
- return this.isDisabled && !this.fetchSettingsError;
- },
- unavailableFeatureMessage() {
- return this.isAdmin ? UNAVAILABLE_ADMIN_FEATURE_TEXT : UNAVAILABLE_USER_FEATURE_TEXT;
- },
- isEdited() {
- if (isEmpty(this.containerExpirationPolicy) && isEmpty(this.workingCopy)) {
- return false;
- }
- return !isEqual(this.containerExpirationPolicy, this.workingCopy);
- },
- },
- methods: {
- restoreOriginal() {
- this.workingCopy = { ...this.containerExpirationPolicy };
- },
+ ContainerExpirationPolicy,
},
};
</script>
<template>
<section data-testid="registry-settings-app">
- <settings-block :collapsible="false">
- <template #title> {{ __('Clean up image tags') }}</template>
- <template #description>
- <span data-testid="description">
- <gl-sprintf
- :message="
- __(
- 'Save storage space by automatically deleting tags from the container registry and keeping the ones you want. %{linkStart}How does cleanup work?%{linkEnd}',
- )
- "
- >
- <template #link="{ content }">
- <gl-link :href="helpPagePath" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </span>
- </template>
- <template #default>
- <settings-form
- v-if="!isDisabled"
- v-model="workingCopy"
- :is-loading="$apollo.queries.containerExpirationPolicy.loading"
- :is-edited="isEdited"
- @reset="restoreOriginal"
- />
- <template v-else>
- <gl-alert
- v-if="showDisabledFormMessage"
- :dismissible="false"
- :title="$options.i18n.UNAVAILABLE_FEATURE_TITLE"
- variant="tip"
- >
- {{ $options.i18n.UNAVAILABLE_FEATURE_INTRO_TEXT }}
-
- <gl-sprintf :message="unavailableFeatureMessage">
- <template #link="{ content }">
- <gl-link :href="adminSettingsPath" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
- <gl-alert v-else-if="fetchSettingsError" variant="warning" :dismissible="false">
- <gl-sprintf :message="$options.i18n.FETCH_SETTINGS_ERROR_MESSAGE" />
- </gl-alert>
- </template>
- </template>
- </settings-block>
+ <container-expiration-policy />
</section>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/constants.js b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
index 841585c5646..40f980d15fb 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
@@ -1,5 +1,9 @@
import { s__, __ } from '~/locale';
+export const CONTAINER_CLEANUP_POLICY_TITLE = s__(`ContainerRegistry|Clean up image tags`);
+export const CONTAINER_CLEANUP_POLICY_DESCRIPTION = s__(
+ `ContainerRegistry|Save storage space by automatically deleting tags from the container registry and keeping the ones you want. %{linkStart}How does cleanup work?%{linkEnd}`,
+);
export const SET_CLEANUP_POLICY_BUTTON = __('Save');
export const UNAVAILABLE_FEATURE_TITLE = s__(
`ContainerRegistry|Cleanup policy for tags is disabled`,
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/cli_commands.vue b/app/assets/javascripts/packages_and_registries/shared/components/cli_commands.vue
index de7ab3e6d7b..dc61f3c788c 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/cli_commands.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/cli_commands.vue
@@ -49,7 +49,7 @@ export default {
<template>
<gl-dropdown
:text="$options.i18n.QUICK_START"
- variant="info"
+ variant="confirm"
right
@shown="track('click_dropdown')"
>
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue b/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue
index 9b2de1a1b84..b2b1d2c8212 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue
@@ -66,7 +66,7 @@ export default {
<template #default="{ updateQuery }">
<registry-search
v-if="mountRegistrySearch"
- :filter="filters"
+ :filters="filters"
:sorting="sorting"
:tokens="$options.tokens"
:sortable-fields="sortableFields"
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue b/app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue
index a1e3c06812c..e7b4229052e 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue
@@ -49,7 +49,7 @@ export default {
<gl-breadcrumb :key="isLoaded" :items="allCrumbs">
<template #separator>
<span class="gl-mx-n5">
- <gl-icon name="angle-right" :size="8" />
+ <gl-icon name="chevron-lg-right" :size="8" />
</span>
</template>
</gl-breadcrumb>
diff --git a/app/assets/javascripts/pages/admin/application_settings/payload_downloader.js b/app/assets/javascripts/pages/admin/application_settings/payload_downloader.js
index 7c81cf80dc6..8cecc1d3ef7 100644
--- a/app/assets/javascripts/pages/admin/application_settings/payload_downloader.js
+++ b/app/assets/javascripts/pages/admin/application_settings/payload_downloader.js
@@ -19,7 +19,7 @@ export default class PayloadDownloader {
}
requestPayload() {
- this.spinner.classList.add('d-inline-flex');
+ this.spinner.classList.add('gl-display-inline');
return axios
.get(this.trigger.dataset.endpoint, {
@@ -34,7 +34,7 @@ export default class PayloadDownloader {
});
})
.finally(() => {
- this.spinner.classList.remove('d-inline-flex');
+ this.spinner.classList.remove('gl-display-inline');
});
}
diff --git a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
index ae08806fe4c..84027203783 100644
--- a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
+++ b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
@@ -29,7 +29,7 @@ export default class PayloadPreviewer {
requestPayload() {
if (this.isInserted) return this.showPayload();
- this.spinner.classList.add('gl-display-inline-flex');
+ this.spinner.classList.add('gl-display-inline');
const container = this.getContainer();
@@ -38,11 +38,11 @@ export default class PayloadPreviewer {
responseType: 'text',
})
.then(({ data }) => {
- this.spinner.classList.remove('gl-display-inline-flex');
+ this.spinner.classList.remove('gl-display-inline');
this.insertPayload(data);
})
.catch(() => {
- this.spinner.classList.remove('gl-display-inline-flex');
+ this.spinner.classList.remove('gl-display-inline');
createFlash({
message: __('Error fetching payload data.'),
});
diff --git a/app/assets/javascripts/pages/admin/application_settings/repository/index.js b/app/assets/javascripts/pages/admin/application_settings/repository/index.js
new file mode 100644
index 00000000000..9a67fe7b6f8
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/repository/index.js
@@ -0,0 +1,3 @@
+import initInactiveProjectDeletion from '~/admin/application_settings/inactive_project_deletion';
+
+initInactiveProjectDeletion();
diff --git a/app/assets/javascripts/pages/admin/groups/edit/index.js b/app/assets/javascripts/pages/admin/groups/edit/index.js
index 01e03ed437d..a4df1cf8274 100644
--- a/app/assets/javascripts/pages/admin/groups/edit/index.js
+++ b/app/assets/javascripts/pages/admin/groups/edit/index.js
@@ -1,3 +1,5 @@
import initFilePickers from '~/file_pickers';
+import { initGroupNameAndPath } from '~/groups/create_edit_form';
initFilePickers();
+initGroupNameAndPath();
diff --git a/app/assets/javascripts/pages/admin/groups/new/index.js b/app/assets/javascripts/pages/admin/groups/new/index.js
index 710d2d72f4c..a341ef9656d 100644
--- a/app/assets/javascripts/pages/admin/groups/new/index.js
+++ b/app/assets/javascripts/pages/admin/groups/new/index.js
@@ -1,6 +1,7 @@
import initFilePickers from '~/file_pickers';
import BindInOut from '~/behaviors/bind_in_out';
import Group from '~/group';
+import { initGroupNameAndPath } from '~/groups/create_edit_form';
(() => {
BindInOut.initAll();
@@ -8,3 +9,5 @@ import Group from '~/group';
return new Group();
})();
+
+initGroupNameAndPath();
diff --git a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js
index 8fbc8dc17bc..d86ac891977 100644
--- a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js
+++ b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js
@@ -1,8 +1,14 @@
-import { initExpiresAtField } from '~/access_tokens';
+import {
+ initAccessTokenTableApp,
+ initExpiresAtField,
+ initNewAccessTokenApp,
+} from '~/access_tokens';
import { initAdminUserActions, initDeleteUserModals } from '~/admin/users';
import initConfirmModal from '~/confirm_modal';
+initAccessTokenTableApp();
+initExpiresAtField();
+initNewAccessTokenApp();
initAdminUserActions();
initDeleteUserModals();
-initExpiresAtField();
initConfirmModal();
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index c4bbbdcd8ec..44299d235d5 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -31,6 +31,7 @@ export default class Todos {
$('.js-done-todo, .js-undo-todo, .js-add-todo').off('click', this.updateRowStateClickedWrapper);
$('.js-todos-mark-all', '.js-todos-undo-all').off('click', this.updateallStateClickedWrapper);
$('.todo').off('click', this.goToTodoUrl);
+ $('.todo').off('auxclick', this.goToTodoUrl);
}
bindEvents() {
@@ -40,6 +41,7 @@ export default class Todos {
$('.js-done-todo, .js-undo-todo, .js-add-todo').on('click', this.updateRowStateClickedWrapper);
$('.js-todos-mark-all, .js-todos-undo-all').on('click', this.updateAllStateClickedWrapper);
$('.todo').on('click', this.goToTodoUrl);
+ $('.todo').on('auxclick', this.goToTodoUrl);
}
initFilters() {
@@ -198,11 +200,13 @@ export default class Todos {
e.stopPropagation();
e.preventDefault();
+ const isPrimaryClick = e.button === 0;
+
if (isMetaClick(e)) {
const windowTarget = '_blank';
window.open(todoLink, windowTarget);
- } else {
+ } else if (isPrimaryClick) {
visitUrl(todoLink);
}
}
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index 79ac31f1659..c7c2f6f773e 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -32,25 +32,19 @@ initMembersApp(document.querySelector('.js-group-members-list-app'), {
},
},
[MEMBER_TYPES.group]: {
- tableFields: gon?.features?.groupMemberInheritedGroup
- ? SHARED_FIELDS.concat(['source', 'granted'])
- : SHARED_FIELDS.concat(['granted']),
+ tableFields: SHARED_FIELDS.concat(['source', 'granted']),
tableAttrs: {
table: { 'data-qa-selector': 'groups_list' },
tr: { 'data-qa-selector': 'group_row' },
},
requestFormatter: groupLinkRequestFormatter,
- ...(gon?.features?.groupMemberInheritedGroup
- ? {
- filteredSearchBar: {
- show: true,
- tokens: ['with_inherited_permissions'],
- searchParam: 'search_groups',
- placeholder: s__('Members|Filter groups'),
- recentSearchesStorageKey: 'group_links_members',
- },
- }
- : {}),
+ filteredSearchBar: {
+ show: true,
+ tokens: ['groups_with_inherited_permissions'],
+ searchParam: 'search_groups',
+ placeholder: s__('Members|Filter groups'),
+ recentSearchesStorageKey: 'group_links_members',
+ },
},
[MEMBER_TYPES.invite]: {
tableFields: SHARED_FIELDS.concat('invited'),
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index 725c38defc3..912a4ea2c11 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -1,26 +1,3 @@
-import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
-import { initBulkUpdateSidebar } from '~/issuable/bulk_update_sidebar';
import { mountIssuesListApp } from '~/issues/list';
-import initManualOrdering from '~/issues/manual_ordering';
-import { FILTERED_SEARCH } from '~/filtered_search/constants';
-import initFilteredSearch from '~/pages/search/init_filtered_search';
-import projectSelect from '~/project_select';
-if (gon.features?.vueIssuesList) {
- mountIssuesListApp();
-} else {
- const ISSUE_BULK_UPDATE_PREFIX = 'issue_';
-
- IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
- IssuableFilteredSearchTokenKeys.removeTokensForKeys('release');
- initBulkUpdateSidebar(ISSUE_BULK_UPDATE_PREFIX);
-
- initFilteredSearch({
- page: FILTERED_SEARCH.ISSUES,
- isGroupDecendent: true,
- useDefaultState: true,
- filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
- });
- projectSelect();
- initManualOrdering();
-}
+mountIssuesListApp();
diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js
index 702b152d25a..7c409010510 100644
--- a/app/assets/javascripts/pages/groups/new/index.js
+++ b/app/assets/javascripts/pages/groups/new/index.js
@@ -2,18 +2,19 @@ import Vue from 'vue';
import BindInOut from '~/behaviors/bind_in_out';
import initFilePickers from '~/file_pickers';
import Group from '~/group';
+import { initGroupNameAndPath } from '~/groups/create_edit_form';
import { parseBoolean } from '~/lib/utils/common_utils';
import NewGroupCreationApp from './components/app.vue';
import GroupPathValidator from './group_path_validator';
import initToggleInviteMembers from './toggle_invite_members';
new GroupPathValidator(); // eslint-disable-line no-new
+new Group(); // eslint-disable-line no-new
+initGroupNameAndPath();
BindInOut.initAll();
initFilePickers();
-new Group(); // eslint-disable-line no-new
-
function initNewGroupCreation(el) {
const { hasErrors, verificationRequired, verificationFormUrl, subscriptionsUrl } = el.dataset;
diff --git a/app/assets/javascripts/pages/groups/settings/access_tokens/index.js b/app/assets/javascripts/pages/groups/settings/access_tokens/index.js
index dc1bb88bf4b..b9f282a123c 100644
--- a/app/assets/javascripts/pages/groups/settings/access_tokens/index.js
+++ b/app/assets/javascripts/pages/groups/settings/access_tokens/index.js
@@ -1,3 +1,9 @@
-import { initExpiresAtField } from '~/access_tokens';
+import {
+ initAccessTokenTableApp,
+ initExpiresAtField,
+ initNewAccessTokenApp,
+} from '~/access_tokens';
+initAccessTokenTableApp();
initExpiresAtField();
+initNewAccessTokenApp();
diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
index 52add416f38..bf77d968e7d 100644
--- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
@@ -1,3 +1,4 @@
+import initStaleRunnerCleanupSetting from 'ee_else_ce/group_settings/stale_runner_cleanup';
import initVariableList from '~/ci_variable_list';
import initSharedRunnersForm from '~/group_settings/mount_shared_runners';
import initSettingsPanels from '~/settings_panels';
@@ -6,4 +7,5 @@ import initSettingsPanels from '~/settings_panels';
initSettingsPanels();
initSharedRunnersForm();
+initStaleRunnerCleanupSetting();
initVariableList();
diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
index 35a8d3d979a..6748a62e777 100644
--- a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
+++ b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
@@ -137,7 +137,7 @@ export default {
{{ s__('BulkImport|Group import history') }}
</h1>
</div>
- <gl-loading-icon v-if="loading" size="md" class="gl-mt-5" />
+ <gl-loading-icon v-if="loading" size="lg" class="gl-mt-5" />
<gl-empty-state
v-else-if="!hasHistoryItems"
:title="s__('BulkImport|No history is available')"
diff --git a/app/assets/javascripts/pages/import/history/components/import_error_details.vue b/app/assets/javascripts/pages/import/history/components/import_error_details.vue
index 33ba73317f8..6af137cd722 100644
--- a/app/assets/javascripts/pages/import/history/components/import_error_details.vue
+++ b/app/assets/javascripts/pages/import/history/components/import_error_details.vue
@@ -36,7 +36,7 @@ export default {
};
</script>
<template>
- <gl-loading-icon v-if="loading" size="md" />
+ <gl-loading-icon v-if="loading" size="lg" />
<pre
v-else
><code>{{ error || s__('BulkImport|No additional information provided.') }}</code></pre>
diff --git a/app/assets/javascripts/pages/import/history/components/import_history_app.vue b/app/assets/javascripts/pages/import/history/components/import_history_app.vue
index 557e25f66e2..db6f0c23dbd 100644
--- a/app/assets/javascripts/pages/import/history/components/import_history_app.vue
+++ b/app/assets/javascripts/pages/import/history/components/import_history_app.vue
@@ -137,7 +137,7 @@ export default {
{{ s__('BulkImport|Project import history') }}
</h1>
</div>
- <gl-loading-icon v-if="loading" size="md" class="gl-mt-5" />
+ <gl-loading-icon v-if="loading" size="lg" class="gl-mt-5" />
<gl-empty-state
v-else-if="!hasHistoryItems"
:title="s__('BulkImport|No history is available')"
diff --git a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js
index 37e9b7e99d4..3fae9809e51 100644
--- a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js
+++ b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js
@@ -1,5 +1,13 @@
-import { initExpiresAtField, initProjectsField, initTokensApp } from '~/access_tokens';
+import {
+ initAccessTokenTableApp,
+ initExpiresAtField,
+ initNewAccessTokenApp,
+ initProjectsField,
+ initTokensApp,
+} from '~/access_tokens';
+initAccessTokenTableApp();
initExpiresAtField();
+initNewAccessTokenApp();
initProjectsField();
initTokensApp();
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 f6f136f2402..49fdf5bb6b5 100644
--- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
+++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
@@ -6,7 +6,7 @@ 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 button = `<br/><a class="btn gl-button btn-sm btn-confirm gl-mt-3" 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);
}
diff --git a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue
index 3b5e764b712..92ae8128285 100644
--- a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue
+++ b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue
@@ -1,8 +1,8 @@
<script>
import { GlAlert, GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
-import dateFormat from 'dateformat';
import { get } from 'lodash';
+import { formatDate } from '~/lib/utils/datetime_utility';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -38,7 +38,10 @@ export default {
},
xAxis: {
name: '',
- type: 'category',
+ type: 'time',
+ axisLabel: {
+ formatter: (value) => formatDate(value, 'mmm dd'),
+ },
},
},
};
@@ -74,7 +77,7 @@ export default {
);
},
formattedData() {
- return this.sortedData.map((value) => [dateFormat(value.date, 'mmm dd'), value.coverage]);
+ return this.sortedData.map((value) => [value.date, value.coverage]);
},
chartData() {
return [
@@ -106,7 +109,7 @@ export default {
this.selectedCoverageIndex = index;
},
formatTooltipText(params) {
- this.tooltipTitle = params.value;
+ this.tooltipTitle = formatDate(params.value, 'mmm dd');
this.coveragePercentage = get(params, 'seriesData[0].data[1]', '');
},
},
diff --git a/app/assets/javascripts/pages/projects/incidents/show/index.js b/app/assets/javascripts/pages/projects/incidents/show/index.js
index 5a8cfcf8462..a83c4f1c0d2 100644
--- a/app/assets/javascripts/pages/projects/incidents/show/index.js
+++ b/app/assets/javascripts/pages/projects/incidents/show/index.js
@@ -1,7 +1,7 @@
import { initShow } from '~/issues';
-import initRelatedIssues from '~/related_issues';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
+import initWorkItemLinks from '~/work_items/components/work_item_links';
initShow();
initSidebarBundle();
-initRelatedIssues();
+initWorkItemLinks();
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index 44b1d5277d1..b320d8a61c2 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -1,34 +1,6 @@
-import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import { initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
-import { initBulkUpdateSidebar, initIssueStatusSelect } from '~/issuable/bulk_update_sidebar';
import { mountIssuesListApp, mountJiraIssuesListApp } from '~/issues/list';
-import initManualOrdering from '~/issues/manual_ordering';
-import { FILTERED_SEARCH } from '~/filtered_search/constants';
-import { ISSUABLE_INDEX } from '~/issuable/constants';
-import initFilteredSearch from '~/pages/search/init_filtered_search';
-import UsersSelect from '~/users_select';
-
-if (gon.features?.vueIssuesList) {
- mountIssuesListApp();
-} else {
- IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
-
- initFilteredSearch({
- page: FILTERED_SEARCH.ISSUES,
- filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
- useDefaultState: true,
- });
-
- initBulkUpdateSidebar(ISSUABLE_INDEX.ISSUE);
- initIssueStatusSelect();
- new UsersSelect(); // eslint-disable-line no-new
-
- initCsvImportExportButtons();
- initIssuableByEmail();
- initManualOrdering();
-}
-
-new ShortcutsNavigation(); // eslint-disable-line no-new
+mountIssuesListApp();
mountJiraIssuesListApp();
+new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js
index 46a34c025b6..ca2b1a08be8 100644
--- a/app/assets/javascripts/pages/projects/issues/show/index.js
+++ b/app/assets/javascripts/pages/projects/issues/show/index.js
@@ -2,7 +2,9 @@ import { initShow } from '~/issues';
import { store } from '~/notes/stores';
import initRelatedIssues from '~/related_issues';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
+import initWorkItemLinks from '~/work_items/components/work_item_links';
initShow();
initSidebarBundle(store);
initRelatedIssues();
+initWorkItemLinks();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js b/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js
index 545a39f4cf1..d3599ce5741 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js
@@ -1,5 +1,3 @@
import initMergeConflicts from '~/merge_conflicts/merge_conflicts_bundle';
-import initSidebarBundle from '~/sidebar/sidebar_bundle';
-initSidebarBundle();
initMergeConflicts();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
index d61209f904d..2d26d3922bf 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
@@ -4,7 +4,8 @@ import { localTimeAgo } from '~/lib/utils/datetime_utility';
import initCompareAutocomplete from './compare_autocomplete';
import initTargetProjectDropdown from './target_project_dropdown';
-const updateCommitList = (url, $loadingIndicator, $commitList, params) => {
+const updateCommitList = (url, $emptyState, $loadingIndicator, $commitList, params) => {
+ $emptyState.hide();
$loadingIndicator.show();
$commitList.empty();
@@ -16,6 +17,10 @@ const updateCommitList = (url, $loadingIndicator, $commitList, params) => {
$loadingIndicator.hide();
$commitList.html(data);
localTimeAgo($commitList.get(0).querySelectorAll('.js-timeago'));
+
+ if (!data) {
+ $emptyState.show();
+ }
});
};
@@ -26,6 +31,7 @@ export default (mrNewCompareNode) => {
const updateSourceBranchCommitList = () =>
updateCommitList(
sourceBranchUrl,
+ $(mrNewCompareNode).find('.js-source-commit-empty'),
$(mrNewCompareNode).find('.js-source-loading'),
$(mrNewCompareNode).find('.mr_source_commit'),
{
@@ -35,6 +41,7 @@ export default (mrNewCompareNode) => {
const updateTargetBranchCommitList = () =>
updateCommitList(
targetBranchUrl,
+ $(mrNewCompareNode).find('.js-target-commit-empty'),
$(mrNewCompareNode).find('.js-target-loading'),
$(mrNewCompareNode).find('.mr_target_commit'),
{
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
index e5f97530c02..9a38c2cc765 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
@@ -12,6 +12,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = (
$('.js-compare-dropdown').each(function () {
const $dropdown = $(this);
const selected = $dropdown.data('selected');
+ const defaultText = $dropdown.data('defaultText').trim();
const $dropdownContainer = $dropdown.closest('.dropdown');
const $fieldInput = $(`input[name="${$dropdown.data('fieldName')}"]`, $dropdownContainer);
const $filterInput = $('input[type="search"]', $dropdownContainer);
@@ -63,7 +64,11 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = (
return $el.attr('data-ref');
},
toggleLabel(obj, $el) {
- return $el.text().trim();
+ if ($el.hasClass('is-active')) {
+ return $el.text().trim();
+ }
+
+ return defaultText;
},
clicked: () => clickHandler($dropdown),
});
diff --git a/app/assets/javascripts/pages/projects/settings/access_tokens/index.js b/app/assets/javascripts/pages/projects/settings/access_tokens/index.js
index dc1bb88bf4b..b9f282a123c 100644
--- a/app/assets/javascripts/pages/projects/settings/access_tokens/index.js
+++ b/app/assets/javascripts/pages/projects/settings/access_tokens/index.js
@@ -1,3 +1,9 @@
-import { initExpiresAtField } from '~/access_tokens';
+import {
+ initAccessTokenTableApp,
+ initExpiresAtField,
+ initNewAccessTokenApp,
+} from '~/access_tokens';
+initAccessTokenTableApp();
initExpiresAtField();
+initNewAccessTokenApp();
diff --git a/app/assets/javascripts/pages/projects/settings/branch_rules/index.js b/app/assets/javascripts/pages/projects/settings/branch_rules/index.js
new file mode 100644
index 00000000000..c3d36ad5651
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/settings/branch_rules/index.js
@@ -0,0 +1,3 @@
+import mountBranchRules from '~/projects/settings/branch_rules/mount_branch_rules';
+
+mountBranchRules(document.getElementById('js-branch-rules'));
diff --git a/app/assets/javascripts/pages/projects/services/edit/index.js b/app/assets/javascripts/pages/projects/settings/integrations/edit/index.js
index 64df0d07d74..64df0d07d74 100644
--- a/app/assets/javascripts/pages/projects/services/edit/index.js
+++ b/app/assets/javascripts/pages/projects/settings/integrations/edit/index.js
diff --git a/app/assets/javascripts/pages/projects/settings/integrations/show/index.js b/app/assets/javascripts/pages/projects/settings/integrations/index/index.js
index 53068f72d3f..53068f72d3f 100644
--- a/app/assets/javascripts/pages/projects/settings/integrations/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/integrations/index/index.js
diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
index d45052d76f4..655243eee30 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
@@ -1,7 +1,10 @@
import MirrorRepos from '~/mirrors/mirror_repos';
+import mountBranchRules from '~/projects/settings/repository/branch_rules/mount_branch_rules';
import initForm from '../form';
initForm();
const mirrorReposContainer = document.querySelector('.js-mirror-settings');
if (mirrorReposContainer) new MirrorRepos(mirrorReposContainer).init();
+
+mountBranchRules(document.getElementById('js-branch-rules'));
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 03bab0fa773..81b0dbec0bd 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
@@ -2,6 +2,7 @@
import { GlButton, GlIcon, GlSprintf, GlLink, GlFormCheckbox, GlToggle } from '@gitlab/ui';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __, s__ } from '~/locale';
import {
visibilityOptions,
@@ -16,7 +17,7 @@ import { toggleHiddenClassBySelector } from '../external';
import projectFeatureSetting from './project_feature_setting.vue';
import projectSettingRow from './project_setting_row.vue';
-const PAGE_FEATURE_ACCESS_LEVEL = s__('ProjectSettings|Everyone');
+const FEATURE_ACCESS_LEVEL_ANONYMOUS = [30, s__('ProjectSettings|Everyone')];
export default {
i18n: {
@@ -28,7 +29,14 @@ export default {
lfsLabel: s__('ProjectSettings|Git Large File Storage (LFS)'),
mergeRequestsLabel: s__('ProjectSettings|Merge requests'),
operationsLabel: s__('ProjectSettings|Operations'),
+ packagesHelpText: s__(
+ 'ProjectSettings|Every project can have its own space to store its packages. Note: The Package Registry is always visible when a project is public.',
+ ),
+ packageRegistryHelpText: s__(
+ 'ProjectSettings|Every project can have its own space to store its packages.',
+ ),
packagesLabel: s__('ProjectSettings|Packages'),
+ packageRegistryLabel: s__('ProjectSettings|Package registry'),
pagesLabel: s__('ProjectSettings|Pages'),
ciCdLabel: __('CI/CD'),
repositoryLabel: s__('ProjectSettings|Repository'),
@@ -54,7 +62,7 @@ export default {
GlToggle,
ConfirmDanger,
},
- mixins: [settingsMixin],
+ mixins: [settingsMixin, glFeatureFlagsMixin()],
props: {
requestCveAvailable: {
@@ -183,6 +191,7 @@ export default {
repositoryAccessLevel: featureAccessLevel.EVERYONE,
forkingAccessLevel: featureAccessLevel.EVERYONE,
mergeRequestsAccessLevel: featureAccessLevel.EVERYONE,
+ packageRegistryAccessLevel: featureAccessLevel.EVERYONE,
buildsAccessLevel: featureAccessLevel.EVERYONE,
wikiAccessLevel: featureAccessLevel.EVERYONE,
snippetsAccessLevel: featureAccessLevel.EVERYONE,
@@ -196,6 +205,7 @@ export default {
warnAboutPotentiallyUnwantedCharacters: true,
lfsEnabled: true,
requestAccessEnabled: true,
+ enforceAuthChecksOnUploads: true,
highlightChangesClass: false,
emailsDisabled: false,
cveIdRequestEnabled: true,
@@ -229,6 +239,18 @@ export default {
);
},
+ packageRegistryFeatureAccessLevelOptions() {
+ const options = [FEATURE_ACCESS_LEVEL_ANONYMOUS];
+
+ if (this.visibilityLevel === visibilityOptions.PRIVATE) {
+ options.unshift(featureAccessLevelMembers);
+ } else if (this.visibilityLevel === visibilityOptions.INTERNAL) {
+ options.unshift(featureAccessLevelEveryone);
+ }
+
+ return options;
+ },
+
pagesFeatureAccessLevelOptions() {
const options = [featureAccessLevelMembers];
@@ -242,7 +264,7 @@ export default {
}
if (this.visibilityLevel !== visibilityOptions.PUBLIC) {
- options.push([30, PAGE_FEATURE_ACCESS_LEVEL]);
+ options.push(FEATURE_ACCESS_LEVEL_ANONYMOUS);
}
}
return options;
@@ -285,6 +307,16 @@ export default {
this.visibilityLevel < this.currentSettings.visibilityLevel
);
},
+ packageRegistryAccessLevelEnabled() {
+ return this.glFeatures.packageRegistryAccessLevel;
+ },
+ showAdditonalSettings() {
+ if (this.glFeatures.enforceAuthChecksOnUploads) {
+ return true;
+ }
+
+ return this.visibilityLevel !== this.visibilityOptions.PRIVATE;
+ },
},
watch: {
@@ -307,6 +339,15 @@ export default {
featureAccessLevel.PROJECT_MEMBERS,
this.buildsAccessLevel,
);
+ if (this.packageRegistryAccessLevelEnabled) {
+ if (
+ this.packageRegistryAccessLevel === featureAccessLevel.EVERYONE ||
+ (this.packageRegistryAccessLevel > featureAccessLevel.EVERYONE &&
+ oldValue === visibilityOptions.PUBLIC)
+ ) {
+ this.packageRegistryAccessLevel = featureAccessLevel.PROJECT_MEMBERS;
+ }
+ }
this.wikiAccessLevel = Math.min(featureAccessLevel.PROJECT_MEMBERS, this.wikiAccessLevel);
this.snippetsAccessLevel = Math.min(
featureAccessLevel.PROJECT_MEMBERS,
@@ -349,6 +390,14 @@ export default {
this.repositoryAccessLevel = featureAccessLevel.EVERYONE;
if (this.mergeRequestsAccessLevel > featureAccessLevel.NOT_ENABLED)
this.mergeRequestsAccessLevel = featureAccessLevel.EVERYONE;
+ if (
+ this.packageRegistryAccessLevelEnabled &&
+ this.packageRegistryAccessLevel === featureAccessLevel.PROJECT_MEMBERS
+ ) {
+ this.packageRegistryAccessLevel = Math.min(
+ ...this.packageRegistryFeatureAccessLevelOptions.map((option) => option[0]),
+ );
+ }
if (this.buildsAccessLevel > featureAccessLevel.NOT_ENABLED)
this.buildsAccessLevel = featureAccessLevel.EVERYONE;
if (this.wikiAccessLevel > featureAccessLevel.NOT_ENABLED)
@@ -369,6 +418,19 @@ export default {
this.containerRegistryAccessLevel = featureAccessLevel.EVERYONE;
this.highlightChanges();
+ } else if (this.packageRegistryAccessLevelEnabled) {
+ if (
+ value === visibilityOptions.PUBLIC &&
+ this.packageRegistryAccessLevel === featureAccessLevel.EVERYONE
+ ) {
+ // eslint-disable-next-line prefer-destructuring
+ this.packageRegistryAccessLevel = FEATURE_ACCESS_LEVEL_ANONYMOUS[0];
+ } else if (
+ value === visibilityOptions.INTERNAL &&
+ this.packageRegistryAccessLevel === FEATURE_ACCESS_LEVEL_ANONYMOUS[0]
+ ) {
+ this.packageRegistryAccessLevel = featureAccessLevel.EVERYONE;
+ }
}
},
@@ -465,15 +527,38 @@ export default {
)
}}</span>
<span class="form-text text-muted">{{ visibilityLevelDescription }}</span>
- <label v-if="visibilityLevel !== visibilityOptions.PRIVATE" class="gl-line-height-28">
- <input
- :value="requestAccessEnabled"
- type="hidden"
- name="project[request_access_enabled]"
- />
- <input v-model="requestAccessEnabled" type="checkbox" />
- {{ s__('ProjectSettings|Users can request access') }}
- </label>
+ <div v-if="showAdditonalSettings" class="gl-mt-4">
+ <strong class="gl-display-block">{{ s__('ProjectSettings|Additional options') }}</strong>
+ <label
+ v-if="visibilityLevel !== visibilityOptions.PRIVATE"
+ class="gl-line-height-28 gl-font-weight-normal gl-mb-0"
+ >
+ <input
+ :value="requestAccessEnabled"
+ type="hidden"
+ name="project[request_access_enabled]"
+ />
+ <input v-model="requestAccessEnabled" type="checkbox" />
+ {{ s__('ProjectSettings|Users can request access') }}
+ </label>
+ <label
+ v-if="
+ visibilityLevel !== visibilityOptions.PUBLIC && glFeatures.enforceAuthChecksOnUploads
+ "
+ class="gl-line-height-28 gl-font-weight-normal gl-display-block gl-mb-0"
+ >
+ <input
+ :value="enforceAuthChecksOnUploads"
+ type="hidden"
+ name="project[project_setting_attributes][enforce_auth_checks_on_uploads]"
+ />
+ <input v-model="enforceAuthChecksOnUploads" type="checkbox" />
+ {{ s__('ProjectSettings|Require authentication to view media files') }}
+ <span class="gl-text-gray-500 gl-display-block gl-ml-5 gl-mt-n3">{{
+ s__('ProjectSettings|Prevents direct linking to potentially sensitive media files')
+ }}</span>
+ </label>
+ </div>
</project-setting-row>
</div>
<div
@@ -587,15 +672,11 @@ export default {
</p>
</project-setting-row>
<project-setting-row
- v-if="packagesAvailable"
+ v-if="packagesAvailable && !packageRegistryAccessLevelEnabled"
ref="package-settings"
:help-path="packagesHelpPath"
:label="$options.i18n.packagesLabel"
- :help-text="
- s__(
- 'ProjectSettings|Every project can have its own space to store its packages. Note: The Package Registry is always visible when a project is public.',
- )
- "
+ :help-text="$options.i18n.packagesHelpText"
>
<gl-toggle
v-model="packagesEnabled"
@@ -710,6 +791,20 @@ export default {
/>
</project-setting-row>
<project-setting-row
+ v-if="packageRegistryAccessLevelEnabled && packagesAvailable"
+ :help-path="packagesHelpPath"
+ :label="$options.i18n.packageRegistryLabel"
+ :help-text="$options.i18n.packageRegistryHelpText"
+ data-testid="package-registry-access-level"
+ >
+ <project-feature-setting
+ v-model="packageRegistryAccessLevel"
+ :label="$options.i18n.packageRegistryLabel"
+ :options="packageRegistryFeatureAccessLevelOptions"
+ name="project[project_feature_attributes][package_registry_access_level]"
+ />
+ </project-setting-row>
+ <project-setting-row
v-if="pagesAvailable && pagesAccessControlEnabled"
ref="pages-settings"
:help-path="pagesHelpPath"
diff --git a/app/assets/javascripts/pages/projects/shared/save_project_loader.js b/app/assets/javascripts/pages/projects/shared/save_project_loader.js
index aa3589ac88d..7fd9e24549f 100644
--- a/app/assets/javascripts/pages/projects/shared/save_project_loader.js
+++ b/app/assets/javascripts/pages/projects/shared/save_project_loader.js
@@ -1,12 +1,14 @@
-import $ from 'jquery';
-
export default function initProjectLoadingSpinner() {
- const $formContainer = $('.project-edit-container');
- const $loadingSpinner = $('.save-project-loader');
+ const formContainer = document.querySelector('.project-edit-container');
+ if (formContainer == null) {
+ return;
+ }
+
+ const loadingSpinner = document.querySelector('.save-project-loader');
// show loading spinner when saving
- $formContainer.on('ajax:before', () => {
- $formContainer.hide();
- $loadingSpinner.show();
+ formContainer.addEventListener('ajax:before', () => {
+ formContainer.style.display = 'none';
+ loadingSpinner.style.display = 'block';
});
}
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index e2b1a702560..eff39a744ad 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -2,6 +2,7 @@ import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
+import initClustersDeprecationAlert from '~/projects/clusters_deprecation_alert';
import leaveByUrl from '~/namespaces/leave_by_url';
import initVueNotificationsDropdown from '~/notifications';
import Star from '~/projects/star';
@@ -50,6 +51,7 @@ new ShortcutsNavigation(); // eslint-disable-line no-new
initUploadFileTrigger();
initInviteMembersModal();
initInviteMembersTrigger();
+initClustersDeprecationAlert();
initReadMore();
new Star(); // eslint-disable-line no-new
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
deleted file mode 100644
index d9d265e4e4a..00000000000
--- a/app/assets/javascripts/pages/projects/static_site_editor/show/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initStaticSiteEditor from '~/static_site_editor';
-
-initStaticSiteEditor(document.querySelector('#static-site-editor'));
diff --git a/app/assets/javascripts/pages/projects/tags/index/index.js b/app/assets/javascripts/pages/projects/tags/index/index.js
index 9e48dd9e463..e62bdc7a8c0 100644
--- a/app/assets/javascripts/pages/projects/tags/index/index.js
+++ b/app/assets/javascripts/pages/projects/tags/index/index.js
@@ -1,9 +1,5 @@
import TagSortDropdown from '~/tags';
-import { initRemoveTag } from '../remove_tag';
+import initDeleteTagModal from '~/tags/init_delete_tag_modal';
-initRemoveTag({
- onDelete: (path) => {
- document.querySelector(`[data-path="${path}"]`).closest('.js-tag-list').remove();
- },
-});
+initDeleteTagModal();
TagSortDropdown();
diff --git a/app/assets/javascripts/pages/projects/tags/remove_tag.js b/app/assets/javascripts/pages/projects/tags/remove_tag.js
deleted file mode 100644
index 7b95560df7b..00000000000
--- a/app/assets/javascripts/pages/projects/tags/remove_tag.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import initConfirmModal from '~/confirm_modal';
-import createFlash from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-
-export const initRemoveTag = ({ onDelete = () => {} }) => {
- return initConfirmModal({
- handleSubmit: (path = '') =>
- axios
- .delete(path)
- .then(() => onDelete(path))
- .catch(({ response: { data } }) => {
- const { message } = data;
- createFlash({ message });
- }),
- });
-};
diff --git a/app/assets/javascripts/pages/projects/tags/show/index.js b/app/assets/javascripts/pages/projects/tags/show/index.js
index 6f5406f554f..0967540f42c 100644
--- a/app/assets/javascripts/pages/projects/tags/show/index.js
+++ b/app/assets/javascripts/pages/projects/tags/show/index.js
@@ -1,8 +1,3 @@
-import { redirectTo, getBaseURL, stripFinalUrlSegment } from '~/lib/utils/url_utility';
-import { initRemoveTag } from '../remove_tag';
+import initDeleteTagModal from '~/tags/init_delete_tag_modal';
-initRemoveTag({
- onDelete: (path = '') => {
- redirectTo(stripFinalUrlSegment([getBaseURL(), path].join('')));
- },
-});
+initDeleteTagModal();
diff --git a/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js b/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js
index 79ce1a37d21..47aae36ecbb 100644
--- a/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js
+++ b/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js
@@ -1,6 +1,6 @@
function onSidebarLinkClick() {
const setDataTrackAction = (element, action) => {
- element.setAttribute('data-track-action', action);
+ element.dataset.trackAction = action;
};
const setDataTrackExtra = (element, value) => {
@@ -12,10 +12,10 @@ function onSidebarLinkClick() {
? SIDEBAR_COLLAPSED
: SIDEBAR_EXPANDED;
- element.setAttribute(
- 'data-track-extra',
- JSON.stringify({ sidebar_display: sidebarCollapsed, menu_display: value }),
- );
+ element.dataset.trackExtra = JSON.stringify({
+ sidebar_display: sidebarCollapsed,
+ menu_display: value,
+ });
};
const EXPANDED = 'Expanded';
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 996e12bc105..94506d33b33 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -298,7 +298,7 @@ export default class ActivityCalendar {
.querySelector(this.activitiesContainer)
.querySelectorAll('.js-localtime')
.forEach((el) => {
- el.setAttribute('title', formatDate(el.getAttribute('data-datetime')));
+ el.setAttribute('title', formatDate(el.dataset.datetime));
});
})
.catch(() =>
diff --git a/app/assets/javascripts/performance_bar/components/add_request.vue b/app/assets/javascripts/performance_bar/components/add_request.vue
index d48a5acb85c..9ac6b0e6403 100644
--- a/app/assets/javascripts/performance_bar/components/add_request.vue
+++ b/app/assets/javascripts/performance_bar/components/add_request.vue
@@ -1,7 +1,12 @@
-import { __ } from '~/locale';
-
<script>
+import { GlForm, GlFormInput, GlButton } from '@gitlab/ui';
+
export default {
+ components: {
+ GlForm,
+ GlButton,
+ GlFormInput,
+ },
data() {
return {
inputEnabled: false,
@@ -24,25 +29,26 @@ export default {
};
</script>
<template>
- <div id="peek-view-add-request" class="view">
- <form class="form-inline" @submit.prevent>
- <button
- class="btn-blank btn-link bold gl-text-blue-300"
- type="button"
- :title="__(`Add request manually`)"
+ <div id="peek-view-add-request" class="view gl-display-flex">
+ <gl-form class="gl-display-flex gl-align-items-center" @submit.prevent>
+ <gl-button
+ class="gl-text-blue-300! gl-mr-2"
+ category="tertiary"
+ variant="link"
+ icon="plus"
+ size="small"
+ :title="__('Add request manually')"
@click="toggleInput"
- >
- +
- </button>
- <input
+ />
+ <gl-form-input
v-if="inputEnabled"
v-model="urlOrRequestId"
type="text"
:placeholder="__(`URL or request ID`)"
- class="form-control form-control-sm d-inline-block ml-1"
+ class="gl-ml-2"
@keyup.enter="addRequest"
@keyup.esc="clearForm"
/>
- </form>
+ </gl-form>
</div>
</template>
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 0f744e858f2..1da4a8fea73 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -121,7 +121,7 @@ export default {
return window.URL.createObjectURL(blob);
},
downloadName() {
- const fileName = this.requests[0].truncatedUrl;
+ const fileName = this.requests[0].displayName;
return `${fileName}_perf_bar_${Date.now()}.json`;
},
memoryReportPath() {
@@ -150,7 +150,7 @@ export default {
<div id="js-peek" :class="env">
<div
v-if="currentRequest"
- class="d-flex container-fluid container-limited justify-content-center"
+ class="d-flex container-fluid container-limited justify-content-center gl-align-items-center"
data-qa-selector="performance_bar"
>
<div id="peek-view-host" class="view">
diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue
index ffc22c2113d..f2177e102ec 100644
--- a/app/assets/javascripts/performance_bar/components/request_selector.vue
+++ b/app/assets/javascripts/performance_bar/components/request_selector.vue
@@ -31,7 +31,7 @@ export default {
:value="request.id"
data-qa-selector="request_dropdown_option"
>
- {{ request.truncatedUrl }}
+ {{ request.displayName }}
</option>
</select>
</div>
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
index e7f84eacdca..84fe14fe056 100644
--- a/app/assets/javascripts/performance_bar/index.js
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -56,12 +56,12 @@ const initPerformanceBar = (el) => {
this.addRequest(urlOrRequestId, urlOrRequestId);
}
},
- addRequest(requestId, requestUrl) {
+ addRequest(requestId, requestUrl, operationName) {
if (!this.store.canTrackRequest(requestUrl)) {
return;
}
- this.store.addRequest(requestId, requestUrl);
+ this.store.addRequest(requestId, requestUrl, operationName);
},
loadRequestDetails(requestId) {
const request = this.store.findRequest(requestId);
diff --git a/app/assets/javascripts/performance_bar/performance_bar_log.js b/app/assets/javascripts/performance_bar/performance_bar_log.js
index aad99e2604e..62ca568adc5 100644
--- a/app/assets/javascripts/performance_bar/performance_bar_log.js
+++ b/app/assets/javascripts/performance_bar/performance_bar_log.js
@@ -10,7 +10,7 @@ const initVitalsLog = () => {
console.log(
`${String.fromCodePoint(
0x1f4d1,
- )} To get the final web vital numbers reported you maybe need to switch away and back to the tab`,
+ )} To get the final web vital numbers report you may need to switch away and back to the tab`,
);
getCLS(reportVital);
getFID(reportVital);
diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
index 4c0293f5b78..e67143f3ede 100644
--- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js
+++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
@@ -10,13 +10,15 @@ export default class PerformanceBarService {
static registerInterceptor(peekUrl, callback) {
PerformanceBarService.interceptor = (response) => {
- const [fireCallback, requestId, requestUrl] = PerformanceBarService.callbackParams(
- response,
- peekUrl,
- );
+ const [
+ fireCallback,
+ requestId,
+ requestUrl,
+ operationName,
+ ] = PerformanceBarService.callbackParams(response, peekUrl);
if (fireCallback) {
- callback(requestId, requestUrl);
+ callback(requestId, requestUrl, operationName);
}
return response;
@@ -36,7 +38,8 @@ export default class PerformanceBarService {
const cachedResponse =
response.headers && parseBoolean(response.headers['x-gitlab-from-cache']);
const fireCallback = requestUrl !== peekUrl && Boolean(requestId) && !cachedResponse;
+ const operationName = response.config?.operationName;
- return [fireCallback, requestId, requestUrl];
+ return [fireCallback, requestId, requestUrl, operationName];
}
}
diff --git a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
index 5a69960e4d9..2011604534c 100644
--- a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
+++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
@@ -3,15 +3,19 @@ export default class PerformanceBarStore {
this.requests = [];
}
- addRequest(requestId, requestUrl) {
+ addRequest(requestId, requestUrl, operationName) {
if (!this.findRequest(requestId)) {
- const shortUrl = PerformanceBarStore.truncateUrl(requestUrl);
+ let displayName = PerformanceBarStore.truncateUrl(requestUrl);
+
+ if (operationName) {
+ displayName += ` (${operationName})`;
+ }
this.requests.push({
id: requestId,
url: requestUrl,
- truncatedUrl: shortUrl,
details: {},
+ displayName,
});
}
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index 100ffc0664b..f836921f5e5 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -10,10 +10,12 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-new-user-signups-cap-reached',
'.js-eoa-bronze-plan-banner',
'.js-security-newsletter-callout',
- '.js-approaching-seats-count-threshold',
+ '.js-approaching-seat-count-threshold',
'.js-storage-enforcement-banner',
'.js-user-over-limit-free-plan-alert',
'.js-minute-limit-banner',
+ '.js-submit-license-usage-data-banner',
+ '.js-project-usage-limitations-callout',
];
const initCallouts = () => {
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
index d9da238358f..4775836fcc6 100644
--- a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
@@ -146,12 +146,10 @@ export default {
</gl-sprintf>
</gl-form-checkbox>
</gl-form-group>
- <div
- class="gl-display-flex gl-justify-content-space-between gl-p-5 gl-bg-gray-10 gl-border-t-gray-100 gl-border-t-solid gl-border-t-1"
- >
+ <div class="gl-display-flex gl-py-5 gl-border-t-gray-100 gl-border-t-solid gl-border-t-1">
<gl-button
type="submit"
- class="js-no-auto-disable"
+ class="js-no-auto-disable gl-mr-3"
category="primary"
variant="confirm"
data-qa-selector="commit_changes_button"
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue b/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
index 897bd2dcccf..1f74e89f90c 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
@@ -1,6 +1,8 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
+import Tracking from '~/tracking';
+import { pipelineEditorTrackingOptions } from '../../../constants';
export default {
i18n: {
@@ -25,7 +27,14 @@ export default {
GlLink,
GlSprintf,
},
+ mixins: [Tracking.mixin()],
inject: ['runnerHelpPagePath'],
+ methods: {
+ trackHelpPageClick() {
+ const { label, actions } = pipelineEditorTrackingOptions;
+ this.track(actions.helpDrawerLinks.runners, { label });
+ },
+ },
};
</script>
<template>
@@ -38,7 +47,7 @@ export default {
<p class="gl-mb-0">
<gl-sprintf :message="$options.i18n.note">
<template #link="{ content }">
- <gl-link :href="runnerHelpPagePath" target="_blank">
+ <gl-link :href="runnerHelpPagePath" target="_blank" @click="trackHelpPageClick()">
{{ content }}
</gl-link>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue b/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue
index 04140434af2..bc9203b9c5b 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue
@@ -1,8 +1,20 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
+import Tracking from '~/tracking';
+import {
+ CI_EXAMPLES_LINK,
+ CI_HELP_LINK,
+ CI_NEEDS_LINK,
+ CI_YAML_LINK,
+ pipelineEditorTrackingOptions,
+} from '../../../constants';
export default {
+ CI_EXAMPLES_LINK,
+ CI_HELP_LINK,
+ CI_NEEDS_LINK,
+ CI_YAML_LINK,
i18n: {
title: s__('PipelineEditorTutorial|⚙️ Pipeline configuration reference'),
firstParagraph: s__('PipelineEditorTutorial|Resources to help with your CI/CD configuration:'),
@@ -23,7 +35,14 @@ export default {
GlLink,
GlSprintf,
},
+ mixins: [Tracking.mixin()],
inject: ['ciExamplesHelpPagePath', 'ciHelpPagePath', 'needsHelpPagePath', 'ymlHelpPagePath'],
+ methods: {
+ trackHelpPageClick(key) {
+ const { label, actions } = pipelineEditorTrackingOptions;
+ this.track(actions.helpDrawerLinks[key], { label });
+ },
+ },
};
</script>
<template>
@@ -34,7 +53,11 @@ export default {
<li>
<gl-sprintf :message="$options.i18n.browseExamples">
<template #link="{ content }">
- <gl-link :href="ciExamplesHelpPagePath" target="_blank">
+ <gl-link
+ :href="ciExamplesHelpPagePath"
+ target="_blank"
+ @click="trackHelpPageClick($options.CI_EXAMPLES_LINK)"
+ >
{{ content }}
</gl-link>
</template>
@@ -43,7 +66,11 @@ export default {
<li>
<gl-sprintf :message="$options.i18n.viewSyntaxRef">
<template #link="{ content }">
- <gl-link :href="ymlHelpPagePath" target="_blank">
+ <gl-link
+ :href="ymlHelpPagePath"
+ target="_blank"
+ @click="trackHelpPageClick($options.CI_YAML_LINK)"
+ >
{{ content }}
</gl-link>
</template>
@@ -52,7 +79,11 @@ export default {
<li>
<gl-sprintf :message="$options.i18n.learnMore">
<template #link="{ content }">
- <gl-link :href="ciHelpPagePath" target="_blank">
+ <gl-link
+ :href="ciHelpPagePath"
+ target="_blank"
+ @click="trackHelpPageClick($options.CI_HELP_LINK)"
+ >
{{ content }}
</gl-link>
</template>
@@ -61,7 +92,11 @@ export default {
<li>
<gl-sprintf :message="$options.i18n.needs">
<template #link="{ content }">
- <gl-link :href="needsHelpPagePath" target="_blank">
+ <gl-link
+ :href="needsHelpPagePath"
+ target="_blank"
+ @click="trackHelpPageClick($options.CI_NEEDS_LINK)"
+ >
{{ content }}
</gl-link>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue b/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue
index 9765d669fc1..65a2a6b56e4 100644
--- a/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue
+++ b/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue
@@ -22,12 +22,21 @@ export default {
},
methods: {
toggleDrawer() {
- this.$emit(this.showDrawer ? 'close-drawer' : 'open-drawer');
+ if (this.showDrawer) {
+ this.$emit('close-drawer');
+ } else {
+ this.$emit('open-drawer');
+ this.trackHelpDrawerClick();
+ }
+ },
+ trackHelpDrawerClick() {
+ const { label, actions } = pipelineEditorTrackingOptions;
+ this.track(actions.openHelpDrawer, { label });
},
trackTemplateBrowsing() {
const { label, actions } = pipelineEditorTrackingOptions;
- this.track(actions.browse_templates, { label });
+ this.track(actions.browseTemplates, { label });
},
},
};
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 ead2076ec3b..4398ba67d47 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
@@ -246,7 +246,7 @@ export default {
</template>
<template #default>
<gl-dropdown-item v-if="isBranchesLoading" key="loading">
- <gl-loading-icon size="md" />
+ <gl-loading-icon size="lg" />
</gl-dropdown-item>
</template>
</gl-infinite-scroll>
diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
index 58df98d0fb7..8e95fad1e48 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
+++ b/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
@@ -1,7 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { EDITOR_APP_STATUS_EMPTY, EDITOR_APP_STATUS_LOADING } from '../../constants';
import FileTreePopover from '../popovers/file_tree_popover.vue';
import BranchSwitcher from './branch_switcher.vue';
@@ -12,7 +11,6 @@ export default {
FileTreePopover,
GlButton,
},
- mixins: [glFeatureFlagMixin()],
props: {
hasUnsavedChanges: {
type: Boolean,
@@ -43,11 +41,7 @@ export default {
return this.appStatus === EDITOR_APP_STATUS_LOADING;
},
showFileTreeToggle() {
- return (
- this.glFeatures.pipelineEditorFileTree &&
- !this.isNewCiConfigFile &&
- this.appStatus !== EDITOR_APP_STATUS_EMPTY
- );
+ return !this.isNewCiConfigFile && this.appStatus !== EDITOR_APP_STATUS_EMPTY;
},
},
methods: {
diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
index 13e254f138a..9a789ccab4d 100644
--- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
+++ b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
@@ -17,7 +17,7 @@ export default {
text: __('Syntax is incorrect.'),
},
includesText: __(
- 'CI configuration validated, including all configuration added with the %{codeStart}includes%{codeEnd} keyword. %{link}',
+ 'CI configuration validated, including all configuration added with the %{codeStart}include%{codeEnd} keyword. %{link}',
),
warningTitle: __('The form contains the following warning:'),
fields: [
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
index da31fc62d09..08d246a9a00 100644
--- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -15,13 +15,15 @@ import {
MERGED_TAB,
TAB_QUERY_PARAM,
TABS_INDEX,
+ VALIDATE_TAB,
VISUALIZE_TAB,
} from '../constants';
import getAppStatus from '../graphql/queries/client/app_status.query.graphql';
import CiConfigMergedPreview from './editor/ci_config_merged_preview.vue';
import CiEditorHeader from './editor/ci_editor_header.vue';
-import TextEditor from './editor/text_editor.vue';
import CiLint from './lint/ci_lint.vue';
+import CiValidate from './validate/ci_validate.vue';
+import TextEditor from './editor/text_editor.vue';
import EditorTab from './ui/editor_tab.vue';
import WalkthroughPopover from './popovers/walkthrough_popover.vue';
@@ -31,6 +33,7 @@ export default {
tabGraph: s__('Pipelines|Visualize'),
tabLint: s__('Pipelines|Lint'),
tabMergedYaml: s__('Pipelines|View merged YAML'),
+ tabValidate: s__('Pipelines|Validate'),
empty: {
visualization: s__(
'PipelineEditor|The pipeline visualization is displayed when the CI/CD configuration file has valid syntax.',
@@ -53,12 +56,14 @@ export default {
CREATE_TAB,
LINT_TAB,
MERGED_TAB,
+ VALIDATE_TAB,
VISUALIZE_TAB,
},
components: {
CiConfigMergedPreview,
CiEditorHeader,
CiLint,
+ CiValidate,
EditorTab,
GlAlert,
GlLoadingIcon,
@@ -121,9 +126,10 @@ export default {
},
created() {
const [tabQueryParam] = getParameterValues(TAB_QUERY_PARAM);
+ const tabName = Object.keys(TABS_INDEX)[tabQueryParam];
- if (tabQueryParam && TABS_INDEX[tabQueryParam]) {
- this.setDefaultTab(tabQueryParam);
+ if (tabName) {
+ this.setDefaultTab(tabName);
}
},
methods: {
@@ -180,6 +186,17 @@ export default {
<pipeline-graph v-else :pipeline-data="ciConfigData" />
</editor-tab>
<editor-tab
+ v-if="glFeatures.simulatePipeline"
+ class="gl-mb-3"
+ data-testid="validate-tab"
+ :title="$options.i18n.tabValidate"
+ @click="setCurrentTab($options.tabConstants.VALIDATE_TAB)"
+ >
+ <gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" />
+ <ci-validate v-else />
+ </editor-tab>
+ <editor-tab
+ v-else
class="gl-mb-3"
:empty-message="$options.i18n.empty.lint"
:is-empty="isEmpty"
diff --git a/app/assets/javascripts/pipeline_editor/components/popovers/file_tree_popover.vue b/app/assets/javascripts/pipeline_editor/components/popovers/file_tree_popover.vue
index 6270429535d..efa6a54c638 100644
--- a/app/assets/javascripts/pipeline_editor/components/popovers/file_tree_popover.vue
+++ b/app/assets/javascripts/pipeline_editor/components/popovers/file_tree_popover.vue
@@ -26,11 +26,8 @@ export default {
this.showPopover = localStorage.getItem(FILE_TREE_POPOVER_DISMISSED_KEY) !== 'true';
},
methods: {
- closePopover() {
- this.showPopover = false;
- },
dismissPermanently() {
- this.closePopover();
+ this.showPopover = false;
localStorage.setItem(FILE_TREE_POPOVER_DISMISSED_KEY, 'true');
},
},
@@ -48,7 +45,7 @@ export default {
data-qa-selector="file_tree_popover"
@close-button-clicked="dismissPermanently"
>
- <div v-outside="closePopover" class="gl-font-base gl-mb-3">
+ <div v-outside="dismissPermanently" class="gl-font-base gl-mb-3">
<gl-sprintf :message="$options.i18n.description">
<template #link="{ content }">
<gl-link :href="includesHelpPagePath" target="_blank">{{ content }}</gl-link>
diff --git a/app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue b/app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue
new file mode 100644
index 00000000000..5f26318497b
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue
@@ -0,0 +1,65 @@
+<script>
+import { GlButton, GlDropdown, GlTooltipDirective, GlSprintf } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+
+export const i18n = {
+ help: __('Help'),
+ pipelineSource: s__('PipelineEditor|Pipeline Source'),
+ pipelineSourceDefault: s__('PipelineEditor|Git push event to the default branch'),
+ pipelineSourceTooltip: s__('PipelineEditor|Other pipeline sources are not available yet.'),
+ title: s__('PipelineEditor|Validate pipeline under selected conditions'),
+ contentNote: s__(
+ 'PipelineEditor|Current content in the Edit tab will be used for the simulation.',
+ ),
+ simulationNote: s__(
+ 'PipelineEditor|Pipeline behavior will be simulated including the %{codeStart}rules%{codeEnd} %{codeStart}only%{codeEnd} %{codeStart}except%{codeEnd} and %{codeStart}needs%{codeEnd} job dependencies.',
+ ),
+ cta: s__('PipelineEditor|Validate pipeline'),
+};
+
+export default {
+ name: 'CiValidateTab',
+ components: {
+ GlButton,
+ GlDropdown,
+ GlSprintf,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: ['validateTabIllustrationPath'],
+ i18n,
+};
+</script>
+
+<template>
+ <div>
+ <div class="gl-mt-3">
+ <label>{{ $options.i18n.pipelineSource }}</label>
+ <gl-dropdown
+ v-gl-tooltip.hover
+ :title="$options.i18n.pipelineSourceTooltip"
+ :text="$options.i18n.pipelineSourceDefault"
+ disabled
+ data-testid="pipeline-source"
+ />
+ </div>
+ <div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11">
+ <img :src="validateTabIllustrationPath" />
+ <h1 class="gl-font-size-h1 gl-mb-6">{{ $options.i18n.title }}</h1>
+ <ul>
+ <li class="gl-mb-3">{{ $options.i18n.contentNote }}</li>
+ <li class="gl-mb-3">
+ <gl-sprintf :message="$options.i18n.simulationNote">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </li>
+ </ul>
+ <gl-button variant="confirm" class="gl-mt-3" data-qa-selector="simulate_pipeline">
+ {{ $options.i18n.cta }}
+ </gl-button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js
index ff7c742f588..8f688e6ba76 100644
--- a/app/assets/javascripts/pipeline_editor/constants.js
+++ b/app/assets/javascripts/pipeline_editor/constants.js
@@ -32,13 +32,15 @@ export const PIPELINE_FAILURE = 'PIPELINE_FAILURE';
export const CREATE_TAB = 'CREATE_TAB';
export const LINT_TAB = 'LINT_TAB';
export const MERGED_TAB = 'MERGED_TAB';
+export const VALIDATE_TAB = 'VALIDATE_TAB';
export const VISUALIZE_TAB = 'VISUALIZE_TAB';
export const TABS_INDEX = {
[CREATE_TAB]: '0',
[VISUALIZE_TAB]: '1',
[LINT_TAB]: '2',
- [MERGED_TAB]: '3',
+ [VALIDATE_TAB]: '3',
+ [MERGED_TAB]: '4',
};
export const TAB_QUERY_PARAM = 'tab';
@@ -55,10 +57,25 @@ export const FILE_TREE_TIP_DISMISSED_KEY = 'pipeline_editor_file_tree_tip_dismis
export const STARTER_TEMPLATE_NAME = 'Getting-Started';
+export const CI_EXAMPLES_LINK = 'CI_EXAMPLES_LINK';
+export const CI_HELP_LINK = 'CI_HELP_LINK';
+export const CI_NEEDS_LINK = 'CI_NEEDS_LINK';
+export const CI_RUNNERS_LINK = 'CI_RUNNERS_LINK';
+export const CI_YAML_LINK = 'CI_YAML_LINK';
+
export const pipelineEditorTrackingOptions = {
label: 'pipeline_editor',
actions: {
- browse_templates: 'browse_templates',
+ browseTemplates: 'browse_templates',
+ closeHelpDrawer: 'close_help_drawer',
+ helpDrawerLinks: {
+ [CI_EXAMPLES_LINK]: 'visit_help_drawer_link_ci_examples',
+ [CI_HELP_LINK]: 'visit_help_drawer_link_ci_help',
+ [CI_NEEDS_LINK]: 'visit_help_drawer_link_needs',
+ [CI_RUNNERS_LINK]: 'visit_help_drawer_link_runners',
+ [CI_YAML_LINK]: 'visit_help_drawer_link_yaml',
+ },
+ openHelpDrawer: 'open_help_drawer',
},
};
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index e13d9cf9df0..4caa253b85e 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -41,6 +41,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
projectNamespace,
runnerHelpPagePath,
totalBranches,
+ validateTabIllustrationPath,
ymlHelpPagePath,
} = el.dataset;
@@ -130,6 +131,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
projectNamespace,
runnerHelpPagePath,
totalBranches: parseInt(totalBranches, 10),
+ validateTabIllustrationPath,
ymlHelpPagePath,
},
render(h) {
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
index 59022a91322..f26cdd8b017 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
@@ -1,7 +1,6 @@
<script>
import { GlModal } from '@gitlab/ui';
import { __ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import CommitSection from './components/commit/commit_section.vue';
import PipelineEditorDrawer from './components/drawer/pipeline_editor_drawer.vue';
import PipelineEditorFileNav from './components/file_nav/pipeline_editor_file_nav.vue';
@@ -34,7 +33,6 @@ export default {
PipelineEditorHeader,
PipelineEditorTabs,
},
- mixins: [glFeatureFlagMixin()],
props: {
ciConfigData: {
type: Object,
@@ -76,9 +74,6 @@ export default {
includesFiles() {
return this.ciConfigData?.includes || [];
},
- isFileTreeVisible() {
- return this.showFileTree && this.glFeatures.pipelineEditorFileTree;
- },
},
mounted() {
this.showFileTree = JSON.parse(localStorage.getItem(FILE_TREE_DISPLAY_KEY)) || false;
@@ -140,7 +135,7 @@ export default {
/>
<div class="gl-display-flex gl-w-full gl-sm-flex-direction-column">
<pipeline-editor-file-tree
- v-if="isFileTreeVisible"
+ v-if="showFileTree"
class="gl-flex-shrink-0"
:includes="includesFiles"
/>
diff --git a/app/assets/javascripts/pipeline_wizard/components/input.vue b/app/assets/javascripts/pipeline_wizard/components/input_wrapper.vue
index 5efae2471e5..5efae2471e5 100644
--- a/app/assets/javascripts/pipeline_wizard/components/input.vue
+++ b/app/assets/javascripts/pipeline_wizard/components/input_wrapper.vue
diff --git a/app/assets/javascripts/pipeline_wizard/components/step.vue b/app/assets/javascripts/pipeline_wizard/components/step.vue
index c6f793e4cc5..220b068f747 100644
--- a/app/assets/javascripts/pipeline_wizard/components/step.vue
+++ b/app/assets/javascripts/pipeline_wizard/components/step.vue
@@ -4,7 +4,7 @@ import { isNode, isDocument, parseDocument, Document } from 'yaml';
import { merge } from '~/lib/utils/yaml';
import { s__ } from '~/locale';
import { logError } from '~/lib/logger';
-import InputWrapper from './input.vue';
+import InputWrapper from './input_wrapper.vue';
import StepNav from './step_nav.vue';
export default {
diff --git a/app/assets/javascripts/pipeline_wizard/templates/.gitkeep b/app/assets/javascripts/pipeline_wizard/templates/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/app/assets/javascripts/pipeline_wizard/templates/.gitkeep
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index 9f76d4cec50..225706265c3 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -13,7 +13,6 @@ import { __, sprintf } from '~/locale';
import CancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
import RetryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { PIPELINE_GRAPHQL_TYPE } from '../../constants';
import { reportToSentry } from '../../utils';
import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from './constants';
@@ -35,7 +34,6 @@ export default {
flatLeftBorder: ['gl-rounded-bottom-left-none!', 'gl-rounded-top-left-none!'],
flatRightBorder: ['gl-rounded-bottom-right-none!', 'gl-rounded-top-right-none!'],
},
- mixins: [glFeatureFlagMixin()],
props: {
columnTitle: {
type: String,
@@ -62,11 +60,12 @@ export default {
return {
hasActionTooltip: false,
isActionLoading: false,
+ isExpandBtnFocus: false,
};
},
computed: {
action() {
- if (this.glFeatures?.downstreamRetryAction && this.isDownstream) {
+ if (this.isDownstream) {
if (this.isCancelable) {
return {
icon: 'cancel',
@@ -89,6 +88,9 @@ export default {
? ['gl-border-r-0!', ...this.$options.styles.flatRightBorder]
: ['gl-border-l-0!', ...this.$options.styles.flatLeftBorder];
},
+ buttonShadowClass() {
+ return this.isExpandBtnFocus ? '' : 'gl-shadow-none!';
+ },
buttonId() {
return `js-linked-pipeline-${this.pipeline.id}`;
},
@@ -99,9 +101,12 @@ export default {
},
expandedIcon() {
if (this.isUpstream) {
- return this.expanded ? 'angle-right' : 'angle-left';
+ return this.expanded ? 'chevron-lg-right' : 'chevron-lg-left';
}
- return this.expanded ? 'angle-left' : 'angle-right';
+ return this.expanded ? 'chevron-lg-left' : 'chevron-lg-right';
+ },
+ expandBtnText() {
+ return this.expanded ? __('Collapse jobs') : __('Expand jobs');
},
childPipeline() {
return this.isDownstream && this.isSameProject;
@@ -157,7 +162,7 @@ export default {
return Boolean(this.action?.method && this.action?.icon && this.action?.ariaLabel);
},
showCardTooltip() {
- return !this.hasActionTooltip;
+ return !this.hasActionTooltip && !this.isExpandBtnFocus;
},
sourceJobName() {
return this.pipeline.sourceJob?.name ?? '';
@@ -214,6 +219,9 @@ export default {
setActionTooltip(flag) {
this.hasActionTooltip = flag;
},
+ setExpandBtnActiveState(flag) {
+ this.isExpandBtnFocus = flag;
+ },
},
};
</script>
@@ -221,7 +229,7 @@ export default {
<template>
<div
ref="linkedPipeline"
- class="gl-h-full gl-display-flex!"
+ class="gl-h-full gl-display-flex! gl-px-2"
:class="flexDirection"
data-qa-selector="linked_pipeline_container"
@mouseover="onDownstreamHovered"
@@ -237,7 +245,11 @@ export default {
<div
class="gl-display-flex gl-downstream-pipeline-job-width gl-flex-direction-column gl-line-height-normal"
>
- <span class="gl-text-truncate" data-testid="downstream-title">
+ <span
+ class="gl-text-truncate"
+ data-testid="downstream-title"
+ data-qa-selector="downstream_title_content"
+ >
{{ downstreamTitle }}
</span>
<div class="gl-text-truncate">
@@ -273,12 +285,18 @@ export default {
<div class="gl-display-flex">
<gl-button
:id="buttonId"
- class="gl-border! gl-shadow-none! gl-rounded-lg!"
- :class="[`js-pipeline-expand-${pipeline.id}`, buttonBorderClasses]"
+ v-gl-tooltip
+ :title="expandBtnText"
+ class="gl-border! gl-rounded-lg!"
+ :class="[`js-pipeline-expand-${pipeline.id}`, buttonBorderClasses, buttonShadowClass]"
:icon="expandedIcon"
- :aria-label="__('Expand pipeline')"
+ :aria-label="expandBtnText"
data-testid="expand-pipeline-button"
data-qa-selector="expand_linked_pipeline_button"
+ @mouseover="setExpandBtnActiveState(true)"
+ @mouseout="setExpandBtnActiveState(false)"
+ @focus="setExpandBtnActiveState(true)"
+ @blur="setExpandBtnActiveState(false)"
@click="onClickLinkedPipeline"
/>
</div>
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
index 1c646bdf3d6..070c5ee59de 100644
--- a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
+++ b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
@@ -56,7 +56,13 @@ export default {
</script>
<template>
- <gl-table-lite :items="failedJobs" :fields="$options.fields" stacked="lg" fixed>
+ <gl-table-lite
+ :items="failedJobs"
+ :fields="$options.fields"
+ stacked="lg"
+ fixed
+ data-testId="tab-failures"
+ >
<template #table-colgroup="{ fields }">
<col v-for="field in fields" :key="field.key" :class="field.columnClass" />
</template>
diff --git a/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue b/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
index b45f3e4f32c..18e9ffa23cf 100644
--- a/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
+++ b/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
@@ -127,7 +127,7 @@ export default {
<jobs-table v-else :jobs="jobs" :table-fields="$options.fields" data-testid="jobs-tab-table" />
<gl-intersection-observer v-if="jobsPageInfo.hasNextPage" @appear="fetchMoreJobs">
- <gl-loading-icon v-if="showLoadingSpinner" size="md" />
+ <gl-loading-icon v-if="showLoadingSpinner" size="lg" />
</gl-intersection-observer>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/notification/deprecated_type_keyword_notification.vue b/app/assets/javascripts/pipelines/components/notification/deprecated_type_keyword_notification.vue
deleted file mode 100644
index b8f9f84c217..00000000000
--- a/app/assets/javascripts/pipelines/components/notification/deprecated_type_keyword_notification.vue
+++ /dev/null
@@ -1,102 +0,0 @@
-<script>
-import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
-import { __ } from '~/locale';
-import getPipelineWarnings from '../../graphql/queries/get_pipeline_warnings.query.graphql';
-
-export default {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- expectedMessage: 'will be removed in',
- i18n: {
- title: __('Found warning in your .gitlab-ci.yml'),
- rootTypesWarning: __(
- '%{codeStart}types%{codeEnd} is deprecated and will be removed in 15.0. Use %{codeStart}stages%{codeEnd} instead. %{linkStart}Learn More %{linkEnd}',
- ),
- typeWarning: __(
- '%{codeStart}type%{codeEnd} is deprecated and will be removed in 15.0. Use %{codeStart}stage%{codeEnd} instead. %{linkStart}Learn More %{linkEnd}',
- ),
- },
- components: {
- GlAlert,
- GlLink,
- GlSprintf,
- },
- inject: ['deprecatedKeywordsDocPath', 'fullPath', 'pipelineIid'],
- apollo: {
- warnings: {
- query: getPipelineWarnings,
- variables() {
- return {
- fullPath: this.fullPath,
- iid: this.pipelineIid,
- };
- },
- update(data) {
- return data?.project?.pipeline?.warningMessages || [];
- },
- error() {
- this.hasError = true;
- },
- },
- },
- data() {
- return {
- warnings: [],
- hasError: false,
- };
- },
- computed: {
- deprecationWarnings() {
- return this.warnings.filter(({ content }) => {
- return content.includes(this.$options.expectedMessage);
- });
- },
- formattedWarnings() {
- // The API doesn't have a mechanism currently to return a
- // type instead of just the error message. To work around this,
- // we check if the deprecation message is found within the warnings
- // and show a FE version of that message with the link to the documentation
- // and translated. We can have only 2 types of warnings: root types and individual
- // type. If the word `root` is present, then we know it's the root type deprecation
- // and if not, it's the normal type. This has to be deleted in 15.0.
- // Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/350810
- return this.deprecationWarnings.map(({ content }) => {
- if (content.includes('root')) {
- return this.$options.i18n.rootTypesWarning;
- }
- return this.$options.i18n.typeWarning;
- });
- },
- hasDeprecationWarning() {
- return this.formattedWarnings.length > 0;
- },
- showWarning() {
- return (
- !this.$apollo.queries.warnings?.loading && !this.hasError && this.hasDeprecationWarning
- );
- },
- },
-};
-</script>
-<template>
- <div>
- <gl-alert
- v-if="showWarning"
- :title="$options.i18n.title"
- variant="warning"
- :dismissible="false"
- >
- <ul class="gl-mb-0">
- <li v-for="warning in formattedWarnings" :key="warning">
- <gl-sprintf :message="warning">
- <template #code="{ content }">
- <code> {{ content }}</code>
- </template>
- <template #link="{ content }">
- <gl-link :href="deprecatedKeywordsDocPath" target="_blank"> {{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </li>
- </ul>
- </gl-alert>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue b/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
index 66d30c10362..e1745969649 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
@@ -1,9 +1,10 @@
<script>
-import { GlTabs, GlTab } from '@gitlab/ui';
+import { GlBadge, GlTabs, GlTab } from '@gitlab/ui';
import { __ } from '~/locale';
import { failedJobsTabName, jobsTabName, needsTabName, testReportTabName } from '../constants';
import PipelineGraphWrapper from './graph/graph_component_wrapper.vue';
import Dag from './dag/dag.vue';
+import FailedJobsApp from './jobs/failed_jobs_app.vue';
import JobsApp from './jobs/jobs_app.vue';
import TestReports from './test_reports/test_reports.vue';
@@ -25,14 +26,20 @@ export default {
},
components: {
Dag,
+ GlBadge,
GlTab,
GlTabs,
JobsApp,
- FailedJobsApp: JobsApp,
+ FailedJobsApp,
PipelineGraphWrapper,
TestReports,
},
- inject: ['defaultTabValue'],
+ inject: ['defaultTabValue', 'failedJobsCount', 'failedJobsSummary', 'totalJobCount'],
+ computed: {
+ showFailedJobsTab() {
+ return this.failedJobsCount > 0;
+ },
+ },
methods: {
isActive(tabName) {
return tabName === this.defaultTabValue;
@@ -54,19 +61,25 @@ export default {
>
<dag />
</gl-tab>
- <gl-tab
- :title="$options.i18n.tabs.jobsTitle"
- :active="isActive($options.tabNames.jobs)"
- data-testid="jobs-tab"
- >
+ <gl-tab :active="isActive($options.tabNames.jobs)" data-testid="jobs-tab" lazy>
+ <template #title>
+ <span class="gl-mr-2">{{ $options.i18n.tabs.jobsTitle }}</span>
+ <gl-badge size="sm" data-testid="builds-counter">{{ totalJobCount }}</gl-badge>
+ </template>
<jobs-app />
</gl-tab>
<gl-tab
+ v-if="showFailedJobsTab"
:title="$options.i18n.tabs.failedJobsTitle"
:active="isActive($options.tabNames.failures)"
data-testid="failed-jobs-tab"
+ lazy
>
- <failed-jobs-app />
+ <template #title>
+ <span class="gl-mr-2">{{ $options.i18n.tabs.failedJobsTitle }}</span>
+ <gl-badge size="sm" data-testid="failed-builds-counter">{{ failedJobsCount }}</gl-badge>
+ </template>
+ <failed-jobs-app :failed-jobs-summary="failedJobsSummary" />
</gl-tab>
<gl-tab
:title="$options.i18n.tabs.testsTitle"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue
index e35fccf2d7e..05cb2ebb769 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue
@@ -36,12 +36,12 @@ export default {
};
</script>
<template>
- <div data-testid="pipeline-mini-graph" class="gl-display-inline gl-vertical-align-middle gl-my-1">
+ <div data-testid="pipeline-mini-graph" class="gl-display-inline gl-vertical-align-middle">
<div
v-for="stage in stages"
:key="stage.name"
:class="stagesClass"
- class="stage-container dropdown"
+ class="dropdown gl-display-inline-block gl-mr-2 gl-my-2 gl-vertical-align-middle stage-container"
>
<pipeline-stage
:stage="stage"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue
index 53e21d4ce8b..d7e55d36ff6 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue
@@ -21,6 +21,13 @@ import eventHub from '../../event_hub';
import JobItem from './job_item.vue';
export default {
+ i18n: {
+ stage: __('Stage:'),
+ loadingText: __('Loading, please wait.'),
+ },
+ dropdownPopperOpts: {
+ placement: 'bottom',
+ },
components: {
CiIcon,
GlLoadingIcon,
@@ -48,20 +55,26 @@ export default {
},
data() {
return {
+ isDropdownOpen: false,
isLoading: false,
dropdownContent: [],
+ stageName: '',
};
},
watch: {
updateDropdown() {
- if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) {
+ if (this.updateDropdown && this.isDropdownOpen && !this.isLoading) {
this.fetchJobs();
}
},
},
methods: {
+ onHideDropdown() {
+ this.isDropdownOpen = false;
+ },
onShowDropdown() {
eventHub.$emit('clickedDropdown');
+ this.isDropdownOpen = true;
this.isLoading = true;
this.fetchJobs();
},
@@ -70,6 +83,7 @@ export default {
.get(this.stage.dropdown_path)
.then(({ data }) => {
this.dropdownContent = data.latest_statuses;
+ this.stageName = data.name;
this.isLoading = false;
})
.catch(() => {
@@ -81,9 +95,6 @@ export default {
});
});
},
- isDropdownOpen() {
- return this.$el.classList.contains('show');
- },
pipelineActionRequestComplete() {
// close the dropdown in MR widget
this.$refs.dropdown.hide();
@@ -107,28 +118,42 @@ export default {
variant="link"
:aria-label="stageAriaLabel(stage.title)"
:lazy="true"
- :popper-opts="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
- placement: 'bottom',
- } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ :popper-opts="$options.dropdownPopperOpts"
:toggle-class="['gl-rounded-full!']"
menu-class="mini-pipeline-graph-dropdown-menu"
+ @hide="onHideDropdown"
@show="onShowDropdown"
>
<template #button-content>
<ci-icon
+ is-borderless
is-interactive
css-classes="gl-rounded-full"
+ :is-active="isDropdownOpen"
:size="24"
:status="stage.status"
- class="gl-align-items-center gl-display-inline-flex"
+ class="gl-align-items-center gl-border gl-display-inline-flex gl-z-index-1"
/>
</template>
- <gl-loading-icon v-if="isLoading" size="sm" />
+ <div
+ v-if="isLoading"
+ class="gl-display-flex gl-justify-content-center gl-p-2"
+ data-testid="pipeline-stage-loading-state"
+ >
+ <gl-loading-icon size="sm" class="gl-mr-3" />
+ <p class="gl-mb-0">{{ $options.i18n.loadingText }}</p>
+ </div>
<ul
v-else
class="js-builds-dropdown-list scrollable-menu"
data-testid="mini-pipeline-graph-dropdown-menu-list"
>
+ <div
+ class="gl-align-items-center gl-border-b gl-display-flex gl-font-weight-bold gl-justify-content-center gl-pb-3"
+ >
+ <span class="gl-mr-1">{{ $options.i18n.stage }}</span>
+ <span data-testid="pipeline-stage-dropdown-menu-title">{{ stageName }}</span>
+ </div>
<li v-for="job in dropdownContent" :key="job.id">
<job-item
:dropdown-length="dropdownContent.length"
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 63c492c8bcd..09d588aaafd 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
@@ -137,9 +137,8 @@ export default {
class="gl-text-decoration-underline gl-text-blue-600! gl-mr-3"
data-testid="pipeline-url-link"
data-qa-selector="pipeline_url_link"
+ >#{{ pipeline[pipelineKey] }}</gl-link
>
- #{{ pipeline[pipelineKey] }}
- </gl-link>
<!--Commit row-->
<div class="icon-container gl-display-inline-block gl-mr-1">
<gl-icon
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue b/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
index 47e5bb0bde8..76ee6ab613b 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
@@ -19,7 +19,10 @@ export default {
},
testCase: {
type: Object,
- required: true,
+ required: false,
+ default: () => {
+ return {};
+ },
},
},
computed: {
@@ -49,6 +52,7 @@ export default {
},
text: {
name: __('Name'),
+ file: __('File'),
duration: __('Execution time'),
history: __('History'),
trace: __('System output'),
@@ -56,7 +60,7 @@ export default {
},
modalCloseButton: {
text: __('Close'),
- attributes: [{ variant: 'info' }],
+ attributes: [{ variant: 'confirm' }],
},
};
</script>
@@ -74,11 +78,24 @@ export default {
</div>
</div>
+ <div v-if="testCase.file" class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
+ <strong class="gl-text-right col-sm-3">{{ $options.text.file }}</strong>
+ <div class="col-sm-9" data-testid="test-case-file">
+ <gl-link v-if="testCase.filePath" :href="testCase.filePath">
+ {{ testCase.file }}
+ </gl-link>
+ <span v-else>{{ testCase.file }}</span>
+ </div>
+ </div>
+
<div class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
<strong class="gl-text-right col-sm-3">{{ $options.text.duration }}</strong>
- <div class="col-sm-9" data-testid="test-case-duration">
+ <div v-if="testCase.formattedTime" class="col-sm-9" data-testid="test-case-duration">
{{ testCase.formattedTime }}
</div>
+ <div v-else-if="testCase.execution_time" class="col-sm-9" data-testid="test-case-duration">
+ {{ sprintf('%{value} s', { value: testCase.execution_time }) }}
+ </div>
</div>
<div v-if="testCase.recent_failures" class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
index 9b0e6560c53..1e481d37017 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
@@ -7,11 +7,21 @@ import {
GlLink,
GlButton,
GlPagination,
+ GlEmptyState,
+ GlSprintf,
} from '@gitlab/ui';
import { mapState, mapGetters, mapActions } from 'vuex';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
import TestCaseDetails from './test_case_details.vue';
+export const i18n = {
+ expiredArtifactsTitle: s__('TestReports|Job artifacts are expired'),
+ expiredArtifactsDescription: s__(
+ 'TestReports|Test reports require job artifacts but all artifacts are expired. %{linkStart}Learn more%{linkEnd}',
+ ),
+};
+
export default {
name: 'TestsSuiteTable',
components: {
@@ -20,12 +30,19 @@ export default {
GlLink,
GlButton,
GlPagination,
+ GlEmptyState,
+ GlSprintf,
TestCaseDetails,
},
directives: {
GlTooltip: GlTooltipDirective,
GlModalDirective,
},
+ inject: {
+ artifactsExpiredImagePath: {
+ default: '',
+ },
+ },
props: {
heading: {
type: String,
@@ -44,18 +61,21 @@ export default {
...mapActions(['setPage']),
},
wrapSymbols: ['::', '#', '.', '_', '-', '/', '\\'],
+ i18n,
+ learnMorePath: helpPagePath('ci/unit_test_reports', {
+ anchor: 'viewing-unit-test-reports-on-gitlab',
+ }),
};
</script>
<template>
<div>
- <div class="row gl-mt-3">
- <div class="col-12">
- <h4>{{ heading }}</h4>
- </div>
- </div>
-
<div v-if="hasSuites" class="test-reports-table gl-mb-3 js-test-cases-table">
+ <div class="row gl-mt-3">
+ <div class="col-12">
+ <h4>{{ heading }}</h4>
+ </div>
+ </div>
<div role="row" class="gl-responsive-table-row table-row-header font-weight-bold fgray">
<div role="rowheader" class="table-section section-20">
{{ __('Suite') }}
@@ -158,16 +178,24 @@ export default {
</div>
<div v-else>
- <p data-testid="no-test-cases">
+ <gl-empty-state
+ v-if="getSuiteArtifactsExpired"
+ :title="$options.i18n.expiredArtifactsTitle"
+ :svg-path="artifactsExpiredImagePath"
+ :svg-height="100"
+ data-testid="artifacts-expired"
+ >
+ <template #description>
+ <gl-sprintf :message="$options.i18n.expiredArtifactsDescription">
+ <template #link="{ content }">
+ <gl-link :href="$options.learnMorePath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-empty-state>
+ <p v-else data-testid="no-test-cases">
{{ s__('TestReports|There are no test cases to display.') }}
</p>
- <p v-if="getSuiteArtifactsExpired" data-testid="artifacts-expired">
- {{
- s__(
- 'TestReports|Test details are populated by job artifacts. The job artifacts from this pipeline are expired.',
- )
- }}
- </p>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
index 79b1b6af38b..2f5301715c3 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
@@ -71,7 +71,7 @@ export default {
v-if="showBack"
size="small"
class="gl-mr-3 js-back-button"
- icon="angle-left"
+ icon="chevron-lg-left"
:aria-label="__('Go back')"
@click="onBackClick"
/>
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_warnings.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_warnings.query.graphql
deleted file mode 100644
index cd1d2b62a3d..00000000000
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_warnings.query.graphql
+++ /dev/null
@@ -1,12 +0,0 @@
-query getPipelineWarnings($fullPath: ID!, $iid: ID!) {
- project(fullPath: $fullPath) {
- id
- pipeline(iid: $iid) {
- id
- warningMessages {
- content
- id
- }
- }
- }
-}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index fd869014570..8bdf18da348 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -3,7 +3,6 @@ import { __, s__ } from '~/locale';
import createDagApp from './pipeline_details_dag';
import { createPipelinesDetailApp } from './pipeline_details_graph';
import { createPipelineHeaderApp } from './pipeline_details_header';
-import { createPipelineNotificationApp } from './pipeline_details_notification';
import { createPipelineJobsApp } from './pipeline_details_jobs';
import { createPipelineFailedJobsApp } from './pipeline_details_failed_jobs';
import { apolloProvider } from './pipeline_shared_client';
@@ -13,7 +12,6 @@ const SELECTORS = {
PIPELINE_DETAILS: '.js-pipeline-details-vue',
PIPELINE_GRAPH: '#js-pipeline-graph-vue',
PIPELINE_HEADER: '#js-pipeline-header-vue',
- PIPELINE_NOTIFICATION: '#js-pipeline-notification',
PIPELINE_TABS: '#js-pipeline-tabs',
PIPELINE_TESTS: '#js-pipeline-tests-detail',
PIPELINE_JOBS: '#js-pipeline-jobs-vue',
@@ -31,19 +29,13 @@ export default async function initPipelineDetailsBundle() {
});
}
- try {
- createPipelineNotificationApp(SELECTORS.PIPELINE_NOTIFICATION, apolloProvider);
- } catch {
- createFlash({
- message: __('An error occurred while loading a section of this page.'),
- });
- }
-
if (gon.features?.pipelineTabsVue) {
+ const { createAppOptions } = await import('ee_else_ce/pipelines/pipeline_tabs');
const { createPipelineTabs } = await import('./pipeline_tabs');
try {
- createPipelineTabs(SELECTORS.PIPELINE_TABS, apolloProvider);
+ const appOptions = createAppOptions(SELECTORS.PIPELINE_TABS, apolloProvider);
+ createPipelineTabs(appOptions);
} catch {
createFlash({
message: __('An error occurred while loading a section of this page.'),
@@ -82,14 +74,12 @@ export default async function initPipelineDetailsBundle() {
});
}
- if (gon.features?.failedJobsTabVue) {
- try {
- createPipelineFailedJobsApp(SELECTORS.PIPELINE_FAILED_JOBS);
- } catch {
- createFlash({
- message: s__('Jobs|An error occurred while loading the Failed Jobs tab.'),
- });
- }
+ try {
+ createPipelineFailedJobsApp(SELECTORS.PIPELINE_FAILED_JOBS);
+ } catch {
+ createFlash({
+ message: s__('Jobs|An error occurred while loading the Failed Jobs tab.'),
+ });
}
}
}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_notification.js b/app/assets/javascripts/pipelines/pipeline_details_notification.js
deleted file mode 100644
index b480fc7c713..00000000000
--- a/app/assets/javascripts/pipelines/pipeline_details_notification.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import DeprecatedKeywordNotification from './components/notification/deprecated_type_keyword_notification.vue';
-
-Vue.use(VueApollo);
-
-export const createPipelineNotificationApp = (elSelector, apolloProvider) => {
- const el = document.querySelector(elSelector);
-
- if (!el) {
- return;
- }
-
- const { deprecatedKeywordsDocPath, fullPath, pipelineIid } = el.dataset;
- // eslint-disable-next-line no-new
- new Vue({
- el,
- components: {
- DeprecatedKeywordNotification,
- },
- provide: {
- deprecatedKeywordsDocPath,
- fullPath,
- pipelineIid,
- },
- apolloProvider,
- render(createElement) {
- return createElement('deprecated-keyword-notification');
- },
- });
-};
diff --git a/app/assets/javascripts/pipelines/pipeline_tabs.js b/app/assets/javascripts/pipelines/pipeline_tabs.js
index 530917f0402..e7c00d89a10 100644
--- a/app/assets/javascripts/pipelines/pipeline_tabs.js
+++ b/app/assets/javascripts/pipelines/pipeline_tabs.js
@@ -8,34 +8,31 @@ import { getPipelineDefaultTab, reportToSentry } from './utils';
Vue.use(VueApollo);
-const createPipelineTabs = (selector, apolloProvider) => {
+export const createAppOptions = (selector, apolloProvider) => {
const el = document.querySelector(selector);
- if (!el) return;
+ if (!el) return null;
- const { dataset } = document.querySelector(selector);
+ const { dataset } = el;
const {
canGenerateCodequalityReports,
codequalityReportDownloadPath,
downloadablePathForReportType,
exposeSecurityDashboard,
exposeLicenseScanningData,
+ failedJobsCount,
+ failedJobsSummary,
+ fullPath,
graphqlResourceEtag,
pipelineIid,
pipelineProjectPath,
+ totalJobCount,
} = dataset;
const defaultTabValue = getPipelineDefaultTab(window.location.href);
- updateHistory({
- url: removeParams([TAB_QUERY_PARAM]),
- title: document.title,
- replace: true,
- });
-
- // eslint-disable-next-line no-new
- new Vue({
- el: selector,
+ return {
+ el,
components: {
PipelineTabs,
},
@@ -47,9 +44,13 @@ const createPipelineTabs = (selector, apolloProvider) => {
downloadablePathForReportType,
exposeSecurityDashboard: parseBoolean(exposeSecurityDashboard),
exposeLicenseScanningData: parseBoolean(exposeLicenseScanningData),
+ failedJobsCount,
+ failedJobsSummary: JSON.parse(failedJobsSummary),
+ fullPath,
graphqlResourceEtag,
pipelineIid,
pipelineProjectPath,
+ totalJobCount,
},
errorCaptured(err, _vm, info) {
reportToSentry('pipeline_tabs', `error: ${err}, info: ${info}`);
@@ -57,7 +58,18 @@ const createPipelineTabs = (selector, apolloProvider) => {
render(createElement) {
return createElement(PipelineTabs);
},
- });
+ };
};
-export { createPipelineTabs };
+export const createPipelineTabs = (options) => {
+ if (!options) return;
+
+ updateHistory({
+ url: removeParams([TAB_QUERY_PARAM]),
+ title: document.title,
+ replace: true,
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue(options);
+};
diff --git a/app/assets/javascripts/pipelines/pipeline_test_details.js b/app/assets/javascripts/pipelines/pipeline_test_details.js
index 46c7ec07d03..27ab2418440 100644
--- a/app/assets/javascripts/pipelines/pipeline_test_details.js
+++ b/app/assets/javascripts/pipelines/pipeline_test_details.js
@@ -8,8 +8,14 @@ Vue.use(Translate);
export const createTestDetails = (selector) => {
const el = document.querySelector(selector);
- const { blobPath, emptyStateImagePath, hasTestReport, summaryEndpoint, suiteEndpoint } =
- el?.dataset || {};
+ const {
+ blobPath,
+ emptyStateImagePath,
+ hasTestReport,
+ summaryEndpoint,
+ suiteEndpoint,
+ artifactsExpiredImagePath,
+ } = el?.dataset || {};
const testReportsStore = createTestReportsStore({
blobPath,
summaryEndpoint,
@@ -24,6 +30,7 @@ export const createTestDetails = (selector) => {
},
provide: {
emptyStateImagePath,
+ artifactsExpiredImagePath,
hasTestReport: parseBoolean(hasTestReport),
},
store: testReportsStore,
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index 45a6130826d..c99133fd251 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -60,7 +60,7 @@ Please update your Git repository remotes as soon as possible.`),
return {
text: __('Update username'),
attributes: [
- { variant: 'warning' },
+ { variant: 'confirm' },
{ category: 'primary' },
{ disabled: this.isRequestPending },
],
@@ -127,8 +127,7 @@ Please update your Git repository remotes as soon as possible.`),
v-gl-modal-directive="$options.modalId"
:disabled="newUsername === username"
:loading="isRequestPending"
- category="primary"
- variant="warning"
+ variant="confirm"
data-testid="username-change-confirmation-modal"
>{{ $options.buttonText }}</gl-button
>
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 7222c2bd908..09acf98001c 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,120 +1,122 @@
/* eslint-disable func-names */
import $ from 'jquery';
+import createFlash from '~/flash';
import Api from './api';
import { loadCSSFile } from './lib/utils/css_utils';
import { s__ } from './locale';
import ProjectSelectComboButton from './project_select_combo_button';
-const projectSelect = () => {
- loadCSSFile(gon.select2_css_path)
- .then(() => {
- $('.ajax-project-select').each(function (i, select) {
- let placeholder;
- const simpleFilter = $(select).data('simpleFilter') || false;
- const isInstantiated = $(select).data('select2');
- this.groupId = $(select).data('groupId');
- this.userId = $(select).data('userId');
- this.includeGroups = $(select).data('includeGroups');
- this.allProjects = $(select).data('allProjects') || false;
- this.orderBy = $(select).data('orderBy') || 'id';
- this.withIssuesEnabled = $(select).data('withIssuesEnabled');
- this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
- this.withShared =
- $(select).data('withShared') === undefined ? true : $(select).data('withShared');
- this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
- this.allowClear = $(select).data('allowClear') || false;
+const projectSelect = async () => {
+ await loadCSSFile(gon.select2_css_path);
- placeholder = s__('ProjectSelect|Search for project');
- if (this.includeGroups) {
- placeholder += s__('ProjectSelect| or group');
- }
+ $('.ajax-project-select').each(function (i, select) {
+ let placeholder;
+ const simpleFilter = $(select).data('simpleFilter') || false;
+ const isInstantiated = $(select).data('select2');
+ this.groupId = $(select).data('groupId');
+ this.userId = $(select).data('userId');
+ this.includeGroups = $(select).data('includeGroups');
+ this.allProjects = $(select).data('allProjects') || false;
+ this.orderBy = $(select).data('orderBy') || 'id';
+ this.withIssuesEnabled = $(select).data('withIssuesEnabled');
+ this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
+ this.withShared =
+ $(select).data('withShared') === undefined ? true : $(select).data('withShared');
+ this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
+ this.allowClear = $(select).data('allowClear') || false;
+
+ placeholder = s__('ProjectSelect|Search for project');
+ if (this.includeGroups) {
+ placeholder += s__('ProjectSelect| or group');
+ }
- $(select).select2({
- placeholder,
- minimumInputLength: 0,
- query: (query) => {
- let projectsCallback;
- const finalCallback = function (projects) {
- const data = {
- results: projects,
- };
- return query.callback(data);
+ $(select).select2({
+ placeholder,
+ minimumInputLength: 0,
+ query: (query) => {
+ let projectsCallback;
+ const finalCallback = function (projects) {
+ const data = {
+ results: projects,
+ };
+ return query.callback(data);
+ };
+ if (this.includeGroups) {
+ projectsCallback = function (projects) {
+ const groupsCallback = function (groups) {
+ const data = groups.concat(projects);
+ return finalCallback(data);
};
- if (this.includeGroups) {
- projectsCallback = function (projects) {
- const groupsCallback = function (groups) {
- const data = groups.concat(projects);
- return finalCallback(data);
- };
- return Api.groups(query.term, {}, groupsCallback);
- };
- } else {
- projectsCallback = finalCallback;
- }
- if (this.groupId) {
- return Api.groupProjects(
- this.groupId,
- query.term,
- {
- with_issues_enabled: this.withIssuesEnabled,
- with_merge_requests_enabled: this.withMergeRequestsEnabled,
- with_shared: this.withShared,
- include_subgroups: this.includeProjectsInSubgroups,
- order_by: 'similarity',
- simple: true,
- },
- projectsCallback,
- );
- } else if (this.userId) {
- return Api.userProjects(
- this.userId,
- query.term,
- {
- with_issues_enabled: this.withIssuesEnabled,
- with_merge_requests_enabled: this.withMergeRequestsEnabled,
- with_shared: this.withShared,
- include_subgroups: this.includeProjectsInSubgroups,
- },
- projectsCallback,
- );
- }
- return Api.projects(
- query.term,
- {
- order_by: this.orderBy,
- with_issues_enabled: this.withIssuesEnabled,
- with_merge_requests_enabled: this.withMergeRequestsEnabled,
- membership: !this.allProjects,
- },
- projectsCallback,
- );
- },
- id(project) {
- if (simpleFilter) return project.id;
- return JSON.stringify({
- name: project.name,
- url: project.web_url,
+ return Api.groups(query.term, {}, groupsCallback);
+ };
+ } else {
+ projectsCallback = finalCallback;
+ }
+ if (this.groupId) {
+ return Api.groupProjects(
+ this.groupId,
+ query.term,
+ {
+ with_issues_enabled: this.withIssuesEnabled,
+ with_merge_requests_enabled: this.withMergeRequestsEnabled,
+ with_shared: this.withShared,
+ include_subgroups: this.includeProjectsInSubgroups,
+ order_by: 'similarity',
+ simple: true,
+ },
+ projectsCallback,
+ ).catch(() => {
+ createFlash({
+ message: s__('ProjectSelect|Something went wrong while fetching projects'),
});
+ });
+ } else if (this.userId) {
+ return Api.userProjects(
+ this.userId,
+ query.term,
+ {
+ with_issues_enabled: this.withIssuesEnabled,
+ with_merge_requests_enabled: this.withMergeRequestsEnabled,
+ with_shared: this.withShared,
+ include_subgroups: this.includeProjectsInSubgroups,
+ },
+ projectsCallback,
+ );
+ }
+ return Api.projects(
+ query.term,
+ {
+ order_by: this.orderBy,
+ with_issues_enabled: this.withIssuesEnabled,
+ with_merge_requests_enabled: this.withMergeRequestsEnabled,
+ membership: !this.allProjects,
},
- text(project) {
- return project.name_with_namespace || project.name;
- },
+ projectsCallback,
+ );
+ },
+ id(project) {
+ if (simpleFilter) return project.id;
+ return JSON.stringify({
+ name: project.name,
+ url: project.web_url,
+ });
+ },
+ text(project) {
+ return project.name_with_namespace || project.name;
+ },
- initSelection(el, callback) {
- // eslint-disable-next-line promise/no-nesting
- return Api.project(el.val()).then(({ data }) => callback(data));
- },
+ initSelection(el, callback) {
+ return Api.project(el.val()).then(({ data }) => callback(data));
+ },
- allowClear: this.allowClear,
+ allowClear: this.allowClear,
- dropdownCssClass: 'ajax-project-dropdown',
- });
- if (isInstantiated || simpleFilter) return select;
- return new ProjectSelectComboButton(select);
- });
- })
- .catch(() => {});
+ dropdownCssClass: 'ajax-project-dropdown',
+ });
+ if (isInstantiated || simpleFilter) return select;
+ return new ProjectSelectComboButton(select);
+ });
};
export default () => {
diff --git a/app/assets/javascripts/projects/clusters_deprecation_alert/components/clusters_deprecation_alert.vue b/app/assets/javascripts/projects/clusters_deprecation_alert/components/clusters_deprecation_alert.vue
new file mode 100644
index 00000000000..e026b3e1060
--- /dev/null
+++ b/app/assets/javascripts/projects/clusters_deprecation_alert/components/clusters_deprecation_alert.vue
@@ -0,0 +1,23 @@
+<script>
+import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+export default {
+ components: {
+ GlAlert,
+ GlSprintf,
+ GlLink,
+ },
+ inject: ['message'],
+ docsLink: helpPagePath('user/infrastructure/clusters/migrate_to_gitlab_agent.md'),
+};
+</script>
+<template>
+ <gl-alert :dismissible="false" variant="warning" class="gl-mt-5">
+ <gl-sprintf :message="message">
+ <template #link="{ content }">
+ <gl-link :href="$options.docsLink">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/projects/clusters_deprecation_alert/index.js b/app/assets/javascripts/projects/clusters_deprecation_alert/index.js
new file mode 100644
index 00000000000..e17c1900dc1
--- /dev/null
+++ b/app/assets/javascripts/projects/clusters_deprecation_alert/index.js
@@ -0,0 +1,21 @@
+import Vue from 'vue';
+import ClustersDeprecationAlert from './components/clusters_deprecation_alert.vue';
+
+export default () => {
+ const el = document.querySelector('.js-clusters-deprecation-alert');
+
+ if (!el) {
+ return false;
+ }
+
+ const { message } = el.dataset;
+
+ return new Vue({
+ el,
+ name: 'ClustersDeprecationAlertRoot',
+ provide: {
+ message,
+ },
+ render: (createElement) => createElement(ClustersDeprecationAlert),
+ });
+};
diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue
index f9dd72119d1..d9aaa574fec 100644
--- a/app/assets/javascripts/projects/commit/components/form_modal.vue
+++ b/app/assets/javascripts/projects/commit/components/form_modal.vue
@@ -53,7 +53,7 @@ export default {
actionPrimary: {
text: this.i18n.actionPrimaryText,
attributes: [
- { variant: 'success' },
+ { variant: 'confirm' },
{ category: 'primary' },
{ 'data-testid': 'submit-commit' },
{ 'data-qa-selector': 'submit_commit_button' },
diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue
index c8a0a3417f3..884ef732144 100644
--- a/app/assets/javascripts/projects/commits/components/author_select.vue
+++ b/app/assets/javascripts/projects/commits/components/author_select.vue
@@ -57,7 +57,7 @@ export default {
if (authorParam) {
commitsSearchInput.setAttribute('disabled', true);
- commitsSearchInput.setAttribute('data-toggle', 'tooltip');
+ commitsSearchInput.dataset.toggle = 'tooltip';
commitsSearchInput.setAttribute('title', tooltipMessage);
this.currentAuthor = authorParam;
}
diff --git a/app/assets/javascripts/projects/compare/components/app.vue b/app/assets/javascripts/projects/compare/components/app.vue
index f2c1c843878..3945bed9649 100644
--- a/app/assets/javascripts/projects/compare/components/app.vue
+++ b/app/assets/javascripts/projects/compare/components/app.vue
@@ -104,7 +104,7 @@ export default {
@selectRevision="onSelectRevision"
/>
<div
- class="compare-ellipsis gl-display-flex gl-justify-content-center gl-align-items-center gl-my-4 gl-md-my-0"
+ class="compare-ellipsis gl-display-flex gl-justify-content-center gl-align-items-center gl-align-self-end gl-my-4 gl-md-my-0"
data-testid="ellipsis"
>
...
@@ -121,7 +121,7 @@ export default {
@selectRevision="onSelectRevision"
/>
</div>
- <div class="gl-mt-4">
+ <div class="gl-mt-6">
<gl-button category="primary" variant="confirm" @click="onSubmit">
{{ s__('CompareRevisions|Compare') }}
</gl-button>
diff --git a/app/assets/javascripts/projects/compare/components/revision_card.vue b/app/assets/javascripts/projects/compare/components/revision_card.vue
index 02a329221cc..d6ada24604d 100644
--- a/app/assets/javascripts/projects/compare/components/revision_card.vue
+++ b/app/assets/javascripts/projects/compare/components/revision_card.vue
@@ -1,5 +1,4 @@
<script>
-import { GlCard } from '@gitlab/ui';
import RepoDropdown from './repo_dropdown.vue';
import RevisionDropdown from './revision_dropdown.vue';
@@ -7,7 +6,6 @@ export default {
components: {
RepoDropdown,
RevisionDropdown,
- GlCard,
},
props: {
refsProjectPath: {
@@ -41,10 +39,10 @@ export default {
</script>
<template>
- <gl-card header-class="gl-py-2 gl-px-3 gl-font-weight-bold" body-class="gl-px-3">
- <template #header>
+ <div class="revision-card gl-flex-basis-half">
+ <h2 class="gl-font-size-h2">
{{ s__(`CompareRevisions|${revisionText}`) }}
- </template>
+ </h2>
<div class="gl-sm-display-flex gl-align-items-center">
<repo-dropdown
class="gl-sm-w-half"
@@ -61,5 +59,5 @@ export default {
v-on="$listeners"
/>
</div>
- </gl-card>
+ </div>
</template>
diff --git a/app/assets/javascripts/projects/default_project_templates.js b/app/assets/javascripts/projects/default_project_templates.js
index 6708b7bd9e2..3671b24b502 100644
--- a/app/assets/javascripts/projects/default_project_templates.js
+++ b/app/assets/javascripts/projects/default_project_templates.js
@@ -41,6 +41,10 @@ export default {
text: s__('ProjectTemplates|Pages/Hugo'),
icon: '.template-option .icon-hugo',
},
+ pelican: {
+ text: s__('ProjectTemplates|Pages/Pelican'),
+ icon: '.template-option .icon-pelican',
+ },
jekyll: {
text: s__('ProjectTemplates|Pages/Jekyll'),
icon: '.template-option .icon-jekyll',
@@ -105,4 +109,8 @@ export default {
text: s__('ProjectTemplates|Kotlin Native for Linux'),
icon: '.template-option .icon-gitlab_logo',
},
+ jsonnet: {
+ text: s__('ProjectTemplates|Jsonnet for Dynamic Child Pipelines'),
+ icon: '.template-option .icon-gitlab_logo',
+ },
};
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
index d4b1f7e57d8..35e7554aee2 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
@@ -12,11 +12,14 @@ export default {
DeploymentFrequencyCharts: () =>
import('ee_component/dora/components/deployment_frequency_charts.vue'),
LeadTimeCharts: () => import('ee_component/dora/components/lead_time_charts.vue'),
+ TimeToRestoreServiceCharts: () =>
+ import('ee_component/dora/components/time_to_restore_service_charts.vue'),
ProjectQualitySummary: () => import('ee_component/project_quality_summary/app.vue'),
},
piplelinesTabEvent: 'p_analytics_ci_cd_pipelines',
deploymentFrequencyTabEvent: 'p_analytics_ci_cd_deployment_frequency',
leadTimeTabEvent: 'p_analytics_ci_cd_lead_time',
+ timeToRestoreServiceTabEvent: 'p_analytics_ci_cd_time_to_restore_service',
inject: {
shouldRenderDoraCharts: {
type: Boolean,
@@ -37,7 +40,7 @@ export default {
const chartsToShow = ['pipelines'];
if (this.shouldRenderDoraCharts) {
- chartsToShow.push('deployment-frequency', 'lead-time');
+ chartsToShow.push('deployment-frequency', 'lead-time', 'time-to-restore-service');
}
if (this.shouldRenderQualitySummary) {
@@ -95,6 +98,13 @@ export default {
>
<lead-time-charts />
</gl-tab>
+ <gl-tab
+ :title="s__('DORA4Metrics|Time to restore service')"
+ data-testid="time-to-restore-service-tab"
+ @click="trackTabClick($options.timeToRestoreServiceTabEvent)"
+ >
+ <time-to-restore-service-charts />
+ </gl-tab>
</template>
<gl-tab v-if="shouldRenderQualitySummary" :title="s__('QualitySummary|Project quality')">
<project-quality-summary />
diff --git a/app/assets/javascripts/projects/project_import_gitlab_project.js b/app/assets/javascripts/projects/project_import_gitlab_project.js
index 4f222438500..0cbd4dbf2cf 100644
--- a/app/assets/javascripts/projects/project_import_gitlab_project.js
+++ b/app/assets/javascripts/projects/project_import_gitlab_project.js
@@ -1,4 +1,3 @@
-import $ from 'jquery';
import { convertToTitleCase, humanize, slugify } from '../lib/utils/text_utility';
import { getParameterValues } from '../lib/utils/url_utility';
import projectNew from './project_new';
@@ -22,24 +21,24 @@ const prepareParameters = () => {
export default () => {
let hasUserDefinedProjectName = false;
- const $projectName = $('.js-project-name');
- const $projectPath = $('.js-path-name');
+ const $projectName = document.querySelector('.js-project-name');
+ const $projectPath = document.querySelector('.js-path-name');
const { name, path } = prepareParameters();
// get the project name from the URL and set it as input value
- $projectName.val(name);
+ $projectName.value = name;
// get the path url and append it in the input
- $projectPath.val(path);
+ $projectPath.value = path;
// generate slug when project name changes
- $projectName.on('keyup', () => {
+ $projectName.addEventListener('keyup', () => {
projectNew.onProjectNameChange($projectName, $projectPath);
- hasUserDefinedProjectName = $projectName.val().trim().length > 0;
+ hasUserDefinedProjectName = $projectName.value.trim().length > 0;
});
// generate project name from the slug if one isn't set
- $projectPath.on('keyup', () =>
+ $projectPath.addEventListener('keyup', () =>
projectNew.onProjectPathChange($projectName, $projectPath, hasUserDefinedProjectName),
);
};
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 2bf13941f6f..2c2f957a75d 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -39,12 +39,18 @@ const validateImportCredentials = (url, user, password) => {
return importCredentialsValidationPromise;
};
-const onProjectNameChange = ($projectNameInput, $projectPathInput) => {
+const onProjectNameChangeJq = ($projectNameInput, $projectPathInput) => {
const slug = slugify(convertUnicodeToAscii($projectNameInput.val()));
$projectPathInput.val(slug);
};
-const onProjectPathChange = ($projectNameInput, $projectPathInput, hasExistingProjectName) => {
+const onProjectNameChange = ($projectNameInput, $projectPathInput) => {
+ const slug = slugify(convertUnicodeToAscii($projectNameInput.value));
+ // eslint-disable-next-line no-param-reassign
+ $projectPathInput.value = slug;
+};
+
+const onProjectPathChangeJq = ($projectNameInput, $projectPathInput, hasExistingProjectName) => {
const slug = $projectPathInput.val();
if (!hasExistingProjectName) {
@@ -52,6 +58,15 @@ const onProjectPathChange = ($projectNameInput, $projectPathInput, hasExistingPr
}
};
+const onProjectPathChange = ($projectNameInput, $projectPathInput, hasExistingProjectName) => {
+ const slug = $projectPathInput.value;
+
+ if (!hasExistingProjectName) {
+ // eslint-disable-next-line no-param-reassign
+ $projectNameInput.value = convertToTitleCase(humanize(slug, '[-_]'));
+ }
+};
+
const selectedNamespaceId = () => document.querySelector('[name="project[selected_namespace_id]"]');
const dropdownButton = () => document.querySelector('.js-group-namespace-dropdown > button');
const namespaceButton = () => document.querySelector('.js-group-namespace-button');
@@ -73,24 +88,31 @@ const validateGroupNamespaceDropdown = (e) => {
const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => {
const specialRepo = document.querySelector('.js-user-readme-repo');
-
- // eslint-disable-next-line @gitlab/no-global-event-off
- $projectNameInput.off('keyup change').on('keyup change', () => {
+ const projectNameInputListener = () => {
onProjectNameChange($projectNameInput, $projectPathInput);
- hasUserDefinedProjectName = $projectNameInput.val().trim().length > 0;
- hasUserDefinedProjectPath = $projectPathInput.val().trim().length > 0;
- });
+ hasUserDefinedProjectName = $projectNameInput.value.trim().length > 0;
+ hasUserDefinedProjectPath = $projectPathInput.value.trim().length > 0;
+ };
+
+ $projectNameInput.removeEventListener('keyup', projectNameInputListener);
+ $projectNameInput.addEventListener('keyup', projectNameInputListener);
+ $projectNameInput.removeEventListener('change', projectNameInputListener);
+ $projectNameInput.addEventListener('change', projectNameInputListener);
- // eslint-disable-next-line @gitlab/no-global-event-off
- $projectPathInput.off('keyup change').on('keyup change', () => {
+ const projectPathInputListener = () => {
onProjectPathChange($projectNameInput, $projectPathInput, hasUserDefinedProjectName);
- hasUserDefinedProjectPath = $projectPathInput.val().trim().length > 0;
+ hasUserDefinedProjectPath = $projectPathInput.value.trim().length > 0;
specialRepo.classList.toggle(
'gl-display-none',
- $projectPathInput.val() !== $projectPathInput.data('username'),
+ $projectPathInput.value !== $projectPathInput.dataset.username,
);
- });
+ };
+
+ $projectPathInput.removeEventListener('keyup', projectPathInputListener);
+ $projectPathInput.addEventListener('keyup', projectPathInputListener);
+ $projectPathInput.removeEventListener('change', projectPathInputListener);
+ $projectPathInput.addEventListener('change', projectPathInputListener);
document.querySelector('.js-create-project-button').addEventListener('click', (e) => {
validateGroupNamespaceDropdown(e);
@@ -99,17 +121,17 @@ const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => {
const deriveProjectPathFromUrl = ($projectImportUrl) => {
const $currentProjectName = $projectImportUrl
- .parents('.toggle-import-form')
- .find('#project_name');
+ .closest('.toggle-import-form')
+ .querySelector('#project_name');
const $currentProjectPath = $projectImportUrl
- .parents('.toggle-import-form')
- .find('#project_path');
+ .closest('.toggle-import-form')
+ .querySelector('#project_path');
if (hasUserDefinedProjectPath || $currentProjectPath.length === 0) {
return;
}
- let importUrl = $projectImportUrl.val().trim();
+ let importUrl = $projectImportUrl.value.trim();
if (importUrl.length === 0) {
return;
}
@@ -125,7 +147,9 @@ const deriveProjectPathFromUrl = ($projectImportUrl) => {
// extract everything after the last slash
const pathMatch = /\/([^/]+)$/.exec(importUrl);
if (pathMatch) {
- $currentProjectPath.val(pathMatch[1]);
+ // eslint-disable-next-line no-unused-vars
+ const [_, matchingString] = pathMatch;
+ $currentProjectPath.value = matchingString;
onProjectPathChange($currentProjectName, $currentProjectPath, false);
}
};
@@ -149,19 +173,20 @@ const bindHowToImport = () => {
const bindEvents = () => {
const $newProjectForm = $('#new_project');
- const $projectImportUrl = $('#project_import_url');
const $projectImportUrlUser = $('#project_import_url_user');
const $projectImportUrlPassword = $('#project_import_url_password');
const $projectImportUrlError = $('.js-import-url-error');
const $projectImportForm = $('form.js-project-import');
- const $projectPath = $('.tab-pane.active #project_path');
const $useTemplateBtn = $('.template-button > input');
- const $projectFieldsForm = $('.project-fields-form');
- const $selectedTemplateText = $('.selected-template');
const $changeTemplateBtn = $('.change-template');
- const $selectedIcon = $('.selected-icon');
- const $projectTemplateButtons = $('.project-templates-buttons');
- const $projectName = $('.tab-pane.active #project_name');
+
+ const $projectImportUrl = document.querySelector('#project_import_url');
+ const $projectPath = document.querySelector('.tab-pane.active #project_path');
+ const $projectFieldsForm = document.querySelector('.project-fields-form');
+ const $selectedIcon = document.querySelector('.selected-icon');
+ const $selectedTemplateText = document.querySelector('.selected-template');
+ const $projectName = document.querySelector('.tab-pane.active #project_name');
+ const $projectTemplateButtons = document.querySelectorAll('.project-templates-buttons');
if ($newProjectForm.length !== 1 && $projectImportForm.length !== 1) {
return;
@@ -170,31 +195,38 @@ const bindEvents = () => {
bindHowToImport();
$('.btn_import_gitlab_project').on('click contextmenu', () => {
- const importHref = $('a.btn_import_gitlab_project').attr('data-href');
- $('.btn_import_gitlab_project').attr(
- 'href',
- `${importHref}?namespace_id=${$(
- '#project_namespace_id',
- ).val()}&name=${$projectName.val()}&path=${$projectPath.val()}`,
- );
+ const importGitlabProjectBtn = document.querySelector('.btn_import_gitlab_project');
+ const projectNamespaceId = document.querySelector('#project_namespace_id');
+
+ const { href: importHref } = importGitlabProjectBtn.dataset;
+ const newHref = `${importHref}?namespace_id=${projectNamespaceId.value}&name=${$projectName.value}&path=${$projectPath.value}`;
+ importGitlabProjectBtn.setAttribute('href', newHref);
});
+ const clearChildren = (el) => {
+ while (el.firstChild) el.removeChild(el.firstChild);
+ };
+
function chooseTemplate() {
- $projectTemplateButtons.addClass('hidden');
- $projectFieldsForm.addClass('selected');
- $selectedIcon.empty();
+ $projectTemplateButtons.forEach((ptb) => ptb.classList.add('hidden'));
+ $projectFieldsForm.classList.add('selected');
- const $selectedTemplate = $(this);
- $selectedTemplate.prop('checked', true);
+ clearChildren($selectedIcon);
- const value = $selectedTemplate.val();
+ const $selectedTemplate = this;
+ $selectedTemplate.checked = true;
+ const { value } = $selectedTemplate;
const selectedTemplate = DEFAULT_PROJECT_TEMPLATES[value];
- $selectedTemplateText.text(selectedTemplate.text);
- $(selectedTemplate.icon).clone().addClass('d-block').appendTo($selectedIcon);
+ $selectedTemplateText.textContent = selectedTemplate.text;
+ const clone = document.querySelector(selectedTemplate.icon).cloneNode(true);
+ clone.classList.add('d-block');
+
+ $selectedIcon.append(clone);
+
+ const $activeTabProjectName = document.querySelector('.tab-pane.active #project_name');
+ const $activeTabProjectPath = document.querySelector('.tab-pane.active #project_path');
- const $activeTabProjectName = $('.tab-pane.active #project_name');
- const $activeTabProjectPath = $('.tab-pane.active #project_path');
$activeTabProjectName.focus();
setProjectNamePathHandlers($activeTabProjectName, $activeTabProjectPath);
}
@@ -216,8 +248,8 @@ const bindEvents = () => {
$useTemplateBtn.on('keypress', chooseTemplateOnEnter);
$changeTemplateBtn.on('click', () => {
- $projectTemplateButtons.removeClass('hidden');
- $projectFieldsForm.removeClass('selected');
+ $projectTemplateButtons.forEach((ptb) => ptb.classList.remove('hidden'));
+ $projectFieldsForm.classList.remove('selected');
$useTemplateBtn.prop('checked', false);
});
@@ -227,7 +259,7 @@ const bindEvents = () => {
const updateUrlPathWarningVisibility = async () => {
const { success: isUrlValid, cancelled } = await validateImportCredentials(
- $projectImportUrl.val(),
+ $projectImportUrl.value,
$projectImportUrlUser.val(),
$projectImportUrlPassword.val(),
);
@@ -235,7 +267,7 @@ const bindEvents = () => {
return;
}
- $projectImportUrl.toggleClass(invalidInputClass, !isUrlValid);
+ $projectImportUrl.classList.toggle(invalidInputClass, !isUrlValid);
$projectImportUrlError.toggleClass('hide', isUrlValid);
};
const debouncedUpdateUrlPathWarningVisibility = debounce(
@@ -244,20 +276,28 @@ const bindEvents = () => {
);
let isProjectImportUrlDirty = false;
- $projectImportUrl.on('blur', () => {
+ $projectImportUrl.addEventListener('blur', () => {
isProjectImportUrlDirty = true;
debouncedUpdateUrlPathWarningVisibility();
});
- $projectImportUrl.on('keyup', () => {
+ $projectImportUrl.addEventListener('keyup', () => {
deriveProjectPathFromUrl($projectImportUrl);
});
[$projectImportUrl, $projectImportUrlUser, $projectImportUrlPassword].forEach(($f) => {
- $f.on('input', () => {
- if (isProjectImportUrlDirty) {
- debouncedUpdateUrlPathWarningVisibility();
- }
- });
+ if ($f?.on) {
+ $f.on('input', () => {
+ if (isProjectImportUrlDirty) {
+ debouncedUpdateUrlPathWarningVisibility();
+ }
+ });
+ } else {
+ $f.addEventListener('input', () => {
+ if (isProjectImportUrlDirty) {
+ debouncedUpdateUrlPathWarningVisibility();
+ }
+ });
+ }
});
$projectImportForm.on('submit', async (e) => {
@@ -287,8 +327,8 @@ const bindEvents = () => {
$('.js-import-git-toggle-button').on('click', () => {
setProjectNamePathHandlers(
- $('.tab-pane.active #project_name'),
- $('.tab-pane.active #project_path'),
+ document.querySelector('.tab-pane.active #project_name'),
+ document.querySelector('.tab-pane.active #project_path'),
);
});
@@ -300,6 +340,8 @@ export default {
deriveProjectPathFromUrl,
onProjectNameChange,
onProjectPathChange,
+ onProjectNameChangeJq,
+ onProjectPathChangeJq,
};
export { bindHowToImport };
diff --git a/app/assets/javascripts/projects/project_visibility.js b/app/assets/javascripts/projects/project_visibility.js
index c962554c9f4..d299e106b14 100644
--- a/app/assets/javascripts/projects/project_visibility.js
+++ b/app/assets/javascripts/projects/project_visibility.js
@@ -1,4 +1,3 @@
-import $ from 'jquery';
import { escape } from 'lodash';
import { __, sprintf } from '~/locale';
import eventHub from '~/projects/new/event_hub';
@@ -63,9 +62,8 @@ export default function initProjectVisibilitySelector() {
const namespaceSelector = document.querySelector('select.js-select-namespace');
if (namespaceSelector) {
- $('.select2.js-select-namespace').on('change', () =>
- handleSelect2DropdownChange(namespaceSelector),
- );
+ const el = document.querySelector('.select2.js-select-namespace');
+ el.addEventListener('change', () => handleSelect2DropdownChange(namespaceSelector));
handleSelect2DropdownChange(namespaceSelector);
}
}
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/branch_dropdown.vue b/app/assets/javascripts/projects/settings/branch_rules/components/branch_dropdown.vue
new file mode 100644
index 00000000000..6bbe0ab7d5f
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/branch_dropdown.vue
@@ -0,0 +1,110 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlDropdownDivider, GlSearchBoxByType } from '@gitlab/ui';
+import { createAlert } from '~/flash';
+import { __, sprintf } from '~/locale';
+import branchesQuery from '../queries/branches.query.graphql';
+
+export const i18n = {
+ fetchBranchesError: __('An error occurred while fetching branches.'),
+ noMatch: __('No matching results'),
+};
+
+export default {
+ i18n,
+ name: 'BranchDropdown',
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlSearchBoxByType,
+ },
+ apollo: {
+ branchNames: {
+ query: branchesQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ searchPattern: `*${this.searchTerm}*`,
+ };
+ },
+ update({ project: { repository = {} } } = {}) {
+ return repository.branchNames || [];
+ },
+ error(e) {
+ createAlert({
+ message: this.$options.i18n.fetchBranchesError,
+ captureError: true,
+ error: e,
+ });
+ },
+ },
+ },
+ props: {
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ searchTerm: '',
+ branchNames: [],
+ };
+ },
+ computed: {
+ createButtonLabel() {
+ return sprintf(__('Create wildcard: %{searchTerm}'), { searchTerm: this.searchTerm });
+ },
+ shouldRenderCreateButton() {
+ return this.searchTerm && !this.branchNames.includes(this.searchTerm);
+ },
+ isLoading() {
+ return this.$apollo.queries.branchNames.loading;
+ },
+ },
+ methods: {
+ selectBranch(selected) {
+ this.$emit('input', selected);
+ },
+ createWildcard() {
+ this.$emit('createWildcard', this.searchTerm);
+ },
+ isSelected(branch) {
+ return this.value === branch;
+ },
+ },
+};
+</script>
+<template>
+ <gl-dropdown :text="value || branchNames[0]">
+ <gl-search-box-by-type
+ v-model.trim="searchTerm"
+ data-testid="branch-search"
+ debounce="250"
+ :is-loading="isLoading"
+ />
+ <gl-dropdown-item
+ v-for="branch in branchNames"
+ :key="branch"
+ :is-checked="isSelected(branch)"
+ is-check-item
+ @click="selectBranch(branch)"
+ >
+ {{ branch }}
+ </gl-dropdown-item>
+ <gl-dropdown-item v-if="!branchNames.length && !isLoading" data-testid="no-data">{{
+ $options.i18n.noMatch
+ }}</gl-dropdown-item>
+ <template v-if="shouldRenderCreateButton">
+ <gl-dropdown-divider />
+ <gl-dropdown-item data-testid="create-wildcard-button" @click="createWildcard">
+ {{ createButtonLabel }}
+ </gl-dropdown-item>
+ </template>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/rule_edit.vue b/app/assets/javascripts/projects/settings/branch_rules/components/rule_edit.vue
new file mode 100644
index 00000000000..c2e7f4e9b1b
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/rule_edit.vue
@@ -0,0 +1,38 @@
+<script>
+import { GlFormGroup } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { getParameterByName } from '~/lib/utils/url_utility';
+import BranchDropdown from './branch_dropdown.vue';
+
+export default {
+ name: 'RuleEdit',
+ i18n: {
+ branch: __('Branch'),
+ },
+ components: { BranchDropdown, GlFormGroup },
+ props: {
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ branch: getParameterByName('branch'),
+ };
+ },
+};
+</script>
+
+<template>
+ <gl-form-group :label="$options.i18n.branch">
+ <branch-dropdown
+ id="branches"
+ v-model="branch"
+ class="gl-w-half"
+ :project-path="projectPath"
+ @createWildcard="branch = $event"
+ />
+ </gl-form-group>
+ <!-- TODO - Add branch protections (https://gitlab.com/gitlab-org/gitlab/-/issues/362212) -->
+</template>
diff --git a/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
new file mode 100644
index 00000000000..8452542540e
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
@@ -0,0 +1,26 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import RuleEdit from './components/rule_edit.vue';
+
+export default function mountBranchRules(el) {
+ if (!el) {
+ return null;
+ }
+
+ Vue.use(VueApollo);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ const { projectPath } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider,
+ render(h) {
+ return h(RuleEdit, { props: { projectPath } });
+ },
+ });
+}
diff --git a/app/assets/javascripts/projects/settings/branch_rules/queries/branches.query.graphql b/app/assets/javascripts/projects/settings/branch_rules/queries/branches.query.graphql
new file mode 100644
index 00000000000..a532b544757
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/queries/branches.query.graphql
@@ -0,0 +1,8 @@
+query getBranches($projectPath: ID!, $searchPattern: String!) {
+ project(fullPath: $projectPath) {
+ id
+ repository {
+ branchNames(searchPattern: $searchPattern, limit: 100, offset: 0)
+ }
+ }
+}
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
new file mode 100644
index 00000000000..ada951f6867
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
@@ -0,0 +1,16 @@
+<script>
+import { __ } from '~/locale';
+
+export default {
+ name: 'BranchRules',
+ i18n: { heading: __('Branch') },
+};
+</script>
+
+<template>
+ <div>
+ <strong>{{ $options.i18n.heading }}</strong>
+
+ <!-- TODO - List branch rules (https://gitlab.com/gitlab-org/gitlab/-/issues/362217) -->
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js b/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js
new file mode 100644
index 00000000000..abe0b93081e
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js
@@ -0,0 +1,13 @@
+import Vue from 'vue';
+import BranchRulesApp from '~/projects/settings/repository/branch_rules/app.vue';
+
+export default function mountBranchRules(el) {
+ if (!el) return null;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(BranchRulesApp);
+ },
+ });
+}
diff --git a/app/assets/javascripts/related_issues/constants.js b/app/assets/javascripts/related_issues/constants.js
index f911468d8f1..3516836952f 100644
--- a/app/assets/javascripts/related_issues/constants.js
+++ b/app/assets/javascripts/related_issues/constants.js
@@ -2,6 +2,7 @@ import { __, sprintf } from '~/locale';
export const issuableTypesMap = {
ISSUE: 'issue',
+ INCIDENT: 'incident',
EPIC: 'epic',
MERGE_REQUEST: 'merge_request',
};
@@ -25,6 +26,11 @@ export const autoCompleteTextMap = {
{ emphasisStart: '<', emphasisEnd: '>' },
false,
),
+ [issuableTypesMap.INCIDENT]: sprintf(
+ __(' or %{emphasisStart}#id%{emphasisEnd}'),
+ { emphasisStart: '<', emphasisEnd: '>' },
+ false,
+ ),
[issuableTypesMap.EPIC]: sprintf(
__(' or %{emphasisStart}&epic id%{emphasisEnd}'),
{ emphasisStart: '<', emphasisEnd: '>' },
@@ -45,6 +51,7 @@ export const autoCompleteTextMap = {
export const inputPlaceholderTextMap = {
[issuableTypesMap.ISSUE]: __('Paste issue link'),
+ [issuableTypesMap.INCIDENT]: __('Paste link'),
[issuableTypesMap.EPIC]: __('Paste epic link'),
[issuableTypesMap.MERGE_REQUEST]: __('Enter merge request URLs'),
};
@@ -88,6 +95,7 @@ export const addRelatedItemErrorMap = {
*/
export const issuableIconMap = {
[issuableTypesMap.ISSUE]: 'issues',
+ [issuableTypesMap.INCIDENT]: 'issues',
[issuableTypesMap.EPIC]: 'epic',
};
@@ -107,6 +115,7 @@ export const PathIdSeparator = {
export const issuablesBlockHeaderTextMap = {
[issuableTypesMap.ISSUE]: __('Linked issues'),
+ [issuableTypesMap.INCIDENT]: __('Related incidents or issues'),
[issuableTypesMap.EPIC]: __('Linked epics'),
};
@@ -122,10 +131,12 @@ export const issuablesBlockAddButtonTextMap = {
export const issuablesFormCategoryHeaderTextMap = {
[issuableTypesMap.ISSUE]: __('The current issue'),
+ [issuableTypesMap.INCIDENT]: __('The current incident'),
[issuableTypesMap.EPIC]: __('The current epic'),
};
export const issuablesFormInputTextMap = {
[issuableTypesMap.ISSUE]: __('the following issue(s)'),
+ [issuableTypesMap.INCIDENT]: __('the following incident(s) or issue(s)'),
[issuableTypesMap.EPIC]: __('the following epic(s)'),
};
diff --git a/app/assets/javascripts/related_issues/index.js b/app/assets/javascripts/related_issues/index.js
index b61f1cf2470..655ec57bc3d 100644
--- a/app/assets/javascripts/related_issues/index.js
+++ b/app/assets/javascripts/related_issues/index.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import RelatedIssuesRoot from './components/related_issues_root.vue';
-export default function initRelatedIssues() {
+export default function initRelatedIssues(issueType = 'issue') {
const relatedIssuesRootElement = document.querySelector('.js-related-issues-root');
if (relatedIssuesRootElement) {
// eslint-disable-next-line no-new
@@ -21,6 +21,7 @@ export default function initRelatedIssues() {
showCategorizedIssues: parseBoolean(
relatedIssuesRootElement.dataset.showCategorizedIssues,
),
+ issuableType: issueType,
autoCompleteEpics: false,
},
}),
diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue
index 59fa2fca736..a949a9d1318 100644
--- a/app/assets/javascripts/releases/components/app_index.vue
+++ b/app/assets/javascripts/releases/components/app_index.vue
@@ -229,7 +229,7 @@ export default {
};
</script>
<template>
- <div class="flex flex-column mt-2">
+ <div class="gl-display-flex gl-flex-direction-column gl-mt-3">
<div class="gl-align-self-end gl-mb-3">
<releases-sort :value="sort" class="gl-mr-2" @input="onSortChanged" />
diff --git a/app/assets/javascripts/releases/components/release_block_header.vue b/app/assets/javascripts/releases/components/release_block_header.vue
index 89bc314db89..def38780545 100644
--- a/app/assets/javascripts/releases/components/release_block_header.vue
+++ b/app/assets/javascripts/releases/components/release_block_header.vue
@@ -72,7 +72,7 @@ export default {
category="primary"
variant="default"
icon="pencil"
- class="gl-mr-3 js-edit-button ml-2 pb-2"
+ class="gl-mr-3 js-edit-button gl-ml-3 gl-pb-3"
:title="$options.i18n.editButton"
:aria-label="$options.i18n.editButton"
:href="editLink"
diff --git a/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql b/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql
index 8a5613c75d2..e0de6d12b13 100644
--- a/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql
+++ b/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql
@@ -1,5 +1,6 @@
fragment Release on Release {
__typename
+ id
name
tagName
tagPath
diff --git a/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql b/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql
index 1823a327350..236d266a40a 100644
--- a/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql
+++ b/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql
@@ -1,4 +1,5 @@
fragment ReleaseForEditing on Release {
+ id
name
tagName
description
diff --git a/app/assets/javascripts/releases/graphql/mutations/create_release.mutation.graphql b/app/assets/javascripts/releases/graphql/mutations/create_release.mutation.graphql
index 56bfe7c23d6..7344772adb9 100644
--- a/app/assets/javascripts/releases/graphql/mutations/create_release.mutation.graphql
+++ b/app/assets/javascripts/releases/graphql/mutations/create_release.mutation.graphql
@@ -1,6 +1,7 @@
mutation createRelease($input: ReleaseCreateInput!) {
releaseCreate(input: $input) {
release {
+ id
links {
selfUrl
}
diff --git a/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
index bda7ac52a47..61a06f268bd 100644
--- a/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
+++ b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
@@ -13,6 +13,7 @@ query allReleases(
__typename
nodes {
__typename
+ id
name
tagName
tagPath
diff --git a/app/assets/javascripts/reports/components/summary_row.vue b/app/assets/javascripts/reports/components/summary_row.vue
index 7419b5b59d6..92d0783749e 100644
--- a/app/assets/javascripts/reports/components/summary_row.vue
+++ b/app/assets/javascripts/reports/components/summary_row.vue
@@ -71,7 +71,7 @@ export default {
<gl-loading-icon
v-if="statusIcon === 'loading'"
css-class="report-block-list-loading-icon"
- size="md"
+ size="lg"
/>
<ci-icon v-else :status="iconStatus" :size="statusIconSize" data-testid="summary-row-icon" />
</div>
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 3729bd4c601..280455c3fed 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -6,9 +6,9 @@ import BlobHeader from '~/blob/components/blob_header.vue';
import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { isLoggedIn } from '~/lib/utils/common_utils';
+import { isLoggedIn, handleLocationHash } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
-import { redirectTo, getLocationHash } from '~/lib/utils/url_utility';
+import { redirectTo } from '~/lib/utils/url_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
import CodeIntelligence from '~/code_navigation/components/app.vue';
@@ -183,7 +183,7 @@ export default {
this.isLoadingLegacyViewer = true;
axios
.get(`${this.blobInfo.webPath}?format=json&viewer=${type}`)
- .then(({ data: { html, binary } }) => {
+ .then(async ({ data: { html, binary } }) => {
if (type === SIMPLE_BLOB_VIEWER) {
this.isRenderingLegacyTextViewer = true;
@@ -197,20 +197,14 @@ export default {
this.legacyRichViewer = html;
}
- this.scrollToHash();
this.isBinary = binary;
this.isLoadingLegacyViewer = false;
+
+ await this.$nextTick();
+ handleLocationHash(); // Ensures that we scroll to the hash when async content is loaded
})
.catch(() => this.displayError());
},
- scrollToHash() {
- const hash = getLocationHash();
- if (hash) {
- // Ensures the browser's native scroll to hash is triggered for async content
- window.location.hash = '';
- window.location.hash = hash;
- }
- },
displayError() {
createFlash({ message: __('An error occurred while loading the file. Please try again.') });
},
@@ -233,6 +227,9 @@ export default {
setForkTarget(target) {
this.forkTarget = target;
},
+ onCopy() {
+ navigator.clipboard.writeText(this.blobInfo.rawTextBlob);
+ },
},
};
</script>
@@ -248,7 +245,9 @@ export default {
:active-viewer-type="viewer.type"
:has-render-error="hasRenderError"
:show-path="false"
+ :override-copy="glFeatures.highlightJs"
@viewer-changed="switchViewer"
+ @copy="onCopy"
>
<template #actions>
<web-ide-link
diff --git a/app/assets/javascripts/repository/components/blob_viewers/index.js b/app/assets/javascripts/repository/components/blob_viewers/index.js
index 81d2168e2ce..3e6d2e675ed 100644
--- a/app/assets/javascripts/repository/components/blob_viewers/index.js
+++ b/app/assets/javascripts/repository/components/blob_viewers/index.js
@@ -9,6 +9,7 @@ const viewers = {
lfs: () => import('./lfs_viewer.vue'),
audio: () => import('./audio_viewer.vue'),
svg: () => import('./image_viewer.vue'),
+ sketch: () => import('./sketch_viewer.vue'),
};
export const loadViewer = (type, isUsingLfs) => {
diff --git a/app/assets/javascripts/repository/components/blob_viewers/sketch_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/sketch_viewer.vue
new file mode 100644
index 00000000000..b48af02e541
--- /dev/null
+++ b/app/assets/javascripts/repository/components/blob_viewers/sketch_viewer.vue
@@ -0,0 +1,31 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import SketchLoader from '~/blob/sketch';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ },
+ props: {
+ blob: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ url: this.blob.rawPath,
+ };
+ },
+ mounted() {
+ // eslint-disable-next-line no-new
+ new SketchLoader(this.$refs.viewer);
+ },
+};
+</script>
+
+<template>
+ <div ref="viewer" class="file-content" :data-endpoint="url" data-testid="sketch">
+ <gl-loading-icon class="my-4 js-loading-icon" size="lg" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 03dd7c6fada..d24d7648f1b 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -114,7 +114,7 @@ export default {
<template>
<div class="well-segment commit gl-p-5 gl-w-full gl-display-flex">
- <gl-loading-icon v-if="isLoading" size="md" color="dark" class="m-auto" />
+ <gl-loading-icon v-if="isLoading" size="lg" color="dark" class="m-auto" />
<template v-else-if="commit">
<user-avatar-link
v-if="commit.author"
diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue
index dc5a031c9f3..4935b8029f9 100644
--- a/app/assets/javascripts/repository/components/preview/index.vue
+++ b/app/assets/javascripts/repository/components/preview/index.vue
@@ -64,7 +64,7 @@ export default {
</div>
</div>
<div class="blob-viewer" data-qa-selector="blob_viewer_content" itemprop="about">
- <gl-loading-icon v-if="loading > 0" size="md" color="dark" class="my-4 mx-auto" />
+ <gl-loading-icon v-if="loading > 0" size="lg" color="dark" class="my-4 mx-auto" />
<div
v-else-if="readme"
ref="readme"
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index c2323d6b286..41f7a4b147f 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlButton } from '@gitlab/ui';
+import { GlSkeletonLoader, GlButton } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { sprintf, __ } from '~/locale';
import getRefMixin from '../../mixins/get_ref';
@@ -10,7 +10,7 @@ import TableRow from './row.vue';
export default {
components: {
- GlSkeletonLoading,
+ GlSkeletonLoader,
TableHeader,
TableRow,
ParentRow,
@@ -158,11 +158,15 @@ export default {
</template>
<template v-if="isLoading">
<tr v-for="i in 5" :key="i" aria-hidden="true">
- <td><gl-skeleton-loading :lines="1" class="h-auto" /></td>
+ <td><gl-skeleton-loader :lines="1" /></td>
<td class="gl-display-none gl-sm-display-block">
- <gl-skeleton-loading :lines="1" class="h-auto" />
+ <gl-skeleton-loader :lines="1" />
+ </td>
+ <td>
+ <div class="gl-display-flex gl-lg-justify-content-end">
+ <gl-skeleton-loader :equal-width-lines="true" :lines="1" />
+ </div>
</td>
- <td><gl-skeleton-loading :lines="1" class="ml-auto h-auto w-50" /></td>
</tr>
</template>
<template v-if="hasMore">
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 7aac35e7613..2b910109f7d 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -2,7 +2,7 @@
import {
GlBadge,
GlLink,
- GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+ GlSkeletonLoader,
GlTooltipDirective,
GlLoadingIcon,
GlIcon,
@@ -25,7 +25,7 @@ export default {
components: {
GlBadge,
GlLink,
- GlSkeletonLoading,
+ GlSkeletonLoader,
GlLoadingIcon,
GlIcon,
TimeagoTooltip,
@@ -277,12 +277,12 @@ export default {
class="str-truncated-100 tree-commit-link"
/>
<gl-intersection-observer @appear="rowAppeared" @disappear="rowDisappeared">
- <gl-skeleton-loading v-if="showSkeletonLoader" :lines="1" class="h-auto" />
+ <gl-skeleton-loader v-if="showSkeletonLoader" :lines="1" />
</gl-intersection-observer>
</td>
<td class="tree-time-ago text-right cursor-default">
<timeago-tooltip v-if="commitData" :time="commitData.committedDate" />
- <gl-skeleton-loading v-if="showSkeletonLoader" :lines="1" class="ml-auto h-auto w-50" />
+ <gl-skeleton-loader v-if="showSkeletonLoader" :lines="1" />
</td>
</tr>
</template>
diff --git a/app/assets/javascripts/rest_api.js b/app/assets/javascripts/rest_api.js
index 0f8e6945bf9..0b6c5063129 100644
--- a/app/assets/javascripts/rest_api.js
+++ b/app/assets/javascripts/rest_api.js
@@ -5,6 +5,7 @@ export * from './api/markdown_api';
export * from './api/bulk_imports_api';
export * from './api/namespaces_api';
export * from './api/tags_api';
+export * from './api/alert_management_alerts_api';
// Note: It's not possible to spy on methods imported from this file in
// Jest tests.
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index a3abc8b8e90..9de67015094 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -8,7 +8,7 @@ import axios from './lib/utils/axios_utils';
import { sprintf, s__, __ } from './locale';
const updateSidebarClasses = (layoutPage, rightSidebar) => {
- if (window.innerWidth >= 768) {
+ if (window.innerWidth >= 992) {
layoutPage.classList.remove('right-sidebar-expanded', 'right-sidebar-collapsed');
rightSidebar.classList.remove('right-sidebar-collapsed');
rightSidebar.classList.add('right-sidebar-expanded');
diff --git a/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue b/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue
index c3f317b40b0..06a8eb790fc 100644
--- a/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue
+++ b/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue
@@ -1,14 +1,16 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlBadge, GlTab, GlTooltipDirective } from '@gitlab/ui';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { redirectTo } from '~/lib/utils/url_utility';
+import { formatJobCount } from '../utils';
import RunnerDeleteButton from '../components/runner_delete_button.vue';
import RunnerEditButton from '../components/runner_edit_button.vue';
import RunnerPauseButton from '../components/runner_pause_button.vue';
import RunnerHeader from '../components/runner_header.vue';
import RunnerDetails from '../components/runner_details.vue';
+import RunnerJobs from '../components/runner_jobs.vue';
import { I18N_FETCH_ERROR } from '../constants';
import runnerQuery from '../graphql/show/runner.query.graphql';
import { captureException } from '../sentry_utils';
@@ -17,11 +19,14 @@ import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_lo
export default {
name: 'AdminRunnerShowApp',
components: {
+ GlBadge,
+ GlTab,
RunnerDeleteButton,
RunnerEditButton,
RunnerPauseButton,
RunnerHeader,
RunnerDetails,
+ RunnerJobs,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -63,6 +68,9 @@ export default {
canDelete() {
return this.runner.userPermissions?.deleteRunner;
},
+ jobCount() {
+ return formatJobCount(this.runner?.jobCount);
+ },
},
errorCaptured(error) {
this.reportToSentry(error);
@@ -88,6 +96,24 @@ export default {
</template>
</runner-header>
- <runner-details :runner="runner" />
+ <runner-details :runner="runner">
+ <template #jobs-tab>
+ <gl-tab>
+ <template #title>
+ {{ s__('Runners|Jobs') }}
+ <gl-badge
+ v-if="jobCount"
+ data-testid="job-count-badge"
+ class="gl-tab-counter-badge"
+ size="sm"
+ >
+ {{ jobCount }}
+ </gl-badge>
+ </template>
+
+ <runner-jobs v-if="runner" :runner="runner" />
+ </gl-tab>
+ </template>
+ </runner-details>
</div>
</template>
diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
index c2bb635e056..a90ef2d3530 100644
--- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
@@ -10,6 +10,7 @@ import RegistrationDropdown from '../components/registration/registration_dropdo
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerBulkDelete from '../components/runner_bulk_delete.vue';
import RunnerList from '../components/runner_list.vue';
+import RunnerListEmptyState from '../components/runner_list_empty_state.vue';
import RunnerName from '../components/runner_name.vue';
import RunnerStats from '../components/stat/runner_stats.vue';
import RunnerPagination from '../components/runner_pagination.vue';
@@ -35,6 +36,7 @@ import {
fromUrlQueryToSearch,
fromSearchToUrl,
fromSearchToVariables,
+ isSearchFiltered,
} from '../runner_search_utils';
import { captureException } from '../sentry_utils';
@@ -91,6 +93,7 @@ export default {
RunnerFilteredSearchBar,
RunnerBulkDelete,
RunnerList,
+ RunnerListEmptyState,
RunnerName,
RunnerStats,
RunnerPagination,
@@ -98,7 +101,7 @@ export default {
RunnerActionsCell,
},
mixins: [glFeatureFlagMixin()],
- inject: ['localMutations'],
+ inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath', 'localMutations'],
props: {
registrationToken: {
type: String,
@@ -190,6 +193,9 @@ export default {
// Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/353981
return this.glFeatures.adminRunnersBulkDelete;
},
+ isSearchFiltered() {
+ return isSearchFiltered(this.search);
+ },
},
watch: {
search: {
@@ -298,9 +304,13 @@ export default {
:stale-runners-count="staleRunnersTotal"
/>
- <div v-if="noRunnersFound" class="gl-text-center gl-p-5">
- {{ __('No runners found') }}
- </div>
+ <runner-list-empty-state
+ v-if="noRunnersFound"
+ :registration-token="registrationToken"
+ :is-search-filtered="isSearchFiltered"
+ :svg-path="emptyStateSvgPath"
+ :filtered-svg-path="emptyStateFilteredSvgPath"
+ />
<template v-else>
<runner-bulk-delete v-if="isBulkDeleteEnabled" />
<runner-list
diff --git a/app/assets/javascripts/runner/admin_runners/index.js b/app/assets/javascripts/runner/admin_runners/index.js
index b1d8442bb32..7bb6cd5689e 100644
--- a/app/assets/javascripts/runner/admin_runners/index.js
+++ b/app/assets/javascripts/runner/admin_runners/index.js
@@ -34,6 +34,8 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
registrationToken,
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ emptyStateSvgPath,
+ emptyStateFilteredSvgPath,
} = el.dataset;
const { cacheConfig, typeDefs, localMutations } = createLocalState();
@@ -50,6 +52,8 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
localMutations,
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ emptyStateSvgPath,
+ emptyStateFilteredSvgPath,
},
render(h) {
return h(AdminRunnersApp, {
diff --git a/app/assets/javascripts/runner/components/cells/runner_status_cell.vue b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue
index 93f86ae2a2c..a48db9f8ac8 100644
--- a/app/assets/javascripts/runner/components/cells/runner_status_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue
@@ -7,6 +7,8 @@ import RunnerPausedBadge from '../runner_paused_badge.vue';
export default {
components: {
RunnerStatusBadge,
+ RunnerUpgradeStatusBadge: () =>
+ import('ee_component/runner/components/runner_upgrade_status_badge.vue'),
RunnerPausedBadge,
},
directives: {
@@ -33,6 +35,11 @@ export default {
size="sm"
class="gl-display-inline-block gl-max-w-full gl-text-truncate"
/>
+ <runner-upgrade-status-badge
+ :runner="runner"
+ size="sm"
+ class="gl-display-inline-block gl-max-w-full gl-text-truncate"
+ />
<runner-paused-badge
v-if="paused"
size="sm"
diff --git a/app/assets/javascripts/runner/components/registration/registration_dropdown.vue b/app/assets/javascripts/runner/components/registration/registration_dropdown.vue
index bb2a8ddf151..212ad5fa5a0 100644
--- a/app/assets/javascripts/runner/components/registration/registration_dropdown.vue
+++ b/app/assets/javascripts/runner/components/registration/registration_dropdown.vue
@@ -1,11 +1,5 @@
<script>
-import {
- GlFormGroup,
- GlDropdown,
- GlDropdownForm,
- GlDropdownItem,
- GlDropdownDivider,
-} from '@gitlab/ui';
+import { GlDropdown, GlDropdownForm, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
import { s__ } from '~/locale';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants';
@@ -17,10 +11,8 @@ export default {
showInstallationInstructions: s__(
'Runners|Show runner installation and registration instructions',
),
- registrationToken: s__('Runners|Registration token'),
},
components: {
- GlFormGroup,
GlDropdown,
GlDropdownForm,
GlDropdownItem,
@@ -45,7 +37,6 @@ export default {
data() {
return {
currentRegistrationToken: this.registrationToken,
- instructionsModalOpened: false,
};
},
computed: {
@@ -64,15 +55,7 @@ export default {
},
methods: {
onShowInstructionsClick() {
- // Rendering the modal on demand, to avoid
- // loading instructions prematurely from API.
- this.instructionsModalOpened = true;
-
- this.$nextTick(() => {
- // $refs.runnerInstructionsModal is defined in
- // the tick after the modal is rendered
- this.$refs.runnerInstructionsModal.show();
- });
+ this.$refs.runnerInstructionsModal.show();
},
onTokenReset(token) {
this.currentRegistrationToken = token;
@@ -94,7 +77,6 @@ export default {
<gl-dropdown-item @click.capture.native.stop="onShowInstructionsClick">
{{ $options.i18n.showInstallationInstructions }}
<runner-instructions-modal
- v-if="instructionsModalOpened"
ref="runnerInstructionsModal"
:registration-token="currentRegistrationToken"
data-testid="runner-instructions-modal"
@@ -102,9 +84,7 @@ export default {
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-form class="gl-p-4!">
- <gl-form-group class="gl-mb-0" :label="$options.i18n.registrationToken">
- <registration-token :value="currentRegistrationToken" />
- </gl-form-group>
+ <registration-token input-id="token-value" :value="currentRegistrationToken" />
</gl-dropdown-form>
<gl-dropdown-divider />
<registration-token-reset-dropdown-item :type="type" @tokenReset="onTokenReset" />
diff --git a/app/assets/javascripts/runner/components/registration/registration_token.vue b/app/assets/javascripts/runner/components/registration/registration_token.vue
index 68c6429a056..6b4e6a929b7 100644
--- a/app/assets/javascripts/runner/components/registration/registration_token.vue
+++ b/app/assets/javascripts/runner/components/registration/registration_token.vue
@@ -6,13 +6,27 @@ export default {
components: {
InputCopyToggleVisibility,
},
+ i18n: {
+ registrationToken: s__('Runners|Registration token'),
+ },
props: {
+ inputId: {
+ type: String,
+ required: true,
+ },
value: {
type: String,
required: false,
default: '',
},
},
+ computed: {
+ formInputGroupProps() {
+ return {
+ id: this.inputId,
+ };
+ },
+ },
methods: {
onCopy() {
// value already in the clipboard, simply notify the user
@@ -26,8 +40,10 @@ export default {
<input-copy-toggle-visibility
class="gl-m-0"
:value="value"
- data-testid="token-value"
+ :label="$options.i18n.registrationToken"
+ :label-for="inputId"
:copy-button-title="$options.I18N_COPY_BUTTON_TITLE"
+ :form-input-group-props="formInputGroupProps"
@copy="onCopy"
/>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_details.vue b/app/assets/javascripts/runner/components/runner_details.vue
index 3734f436034..75ddec6c716 100644
--- a/app/assets/javascripts/runner/components/runner_details.vue
+++ b/app/assets/javascripts/runner/components/runner_details.vue
@@ -1,26 +1,24 @@
<script>
-import { GlBadge, GlTabs, GlTab, GlIntersperse } from '@gitlab/ui';
+import { GlTabs, GlTab, GlIntersperse } from '@gitlab/ui';
import { s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants';
-import { formatJobCount } from '../utils';
import RunnerDetail from './runner_detail.vue';
import RunnerGroups from './runner_groups.vue';
import RunnerProjects from './runner_projects.vue';
-import RunnerJobs from './runner_jobs.vue';
import RunnerTags from './runner_tags.vue';
export default {
components: {
- GlBadge,
GlTabs,
GlTab,
GlIntersperse,
RunnerDetail,
+ RunnerMaintenanceNoteDetail: () =>
+ import('ee_component/runner/components/runner_maintenance_note_detail.vue'),
RunnerGroups,
RunnerProjects,
- RunnerJobs,
RunnerTags,
TimeAgo,
},
@@ -57,9 +55,6 @@ export default {
isProjectRunner() {
return this.runner?.runnerType === PROJECT_TYPE;
},
- jobCount() {
- return formatJobCount(this.runner?.jobCount);
- },
},
ACCESS_LEVEL_REF_PROTECTED,
};
@@ -106,6 +101,11 @@ export default {
/>
</template>
</runner-detail>
+
+ <runner-maintenance-note-detail
+ class="gl-pt-4 gl-border-t-gray-100 gl-border-t-1 gl-border-t-solid"
+ :value="runner.maintenanceNoteHtml"
+ />
</dl>
</div>
@@ -113,15 +113,6 @@ export default {
<runner-projects v-if="isProjectRunner" :runner="runner" />
</template>
</gl-tab>
- <gl-tab>
- <template #title>
- {{ s__('Runners|Jobs') }}
- <gl-badge v-if="jobCount" data-testid="job-count-badge" class="gl-ml-1" size="sm">
- {{ jobCount }}
- </gl-badge>
- </template>
-
- <runner-jobs v-if="runner" :runner="runner" />
- </gl-tab>
+ <slot name="jobs-tab"></slot>
</gl-tabs>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_jobs.vue b/app/assets/javascripts/runner/components/runner_jobs.vue
index 4eb1312b204..57afdc4b9be 100644
--- a/app/assets/javascripts/runner/components/runner_jobs.vue
+++ b/app/assets/javascripts/runner/components/runner_jobs.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlSkeletonLoader } from '@gitlab/ui';
import { createAlert } from '~/flash';
import runnerJobsQuery from '../graphql/show/runner_jobs.query.graphql';
import { I18N_FETCH_ERROR, I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '../constants';
@@ -11,7 +11,7 @@ import RunnerPagination from './runner_pagination.vue';
export default {
name: 'RunnerJobs',
components: {
- GlSkeletonLoading,
+ GlSkeletonLoader,
RunnerJobsTable,
RunnerPagination,
},
@@ -68,7 +68,9 @@ export default {
<template>
<div class="gl-pt-3">
- <gl-skeleton-loading v-if="loading" class="gl-py-5" />
+ <div v-if="loading" class="gl-py-5">
+ <gl-skeleton-loader />
+ </div>
<runner-jobs-table v-else-if="jobs.items.length" :jobs="jobs.items" />
<p v-else>{{ $options.I18N_NO_JOBS_FOUND }}</p>
diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue
index dcfd4b84dd2..f1f99c728c5 100644
--- a/app/assets/javascripts/runner/components/runner_list.vue
+++ b/app/assets/javascripts/runner/components/runner_list.vue
@@ -12,7 +12,7 @@ import RunnerStatusCell from './cells/runner_status_cell.vue';
import RunnerTags from './runner_tags.vue';
const defaultFields = [
- tableField({ key: 'status', label: s__('Runners|Status') }),
+ tableField({ key: 'status', label: s__('Runners|Status'), thClasses: ['gl-w-15p'] }),
tableField({ key: 'summary', label: s__('Runners|Runner'), thClasses: ['gl-lg-w-25p'] }),
tableField({ key: 'version', label: __('Version') }),
tableField({ key: 'jobCount', label: __('Jobs') }),
diff --git a/app/assets/javascripts/runner/components/runner_list_empty_state.vue b/app/assets/javascripts/runner/components/runner_list_empty_state.vue
new file mode 100644
index 00000000000..ab9cde6a401
--- /dev/null
+++ b/app/assets/javascripts/runner/components/runner_list_empty_state.vue
@@ -0,0 +1,75 @@
+<script>
+import { GlEmptyState, GlLink, GlSprintf, GlModalDirective } from '@gitlab/ui';
+import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+
+export default {
+ components: {
+ GlEmptyState,
+ GlLink,
+ GlSprintf,
+ RunnerInstructionsModal,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ props: {
+ isSearchFiltered: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ svgPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ filteredSvgPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ registrationToken: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ modalId: 'runners-empty-state-instructions-modal',
+ svgHeight: 145,
+};
+</script>
+
+<template>
+ <gl-empty-state
+ v-if="isSearchFiltered"
+ :title="s__('Runners|No results found')"
+ :svg-path="filteredSvgPath"
+ :svg-height="$options.svgHeight"
+ :description="s__('Runners|Edit your search and try again')"
+ />
+ <gl-empty-state
+ v-else
+ :title="s__('Runners|Get started with runners')"
+ :svg-path="svgPath"
+ :svg-height="$options.svgHeight"
+ >
+ <template #description>
+ <gl-sprintf
+ :message="
+ s__(
+ 'Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link v-gl-modal="$options.modalId">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+
+ <runner-instructions-modal
+ :modal-id="$options.modalId"
+ :registration-token="registrationToken"
+ />
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/runner/components/runner_projects.vue b/app/assets/javascripts/runner/components/runner_projects.vue
index daca718e2b5..c0c0c14e91e 100644
--- a/app/assets/javascripts/runner/components/runner_projects.vue
+++ b/app/assets/javascripts/runner/components/runner_projects.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlSkeletonLoader } from '@gitlab/ui';
import { sprintf, formatNumber } from '~/locale';
import { createAlert } from '~/flash';
import runnerProjectsQuery from '../graphql/show/runner_projects.query.graphql';
@@ -17,7 +17,7 @@ import RunnerPagination from './runner_pagination.vue';
export default {
name: 'RunnerProjects',
components: {
- GlSkeletonLoading,
+ GlSkeletonLoader,
RunnerAssignedItem,
RunnerPagination,
},
@@ -86,7 +86,9 @@ export default {
{{ heading }}
</h3>
- <gl-skeleton-loading v-if="loading" class="gl-py-5" />
+ <div v-if="loading" class="gl-py-5">
+ <gl-skeleton-loader />
+ </div>
<template v-else-if="projects.items.length">
<runner-assigned-item
v-for="(project, i) in projects.items"
diff --git a/app/assets/javascripts/runner/components/runner_update_form.vue b/app/assets/javascripts/runner/components/runner_update_form.vue
index 56c9007a781..c613e2d2467 100644
--- a/app/assets/javascripts/runner/components/runner_update_form.vue
+++ b/app/assets/javascripts/runner/components/runner_update_form.vue
@@ -31,6 +31,8 @@ export default {
GlFormGroup,
GlFormInputGroup,
GlSkeletonLoader,
+ RunnerMaintenanceNoteField: () =>
+ import('ee_component/runner/components/runner_maintenance_note_field.vue'),
RunnerUpdateCostFactorFields: () =>
import('ee_component/runner/components/runner_update_cost_factor_fields.vue'),
},
@@ -115,9 +117,13 @@ export default {
<h4 class="gl-font-lg gl-my-5">{{ s__('Runners|Details') }}</h4>
<gl-skeleton-loader v-if="loading" />
- <gl-form-group v-else :label="__('Description')" data-testid="runner-field-description">
- <gl-form-input-group v-model="model.description" />
- </gl-form-group>
+
+ <template v-else>
+ <gl-form-group :label="__('Description')" data-testid="runner-field-description">
+ <gl-form-input-group v-model="model.description" />
+ </gl-form-group>
+ <runner-maintenance-note-field v-model="model.maintenanceNote" />
+ </template>
<hr />
diff --git a/app/assets/javascripts/runner/graphql/list/admin_runners.query.graphql b/app/assets/javascripts/runner/graphql/list/admin_runners.query.graphql
index 5d0450e7418..61bfe03bf6e 100644
--- a/app/assets/javascripts/runner/graphql/list/admin_runners.query.graphql
+++ b/app/assets/javascripts/runner/graphql/list/admin_runners.query.graphql
@@ -1,4 +1,4 @@
-#import "~/runner/graphql/list/list_item.fragment.graphql"
+#import "ee_else_ce/runner/graphql/list/list_item.fragment.graphql"
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
query getRunners(
diff --git a/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql b/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql
index b4f2b5cd8c8..8755636a7ad 100644
--- a/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql
+++ b/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql
@@ -1,4 +1,4 @@
-#import "~/runner/graphql/list/list_item.fragment.graphql"
+#import "ee_else_ce/runner/graphql/list/list_item.fragment.graphql"
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
query getGroupRunners(
diff --git a/app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql b/app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql
index 620c18c5bc0..19a5a48ea75 100644
--- a/app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql
+++ b/app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql
@@ -1,20 +1,5 @@
+#import "./list_item_shared.fragment.graphql"
+
fragment ListItem on CiRunner {
- __typename
- id
- description
- runnerType
- shortSha
- version
- revision
- ipAddress
- active
- locked
- jobCount
- tagList
- contactedAt
- status(legacyMode: null)
- userPermissions {
- updateRunner
- deleteRunner
- }
+ ...ListItemShared
}
diff --git a/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql
new file mode 100644
index 00000000000..cf925359ffb
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql
@@ -0,0 +1,20 @@
+fragment ListItemShared on CiRunner {
+ __typename
+ id
+ description
+ runnerType
+ shortSha
+ version
+ revision
+ ipAddress
+ active
+ locked
+ jobCount
+ tagList
+ contactedAt
+ status(legacyMode: null)
+ userPermissions {
+ updateRunner
+ deleteRunner
+ }
+}
diff --git a/app/assets/javascripts/runner/graphql/show/runner.query.graphql b/app/assets/javascripts/runner/graphql/show/runner.query.graphql
index 178816b58bd..dec434b43a5 100644
--- a/app/assets/javascripts/runner/graphql/show/runner.query.graphql
+++ b/app/assets/javascripts/runner/graphql/show/runner.query.graphql
@@ -1,41 +1,7 @@
+#import "ee_else_ce/runner/graphql/show/runner_details.fragment.graphql"
+
query getRunner($id: CiRunnerID!) {
runner(id: $id) {
- __typename
- id
- shortSha
- runnerType
- active
- accessLevel
- runUntagged
- locked
- ipAddress
- executorName
- architectureName
- platformName
- description
- maximumTimeout
- jobCount
- tagList
- createdAt
- status(legacyMode: null)
- contactedAt
- version
- editAdminUrl
- userPermissions {
- updateRunner
- deleteRunner
- }
- groups {
- # Only a single group can be loaded here, while projects
- # are loaded separately using the query with pagination
- # parameters `runner_projects.query.graphql`.
- nodes {
- id
- avatarUrl
- name
- fullName
- webUrl
- }
- }
+ ...RunnerDetails
}
}
diff --git a/app/assets/javascripts/runner/graphql/show/runner_details.fragment.graphql b/app/assets/javascripts/runner/graphql/show/runner_details.fragment.graphql
new file mode 100644
index 00000000000..2449ee0fc0f
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/show/runner_details.fragment.graphql
@@ -0,0 +1,5 @@
+#import "./runner_details_shared.fragment.graphql"
+
+fragment RunnerDetails on CiRunner {
+ ...RunnerDetailsShared
+}
diff --git a/app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql
new file mode 100644
index 00000000000..b79ad4d9280
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql
@@ -0,0 +1,39 @@
+fragment RunnerDetailsShared on CiRunner {
+ __typename
+ id
+ shortSha
+ runnerType
+ active
+ accessLevel
+ runUntagged
+ locked
+ ipAddress
+ executorName
+ architectureName
+ platformName
+ description
+ maximumTimeout
+ jobCount
+ tagList
+ createdAt
+ status(legacyMode: null)
+ contactedAt
+ version
+ editAdminUrl
+ userPermissions {
+ updateRunner
+ deleteRunner
+ }
+ groups {
+ # Only a single group can be loaded here, while projects
+ # are loaded separately using the query with pagination
+ # parameters `runner_projects.query.graphql`.
+ nodes {
+ id
+ avatarUrl
+ name
+ fullName
+ webUrl
+ }
+ }
+}
diff --git a/app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue b/app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue
new file mode 100644
index 00000000000..c336e091fdf
--- /dev/null
+++ b/app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue
@@ -0,0 +1,114 @@
+<script>
+import { GlBadge, GlTab, GlTooltipDirective } from '@gitlab/ui';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
+import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { formatJobCount } from '../utils';
+import RunnerDeleteButton from '../components/runner_delete_button.vue';
+import RunnerEditButton from '../components/runner_edit_button.vue';
+import RunnerPauseButton from '../components/runner_pause_button.vue';
+import RunnerHeader from '../components/runner_header.vue';
+import RunnerDetails from '../components/runner_details.vue';
+import RunnerJobs from '../components/runner_jobs.vue';
+import { I18N_FETCH_ERROR } from '../constants';
+import runnerQuery from '../graphql/show/runner.query.graphql';
+import { captureException } from '../sentry_utils';
+import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage';
+
+export default {
+ name: 'GroupRunnerShowApp',
+ components: {
+ GlBadge,
+ GlTab,
+ RunnerDeleteButton,
+ RunnerEditButton,
+ RunnerPauseButton,
+ RunnerHeader,
+ RunnerDetails,
+ RunnerJobs,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ runnerId: {
+ type: String,
+ required: true,
+ },
+ runnersPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ runner: null,
+ };
+ },
+ apollo: {
+ runner: {
+ query: runnerQuery,
+ variables() {
+ return {
+ id: convertToGraphQLId(TYPE_CI_RUNNER, this.runnerId),
+ };
+ },
+ error(error) {
+ createAlert({ message: I18N_FETCH_ERROR });
+
+ this.reportToSentry(error);
+ },
+ },
+ },
+ computed: {
+ canUpdate() {
+ return this.runner.userPermissions?.updateRunner;
+ },
+ canDelete() {
+ return this.runner.userPermissions?.deleteRunner;
+ },
+ jobCount() {
+ return formatJobCount(this.runner?.jobCount);
+ },
+ },
+ errorCaptured(error) {
+ this.reportToSentry(error);
+ },
+ methods: {
+ reportToSentry(error) {
+ captureException({ error, component: this.$options.name });
+ },
+ onDeleted({ message }) {
+ saveAlertToLocalStorage({ message, variant: VARIANT_SUCCESS });
+ redirectTo(this.runnersPath);
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <runner-header v-if="runner" :runner="runner">
+ <template #actions>
+ <runner-edit-button v-if="canUpdate && runner.editAdminUrl" :href="runner.editAdminUrl" />
+ <runner-pause-button v-if="canUpdate" :runner="runner" />
+ <runner-delete-button v-if="canDelete" :runner="runner" @deleted="onDeleted" />
+ </template>
+ </runner-header>
+
+ <runner-details :runner="runner">
+ <template #jobs-tab>
+ <gl-tab>
+ <template #title>
+ {{ s__('Runners|Jobs') }}
+ <gl-badge v-if="jobCount" data-testid="job-count-badge" class="gl-ml-1" size="sm">
+ {{ jobCount }}
+ </gl-badge>
+ </template>
+
+ <runner-jobs v-if="runner" :runner="runner" />
+ </gl-tab>
+ </template>
+ </runner-details>
+ </div>
+</template>
diff --git a/app/assets/javascripts/runner/group_runner_show/index.js b/app/assets/javascripts/runner/group_runner_show/index.js
new file mode 100644
index 00000000000..d1b87c8e427
--- /dev/null
+++ b/app/assets/javascripts/runner/group_runner_show/index.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { showAlertFromLocalStorage } from '../local_storage_alert/show_alert_from_local_storage';
+import GroupRunnerShowApp from './group_runner_show_app.vue';
+
+Vue.use(VueApollo);
+
+export const initAdminRunnerShow = (selector = '#js-group-runner-show') => {
+ showAlertFromLocalStorage();
+
+ const el = document.querySelector(selector);
+
+ if (!el) {
+ return null;
+ }
+
+ const { runnerId, runnersPath } = el.dataset;
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ return new Vue({
+ el,
+ apolloProvider,
+ render(h) {
+ return h(GroupRunnerShowApp, {
+ props: {
+ runnerId,
+ runnersPath,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
index b5bd4b111fd..641b3a8f560 100644
--- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
@@ -8,6 +8,7 @@ import { fetchPolicies } from '~/lib/graphql';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
+import RunnerListEmptyState from '../components/runner_list_empty_state.vue';
import RunnerName from '../components/runner_name.vue';
import RunnerStats from '../components/stat/runner_stats.vue';
import RunnerPagination from '../components/runner_pagination.vue';
@@ -31,6 +32,7 @@ import {
fromUrlQueryToSearch,
fromSearchToUrl,
fromSearchToVariables,
+ isSearchFiltered,
} from '../runner_search_utils';
import { captureException } from '../sentry_utils';
@@ -86,12 +88,14 @@ export default {
RegistrationDropdown,
RunnerFilteredSearchBar,
RunnerList,
+ RunnerListEmptyState,
RunnerName,
RunnerStats,
RunnerPagination,
RunnerTypeTabs,
RunnerActionsCell,
},
+ inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'],
props: {
registrationToken: {
type: String,
@@ -196,6 +200,9 @@ export default {
filteredSearchNamespace() {
return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`;
},
+ isSearchFiltered() {
+ return isSearchFiltered(this.search);
+ },
},
watch: {
search: {
@@ -299,9 +306,13 @@ export default {
:stale-runners-count="staleRunnersTotal"
/>
- <div v-if="noRunnersFound" class="gl-text-center gl-p-5">
- {{ __('No runners found') }}
- </div>
+ <runner-list-empty-state
+ v-if="noRunnersFound"
+ :registration-token="registrationToken"
+ :is-search-filtered="isSearchFiltered"
+ :svg-path="emptyStateSvgPath"
+ :filtered-svg-path="emptyStateFilteredSvgPath"
+ />
<template v-else>
<runner-list :runners="runners.items" :loading="runnersLoading">
<template #runner-name="{ runner }">
diff --git a/app/assets/javascripts/runner/group_runners/index.js b/app/assets/javascripts/runner/group_runners/index.js
index 0dade30f820..feed6b0ceb7 100644
--- a/app/assets/javascripts/runner/group_runners/index.js
+++ b/app/assets/javascripts/runner/group_runners/index.js
@@ -22,6 +22,8 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
groupRunnersLimitedCount,
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ emptyStateSvgPath,
+ emptyStateFilteredSvgPath,
} = el.dataset;
const apolloProvider = new VueApollo({
@@ -36,6 +38,8 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
groupId,
onlineContactTimeoutSecs: parseInt(onlineContactTimeoutSecs, 10),
staleTimeoutSecs: parseInt(staleTimeoutSecs, 10),
+ emptyStateSvgPath,
+ emptyStateFilteredSvgPath,
},
render(h) {
return h(GroupRunnersApp, {
diff --git a/app/assets/javascripts/runner/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js
index 0d688ed65ef..e01878f355a 100644
--- a/app/assets/javascripts/runner/runner_search_utils.js
+++ b/app/assets/javascripts/runner/runner_search_utils.js
@@ -236,3 +236,17 @@ export const fromSearchToVariables = ({
...paginationVariables,
};
};
+
+/**
+ * Decides whether or not a search object is the "default" or empty.
+ *
+ * A search is filtered if the user has entered filtering criteria.
+ *
+ * @param {Object} search
+ * @returns true if this search is filtered, false otherwise
+ */
+export const isSearchFiltered = ({ runnerType = null, filters = [], pagination = {} } = {}) => {
+ return Boolean(
+ runnerType !== null || filters?.length !== 0 || (pagination && pagination?.page !== 1),
+ );
+};
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
index 40513a7f363..dc8b6201953 100644
--- a/app/assets/javascripts/search/store/actions.js
+++ b/app/assets/javascripts/search/store/actions.js
@@ -18,7 +18,7 @@ export const fetchGroups = ({ commit }, search) => {
});
};
-export const fetchProjects = ({ commit, state }, search, emptyCallback = () => {}) => {
+export const fetchProjects = ({ commit, state }, search) => {
commit(types.REQUEST_PROJECTS);
const groupId = state.query?.group_id;
@@ -31,17 +31,11 @@ export const fetchProjects = ({ commit, state }, search, emptyCallback = () => {
};
if (groupId) {
- Api.groupProjects(
- groupId,
- search,
- {
- order_by: 'similarity',
- with_shared: false,
- include_subgroups: true,
- },
- emptyCallback,
- true,
- )
+ Api.groupProjects(groupId, search, {
+ order_by: 'similarity',
+ with_shared: false,
+ include_subgroups: true,
+ })
.then(handleSuccess)
.catch(handleCatch);
} else {
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index b2bf913fe45..94244eeb12e 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -477,7 +477,7 @@ export class SearchAutocomplete {
}
getAvatar(item) {
- if (!Object.hasOwnProperty.call(item, 'avatar_url')) {
+ if (!Object.prototype.hasOwnProperty.call(item, 'avatar_url')) {
return false;
}
diff --git a/app/assets/javascripts/search_settings/components/search_settings.vue b/app/assets/javascripts/search_settings/components/search_settings.vue
index f6439c6f4c4..e9cc9616fd0 100644
--- a/app/assets/javascripts/search_settings/components/search_settings.vue
+++ b/app/assets/javascripts/search_settings/components/search_settings.vue
@@ -184,7 +184,7 @@ export default {
<gl-search-box-by-type
:value="searchTerm"
:debounce="$options.TYPING_DELAY"
- :placeholder="__('Search settings')"
+ :placeholder="__('Search page')"
@input="search"
/>
</template>
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index d0c4ad3646c..34910781247 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -4,10 +4,9 @@ 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 SectionLayout from '~/vue_shared/security_configuration/components/section_layout.vue';
-import currentLicenseQuery from '~/security_configuration/graphql/current_license.query.graphql';
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, LICENSE_ULTIMATE } from './constants';
+import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
import FeatureCard from './feature_card.vue';
import TrainingProviderList from './training_provider_list.vue';
import UpgradeBanner from './upgrade_banner.vue';
@@ -51,17 +50,6 @@ export default {
TrainingProviderList,
},
inject: ['projectFullPath', 'vulnerabilityTrainingDocsPath'],
- apollo: {
- currentLicensePlan: {
- query: currentLicenseQuery,
- update({ currentLicense }) {
- return currentLicense?.plan;
- },
- error() {
- this.hasCurrentLicenseFetchError = true;
- },
- },
- },
props: {
augmentedSecurityFeatures: {
type: Array,
@@ -96,13 +84,15 @@ export default {
required: false,
default: '',
},
+ securityTrainingEnabled: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
autoDevopsEnabledAlertDismissedProjects: [],
errorMessage: '',
- currentLicensePlan: '',
- hasCurrentLicenseFetchError: false,
};
},
computed: {
@@ -123,12 +113,6 @@ export default {
!this.autoDevopsEnabledAlertDismissedProjects.includes(this.projectFullPath)
);
},
- shouldShowVulnerabilityManagementTab() {
- // if the query fails (if the plan is `null` also means an error has occurred) we still want to show the feature
- const hasQueryError = this.hasCurrentLicenseFetchError || this.currentLicensePlan === null;
-
- return hasQueryError || this.currentLicensePlan === LICENSE_ULTIMATE;
- },
},
methods: {
dismissAutoDevopsEnabledAlert() {
@@ -270,7 +254,7 @@ export default {
</section-layout>
</gl-tab>
<gl-tab
- v-if="shouldShowVulnerabilityManagementTab"
+ v-if="securityTrainingEnabled"
data-testid="vulnerability-management-tab"
:title="$options.i18n.vulnerabilityManagement"
query-param-value="vulnerability-management"
diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js
index 5b04ad6f9ba..e4d2bd08f50 100644
--- a/app/assets/javascripts/security_configuration/components/constants.js
+++ b/app/assets/javascripts/security_configuration/components/constants.js
@@ -310,7 +310,3 @@ export const TEMP_PROVIDER_URLS = {
Kontra: 'https://application.security/',
[__('Secure Code Warrior')]: 'https://www.securecodewarrior.com/',
};
-
-export const LICENSE_ULTIMATE = 'ultimate';
-export const LICENSE_FREE = 'free';
-export const LICENSE_PREMIUM = 'premium';
diff --git a/app/assets/javascripts/security_configuration/graphql/current_license.query.graphql b/app/assets/javascripts/security_configuration/graphql/current_license.query.graphql
deleted file mode 100644
index 9ab4f4d4347..00000000000
--- a/app/assets/javascripts/security_configuration/graphql/current_license.query.graphql
+++ /dev/null
@@ -1,6 +0,0 @@
-query getCurrentLicensePlan {
- currentLicense {
- id
- plan
- }
-}
diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js
index dcc41a38067..637d510e684 100644
--- a/app/assets/javascripts/security_configuration/index.js
+++ b/app/assets/javascripts/security_configuration/index.js
@@ -56,6 +56,7 @@ export const initSecurityConfiguration = (el) => {
'gitlabCiPresent',
'autoDevopsEnabled',
'canEnableAutoDevops',
+ 'securityTrainingEnabled',
]),
},
});
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 ef40de82d01..c20dd3b677d 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,7 @@
<script>
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import { IssuableType } from '~/issues/constants';
+import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import { isUserBusy } from '~/set_status_modal/utils';
import AssigneeAvatar from './assignee_avatar.vue';
@@ -94,6 +95,9 @@ export default {
assigneeUrl() {
return this.user.web_url || this.user.webUrl;
},
+ assigneeId() {
+ return isGid(this.user.id) ? getIdFromGraphQLId(this.user.id) : this.user.id;
+ },
},
};
</script>
@@ -103,7 +107,7 @@ export default {
<gl-link
:href="assigneeUrl"
:title="tooltipTitle"
- :data-user-id="user.id"
+ :data-user-id="assigneeId"
data-placement="left"
class="gl-display-inline-block js-user-link"
>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index bdd014163a0..3602b5ec4f6 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -55,7 +55,12 @@ export default {
{{ __('None') }}
<template v-if="editable">
-
- <button type="button" class="btn-link" data-testid="assign-yourself" @click="assignSelf">
+ <button
+ type="button"
+ class="gl-button btn-link gl-reset-color!"
+ data-testid="assign-yourself"
+ @click="assignSelf"
+ >
{{ __('assign yourself') }}
</button>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue
index af4227fa48d..46bda26c327 100644
--- a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue
@@ -26,7 +26,7 @@ export default {
};
</script>
<template>
- <button type="button" class="btn-link">
+ <button type="button" class="gl-button btn-link">
<assignee-avatar :user="user" :img-size="24" :issuable-type="issuableType" />
<user-name-with-status
:name="user.name"
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
index 50b1955abcc..f894ef0c42d 100644
--- a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
@@ -123,7 +123,7 @@ export default {
:user="user"
:issuable-type="issuableType"
/>
- <button v-if="hasMoreThanTwoAssignees" class="btn-link" type="button">
+ <button v-if="hasMoreThanTwoAssignees" class="btn-link gl-button" type="button">
<span
class="avatar-counter sidebar-avatar-counter gl-display-flex gl-align-items-center gl-pl-3"
>
diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
index 01d29da5486..b6260418837 100644
--- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -120,7 +120,7 @@ export default {
<div v-if="renderShowMoreSection" class="user-list-more gl-hover-text-blue-800">
<button
type="button"
- class="btn-link"
+ class="btn-link gl-button gl-reset-color!"
data-qa-selector="more_assignees_link"
@click="toggleShowLess"
>
diff --git a/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue
index 031de669489..974ad189f32 100644
--- a/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue
+++ b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue
@@ -49,14 +49,14 @@ export default {
},
request() {
const state = {
- variant: 'default',
+ selected: false,
icon: 'attention',
direction: 'add',
};
if (this.user.attention_requested) {
Object.assign(state, {
- variant: 'warning',
+ selected: true,
icon: 'attention-solid',
direction: 'remove',
});
@@ -92,7 +92,7 @@ export default {
>
<gl-button
:loading="loading"
- :variant="request.variant"
+ :selected="request.selected"
:icon="request.icon"
:aria-label="tooltipTitle"
:class="{ 'gl-pointer-events-none': !user.can_update_merge_request }"
diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
index 71e40fde77d..c44ce8b0057 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
@@ -8,7 +8,7 @@ import { confidentialityQueries } from '~/sidebar/constants';
export default {
i18n: {
confidentialityOnWarning: __(
- 'You are going to turn on confidentiality. Only %{context} members with %{strongStart}at least Reporter role%{strongEnd} can view or be notified about this %{issuableType}.',
+ 'You are going to turn on confidentiality. Only %{context} members with %{strongStart}%{permissions}%{strongEnd} can view or be notified about this %{issuableType}.',
),
confidentialityOffWarning: __(
'You are going to turn off the confidentiality. This means %{strongStart}everyone%{strongEnd} will be able to see and leave a comment on this %{issuableType}.',
@@ -65,6 +65,11 @@ export default {
groupPath: this.fullPath,
};
},
+ permissions() {
+ return this.issuableType === IssuableType.Issue
+ ? __('at least the Reporter role, the author, and assignees')
+ : __('at least the Reporter role');
+ },
},
methods: {
submitForm() {
@@ -120,7 +125,11 @@ export default {
<p data-testid="warning-message">
<gl-sprintf :message="warningMessage">
<template #strong="{ content }">
- <strong>{{ content }}</strong>
+ <strong>
+ <gl-sprintf :message="content">
+ <template #permissions>{{ permissions }}</template>
+ </gl-sprintf>
+ </strong>
</template>
<template #context>{{ context }}</template>
<template #issuableType>{{ issuableType }}</template>
diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
index be7a89c2869..ef99d540c86 100644
--- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
@@ -274,7 +274,7 @@ export default {
<template #collapsed>
<div v-gl-tooltip.viewport.left :title="dateLabel" class="sidebar-collapsed-icon">
<gl-icon :size="16" name="calendar" />
- <span class="collapse-truncated-title">{{ formattedDate }}</span>
+ <span class="gl-pt-2 gl-px-3 gl-font-sm">{{ formattedDate }}</span>
</div>
<sidebar-inherit-date
v-if="canInherit && !initialLoading"
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
index 2ab46a7a655..8145506f32c 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -74,7 +74,7 @@ export default {
<gl-button
data-testid="lock-toggle"
category="secondary"
- variant="warning"
+ variant="confirm"
:disabled="isLoading"
:loading="isLoading"
@click.prevent="submitForm"
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 77e41648e9b..b8804de653f 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -99,7 +99,9 @@ export default {
>
<gl-icon name="users" />
<gl-loading-icon v-if="loading" size="sm" />
- <span v-else data-testid="collapsed-count"> {{ participantCount }} </span>
+ <span v-else data-testid="collapsed-count" class="gl-pt-2 gl-px-3 gl-font-sm">
+ {{ participantCount }}
+ </span>
</div>
<div
v-if="showParticipantLabel"
diff --git a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer.vue b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer.vue
index 6de926e0ff9..2ea7c125a85 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer.vue
@@ -17,7 +17,7 @@ export default {
</script>
<template>
- <button type="button" class="btn-link">
+ <button type="button" class="btn-link gl-button">
<reviewer-avatar :user="user" :img-size="24" />
<span class="author"> {{ user.name }} </span>
</button>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue
index e09b5d913f7..9502b2e78b3 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue
@@ -95,7 +95,7 @@ export default {
>
<gl-icon v-if="hasNoUsers" name="user" :aria-label="__('None')" />
<collapsed-reviewer v-for="user in collapsedUsers" :key="user.id" :user="user" />
- <button v-if="hasMoreThanTwoReviewers" class="btn-link" type="button">
+ <button v-if="hasMoreThanTwoReviewers" class="btn-link gl-button" type="button">
<span
class="avatar-counter sidebar-avatar-counter gl-display-flex gl-align-items-center gl-pl-3"
>
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index 897cab45fe4..3d8a2cd847c 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -322,7 +322,7 @@ export default {
class="sidebar-collapsed-icon"
>
<gl-icon :aria-label="attributeTypeTitle" :name="attributeTypeIcon" />
- <span class="collapse-truncated-title">
+ <span class="collapse-truncated-title gl-pt-2 gl-px-3 gl-font-sm">
{{ attributeTitle }}
</span>
</div>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
index 7b67c34ded6..465f971717f 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -58,7 +58,7 @@ export default {
} else if (this.showEstimateOnlyState || this.showSpentOnlyState) {
return 'bold';
} else if (this.showNoTimeTrackingState) {
- return 'no-value';
+ return 'no-value collapse-truncated-title gl-pt-2 gl-px-3 gl-font-sm';
}
return '';
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/graphql/cache_update.js b/app/assets/javascripts/sidebar/components/time_tracking/graphql/cache_update.js
new file mode 100644
index 00000000000..70177d84b1b
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/graphql/cache_update.js
@@ -0,0 +1,20 @@
+import produce from 'immer';
+
+export function removeTimelogFromStore(store, deletedTimelogId, query, variables) {
+ const sourceData = store.readQuery({
+ query,
+ variables,
+ });
+
+ const data = produce(sourceData, (draftData) => {
+ draftData.issuable.timelogs.nodes = draftData.issuable.timelogs.nodes.filter(
+ ({ id }) => id !== deletedTimelogId,
+ );
+ });
+
+ store.writeQuery({
+ query,
+ variables,
+ data,
+ });
+}
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/graphql/mutations/delete_timelog.mutation.graphql b/app/assets/javascripts/sidebar/components/time_tracking/graphql/mutations/delete_timelog.mutation.graphql
new file mode 100644
index 00000000000..17bbad1acb1
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/graphql/mutations/delete_timelog.mutation.graphql
@@ -0,0 +1,5 @@
+mutation deleteTimelog($input: TimelogDeleteInput!) {
+ timelogDelete(input: $input) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
index d9797961d40..79ef5a32474 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
@@ -1,11 +1,13 @@
<script>
-import { GlLoadingIcon, GlTableLite } from '@gitlab/ui';
+import { GlLoadingIcon, GlTableLite, GlButton, GlTooltipDirective } from '@gitlab/ui';
import createFlash from '~/flash';
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { formatDate, parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
import { timelogQueries } from '~/sidebar/constants';
+import deleteTimelogMutation from './graphql/mutations/delete_timelog.mutation.graphql';
+import { removeTimelogFromStore } from './graphql/cache_update';
const TIME_DATE_FORMAT = 'mmmm d, yyyy, HH:MM ("UTC:" o)';
@@ -13,6 +15,10 @@ export default {
components: {
GlLoadingIcon,
GlTableLite,
+ GlButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
inject: ['issuableType'],
props: {
@@ -27,7 +33,7 @@ export default {
},
},
data() {
- return { report: [], isLoading: true };
+ return { report: [], isLoading: true, removingIds: [] };
},
apollo: {
report: {
@@ -35,9 +41,7 @@ export default {
return timelogQueries[this.issuableType].query;
},
variables() {
- return {
- id: convertToGraphQLId(this.getGraphQLEntityType(), this.issuableId),
- };
+ return this.getQueryVariables();
},
update(data) {
this.isLoading = false;
@@ -48,10 +52,23 @@ export default {
},
},
},
+ computed: {
+ deleteButtonTooltip() {
+ return s__('TimeTracking|Delete time spent');
+ },
+ },
methods: {
+ isDeletingTimelog(timelogId) {
+ return this.removingIds.includes(timelogId);
+ },
isIssue() {
return this.issuableType === 'issue';
},
+ getQueryVariables() {
+ return {
+ id: convertToGraphQLId(this.getGraphQLEntityType(), this.issuableId),
+ };
+ },
getGraphQLEntityType() {
return this.isIssue() ? TYPE_ISSUE : TYPE_MERGE_REQUEST;
},
@@ -76,19 +93,51 @@ export default {
stringifyTime(parseSeconds(seconds, { limitToHours: this.limitToHours }))
);
},
+ deleteTimelog(timelogId) {
+ this.removingIds.push(timelogId);
+ this.$apollo
+ .mutate({
+ mutation: deleteTimelogMutation,
+ variables: { input: { id: timelogId } },
+ update: (store) => {
+ removeTimelogFromStore(
+ store,
+ timelogId,
+ timelogQueries[this.issuableType].query,
+ this.getQueryVariables(),
+ );
+ },
+ })
+ .then(({ data }) => {
+ if (data.timelogDelete?.errors?.length) {
+ throw new Error(data.timelogDelete.errors[0]);
+ }
+ })
+ .catch((error) => {
+ createFlash({
+ message: s__('TimeTracking|An error occurred while removing the timelog.'),
+ captureError: true,
+ error,
+ });
+ })
+ .finally(() => {
+ this.removingIds.splice(this.removingIds.indexOf(timelogId), 1);
+ });
+ },
},
fields: [
- { key: 'spentAt', label: __('Spent At'), sortable: true, tdClass: 'gl-w-quarter' },
+ { key: 'spentAt', label: __('Spent at'), sortable: true, tdClass: 'gl-w-quarter' },
{ key: 'user', label: __('User'), sortable: true },
- { key: 'timeSpent', label: __('Time Spent'), sortable: true, tdClass: 'gl-w-15' },
- { key: 'summary', label: __('Summary / Note'), sortable: true },
+ { key: 'timeSpent', label: __('Time spent'), sortable: true, tdClass: 'gl-w-15' },
+ { key: 'summary', label: __('Summary / note'), sortable: true },
+ { key: 'actions', label: '', tdClass: 'gl-w-10' },
],
};
</script>
<template>
<div>
- <div v-if="isLoading"><gl-loading-icon size="md" /></div>
+ <div v-if="isLoading"><gl-loading-icon size="lg" /></div>
<gl-table-lite v-else :items="report" :fields="$options.fields" foot-clone>
<template #cell(spentAt)="{ item: { spentAt } }">
<div>{{ formatDate(spentAt) }}</div>
@@ -110,7 +159,28 @@ export default {
<template #cell(summary)="{ item: { summary, note } }">
<div>{{ getSummary(summary, note) }}</div>
</template>
- <template #foot(note)>&nbsp;</template>
+ <template #foot(summary)>&nbsp;</template>
+
+ <template
+ #cell(actions)="{
+ item: {
+ id,
+ userPermissions: { adminTimelog },
+ },
+ }"
+ >
+ <div v-if="adminTimelog">
+ <gl-button
+ v-gl-tooltip="{ title: deleteButtonTooltip }"
+ category="secondary"
+ icon="remove"
+ data-testid="deleteButton"
+ :loading="isDeletingTimelog(id)"
+ @click="deleteTimelog(id)"
+ />
+ </div>
+ </template>
+ <template #foot(actions)>&nbsp;</template>
</gl-table-lite>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index 057bb9f0100..e39d9f9fb49 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -252,6 +252,7 @@ export default {
size="lg"
:title="__('Time tracking report')"
:hide-footer="true"
+ @hide="refresh"
>
<time-tracking-report :limit-to-hours="limitToHours" :issuable-id="issuableId" />
</gl-modal>
diff --git a/app/assets/javascripts/sidebar/graphql.js b/app/assets/javascripts/sidebar/graphql.js
index 034bdc71122..ff3fb4aae6b 100644
--- a/app/assets/javascripts/sidebar/graphql.js
+++ b/app/assets/javascripts/sidebar/graphql.js
@@ -2,6 +2,7 @@ import produce from 'immer';
import VueApollo from 'vue-apollo';
import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql';
import createDefaultClient from '~/lib/graphql';
+import { temporaryConfig } from '~/work_items/graphql/provider';
const resolvers = {
Mutation: {
@@ -15,7 +16,12 @@ const resolvers = {
},
};
-export const defaultClient = createDefaultClient(resolvers);
+export const defaultClient = createDefaultClient(
+ resolvers,
+ // should be removed with the rollout of work item assignees FF
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/363030
+ temporaryConfig,
+);
export const apolloProvider = new VueApollo({
defaultClient,
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 351bb50d941..bb40ac14438 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -119,7 +119,7 @@ function mountAssigneesComponentDeprecated(mediator) {
issuableIid: String(iid),
projectPath: fullPath,
field: el.dataset.field,
- signedIn: el.hasAttribute('data-signed-in'),
+ signedIn: Object.prototype.hasOwnProperty.call(el.dataset, 'signedIn'),
issuableType:
isInIssuePage() || isInIncidentPage() || isInDesignPage()
? IssuableType.Issue
@@ -149,7 +149,10 @@ function mountAssigneesComponent() {
},
provide: {
canUpdate: editable,
- directlyInviteMembers: el.hasAttribute('data-directly-invite-members'),
+ directlyInviteMembers: Object.prototype.hasOwnProperty.call(
+ el.dataset,
+ 'directlyInviteMembers',
+ ),
},
render: (createElement) =>
createElement('sidebar-assignees-widget', {
diff --git a/app/assets/javascripts/sidebar/queries/escalation_status.fragment.graphql b/app/assets/javascripts/sidebar/queries/escalation_status.fragment.graphql
new file mode 100644
index 00000000000..6021570557e
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/escalation_status.fragment.graphql
@@ -0,0 +1,4 @@
+fragment EscalationStatusFragment on Issue {
+ id
+ escalationStatus
+}
diff --git a/app/assets/javascripts/sidebar/queries/update_escalation_status.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_escalation_status.mutation.graphql
index a4aff7968df..d271ae5ff3e 100644
--- a/app/assets/javascripts/sidebar/queries/update_escalation_status.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_escalation_status.mutation.graphql
@@ -1,10 +1,12 @@
+#import "ee_else_ce/sidebar/queries/escalation_status.fragment.graphql"
+
mutation updateEscalationStatus($projectPath: ID!, $status: IssueEscalationStatus!, $iid: String!) {
issueSetEscalationStatus(input: { projectPath: $projectPath, status: $status, iid: $iid }) {
errors
clientMutationId
issue {
id
- escalationStatus
+ ...EscalationStatusFragment
}
}
}
diff --git a/app/assets/javascripts/sidebar/utils.js b/app/assets/javascripts/sidebar/utils.js
deleted file mode 100644
index 20cd4ce9d99..00000000000
--- a/app/assets/javascripts/sidebar/utils.js
+++ /dev/null
@@ -1 +0,0 @@
-export const toLabelGid = (id) => `gid://gitlab/Label/${id}`;
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index b7159fd6835..26838682fc8 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -17,7 +17,7 @@ const ERROR_HTML = `<div class="nothing-here-block">${spriteIcon(
's16',
)} Could not load diff</div>`;
const COLLAPSED_HTML =
- '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <button class="click-to-expand btn btn-link">Click to expand it.</button></div>';
+ '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <button class="click-to-expand btn btn-link gl-button">Click to expand it.</button></div>';
export default class SingleFileDiff {
constructor(file) {
diff --git a/app/assets/javascripts/static_site_editor/components/app.vue b/app/assets/javascripts/static_site_editor/components/app.vue
deleted file mode 100644
index 365fc7ce6e9..00000000000
--- a/app/assets/javascripts/static_site_editor/components/app.vue
+++ /dev/null
@@ -1,13 +0,0 @@
-<script>
-export default {
- props: {
- mergeRequestsIllustrationPath: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-<template>
- <router-view :merge-requests-illustration-path="mergeRequestsIllustrationPath" />
-</template>
diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue
deleted file mode 100644
index 2f2efe290ec..00000000000
--- a/app/assets/javascripts/static_site_editor/components/edit_area.vue
+++ /dev/null
@@ -1,190 +0,0 @@
-<script>
-import { EDITOR_TYPES } from '~/static_site_editor/rich_content_editor/constants';
-import RichContentEditor from '~/static_site_editor/rich_content_editor/rich_content_editor.vue';
-import parseSourceFile from '~/static_site_editor/services/parse_source_file';
-import imageRepository from '../image_repository';
-import formatter from '../services/formatter';
-import renderImage from '../services/renderers/render_image';
-import templater from '../services/templater';
-import EditDrawer from './edit_drawer.vue';
-import EditHeader from './edit_header.vue';
-import PublishToolbar from './publish_toolbar.vue';
-import UnsavedChangesConfirmDialog from './unsaved_changes_confirm_dialog.vue';
-
-export default {
- components: {
- RichContentEditor,
- PublishToolbar,
- EditHeader,
- EditDrawer,
- UnsavedChangesConfirmDialog,
- },
- props: {
- title: {
- type: String,
- required: true,
- },
- content: {
- type: String,
- required: true,
- },
- savingChanges: {
- type: Boolean,
- required: true,
- },
- returnUrl: {
- type: String,
- required: false,
- default: '',
- },
- branch: {
- type: String,
- required: true,
- },
- baseUrl: {
- type: String,
- required: true,
- },
- mounts: {
- type: Array,
- required: true,
- },
- project: {
- type: String,
- required: true,
- },
- imageRoot: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- formattedMarkdown: null,
- parsedSource: parseSourceFile(this.preProcess(true, this.content)),
- editorMode: EDITOR_TYPES.wysiwyg,
- hasMatter: false,
- isDrawerOpen: false,
- isModified: false,
- isSaveable: false,
- };
- },
- imageRepository: imageRepository(),
- computed: {
- editableContent() {
- return this.parsedSource.content(this.isWysiwygMode);
- },
- editableMatter() {
- return this.isDrawerOpen ? this.parsedSource.matter() : {};
- },
- hasSettings() {
- return this.hasMatter && this.isWysiwygMode;
- },
- isWysiwygMode() {
- return this.editorMode === EDITOR_TYPES.wysiwyg;
- },
- customRenderers() {
- const imageRenderer = renderImage.build(
- this.mounts,
- this.project,
- this.branch,
- this.baseUrl,
- this.$options.imageRepository,
- );
- return {
- image: [imageRenderer],
- };
- },
- },
- created() {
- this.refreshEditHelpers();
- },
- methods: {
- preProcess(isWrap, value) {
- const formattedContent = formatter(value);
- const templatedContent = isWrap
- ? templater.wrap(formattedContent)
- : templater.unwrap(formattedContent);
- return templatedContent;
- },
- refreshEditHelpers() {
- const { isModified, hasMatter, isMatterValid } = this.parsedSource;
- this.isModified = isModified();
- this.hasMatter = hasMatter();
- const hasValidMatter = this.hasMatter ? isMatterValid() : true;
- this.isSaveable = this.isModified && hasValidMatter;
- },
- onDrawerOpen() {
- this.isDrawerOpen = true;
- this.refreshEditHelpers();
- },
- onDrawerClose() {
- this.isDrawerOpen = false;
- this.refreshEditHelpers();
- },
- onInputChange(newVal) {
- this.parsedSource.syncContent(newVal, this.isWysiwygMode);
- this.refreshEditHelpers();
- },
- onModeChange(mode) {
- this.editorMode = mode;
-
- const preProcessedContent = this.preProcess(this.isWysiwygMode, this.editableContent);
- this.$refs.editor.resetInitialValue(preProcessedContent);
- },
- onUpdateSettings(settings) {
- this.parsedSource.syncMatter(settings);
- },
- onUploadImage({ file, imageUrl }) {
- this.$options.imageRepository.add(file, imageUrl);
- },
- onSubmit() {
- const preProcessedContent = this.preProcess(false, this.parsedSource.content());
- this.$emit('submit', {
- formattedMarkdown: this.formattedMarkdown,
- content: preProcessedContent,
- images: this.$options.imageRepository.getAll(),
- });
- },
- onEditorLoad({ formattedMarkdown }) {
- this.formattedMarkdown = formattedMarkdown;
- },
- },
-};
-</script>
-<template>
- <div class="d-flex flex-grow-1 flex-column h-100">
- <edit-header class="py-2" :title="title" />
- <edit-drawer
- v-if="hasMatter"
- :is-open="isDrawerOpen"
- :settings="editableMatter"
- @close="onDrawerClose"
- @updateSettings="onUpdateSettings"
- />
- <rich-content-editor
- ref="editor"
- :content="editableContent"
- :initial-edit-type="editorMode"
- :image-root="imageRoot"
- :options="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
- customRenderers,
- } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
- class="mb-9 pb-6 h-100"
- @modeChange="onModeChange"
- @input="onInputChange"
- @uploadImage="onUploadImage"
- @load="onEditorLoad"
- />
- <unsaved-changes-confirm-dialog :modified="isSaveable" />
- <publish-toolbar
- class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full"
- :has-settings="hasSettings"
- :return-url="returnUrl"
- :saveable="isSaveable"
- :saving-changes="savingChanges"
- @editSettings="onDrawerOpen"
- @submit="onSubmit"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/components/edit_drawer.vue b/app/assets/javascripts/static_site_editor/components/edit_drawer.vue
deleted file mode 100644
index 781e23cd6c8..00000000000
--- a/app/assets/javascripts/static_site_editor/components/edit_drawer.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<script>
-import { GlDrawer } from '@gitlab/ui';
-import FrontMatterControls from './front_matter_controls.vue';
-
-export default {
- components: {
- GlDrawer,
- FrontMatterControls,
- },
- props: {
- isOpen: {
- type: Boolean,
- required: true,
- },
- settings: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-<template>
- <gl-drawer class="gl-pt-8" :open="isOpen" @close="$emit('close')">
- <template #title>{{ __('Page settings') }}</template>
- <front-matter-controls :settings="settings" @updateSettings="$emit('updateSettings', $event)" />
- </gl-drawer>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/components/edit_header.vue b/app/assets/javascripts/static_site_editor/components/edit_header.vue
deleted file mode 100644
index 5660bfbe5ae..00000000000
--- a/app/assets/javascripts/static_site_editor/components/edit_header.vue
+++ /dev/null
@@ -1,23 +0,0 @@
-<script>
-import { DEFAULT_HEADING } from '../constants';
-
-export default {
- props: {
- title: {
- type: String,
- required: false,
- default: '',
- },
- },
- computed: {
- heading() {
- return this.title || DEFAULT_HEADING;
- },
- },
-};
-</script>
-<template>
- <div>
- <h3 ref="sseHeading">{{ heading }}</h3>
- </div>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/components/edit_meta_controls.vue b/app/assets/javascripts/static_site_editor/components/edit_meta_controls.vue
deleted file mode 100644
index c6247632b6e..00000000000
--- a/app/assets/javascripts/static_site_editor/components/edit_meta_controls.vue
+++ /dev/null
@@ -1,130 +0,0 @@
-<script>
-import {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- GlForm,
- GlFormGroup,
- GlFormInput,
- GlFormTextarea,
-} from '@gitlab/ui';
-
-import { __ } from '~/locale';
-
-export default {
- components: {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- GlForm,
- GlFormGroup,
- GlFormInput,
- GlFormTextarea,
- },
- props: {
- title: {
- type: String,
- required: true,
- },
- description: {
- type: String,
- required: true,
- },
- templates: {
- type: Array,
- required: false,
- default: null,
- },
- currentTemplate: {
- type: Object,
- required: false,
- default: null,
- },
- },
- computed: {
- dropdownLabel() {
- return this.currentTemplate ? this.currentTemplate.name : __('None');
- },
- hasTemplates() {
- return this.templates?.length > 0;
- },
- },
- mounted() {
- this.preSelect();
- },
- methods: {
- getId(type, key) {
- return `sse-merge-request-meta-${type}-${key}`;
- },
- preSelect() {
- this.$nextTick(() => {
- this.$refs.title.$el.select();
- });
- },
- onChangeTemplate(template) {
- this.$emit('changeTemplate', template || null);
- },
- onUpdate(field, value) {
- const payload = {
- title: this.title,
- description: this.description,
- [field]: value,
- };
- this.$emit('updateSettings', payload);
- },
- },
-};
-</script>
-
-<template>
- <gl-form>
- <gl-form-group
- key="title"
- :label="__('Brief title about the change')"
- :label-for="getId('control', 'title')"
- >
- <gl-form-input
- :id="getId('control', 'title')"
- ref="title"
- :value="title"
- type="text"
- @input="onUpdate('title', $event)"
- />
- </gl-form-group>
-
- <gl-form-group
- v-if="hasTemplates"
- key="template"
- :label="__('Description template')"
- :label-for="getId('control', 'template')"
- >
- <gl-dropdown :text="dropdownLabel">
- <gl-dropdown-item key="none" @click="onChangeTemplate(null)">
- {{ __('None') }}
- </gl-dropdown-item>
-
- <gl-dropdown-divider />
-
- <gl-dropdown-item
- v-for="template in templates"
- :key="template.key"
- @click="onChangeTemplate(template)"
- >
- {{ template.name }}
- </gl-dropdown-item>
- </gl-dropdown>
- </gl-form-group>
-
- <gl-form-group
- key="description"
- :label="__('Goal of the changes and what reviewers should be aware of')"
- :label-for="getId('control', 'description')"
- >
- <gl-form-textarea
- :id="getId('control', 'description')"
- :value="description"
- @input="onUpdate('description', $event)"
- />
- </gl-form-group>
- </gl-form>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/components/edit_meta_modal.vue b/app/assets/javascripts/static_site_editor/components/edit_meta_modal.vue
deleted file mode 100644
index e69a6b8cd69..00000000000
--- a/app/assets/javascripts/static_site_editor/components/edit_meta_modal.vue
+++ /dev/null
@@ -1,126 +0,0 @@
-<script>
-import { GlModal } from '@gitlab/ui';
-import Api from '~/api';
-import { __, s__, sprintf } from '~/locale';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-
-import { ISSUABLE_TYPE, MR_META_LOCAL_STORAGE_KEY } from '../constants';
-import EditMetaControls from './edit_meta_controls.vue';
-
-export default {
- components: {
- GlModal,
- EditMetaControls,
- LocalStorageSync,
- },
- props: {
- sourcePath: {
- type: String,
- required: true,
- },
- namespace: {
- type: String,
- required: true,
- },
- project: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- clearStorage: false,
- currentTemplate: null,
- mergeRequestTemplates: null,
- mergeRequestMeta: {
- title: sprintf(s__(`StaticSiteEditor|Update %{sourcePath} file`), {
- sourcePath: this.sourcePath,
- }),
- description: s__('StaticSiteEditor|Copy update'),
- },
- };
- },
- computed: {
- disabled() {
- return this.mergeRequestMeta.title === '';
- },
- primaryProps() {
- return {
- text: __('Submit changes'),
- attributes: [{ variant: 'success' }, { disabled: this.disabled }],
- };
- },
- secondaryProps() {
- return {
- text: __('Keep editing'),
- attributes: [{ variant: 'default' }],
- };
- },
- },
- mounted() {
- this.initTemplates();
- },
- methods: {
- hide() {
- this.$refs.modal.hide();
- },
- initTemplates() {
- const { namespace, project } = this;
- Api.issueTemplates(namespace, project, ISSUABLE_TYPE, (err, templates) => {
- if (err) return; // Error handled by global AJAX error handler
- this.mergeRequestTemplates = templates;
- });
- },
- show() {
- this.$refs.modal.show();
- },
- onPrimary() {
- this.$emit('primary', this.mergeRequestMeta);
- this.clearStorage = true;
- },
- onSecondary() {
- this.hide();
- },
- onChangeTemplate(template) {
- this.currentTemplate = template;
-
- const description = this.currentTemplate ? this.currentTemplate.content : '';
- const mergeRequestMeta = { ...this.mergeRequestMeta, description };
- this.onUpdateSettings(mergeRequestMeta);
- },
- onUpdateSettings(mergeRequestMeta) {
- this.mergeRequestMeta = { ...mergeRequestMeta };
- },
- },
- storageKey: MR_META_LOCAL_STORAGE_KEY,
-};
-</script>
-
-<template>
- <gl-modal
- ref="modal"
- modal-id="edit-meta-modal"
- :title="__('Submit your changes')"
- :action-primary="primaryProps"
- :action-secondary="secondaryProps"
- size="sm"
- @primary="onPrimary"
- @secondary="onSecondary"
- @hide="() => $emit('hide')"
- >
- <local-storage-sync
- v-model="mergeRequestMeta"
- :storage-key="$options.storageKey"
- :clear="clearStorage"
- />
- <edit-meta-controls
- ref="editMetaControls"
- :title="mergeRequestMeta.title"
- :description="mergeRequestMeta.description"
- :templates="mergeRequestTemplates"
- :current-template="currentTemplate"
- @updateSettings="onUpdateSettings"
- @changeTemplate="onChangeTemplate"
- />
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue b/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue
deleted file mode 100644
index dad3907c3ff..00000000000
--- a/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<script>
-import { GlForm, GlFormInput, GlFormGroup } from '@gitlab/ui';
-import { humanize } from '~/lib/utils/text_utility';
-
-export default {
- components: {
- GlForm,
- GlFormInput,
- GlFormGroup,
- },
- props: {
- settings: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- editableSettings: { ...this.settings },
- };
- },
- methods: {
- getId(type, key) {
- return `sse-front-matter-${type}-${key}`;
- },
- getIsSupported(val) {
- return ['string', 'number'].includes(typeof val);
- },
- getLabel(str) {
- return humanize(str);
- },
- onUpdate() {
- this.$emit('updateSettings', { ...this.editableSettings });
- },
- },
-};
-</script>
-<template>
- <gl-form>
- <template v-for="(value, key) of editableSettings">
- <gl-form-group
- v-if="getIsSupported(value)"
- :id="getId('form-group', key)"
- :key="key"
- :label="getLabel(key)"
- :label-for="getId('control', key)"
- >
- <gl-form-input
- :id="getId('control', key)"
- v-model.lazy="editableSettings[key]"
- type="text"
- @input="onUpdate"
- />
- </gl-form-group>
- </template>
- </gl-form>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/components/invalid_content_message.vue b/app/assets/javascripts/static_site_editor/components/invalid_content_message.vue
deleted file mode 100644
index fef87057307..00000000000
--- a/app/assets/javascripts/static_site_editor/components/invalid_content_message.vue
+++ /dev/null
@@ -1,29 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-
-export default {
- components: {
- GlButton,
- },
-};
-</script>
-
-<template>
- <div>
- <h3>{{ s__('StaticSiteEditor|Incompatible file content') }}</h3>
- <p>
- {{
- s__(
- 'StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor.',
- )
- }}
- </p>
- <div>
- <gl-button
- ref="documentationButton"
- href="https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman"
- >{{ s__('StaticSiteEditor|View documentation') }}</gl-button
- >
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue b/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue
deleted file mode 100644
index 3bb5a0b8fd5..00000000000
--- a/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-
-export default {
- components: {
- GlButton,
- },
- props: {
- hasSettings: {
- type: Boolean,
- required: false,
- default: false,
- },
- returnUrl: {
- type: String,
- required: false,
- default: '',
- },
- saveable: {
- type: Boolean,
- required: false,
- default: false,
- },
- savingChanges: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
-};
-</script>
-<template>
- <div class="d-flex bg-light border-top justify-content-end align-items-center py-3 px-4">
- <div>
- <gl-button v-if="returnUrl" ref="returnUrlLink" :href="returnUrl">{{
- s__('StaticSiteEditor|Return to site')
- }}</gl-button>
- <gl-button
- v-if="hasSettings"
- ref="settings"
- :disabled="savingChanges"
- @click="$emit('editSettings')"
- >
- {{ __('Page settings') }}
- </gl-button>
- <gl-button
- ref="submit"
- variant="success"
- :disabled="!saveable"
- :loading="savingChanges"
- @click="$emit('submit')"
- >
- {{ __('Submit changes...') }}
- </gl-button>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/components/skeleton_loader.vue b/app/assets/javascripts/static_site_editor/components/skeleton_loader.vue
deleted file mode 100644
index 1b6179883aa..00000000000
--- a/app/assets/javascripts/static_site_editor/components/skeleton_loader.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-<script>
-import { GlSkeletonLoader } from '@gitlab/ui';
-
-export default {
- components: {
- GlSkeletonLoader,
- },
-};
-</script>
-<template>
- <gl-skeleton-loader :width="500" :height="102">
- <rect width="500" height="16" rx="4" />
- <rect y="20" width="375" height="16" rx="4" />
- <rect x="380" y="20" width="120" height="16" rx="4" />
- <rect y="40" width="250" height="16" rx="4" />
- <rect x="255" y="40" width="150" height="16" rx="4" />
- <rect x="410" y="40" width="90" height="16" rx="4" />
- </gl-skeleton-loader>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/components/submit_changes_error.vue b/app/assets/javascripts/static_site_editor/components/submit_changes_error.vue
deleted file mode 100644
index c5b6c685124..00000000000
--- a/app/assets/javascripts/static_site_editor/components/submit_changes_error.vue
+++ /dev/null
@@ -1,24 +0,0 @@
-<script>
-import { GlAlert, GlButton } from '@gitlab/ui';
-
-export default {
- components: {
- GlAlert,
- GlButton,
- },
- props: {
- error: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-<template>
- <gl-alert variant="danger" dismissible @dismiss="$emit('dismiss')">
- {{ s__('StaticSiteEditor|An error occurred while submitting your changes.') }} {{ error }}
- <template #actions>
- <gl-button variant="danger" @click="$emit('retry')">{{ __('Retry') }}</gl-button>
- </template>
- </gl-alert>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/components/unsaved_changes_confirm_dialog.vue b/app/assets/javascripts/static_site_editor/components/unsaved_changes_confirm_dialog.vue
deleted file mode 100644
index 255f029bd27..00000000000
--- a/app/assets/javascripts/static_site_editor/components/unsaved_changes_confirm_dialog.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<script>
-export default {
- props: {
- modified: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- created() {
- window.addEventListener('beforeunload', this.requestConfirmation);
- },
- destroyed() {
- window.removeEventListener('beforeunload', this.requestConfirmation);
- },
- methods: {
- requestConfirmation(e) {
- if (this.modified) {
- e.preventDefault();
- // eslint-disable-next-line no-param-reassign
- e.returnValue = '';
- }
- },
- },
- render: () => null,
-};
-</script>
diff --git a/app/assets/javascripts/static_site_editor/constants.js b/app/assets/javascripts/static_site_editor/constants.js
deleted file mode 100644
index ab7fd0542bf..00000000000
--- a/app/assets/javascripts/static_site_editor/constants.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { s__, __ } from '~/locale';
-
-export const BRANCH_SUFFIX_COUNT = 8;
-export const ISSUABLE_TYPE = 'merge_request';
-
-export const SUBMIT_CHANGES_BRANCH_ERROR = s__('StaticSiteEditor|Branch could not be created.');
-export const SUBMIT_CHANGES_COMMIT_ERROR = s__(
- 'StaticSiteEditor|Could not commit the content changes.',
-);
-export const SUBMIT_CHANGES_MERGE_REQUEST_ERROR = s__(
- 'StaticSiteEditor|Could not create merge request.',
-);
-export const LOAD_CONTENT_ERROR = __(
- 'An error occurred while loading your content. Please try again.',
-);
-
-export const DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE = s__(
- 'StaticSiteEditor|Automatic formatting changes',
-);
-
-export const DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION = s__(
- 'StaticSiteEditor|Markdown formatting preferences introduced by the Static Site Editor',
-);
-
-export const DEFAULT_HEADING = s__('StaticSiteEditor|Static site editor');
-
-export const TRACKING_ACTION_CREATE_COMMIT = 'create_commit';
-export const TRACKING_ACTION_CREATE_MERGE_REQUEST = 'create_merge_request';
-export const TRACKING_ACTION_INITIALIZE_EDITOR = 'initialize_editor';
-
-export const SERVICE_PING_TRACKING_ACTION_CREATE_COMMIT = 'static_site_editor_commits';
-export const SERVICE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST =
- 'static_site_editor_merge_requests';
-
-export const MR_META_LOCAL_STORAGE_KEY = 'sse-merge-request-meta-storage-key';
diff --git a/app/assets/javascripts/static_site_editor/graphql/index.js b/app/assets/javascripts/static_site_editor/graphql/index.js
deleted file mode 100644
index 53572e680e5..00000000000
--- a/app/assets/javascripts/static_site_editor/graphql/index.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
-import appDataQuery from './queries/app_data.query.graphql';
-import fileResolver from './resolvers/file';
-import hasSubmittedChangesResolver from './resolvers/has_submitted_changes';
-import submitContentChangesResolver from './resolvers/submit_content_changes';
-import typeDefs from './typedefs.graphql';
-
-Vue.use(VueApollo);
-
-const createApolloProvider = (appData) => {
- const defaultClient = createDefaultClient(
- {
- Project: {
- file: fileResolver,
- },
- Mutation: {
- submitContentChanges: submitContentChangesResolver,
- hasSubmittedChanges: hasSubmittedChangesResolver,
- },
- },
- {
- typeDefs,
- },
- );
-
- // eslint-disable-next-line @gitlab/require-i18n-strings
- const mounts = appData.mounts.map((mount) => ({ __typename: 'Mount', ...mount }));
-
- defaultClient.cache.writeQuery({
- query: appDataQuery,
- data: {
- appData: {
- __typename: 'AppData',
- ...appData,
- mounts,
- },
- },
- });
-
- return new VueApollo({
- defaultClient,
- });
-};
-
-export default createApolloProvider;
diff --git a/app/assets/javascripts/static_site_editor/graphql/mutations/has_submitted_changes.mutation.graphql b/app/assets/javascripts/static_site_editor/graphql/mutations/has_submitted_changes.mutation.graphql
deleted file mode 100644
index 1f47929556a..00000000000
--- a/app/assets/javascripts/static_site_editor/graphql/mutations/has_submitted_changes.mutation.graphql
+++ /dev/null
@@ -1,5 +0,0 @@
-mutation hasSubmittedChanges($input: HasSubmittedChangesInput) {
- hasSubmittedChanges(input: $input) @client {
- hasSubmittedChanges
- }
-}
diff --git a/app/assets/javascripts/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql b/app/assets/javascripts/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql
deleted file mode 100644
index cd130aa7dbb..00000000000
--- a/app/assets/javascripts/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql
+++ /dev/null
@@ -1,7 +0,0 @@
-mutation submitContentChanges($input: SubmitContentChangesInput) {
- submitContentChanges(input: $input) @client {
- branch
- commit
- mergeRequest
- }
-}
diff --git a/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql b/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql
deleted file mode 100644
index e422a4b6036..00000000000
--- a/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql
+++ /dev/null
@@ -1,17 +0,0 @@
-query appData {
- appData @client {
- isSupportedContent
- hasSubmittedChanges
- project
- sourcePath
- username
- returnUrl
- branch
- baseUrl
- mounts {
- source
- target
- }
- imageUploadPath
- }
-}
diff --git a/app/assets/javascripts/static_site_editor/graphql/queries/saved_content_meta.query.graphql b/app/assets/javascripts/static_site_editor/graphql/queries/saved_content_meta.query.graphql
deleted file mode 100644
index c29b6f93b81..00000000000
--- a/app/assets/javascripts/static_site_editor/graphql/queries/saved_content_meta.query.graphql
+++ /dev/null
@@ -1,3 +0,0 @@
-query savedContentMeta {
- savedContentMeta @client
-}
diff --git a/app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql b/app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql
deleted file mode 100644
index c8c4195e1cd..00000000000
--- a/app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-query sourceContent($project: ID!, $sourcePath: String!) {
- project(fullPath: $project) {
- id
- fullPath
- file(path: $sourcePath) @client {
- title
- content
- }
- }
-}
diff --git a/app/assets/javascripts/static_site_editor/graphql/resolvers/file.js b/app/assets/javascripts/static_site_editor/graphql/resolvers/file.js
deleted file mode 100644
index fc3cac52e2a..00000000000
--- a/app/assets/javascripts/static_site_editor/graphql/resolvers/file.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import loadSourceContent from '../../services/load_source_content';
-
-const fileResolver = ({ fullPath: projectId }, { path: sourcePath }) => {
- return loadSourceContent({ projectId, sourcePath }).then((sourceContent) => ({
- // eslint-disable-next-line @gitlab/require-i18n-strings
- __typename: 'File',
- ...sourceContent,
- }));
-};
-
-export default fileResolver;
diff --git a/app/assets/javascripts/static_site_editor/graphql/resolvers/has_submitted_changes.js b/app/assets/javascripts/static_site_editor/graphql/resolvers/has_submitted_changes.js
deleted file mode 100644
index 35ecf6d698c..00000000000
--- a/app/assets/javascripts/static_site_editor/graphql/resolvers/has_submitted_changes.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { produce } from 'immer';
-import query from '../queries/app_data.query.graphql';
-
-const hasSubmittedChangesResolver = (_, { input: { hasSubmittedChanges } }, { cache }) => {
- const oldData = cache.readQuery({ query });
-
- const data = produce(oldData, (draftState) => {
- // punctually modifying draftState as per immer docs upsets our linters
- return {
- ...draftState,
- appData: {
- __typename: 'AppData',
- ...draftState.appData,
- hasSubmittedChanges,
- },
- };
- });
-
- cache.writeQuery({
- query,
- data,
- });
-};
-
-export default hasSubmittedChangesResolver;
diff --git a/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js b/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js
deleted file mode 100644
index e9f1828bff8..00000000000
--- a/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import { produce } from 'immer';
-import submitContentChanges from '../../services/submit_content_changes';
-import savedContentMetaQuery from '../queries/saved_content_meta.query.graphql';
-
-const submitContentChangesResolver = (
- _,
- {
- input: {
- project: projectId,
- username,
- sourcePath,
- targetBranch,
- content,
- images,
- mergeRequestMeta,
- formattedMarkdown,
- },
- },
- { cache },
-) => {
- return submitContentChanges({
- projectId,
- username,
- sourcePath,
- targetBranch,
- content,
- images,
- mergeRequestMeta,
- formattedMarkdown,
- }).then((savedContentMeta) => {
- const data = produce(savedContentMeta, (draftState) => {
- return {
- savedContentMeta: {
- __typename: 'SavedContentMeta',
- ...draftState,
- },
- };
- });
-
- cache.writeQuery({
- query: savedContentMetaQuery,
- data,
- });
- });
-};
-
-export default submitContentChangesResolver;
diff --git a/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql b/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql
deleted file mode 100644
index 00af6c10359..00000000000
--- a/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql
+++ /dev/null
@@ -1,58 +0,0 @@
-type File {
- title: String
- content: String!
-}
-
-type SavedContentField {
- label: String!
- url: String!
-}
-
-type SavedContentMeta {
- mergeRequest: SavedContentField!
- commit: SavedContentField!
- branch: SavedContentField!
-}
-
-type Mount {
- source: String!
- target: String
-}
-
-type AppData {
- isSupportedContent: Boolean!
- hasSubmittedChanges: Boolean!
- project: String!
- returnUrl: String
- sourcePath: String!
- username: String!
- branch: String!
- baseUrl: String!
- mounts: [Mount]!
- imageUploadPath: String!
-}
-
-input HasSubmittedChangesInput {
- hasSubmittedChanges: Boolean!
-}
-
-input SubmitContentChangesInput {
- project: String!
- sourcePath: String!
- content: String!
- username: String!
-}
-
-extend type Project {
- file(path: ID!): File
-}
-
-extend type Query {
- appData: AppData!
- savedContentMeta: SavedContentMeta
-}
-
-extend type Mutation {
- submitContentChanges(input: SubmitContentChangesInput!): SavedContentMeta
- hasSubmittedChanges(input: HasSubmittedChangesInput!): AppData
-}
diff --git a/app/assets/javascripts/static_site_editor/image_repository.js b/app/assets/javascripts/static_site_editor/image_repository.js
deleted file mode 100644
index 4ad2e2618ac..00000000000
--- a/app/assets/javascripts/static_site_editor/image_repository.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import createFlash from '~/flash';
-import { __ } from '~/locale';
-import { getBinary } from './services/image_service';
-
-const imageRepository = () => {
- const images = new Map();
- const flash = (message) =>
- createFlash({
- message,
- });
-
- const add = (file, url) => {
- getBinary(file)
- .then((content) => images.set(url, content))
- .catch(() => flash(__('Something went wrong while inserting your image. Please try again.')));
- };
-
- const get = (path) => images.get(path);
-
- const getAll = () => images;
-
- return { add, get, getAll };
-};
-
-export default imageRepository;
diff --git a/app/assets/javascripts/static_site_editor/index.js b/app/assets/javascripts/static_site_editor/index.js
deleted file mode 100644
index 985579f68e8..00000000000
--- a/app/assets/javascripts/static_site_editor/index.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import Vue from 'vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import App from './components/app.vue';
-import createApolloProvider from './graphql';
-import createRouter from './router';
-
-const initStaticSiteEditor = (el) => {
- const {
- isSupportedContent,
- path: sourcePath,
- baseUrl,
- branch,
- namespace,
- project,
- mergeRequestsIllustrationPath,
- // NOTE: The following variables are not yet used, but are supported by the config file,
- // so we are adding them here as a convenience for future use.
- // eslint-disable-next-line no-unused-vars
- staticSiteGenerator,
- imageUploadPath,
- mounts,
- } = el.dataset;
- const { current_username: username } = window.gon;
- const returnUrl = el.dataset.returnUrl || null;
- const router = createRouter(baseUrl);
- const apolloProvider = createApolloProvider({
- isSupportedContent: parseBoolean(isSupportedContent),
- hasSubmittedChanges: false,
- project: `${namespace}/${project}`,
- mounts: JSON.parse(mounts), // NOTE that the object in 'mounts' is a JSON string from the data attribute, so it must be parsed into an object.
- branch,
- baseUrl,
- returnUrl,
- sourcePath,
- username,
- imageUploadPath,
- });
-
- return new Vue({
- el,
- router,
- apolloProvider,
- components: {
- App,
- },
- render(createElement) {
- return createElement('app', {
- props: {
- mergeRequestsIllustrationPath,
- },
- });
- },
- });
-};
-
-export default initStaticSiteEditor;
diff --git a/app/assets/javascripts/static_site_editor/pages/home.vue b/app/assets/javascripts/static_site_editor/pages/home.vue
deleted file mode 100644
index beec1b515ad..00000000000
--- a/app/assets/javascripts/static_site_editor/pages/home.vue
+++ /dev/null
@@ -1,169 +0,0 @@
-<script>
-import createFlash from '~/flash';
-import Tracking from '~/tracking';
-
-import EditArea from '../components/edit_area.vue';
-import EditMetaModal from '../components/edit_meta_modal.vue';
-import InvalidContentMessage from '../components/invalid_content_message.vue';
-import SkeletonLoader from '../components/skeleton_loader.vue';
-import SubmitChangesError from '../components/submit_changes_error.vue';
-import { LOAD_CONTENT_ERROR, TRACKING_ACTION_INITIALIZE_EDITOR } from '../constants';
-import hasSubmittedChangesMutation from '../graphql/mutations/has_submitted_changes.mutation.graphql';
-import submitContentChangesMutation from '../graphql/mutations/submit_content_changes.mutation.graphql';
-import appDataQuery from '../graphql/queries/app_data.query.graphql';
-import sourceContentQuery from '../graphql/queries/source_content.query.graphql';
-import { SUCCESS_ROUTE } from '../router/constants';
-
-export default {
- components: {
- SkeletonLoader,
- EditArea,
- EditMetaModal,
- InvalidContentMessage,
- SubmitChangesError,
- },
- apollo: {
- appData: {
- query: appDataQuery,
- },
- sourceContent: {
- query: sourceContentQuery,
- update: ({
- project: {
- file: { title, content },
- },
- }) => {
- return { title, content };
- },
- variables() {
- return {
- project: this.appData.project,
- sourcePath: this.appData.sourcePath,
- };
- },
- skip() {
- return !this.appData.isSupportedContent;
- },
- error() {
- createFlash({
- message: LOAD_CONTENT_ERROR,
- });
- },
- },
- },
- data() {
- return {
- content: null,
- images: null,
- formattedMarkdown: null,
- submitChangesError: null,
- isSavingChanges: false,
- };
- },
- computed: {
- isLoadingContent() {
- return this.$apollo.queries.sourceContent.loading;
- },
- isContentLoaded() {
- return Boolean(this.sourceContent);
- },
- projectSplit() {
- return this.appData.project.split('/'); // TODO: refactor so `namespace` and `project` remain distinct
- },
- },
- mounted() {
- Tracking.event(document.body.dataset.page, TRACKING_ACTION_INITIALIZE_EDITOR);
- },
- methods: {
- onHideModal() {
- this.isSavingChanges = false;
- this.$refs.editMetaModal.hide();
- },
- onDismissError() {
- this.submitChangesError = null;
- },
- onPrepareSubmit({ formattedMarkdown, content, images }) {
- this.content = content;
- this.images = images;
- this.formattedMarkdown = formattedMarkdown;
-
- this.isSavingChanges = true;
- this.$refs.editMetaModal.show();
- },
- onSubmit(mergeRequestMeta) {
- // eslint-disable-next-line promise/catch-or-return
- this.$apollo
- .mutate({
- mutation: hasSubmittedChangesMutation,
- variables: {
- input: {
- hasSubmittedChanges: true,
- },
- },
- })
- .finally(() => {
- this.$router.push(SUCCESS_ROUTE);
- });
-
- this.$apollo
- .mutate({
- mutation: submitContentChangesMutation,
- variables: {
- input: {
- project: this.appData.project,
- username: this.appData.username,
- sourcePath: this.appData.sourcePath,
- targetBranch: this.appData.branch,
- content: this.content,
- formattedMarkdown: this.formattedMarkdown,
- images: this.images,
- mergeRequestMeta,
- },
- },
- })
- .catch((e) => {
- this.submitChangesError = e.message;
- })
- .finally(() => {
- this.isSavingChanges = false;
- });
- },
- },
-};
-</script>
-<template>
- <div class="container d-flex gl-flex-direction-column pt-2 h-100">
- <template v-if="appData.isSupportedContent">
- <skeleton-loader v-if="isLoadingContent" class="w-75 gl-align-self-center gl-mt-5" />
- <submit-changes-error
- v-if="submitChangesError"
- :error="submitChangesError"
- @retry="onSubmit"
- @dismiss="onDismissError"
- />
- <edit-area
- v-if="isContentLoaded"
- :title="sourceContent.title"
- :content="sourceContent.content"
- :saving-changes="isSavingChanges"
- :return-url="appData.returnUrl"
- :mounts="appData.mounts"
- :branch="appData.branch"
- :base-url="appData.baseUrl"
- :project="appData.project"
- :image-root="appData.imageUploadPath"
- @submit="onPrepareSubmit"
- />
- <edit-meta-modal
- ref="editMetaModal"
- :source-path="appData.sourcePath"
- :namespace="projectSplit[0]"
- :project="projectSplit[1]"
- @primary="onSubmit"
- @hide="onHideModal"
- />
- </template>
-
- <invalid-content-message v-else class="w-75" />
- </div>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/pages/success.vue b/app/assets/javascripts/static_site_editor/pages/success.vue
deleted file mode 100644
index eb03aa3cca3..00000000000
--- a/app/assets/javascripts/static_site_editor/pages/success.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-<script>
-import { GlButton, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
-import { s__, __, sprintf } from '~/locale';
-
-import appDataQuery from '../graphql/queries/app_data.query.graphql';
-import savedContentMetaQuery from '../graphql/queries/saved_content_meta.query.graphql';
-import { HOME_ROUTE } from '../router/constants';
-
-export default {
- components: {
- GlButton,
- GlEmptyState,
- GlLoadingIcon,
- },
- props: {
- mergeRequestsIllustrationPath: {
- type: String,
- required: true,
- },
- },
- apollo: {
- savedContentMeta: {
- query: savedContentMetaQuery,
- },
- appData: {
- query: appDataQuery,
- },
- },
- computed: {
- updatedFileDescription() {
- const { sourcePath } = this.appData;
-
- return sprintf(__('Update %{sourcePath} file'), { sourcePath });
- },
- },
- created() {
- if (!this.appData.hasSubmittedChanges) {
- this.$router.push(HOME_ROUTE);
- }
- },
- title: s__('StaticSiteEditor|Your merge request has been created'),
- primaryButtonText: __('View merge request'),
- returnToSiteBtnText: s__('StaticSiteEditor|Return to site'),
- mergeRequestInstructionsHeading: s__(
- 'StaticSiteEditor|To see your changes live you will need to do the following things:',
- ),
- addTitleInstruction: s__('StaticSiteEditor|1. Add a clear title to describe the change.'),
- addDescriptionInstruction: s__(
- 'StaticSiteEditor|2. Add a description to explain why the change is being made.',
- ),
- assignMergeRequestInstruction: s__(
- 'StaticSiteEditor|3. Assign a person to review and accept the merge request.',
- ),
- submittingTitle: s__('StaticSiteEditor|Creating your merge request'),
- submittingNotePrimary: s__(
- 'StaticSiteEditor|You can set an assignee to get your changes reviewed and deployed once your merge request is created.',
- ),
- submittingNoteSecondary: s__(
- 'StaticSiteEditor|A link to view the merge request will appear once ready.',
- ),
-};
-</script>
-<template>
- <div>
- <div class="gl-border-b-solid gl-border-b-1 gl-border-b-gray-100">
- <div class="container gl-py-4">
- <div class="gl-display-flex">
- <gl-button
- v-if="appData.returnUrl"
- ref="returnToSiteButton"
- class="gl-mr-5 gl-align-self-start"
- :href="appData.returnUrl"
- >{{ $options.returnToSiteBtnText }}</gl-button
- >
- <strong class="gl-mt-2">
- {{ updatedFileDescription }}
- </strong>
- </div>
- </div>
- </div>
- <div class="container">
- <gl-empty-state
- class="gl-my-7"
- :title="savedContentMeta ? $options.title : $options.submittingTitle"
- :primary-button-text="savedContentMeta && $options.primaryButtonText"
- :primary-button-link="savedContentMeta && savedContentMeta.mergeRequest.url"
- :svg-path="mergeRequestsIllustrationPath"
- :svg-height="146"
- >
- <template #description>
- <div v-if="savedContentMeta">
- <p>{{ $options.mergeRequestInstructionsHeading }}</p>
- <p>{{ $options.addTitleInstruction }}</p>
- <p>{{ $options.addDescriptionInstruction }}</p>
- <p>{{ $options.assignMergeRequestInstruction }}</p>
- </div>
- <div v-else>
- <p>{{ $options.submittingNotePrimary }}</p>
- <p>{{ $options.submittingNoteSecondary }}</p>
- <gl-loading-icon size="xl" />
- </div>
- </template>
- </gl-empty-state>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/constants.js b/app/assets/javascripts/static_site_editor/rich_content_editor/constants.js
deleted file mode 100644
index cbb30baa488..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/constants.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import { __ } from '~/locale';
-
-export const CUSTOM_EVENTS = {
- openAddImageModal: 'gl_openAddImageModal',
- openInsertVideoModal: 'gl_openInsertVideoModal',
-};
-
-export const YOUTUBE_URL = 'https://www.youtube.com';
-
-export const YOUTUBE_EMBED_URL = `${YOUTUBE_URL}/embed`;
-
-export const ALLOWED_VIDEO_ORIGINS = [YOUTUBE_URL];
-
-/* eslint-disable @gitlab/require-i18n-strings */
-export const TOOLBAR_ITEM_CONFIGS = [
- { icon: 'heading', event: 'openHeadingSelect', classes: 'tui-heading', tooltip: __('Headings') },
- { icon: 'bold', command: 'Bold', tooltip: __('Add bold text') },
- { icon: 'italic', command: 'Italic', tooltip: __('Add italic text') },
- { icon: 'strikethrough', command: 'Strike', tooltip: __('Add strikethrough text') },
- { isDivider: true },
- { icon: 'quote', command: 'Blockquote', tooltip: __('Insert a quote') },
- { icon: 'link', event: 'openPopupAddLink', tooltip: __('Add a link') },
- { isDivider: true },
- { icon: 'list-bulleted', command: 'UL', tooltip: __('Add a bullet list') },
- { icon: 'list-numbered', command: 'OL', tooltip: __('Add a numbered list') },
- { icon: 'list-task', command: 'Task', tooltip: __('Add a task list') },
- { icon: 'list-indent', command: 'Indent', tooltip: __('Indent') },
- { icon: 'list-outdent', command: 'Outdent', tooltip: __('Outdent') },
- { isDivider: true },
- { icon: 'dash', command: 'HR', tooltip: __('Add a line') },
- { icon: 'table', event: 'openPopupAddTable', classes: 'tui-table', tooltip: __('Add a table') },
- { icon: 'doc-image', event: CUSTOM_EVENTS.openAddImageModal, tooltip: __('Insert an image') },
- { icon: 'live-preview', event: CUSTOM_EVENTS.openInsertVideoModal, tooltip: __('Insert video') },
- { isDivider: true },
- { icon: 'code', command: 'Code', tooltip: __('Insert inline code') },
- { icon: 'doc-code', command: 'CodeBlock', tooltip: __('Insert a code block') },
-];
-
-export const EDITOR_TYPES = {
- markdown: 'markdown',
- wysiwyg: 'wysiwyg',
-};
-
-export const EDITOR_HEIGHT = '100%';
-
-export const EDITOR_PREVIEW_STYLE = 'horizontal';
-
-export const IMAGE_TABS = { UPLOAD_TAB: 0, URL_TAB: 1 };
-
-export const MAX_FILE_SIZE = 2097152; // 2Mb
-
-export const VIDEO_ATTRIBUTES = {
- width: '560',
- height: '315',
- frameBorder: '0',
- allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
-};
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/modals/add_image/add_image_modal.vue b/app/assets/javascripts/static_site_editor/rich_content_editor/modals/add_image/add_image_modal.vue
deleted file mode 100644
index 82060d2e4ad..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/modals/add_image/add_image_modal.vue
+++ /dev/null
@@ -1,134 +0,0 @@
-<script>
-import { GlModal, GlFormGroup, GlFormInput, GlTabs, GlTab } from '@gitlab/ui';
-import { isSafeURL, joinPaths } from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
-import { IMAGE_TABS } from '../../constants';
-import UploadImageTab from './upload_image_tab.vue';
-
-export default {
- components: {
- UploadImageTab,
- GlModal,
- GlFormGroup,
- GlFormInput,
- GlTabs,
- GlTab,
- },
- props: {
- imageRoot: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- file: null,
- urlError: null,
- imageUrl: null,
- description: null,
- tabIndex: IMAGE_TABS.UPLOAD_TAB,
- uploadImageTab: null,
- };
- },
- modalTitle: __('Image details'),
- okTitle: __('Insert image'),
- urlTabTitle: __('Link to an image'),
- urlLabel: __('Image URL'),
- descriptionLabel: __('Description'),
- uploadTabTitle: __('Upload an image'),
- computed: {
- altText() {
- return this.description;
- },
- },
- methods: {
- show() {
- this.file = null;
- this.urlError = null;
- this.imageUrl = null;
- this.description = null;
- this.tabIndex = IMAGE_TABS.UPLOAD_TAB;
-
- this.$refs.modal.show();
- },
- onOk(event) {
- if (this.tabIndex === IMAGE_TABS.UPLOAD_TAB) {
- this.submitFile(event);
- return;
- }
- this.submitURL(event);
- },
- setFile(file) {
- this.file = file;
- },
- submitFile(event) {
- const { file, altText } = this;
- const { uploadImageTab } = this.$refs;
-
- uploadImageTab.validateFile();
-
- if (uploadImageTab.fileError) {
- event.preventDefault();
- return;
- }
-
- const imageUrl = joinPaths(this.imageRoot, file.name);
-
- this.$emit('addImage', { imageUrl, file, altText: altText || file.name });
- },
- submitURL(event) {
- if (!this.validateUrl()) {
- event.preventDefault();
- return;
- }
-
- const { imageUrl, altText } = this;
-
- this.$emit('addImage', { imageUrl, altText: altText || imageUrl });
- },
- validateUrl() {
- if (!isSafeURL(this.imageUrl)) {
- this.urlError = __('Please provide a valid URL');
- this.$refs.urlInput.$el.focus();
- return false;
- }
-
- return true;
- },
- },
-};
-</script>
-<template>
- <gl-modal
- ref="modal"
- modal-id="add-image-modal"
- :title="$options.modalTitle"
- :ok-title="$options.okTitle"
- @ok="onOk"
- >
- <gl-tabs v-model="tabIndex">
- <!-- Upload file Tab -->
- <gl-tab :title="$options.uploadTabTitle">
- <upload-image-tab ref="uploadImageTab" @input="setFile" />
- </gl-tab>
-
- <!-- By URL Tab -->
- <gl-tab :title="$options.urlTabTitle">
- <gl-form-group
- class="gl-mt-5 gl-mb-3"
- :label="$options.urlLabel"
- label-for="url-input"
- :state="!Boolean(urlError)"
- :invalid-feedback="urlError"
- >
- <gl-form-input id="url-input" ref="urlInput" v-model="imageUrl" />
- </gl-form-group>
- </gl-tab>
- </gl-tabs>
-
- <!-- Description Input -->
- <gl-form-group :label="$options.descriptionLabel" label-for="description-input">
- <gl-form-input id="description-input" ref="descriptionInput" v-model="description" />
- </gl-form-group>
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/modals/add_image/upload_image_tab.vue b/app/assets/javascripts/static_site_editor/rich_content_editor/modals/add_image/upload_image_tab.vue
deleted file mode 100644
index 9baa7f286d7..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/modals/add_image/upload_image_tab.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-<script>
-import { GlFormGroup } from '@gitlab/ui';
-import { __ } from '~/locale';
-import { MAX_FILE_SIZE } from '../../constants';
-
-export default {
- components: {
- GlFormGroup,
- },
- data() {
- return {
- file: null,
- fileError: null,
- };
- },
- fileLabel: __('Select file'),
- methods: {
- onInput(event) {
- [this.file] = event.target.files;
-
- this.validateFile();
-
- if (!this.fileError) {
- this.$emit('input', this.file);
- }
- },
- validateFile() {
- this.fileError = null;
-
- if (!this.file) {
- this.fileError = __('Please choose a file');
- } else if (this.file.size > MAX_FILE_SIZE) {
- this.fileError = __('Maximum file size is 2MB. Please select a smaller file.');
- }
- },
- },
-};
-</script>
-<template>
- <gl-form-group
- class="gl-mt-5 gl-mb-3"
- :label="$options.fileLabel"
- label-for="file-input"
- :state="!Boolean(fileError)"
- :invalid-feedback="fileError"
- >
- <input
- id="file-input"
- ref="fileInput"
- class="gl-mt-3 gl-mb-2"
- type="file"
- accept="image/*"
- @input="onInput"
- />
- </gl-form-group>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/modals/insert_video_modal.vue b/app/assets/javascripts/static_site_editor/rich_content_editor/modals/insert_video_modal.vue
deleted file mode 100644
index 5ce2c17f8de..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/modals/insert_video_modal.vue
+++ /dev/null
@@ -1,93 +0,0 @@
-<script>
-import { GlModal, GlFormGroup, GlFormInput, GlSprintf } from '@gitlab/ui';
-import { isSafeURL } from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
-import { YOUTUBE_URL, YOUTUBE_EMBED_URL } from '../constants';
-
-export default {
- components: {
- GlModal,
- GlFormGroup,
- GlFormInput,
- GlSprintf,
- },
- data() {
- return {
- url: null,
- urlError: null,
- description: __(
- 'If the YouTube URL is https://www.youtube.com/watch?v=0t1DgySidms then the video ID is %{id}',
- ),
- };
- },
- modalTitle: __('Insert a video'),
- okTitle: __('Insert video'),
- label: __('YouTube URL or ID'),
- methods: {
- show() {
- this.urlError = null;
- this.url = null;
-
- this.$refs.modal.show();
- },
- onPrimary(event) {
- this.submitURL(event);
- },
- submitURL(event) {
- const url = this.generateUrl();
-
- if (!url) {
- event.preventDefault();
- return;
- }
-
- this.$emit('insertVideo', url);
- },
- generateUrl() {
- let { url } = this;
- const reYouTubeId = /^[A-z0-9]*$/;
- const reYouTubeUrl = RegExp(`${YOUTUBE_URL}/(embed/|watch\\?v=)([A-z0-9]+)`);
-
- if (reYouTubeId.test(url)) {
- url = `${YOUTUBE_EMBED_URL}/${url}`;
- } else if (reYouTubeUrl.test(url)) {
- url = `${YOUTUBE_EMBED_URL}/${reYouTubeUrl.exec(url)[2]}`;
- }
-
- if (!isSafeURL(url) || !reYouTubeUrl.test(url)) {
- this.urlError = __('Please provide a valid YouTube URL or ID');
- this.$refs.urlInput.$el.focus();
- return null;
- }
-
- return url;
- },
- },
-};
-</script>
-<template>
- <gl-modal
- ref="modal"
- size="sm"
- modal-id="insert-video-modal"
- :title="$options.modalTitle"
- :ok-title="$options.okTitle"
- @primary="onPrimary"
- >
- <gl-form-group
- :label="$options.label"
- label-for="video-modal-url-input"
- :state="!Boolean(urlError)"
- :invalid-feedback="urlError"
- >
- <gl-form-input id="video-modal-url-input" ref="urlInput" v-model="url" />
- <template #description>
- <gl-sprintf :message="description" class="text-gl-muted">
- <template #id>
- <strong>{{ __('0t1DgySidms') }}</strong>
- </template>
- </gl-sprintf>
- </template>
- </gl-form-group>
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/rich_content_editor.vue b/app/assets/javascripts/static_site_editor/rich_content_editor/rich_content_editor.vue
deleted file mode 100644
index 8988dab85d2..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/rich_content_editor.vue
+++ /dev/null
@@ -1,150 +0,0 @@
-<script>
-import 'codemirror/lib/codemirror.css';
-import '@toast-ui/editor/dist/toastui-editor.css';
-
-import { EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE, CUSTOM_EVENTS } from './constants';
-import AddImageModal from './modals/add_image/add_image_modal.vue';
-import InsertVideoModal from './modals/insert_video_modal.vue';
-
-import {
- registerHTMLToMarkdownRenderer,
- getEditorOptions,
- addCustomEventListener,
- removeCustomEventListener,
- addImage,
- getMarkdown,
- insertVideo,
-} from './services/editor_service';
-
-export default {
- components: {
- ToastEditor: () =>
- import(/* webpackChunkName: 'toast_editor' */ '@toast-ui/vue-editor').then(
- (toast) => toast.Editor,
- ),
- AddImageModal,
- InsertVideoModal,
- },
- props: {
- content: {
- type: String,
- required: true,
- },
- options: {
- type: Object,
- required: false,
- default: () => null,
- },
- initialEditType: {
- type: String,
- required: false,
- default: EDITOR_TYPES.wysiwyg,
- },
- height: {
- type: String,
- required: false,
- default: EDITOR_HEIGHT,
- },
- previewStyle: {
- type: String,
- required: false,
- default: EDITOR_PREVIEW_STYLE,
- },
- imageRoot: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- editorApi: null,
- previousMode: null,
- };
- },
- computed: {
- editorInstance() {
- return this.$refs.editor;
- },
- customEventListeners() {
- return [
- { event: CUSTOM_EVENTS.openAddImageModal, listener: this.onOpenAddImageModal },
- { event: CUSTOM_EVENTS.openInsertVideoModal, listener: this.onOpenInsertVideoModal },
- ];
- },
- },
- created() {
- this.editorOptions = getEditorOptions(this.options);
- },
- beforeDestroy() {
- this.removeListeners();
- },
- methods: {
- addListeners(editorApi) {
- this.customEventListeners.forEach(({ event, listener }) => {
- addCustomEventListener(editorApi, event, listener);
- });
-
- editorApi.eventManager.listen('changeMode', this.onChangeMode);
- },
- removeListeners() {
- this.customEventListeners.forEach(({ event, listener }) => {
- removeCustomEventListener(this.editorApi, event, listener);
- });
-
- this.editorApi.eventManager.removeEventHandler('changeMode', this.onChangeMode);
- },
- resetInitialValue(newVal) {
- this.editorInstance.invoke('setMarkdown', newVal);
- },
- onContentChanged() {
- this.$emit('input', getMarkdown(this.editorInstance));
- },
- onLoad(editorApi) {
- this.editorApi = editorApi;
-
- registerHTMLToMarkdownRenderer(editorApi);
-
- this.addListeners(editorApi);
-
- this.$emit('load', { formattedMarkdown: editorApi.getMarkdown() });
- },
- onOpenAddImageModal() {
- this.$refs.addImageModal.show();
- },
- onAddImage({ imageUrl, altText, file }) {
- const image = { imageUrl, altText };
-
- if (file) {
- this.$emit('uploadImage', { file, imageUrl });
- }
-
- addImage(this.editorInstance, image, file);
- },
- onOpenInsertVideoModal() {
- this.$refs.insertVideoModal.show();
- },
- onInsertVideo(url) {
- insertVideo(this.editorInstance, url);
- },
- onChangeMode(newMode) {
- this.$emit('modeChange', newMode);
- },
- },
-};
-</script>
-<template>
- <div>
- <toast-editor
- ref="editor"
- :initial-value="content"
- :options="editorOptions"
- :preview-style="previewStyle"
- :initial-edit-type="initialEditType"
- :height="height"
- @change="onContentChanged"
- @load="onLoad"
- />
- <add-image-modal ref="addImageModal" :image-root="imageRoot" @addImage="onAddImage" />
- <insert-video-modal ref="insertVideoModal" @insertVideo="onInsertVideo" />
- </div>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/build_custom_renderer.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/build_custom_renderer.js
deleted file mode 100644
index 6ffd280e005..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/services/build_custom_renderer.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { union, mapValues } from 'lodash';
-import renderAttributeDefinition from './renderers/render_attribute_definition';
-import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline';
-import renderHeading from './renderers/render_heading';
-import renderBlockHtml from './renderers/render_html_block';
-import renderIdentifierInstanceText from './renderers/render_identifier_instance_text';
-import renderIdentifierParagraph from './renderers/render_identifier_paragraph';
-import renderListItem from './renderers/render_list_item';
-import renderSoftbreak from './renderers/render_softbreak';
-
-const htmlInlineRenderers = [renderFontAwesomeHtmlInline];
-const htmlBlockRenderers = [renderBlockHtml];
-const headingRenderers = [renderHeading];
-const paragraphRenderers = [renderIdentifierParagraph, renderBlockHtml];
-const textRenderers = [renderIdentifierInstanceText, renderAttributeDefinition];
-const listItemRenderers = [renderListItem];
-const softbreakRenderers = [renderSoftbreak];
-
-const executeRenderer = (renderers, node, context) => {
- const availableRenderer = renderers.find((renderer) => renderer.canRender(node, context));
-
- return availableRenderer ? availableRenderer.render(node, context) : context.origin();
-};
-
-const buildCustomHTMLRenderer = (customRenderers) => {
- const renderersByType = {
- ...customRenderers,
- htmlBlock: union(htmlBlockRenderers, customRenderers?.htmlBlock),
- htmlInline: union(htmlInlineRenderers, customRenderers?.htmlInline),
- heading: union(headingRenderers, customRenderers?.heading),
- item: union(listItemRenderers, customRenderers?.listItem),
- paragraph: union(paragraphRenderers, customRenderers?.paragraph),
- text: union(textRenderers, customRenderers?.text),
- softbreak: union(softbreakRenderers, customRenderers?.softbreak),
- };
-
- return mapValues(renderersByType, (renderers) => {
- return (node, context) => executeRenderer(renderers, node, context);
- });
-};
-
-export default buildCustomHTMLRenderer;
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/build_html_to_markdown_renderer.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/build_html_to_markdown_renderer.js
deleted file mode 100644
index 273e0a59963..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/services/build_html_to_markdown_renderer.js
+++ /dev/null
@@ -1,109 +0,0 @@
-/* eslint-disable @gitlab/require-i18n-strings */
-import { defaults, repeat } from 'lodash';
-
-const DEFAULTS = {
- subListIndentSpaces: 4,
- unorderedListBulletChar: '-',
- incrementListMarker: false,
- strong: '*',
- emphasis: '_',
-};
-
-const countIndentSpaces = (text) => {
- const matches = text.match(/^\s+/m);
-
- return matches ? matches[0].length : 0;
-};
-
-const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => {
- const {
- subListIndentSpaces,
- unorderedListBulletChar,
- incrementListMarker,
- strong,
- emphasis,
- } = defaults(formattingPreferences, DEFAULTS);
- const sublistNode = 'LI OL, LI UL';
- const unorderedListItemNode = 'UL LI';
- const orderedListItemNode = 'OL LI';
- const emphasisNode = 'EM, I';
- const strongNode = 'STRONG, B';
- const headingNode = 'H1, H2, H3, H4, H5, H6';
- const preCodeNode = 'PRE CODE';
-
- return {
- TEXT_NODE(node) {
- return baseRenderer.getSpaceControlled(
- baseRenderer.trim(baseRenderer.getSpaceCollapsedText(node.nodeValue)),
- node,
- );
- },
- /*
- * This converter overwrites the default indented list converter
- * to allow us to parameterize the number of indent spaces for
- * sublists.
- *
- * See the original implementation in
- * https://github.com/nhn/tui.editor/blob/master/libs/to-mark/src/renderer.basic.js#L161
- */
- [sublistNode](node, subContent) {
- const baseResult = baseRenderer.convert(node, subContent);
- // Default to 1 to prevent possible divide by 0
- const firstLevelIndentSpacesCount = countIndentSpaces(baseResult) || 1;
- const reindentedList = baseResult
- .split('\n')
- .map((line) => {
- const itemIndentSpacesCount = countIndentSpaces(line);
- const nestingLevel = Math.ceil(itemIndentSpacesCount / firstLevelIndentSpacesCount);
- const indentSpaces = repeat(' ', subListIndentSpaces * nestingLevel);
-
- return line.replace(/^ +/, indentSpaces);
- })
- .join('\n');
-
- return reindentedList;
- },
- [unorderedListItemNode](node, subContent) {
- const baseResult = baseRenderer.convert(node, subContent);
- const formatted = baseResult.replace(/^(\s*)([*|-])/, `$1${unorderedListBulletChar}`);
- const { attributeDefinition } = node.dataset;
-
- return attributeDefinition ? `${formatted.trimRight()}\n${attributeDefinition}\n` : formatted;
- },
- [orderedListItemNode](node, subContent) {
- const baseResult = baseRenderer.convert(node, subContent);
-
- return incrementListMarker ? baseResult : baseResult.replace(/^(\s*)\d+?\./, '$11.');
- },
- [emphasisNode](node, subContent) {
- const result = baseRenderer.convert(node, subContent);
-
- return result.replace(/(^[*_]{1}|[*_]{1}$)/g, emphasis);
- },
- [strongNode](node, subContent) {
- const result = baseRenderer.convert(node, subContent);
- const strongSyntax = repeat(strong, 2);
-
- return result.replace(/^[*_]{2}/, strongSyntax).replace(/[*_]{2}$/, strongSyntax);
- },
- [headingNode](node, subContent) {
- const result = baseRenderer.convert(node, subContent);
- const { attributeDefinition } = node.dataset;
-
- return attributeDefinition ? `${result.trimRight()}\n${attributeDefinition}\n\n` : result;
- },
- [preCodeNode](node, subContent) {
- const isReferenceDefinition = Boolean(node.dataset.sseReferenceDefinition);
-
- return isReferenceDefinition
- ? `\n\n${node.innerText}\n\n`
- : baseRenderer.convert(node, subContent);
- },
- IMG(node) {
- const { originalSrc } = node.dataset;
- return `![${node.alt}](${originalSrc || node.src})`;
- },
- };
-};
-
-export default buildHTMLToMarkdownRender;
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/editor_service.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/editor_service.js
deleted file mode 100644
index 026a4069d9b..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/services/editor_service.js
+++ /dev/null
@@ -1,116 +0,0 @@
-import { defaults } from 'lodash';
-import Vue from 'vue';
-import { TOOLBAR_ITEM_CONFIGS, VIDEO_ATTRIBUTES } from '../constants';
-import ToolbarItem from '../toolbar_item.vue';
-import buildCustomHTMLRenderer from './build_custom_renderer';
-import buildHtmlToMarkdownRenderer from './build_html_to_markdown_renderer';
-import sanitizeHTML from './sanitize_html';
-
-const buildWrapper = (propsData) => {
- const instance = new Vue({
- render(createElement) {
- return createElement(ToolbarItem, propsData);
- },
- });
-
- instance.$mount();
- return instance.$el;
-};
-
-const buildVideoIframe = (src) => {
- const wrapper = document.createElement('figure');
- const iframe = document.createElement('iframe');
- const videoAttributes = { ...VIDEO_ATTRIBUTES, src };
- const wrapperClasses = ['gl-relative', 'gl-h-0', 'video_container'];
- const iframeClasses = ['gl-absolute', 'gl-top-0', 'gl-left-0', 'gl-w-full', 'gl-h-full'];
-
- wrapper.setAttribute('contenteditable', 'false');
- wrapper.classList.add(...wrapperClasses);
- iframe.classList.add(...iframeClasses);
- Object.assign(iframe, videoAttributes);
-
- wrapper.appendChild(iframe);
-
- return wrapper;
-};
-
-const buildImg = (alt, originalSrc, file) => {
- const img = document.createElement('img');
- const src = file ? URL.createObjectURL(file) : originalSrc;
- const attributes = { alt, src };
-
- if (file) {
- img.dataset.originalSrc = originalSrc;
- }
-
- Object.assign(img, attributes);
-
- return img;
-};
-
-export const generateToolbarItem = (config) => {
- const { icon, classes, event, command, tooltip, isDivider } = config;
-
- if (isDivider) {
- return 'divider';
- }
-
- return {
- type: 'button',
- options: {
- el: buildWrapper({ props: { icon, tooltip }, class: classes }),
- event,
- command,
- },
- };
-};
-
-export const addCustomEventListener = (editorApi, event, handler) => {
- editorApi.eventManager.addEventType(event);
- editorApi.eventManager.listen(event, handler);
-};
-
-export const removeCustomEventListener = (editorApi, event, handler) =>
- editorApi.eventManager.removeEventHandler(event, handler);
-
-export const addImage = ({ editor }, { altText, imageUrl }, file) => {
- if (editor.isWysiwygMode()) {
- const img = buildImg(altText, imageUrl, file);
- editor.getSquire().insertElement(img);
- } else {
- editor.insertText(`![${altText}](${imageUrl})`);
- }
-};
-
-export const insertVideo = ({ editor }, url) => {
- const videoIframe = buildVideoIframe(url);
-
- if (editor.isWysiwygMode()) {
- editor.getSquire().insertElement(videoIframe);
- } else {
- editor.insertText(videoIframe.outerHTML);
- }
-};
-
-export const getMarkdown = (editorInstance) => editorInstance.invoke('getMarkdown');
-
-/**
- * This function allow us to extend Toast UI HTML to Markdown renderer. It is
- * a temporary measure because Toast UI does not provide an API
- * to achieve this goal.
- */
-export const registerHTMLToMarkdownRenderer = (editorApi) => {
- const { renderer } = editorApi.toMarkOptions;
-
- Object.assign(editorApi.toMarkOptions, {
- renderer: renderer.constructor.factory(renderer, buildHtmlToMarkdownRenderer(renderer)),
- });
-};
-
-export const getEditorOptions = (externalOptions) => {
- return defaults({
- customHTMLRenderer: buildCustomHTMLRenderer(externalOptions?.customRenderers),
- toolbarItems: TOOLBAR_ITEM_CONFIGS.map((toolbarItem) => generateToolbarItem(toolbarItem)),
- customHTMLSanitizer: (html) => sanitizeHTML(html),
- });
-};
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token.js
deleted file mode 100644
index 638e5fd6f60..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token.js
+++ /dev/null
@@ -1,63 +0,0 @@
-const buildToken = (type, tagName, props) => {
- return { type, tagName, ...props };
-};
-
-const TAG_TYPES = {
- block: 'div',
- inline: 'a',
-};
-
-// Open helpers (singular and multiple)
-
-const buildUneditableOpenToken = (tagType = TAG_TYPES.block) =>
- buildToken('openTag', tagType, {
- attributes: { contenteditable: false },
- classNames: [
- 'gl-px-4 gl-py-2 gl-my-5 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed',
- ],
- });
-
-export const buildUneditableOpenTokens = (token, tagType = TAG_TYPES.block) => {
- return [buildUneditableOpenToken(tagType), token];
-};
-
-// Close helpers (singular and multiple)
-
-export const buildUneditableCloseToken = (tagType = TAG_TYPES.block) =>
- buildToken('closeTag', tagType);
-
-export const buildUneditableCloseTokens = (token, tagType = TAG_TYPES.block) => {
- return [token, buildUneditableCloseToken(tagType)];
-};
-
-// Complete helpers (open plus close)
-
-export const buildTextToken = (content) => buildToken('text', null, { content });
-
-export const buildUneditableBlockTokens = (token) => {
- return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()];
-};
-
-export const buildUneditableInlineTokens = (token) => {
- return [
- ...buildUneditableOpenTokens(token, TAG_TYPES.inline),
- buildUneditableCloseToken(TAG_TYPES.inline),
- ];
-};
-
-export const buildUneditableHtmlAsTextTokens = (node) => {
- /*
- Toast UI internally appends ' data-tomark-pass ' attribute flags so it can target certain
- nested nodes for internal use during Markdown <=> WYSIWYG conversions. In our case, we want
- to prevent HTML being rendered completely in WYSIWYG mode and thus we use a `text` vs. `html`
- type when building the token. However, in doing so, we need to strip out the ` data-tomark-pass `
- to prevent their persistence within the `text` content as the user did not intend these as edits.
-
- https://github.com/nhn/tui.editor/blob/cc54ec224fc3a4b6e5a2b19a71650959f41adc0e/apps/editor/src/js/convertor.js#L72
- */
- const regex = / data-tomark-pass /gm;
- const content = node.literal.replace(regex, '');
- const htmlAsTextToken = buildToken('text', null, { content });
-
- return [buildUneditableOpenToken(), htmlAsTextToken, buildUneditableCloseToken()];
-};
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_attribute_definition.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_attribute_definition.js
deleted file mode 100644
index bd419447a48..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_attribute_definition.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { isAttributeDefinition } from './render_utils';
-
-const canRender = ({ literal }) => isAttributeDefinition(literal);
-
-const render = () => ({ type: 'html', content: '<!-- sse-attribute-definition -->' });
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_embedded_ruby_text.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_embedded_ruby_text.js
deleted file mode 100644
index 0e122f598e5..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_embedded_ruby_text.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { renderUneditableLeaf as render } from './render_utils';
-
-const embeddedRubyRegex = /(^<%.+%>$)/;
-
-const canRender = ({ literal }) => {
- return embeddedRubyRegex.test(literal);
-};
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_font_awesome_html_inline.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_font_awesome_html_inline.js
deleted file mode 100644
index 572f6e3cf9d..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_font_awesome_html_inline.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { buildUneditableInlineTokens } from './build_uneditable_token';
-
-const fontAwesomeRegexOpen = /<i class="fa.+>/;
-
-const canRender = ({ literal }) => {
- return fontAwesomeRegexOpen.test(literal);
-};
-
-const render = (_, { origin }) => buildUneditableInlineTokens(origin());
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_heading.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_heading.js
deleted file mode 100644
index 71026fd0d65..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_heading.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import {
- renderWithAttributeDefinitions as render,
- willAlwaysRender as canRender,
-} from './render_utils';
-
-export default { render, canRender };
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_html_block.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_html_block.js
deleted file mode 100644
index 710b807275b..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_html_block.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { getURLOrigin } from '~/lib/utils/url_utility';
-import { ALLOWED_VIDEO_ORIGINS } from '../../constants';
-import { buildUneditableHtmlAsTextTokens } from './build_uneditable_token';
-
-const isVideoFrame = (html) => {
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, 'text/html');
- const {
- children: { length },
- } = doc;
- const iframe = doc.querySelector('iframe');
- const origin = iframe && getURLOrigin(iframe.getAttribute('src'));
-
- return length === 1 && ALLOWED_VIDEO_ORIGINS.includes(origin);
-};
-
-const canRender = ({ type, literal }) => {
- return type === 'htmlBlock' && !isVideoFrame(literal);
-};
-
-const render = (node) => buildUneditableHtmlAsTextTokens(node);
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text.js
deleted file mode 100644
index e41dc51457a..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { buildTextToken, buildUneditableInlineTokens } from './build_uneditable_token';
-
-/*
-Use case examples:
-- Majority: two bracket pairs, back-to-back, each with content (including spaces)
- - `[environment terraform plans][terraform]`
- - `[an issue labelled `~"main:broken"`][broken-main-issues]`
-- Minority: two bracket pairs the latter being empty or only one pair with content (including spaces)
- - `[this link][]`
- - `[this link]`
-
-Regexp notes:
- - `(?:\[.+?\]){1}`: Always one bracket pair with content (including spaces)
- - `(?:\[\]|\[.+?\])?`: Optional second pair that may or may not contain content (including spaces)
- - `(?!:)`: Never followed by a `:` which is reserved for identifier definition syntax (`[identifier]: /the-link`)
- - Each of the three parts is non-captured, but the match as a whole is captured
-*/
-const identifierInstanceRegex = /((?:\[.+?\]){1}(?:\[\]|\[.+?\])?(?!:))/g;
-
-const isIdentifierInstance = (literal) => {
- // Reset lastIndex as global flag in regexp are stateful
- identifierInstanceRegex.lastIndex = 0;
- return identifierInstanceRegex.test(literal);
-};
-
-const canRender = ({ literal }) => isIdentifierInstance(literal);
-
-const tokenize = (text) => {
- const matches = text.split(identifierInstanceRegex);
- const tokens = matches.map((match) => {
- const token = buildTextToken(match);
- return isIdentifierInstance(match) ? buildUneditableInlineTokens(token) : token;
- });
-
- return tokens.flat();
-};
-
-const render = (_, { origin }) => tokenize(origin().content);
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph.js
deleted file mode 100644
index 4829f0f2243..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph.js
+++ /dev/null
@@ -1,40 +0,0 @@
-const identifierRegex = /(^\[.+\]: .+)/;
-
-const isIdentifier = (text) => {
- return identifierRegex.test(text);
-};
-
-const canRender = (node, context) => {
- return isIdentifier(context.getChildrenText(node));
-};
-
-const getReferenceDefinitions = (node, definitions = '') => {
- if (!node) {
- return definitions;
- }
-
- const definition = node.type === 'text' ? node.literal : '\n';
-
- return getReferenceDefinitions(node.next, `${definitions}${definition}`);
-};
-
-const render = (node, { skipChildren }) => {
- const content = getReferenceDefinitions(node.firstChild);
-
- skipChildren();
-
- return [
- {
- type: 'openTag',
- tagName: 'pre',
- classNames: ['code-block', 'language-markdown'],
- attributes: { 'data-sse-reference-definition': true },
- },
- { type: 'openTag', tagName: 'code' },
- { type: 'text', content },
- { type: 'closeTag', tagName: 'code' },
- { type: 'closeTag', tagName: 'pre' },
- ];
-};
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_list_item.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_list_item.js
deleted file mode 100644
index 71026fd0d65..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_list_item.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import {
- renderWithAttributeDefinitions as render,
- willAlwaysRender as canRender,
-} from './render_utils';
-
-export default { render, canRender };
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_softbreak.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_softbreak.js
deleted file mode 100644
index c004e839821..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_softbreak.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const canRender = (node) => ['emph', 'strong'].includes(node.parent?.type);
-const render = () => ({
- type: 'text',
- content: ' ',
-});
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_utils.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_utils.js
deleted file mode 100644
index eff5dbf59f2..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_utils.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import {
- buildUneditableBlockTokens,
- buildUneditableOpenTokens,
- buildUneditableCloseToken,
-} from './build_uneditable_token';
-
-export const renderUneditableLeaf = (_, { origin }) => buildUneditableBlockTokens(origin());
-
-export const renderUneditableBranch = (_, { entering, origin }) =>
- entering ? buildUneditableOpenTokens(origin()) : buildUneditableCloseToken();
-
-const attributeDefinitionRegexp = /(^{:.+}$)/;
-
-export const isAttributeDefinition = (text) => attributeDefinitionRegexp.test(text);
-
-const findAttributeDefinition = (node) => {
- const literal =
- node?.next?.firstChild?.literal || node?.firstChild?.firstChild?.next?.next?.literal; // for headings // for list items;
-
- return isAttributeDefinition(literal) ? literal : null;
-};
-
-export const renderWithAttributeDefinitions = (node, { origin }) => {
- const attributes = findAttributeDefinition(node);
- const token = origin();
-
- if (token.type === 'openTag' && attributes) {
- Object.assign(token, {
- attributes: {
- 'data-attribute-definition': attributes,
- },
- });
- }
-
- return token;
-};
-
-export const willAlwaysRender = () => true;
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/sanitize_html.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/sanitize_html.js
deleted file mode 100644
index 486d88466b7..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/services/sanitize_html.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import createSanitizer from 'dompurify';
-import { getURLOrigin } from '~/lib/utils/url_utility';
-import { ALLOWED_VIDEO_ORIGINS } from '../constants';
-
-const sanitizer = createSanitizer(window);
-const ADD_TAGS = ['iframe'];
-
-sanitizer.addHook('uponSanitizeElement', (node) => {
- if (node.tagName !== 'IFRAME') {
- return;
- }
-
- const origin = getURLOrigin(node.getAttribute('src'));
-
- if (!ALLOWED_VIDEO_ORIGINS.includes(origin)) {
- node.remove();
- }
-});
-
-const sanitize = (content) => sanitizer.sanitize(content, { ADD_TAGS });
-
-export default sanitize;
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/toolbar_item.vue b/app/assets/javascripts/static_site_editor/rich_content_editor/toolbar_item.vue
deleted file mode 100644
index 85a67c087bb..00000000000
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/toolbar_item.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-
-export default {
- components: {
- GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- icon: {
- type: String,
- required: true,
- },
- tooltip: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-<template>
- <button
- v-gl-tooltip="{ title: tooltip }"
- :aria-label="tooltip"
- class="p-0 gl-display-flex toolbar-button"
- >
- <gl-icon class="gl-mx-auto gl-align-self-center" :name="icon" />
- </button>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/router/constants.js b/app/assets/javascripts/static_site_editor/router/constants.js
deleted file mode 100644
index fd715f918ce..00000000000
--- a/app/assets/javascripts/static_site_editor/router/constants.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export const HOME_ROUTE = { name: 'home' };
-export const SUCCESS_ROUTE = { name: 'success' };
diff --git a/app/assets/javascripts/static_site_editor/router/index.js b/app/assets/javascripts/static_site_editor/router/index.js
deleted file mode 100644
index 12692612bbc..00000000000
--- a/app/assets/javascripts/static_site_editor/router/index.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import Vue from 'vue';
-import VueRouter from 'vue-router';
-import routes from './routes';
-
-Vue.use(VueRouter);
-
-export default function createRouter(base) {
- const router = new VueRouter({
- base,
- mode: 'history',
- routes,
- });
-
- return router;
-}
diff --git a/app/assets/javascripts/static_site_editor/router/routes.js b/app/assets/javascripts/static_site_editor/router/routes.js
deleted file mode 100644
index 6fb9dbe0182..00000000000
--- a/app/assets/javascripts/static_site_editor/router/routes.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import Home from '../pages/home.vue';
-import Success from '../pages/success.vue';
-
-import { HOME_ROUTE, SUCCESS_ROUTE } from './constants';
-
-export default [
- {
- ...HOME_ROUTE,
- path: '/',
- component: Home,
- },
- {
- ...SUCCESS_ROUTE,
- path: '/success',
- component: Success,
- },
- {
- path: '*',
- redirect: HOME_ROUTE,
- },
-];
diff --git a/app/assets/javascripts/static_site_editor/services/formatter.js b/app/assets/javascripts/static_site_editor/services/formatter.js
deleted file mode 100644
index e841c664406..00000000000
--- a/app/assets/javascripts/static_site_editor/services/formatter.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import { repeat } from 'lodash';
-
-const topLevelOrderedRegexp = /^\d{1,3}/;
-const nestedLineRegexp = /^\s+/;
-
-/**
- * DISCLAIMER: This is a temporary fix that corrects the indentation
- * spaces of list items. This workaround originates in the usage of
- * the Static Site Editor to edit the Handbook. The Handbook uses a
- * Markdown parser called Kramdown interprets lines indented
- * with two spaces as content within a list. For example:
- *
- * 1. ordered list
- * - nested unordered list
- *
- * The Static Site Editor uses a different Markdown parser based on the
- * CommonMark specification (official Markdown spec) called ToastMark.
- * When the SSE encounters a nested list with only two spaces, it flattens
- * the list:
- *
- * 1. ordered list
- * - nested unordered list
- *
- * This function attempts to correct this problem before the content is loaded
- * by Toast UI.
- */
-const correctNestedContentIndenation = (source) => {
- const lines = source.split('\n');
- let topLevelOrderedListDetected = false;
-
- return lines
- .reduce((result, line) => {
- if (topLevelOrderedListDetected && nestedLineRegexp.test(line)) {
- return [...result, line.replace(nestedLineRegexp, repeat(' ', 4))];
- }
-
- topLevelOrderedListDetected = topLevelOrderedRegexp.test(line);
- return [...result, line];
- }, [])
- .join('\n');
-};
-
-const removeOrphanedBrTags = (source) => {
- /* Until the underlying Squire editor of Toast UI Editor resolves duplicate `<br>` tags, this
- `replace` solution will clear out orphaned `<br>` tags that it generates. Additionally,
- it cleans up orphaned `<br>` tags in the source markdown document that should be new lines.
- https://gitlab.com/gitlab-org/gitlab/-/issues/227602#note_380765330
- */
- return source.replace(/\n^<br>$/gm, '');
-};
-
-const format = (source) => {
- return correctNestedContentIndenation(removeOrphanedBrTags(source));
-};
-
-export default format;
diff --git a/app/assets/javascripts/static_site_editor/services/front_matterify.js b/app/assets/javascripts/static_site_editor/services/front_matterify.js
deleted file mode 100644
index 6b897b42648..00000000000
--- a/app/assets/javascripts/static_site_editor/services/front_matterify.js
+++ /dev/null
@@ -1,75 +0,0 @@
-import jsYaml from 'js-yaml';
-
-const NEW_LINE = '\n';
-
-const hasMatter = (firstThreeChars, fourthChar) => {
- const isYamlDelimiter = firstThreeChars === '---';
- const isFourthCharNewline = fourthChar === NEW_LINE;
- return isYamlDelimiter && isFourthCharNewline;
-};
-
-export const frontMatterify = (source) => {
- let index = 3;
- let offset;
- const delimiter = source.slice(0, index);
- const type = 'yaml';
- const NO_FRONTMATTER = {
- source,
- matter: null,
- hasMatter: false,
- spacing: null,
- content: source,
- delimiter: null,
- type: null,
- };
-
- if (!hasMatter(delimiter, source.charAt(index))) {
- return NO_FRONTMATTER;
- }
-
- offset = source.indexOf(delimiter, index);
-
- // Finds the end delimiter that starts at a new line
- while (offset !== -1 && source.charAt(offset - 1) !== NEW_LINE) {
- index = offset + delimiter.length;
- offset = source.indexOf(delimiter, index);
- }
-
- if (offset === -1) {
- return NO_FRONTMATTER;
- }
-
- const matterStr = source.slice(index, offset);
- const matter = jsYaml.safeLoad(matterStr);
-
- let content = source.slice(offset + delimiter.length);
- let spacing = '';
- let idx = 0;
- while (content.charAt(idx).match(/(\s|\n)/)) {
- spacing += content.charAt(idx);
- idx += 1;
- }
- content = content.replace(spacing, '');
-
- return {
- source,
- matter,
- hasMatter: true,
- spacing,
- content,
- delimiter,
- type,
- };
-};
-
-export const stringify = ({ matter, spacing, content, delimiter }, newMatter) => {
- const matterObj = newMatter || matter;
-
- if (!matterObj) {
- return content;
- }
-
- const header = `${delimiter}${NEW_LINE}${jsYaml.safeDump(matterObj)}${delimiter}`;
- const body = `${spacing}${content}`;
- return `${header}${body}`;
-};
diff --git a/app/assets/javascripts/static_site_editor/services/generate_branch_name.js b/app/assets/javascripts/static_site_editor/services/generate_branch_name.js
deleted file mode 100644
index cbf03a41ce2..00000000000
--- a/app/assets/javascripts/static_site_editor/services/generate_branch_name.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { BRANCH_SUFFIX_COUNT } from '../constants';
-
-const generateBranchSuffix = () => `${Date.now()}`.substr(BRANCH_SUFFIX_COUNT);
-
-const generateBranchName = (username, targetBranch) =>
- `${username}-${targetBranch}-patch-${generateBranchSuffix()}`;
-
-export default generateBranchName;
diff --git a/app/assets/javascripts/static_site_editor/services/image_service.js b/app/assets/javascripts/static_site_editor/services/image_service.js
deleted file mode 100644
index a9b85057e3d..00000000000
--- a/app/assets/javascripts/static_site_editor/services/image_service.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export const getBinary = (file) => {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.readAsDataURL(file);
- reader.onload = () => resolve(reader.result.split(',')[1]);
- reader.onerror = (error) => reject(error);
- });
-};
diff --git a/app/assets/javascripts/static_site_editor/services/load_source_content.js b/app/assets/javascripts/static_site_editor/services/load_source_content.js
deleted file mode 100644
index fcf69efafd8..00000000000
--- a/app/assets/javascripts/static_site_editor/services/load_source_content.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import Api from '~/api';
-
-const extractTitle = (content) => {
- const matches = content.match(/title: (.+)\n/i);
-
- return matches ? Array.from(matches)[1] : '';
-};
-
-const loadSourceContent = ({ projectId, sourcePath }) =>
- Api.getRawFile(projectId, sourcePath).then(({ data }) => ({
- title: extractTitle(data),
- content: data,
- }));
-
-export default loadSourceContent;
diff --git a/app/assets/javascripts/static_site_editor/services/parse_source_file.js b/app/assets/javascripts/static_site_editor/services/parse_source_file.js
deleted file mode 100644
index d7499d75a21..00000000000
--- a/app/assets/javascripts/static_site_editor/services/parse_source_file.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import { frontMatterify, stringify } from './front_matterify';
-
-const parseSourceFile = (raw) => {
- let editable;
-
- const syncContent = (newVal, isBody) => {
- if (isBody) {
- editable.content = newVal;
- } else {
- try {
- editable = frontMatterify(newVal);
- editable.isMatterValid = true;
- } catch (e) {
- editable.isMatterValid = false;
- }
- }
- };
-
- const content = (isBody = false) => (isBody ? editable.content : stringify(editable));
-
- const matter = () => editable.matter;
-
- const syncMatter = (settings) => {
- editable.matter = settings;
- };
-
- const isModified = () => stringify(editable) !== raw;
-
- const hasMatter = () => editable.hasMatter;
-
- const isMatterValid = () => editable.isMatterValid;
-
- syncContent(raw);
-
- return {
- matter,
- isMatterValid,
- syncMatter,
- content,
- syncContent,
- isModified,
- hasMatter,
- };
-};
-
-export default parseSourceFile;
diff --git a/app/assets/javascripts/static_site_editor/services/renderers/render_image.js b/app/assets/javascripts/static_site_editor/services/renderers/render_image.js
deleted file mode 100644
index b5651e7163e..00000000000
--- a/app/assets/javascripts/static_site_editor/services/renderers/render_image.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import { isAbsolute, getBaseURL, joinPaths } from '~/lib/utils/url_utility';
-
-const canRender = ({ type }) => type === 'image';
-
-let metadata;
-
-const getCachedContent = (basePath) => metadata.imageRepository.get(basePath);
-
-const isRelativeToCurrentDirectory = (basePath) => !basePath.startsWith('/');
-
-const extractSourceDirectory = (url) => {
- const sourceDir = /^(.+)\/([^/]+)$/.exec(url); // Extracts the base path and fileName from an image path
- return sourceDir || [null, null, url]; // If no source directory was extracted it means only a fileName was specified (e.g. url='file.png')
-};
-
-const parseCurrentDirectory = (basePath) => {
- const baseUrl = decodeURIComponent(metadata.baseUrl);
- const sourceDirectory = extractSourceDirectory(baseUrl)[1];
- const currentDirectory = sourceDirectory.split(`/-/sse/${metadata.branch}`)[1];
-
- return joinPaths(currentDirectory, basePath);
-};
-
-// For more context around this logic, please see the following comment:
-// https://gitlab.com/gitlab-org/gitlab/-/issues/241166#note_409413500
-const generateSourceDirectory = (basePath) => {
- let sourceDir = '';
- let defaultSourceDir = '';
-
- if (!basePath || isRelativeToCurrentDirectory(basePath)) {
- return parseCurrentDirectory(basePath);
- }
-
- if (!metadata.mounts.length) {
- return basePath;
- }
-
- metadata.mounts.forEach(({ source, target }) => {
- const hasTarget = target !== '';
-
- if (hasTarget && basePath.includes(target)) {
- sourceDir = source;
- } else if (!hasTarget) {
- defaultSourceDir = joinPaths(source, basePath);
- }
- });
-
- return sourceDir || defaultSourceDir;
-};
-
-const resolveFullPath = (originalSrc, cachedContent) => {
- if (cachedContent) {
- return `data:image;base64,${cachedContent}`;
- }
-
- if (isAbsolute(originalSrc)) {
- return originalSrc;
- }
-
- const sourceDirectory = extractSourceDirectory(originalSrc);
- const [, basePath, fileName] = sourceDirectory;
- const sourceDir = generateSourceDirectory(basePath);
-
- return joinPaths(getBaseURL(), metadata.project, '/-/raw/', metadata.branch, sourceDir, fileName);
-};
-
-const render = ({ destination: originalSrc, firstChild }, { skipChildren }) => {
- skipChildren();
-
- const cachedContent = getCachedContent(originalSrc);
-
- return {
- type: 'openTag',
- tagName: 'img',
- selfClose: true,
- attributes: {
- 'data-original-src': !isAbsolute(originalSrc) || cachedContent ? originalSrc : '',
- src: resolveFullPath(originalSrc, cachedContent),
- alt: firstChild.literal,
- },
- };
-};
-
-const build = (mounts = [], project, branch, baseUrl, imageRepository) => {
- metadata = { mounts, project, branch, baseUrl, imageRepository };
- return { canRender, render };
-};
-
-export default { build };
diff --git a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
deleted file mode 100644
index 99534413d92..00000000000
--- a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
+++ /dev/null
@@ -1,145 +0,0 @@
-import Api from '~/api';
-import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
-import generateBranchName from '~/static_site_editor/services/generate_branch_name';
-import Tracking from '~/tracking';
-
-import {
- SUBMIT_CHANGES_BRANCH_ERROR,
- SUBMIT_CHANGES_COMMIT_ERROR,
- SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
- TRACKING_ACTION_CREATE_COMMIT,
- TRACKING_ACTION_CREATE_MERGE_REQUEST,
- SERVICE_PING_TRACKING_ACTION_CREATE_COMMIT,
- SERVICE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
- DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE,
- DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION,
-} from '../constants';
-
-const createBranch = (projectId, branch, targetBranch) =>
- Api.createBranch(projectId, {
- ref: targetBranch,
- branch,
- }).catch(() => {
- throw new Error(SUBMIT_CHANGES_BRANCH_ERROR);
- });
-
-const createImageActions = (images, markdown) => {
- const actions = [];
-
- if (!markdown) {
- return actions;
- }
-
- images.forEach((imageContent, filePath) => {
- const imageExistsInMarkdown = (path) => new RegExp(`!\\[([^[\\]\\n]*)\\](\\(${path})\\)`); // matches the image markdown syntax: ![<any-string-except-newline>](<path>)
-
- if (imageExistsInMarkdown(filePath).test(markdown)) {
- actions.push(
- convertObjectPropsToSnakeCase({
- encoding: 'base64',
- action: 'create',
- content: imageContent,
- filePath,
- }),
- );
- }
- });
-
- return actions;
-};
-
-const createUpdateSourceFileAction = (sourcePath, content) => [
- convertObjectPropsToSnakeCase({
- action: 'update',
- filePath: sourcePath,
- content,
- }),
-];
-
-const commit = (projectId, message, branch, actions) => {
- Tracking.event(document.body.dataset.page, TRACKING_ACTION_CREATE_COMMIT);
- Api.trackRedisCounterEvent(SERVICE_PING_TRACKING_ACTION_CREATE_COMMIT);
-
- return Api.commitMultiple(
- projectId,
- convertObjectPropsToSnakeCase({
- branch,
- commitMessage: message,
- actions,
- }),
- ).catch(() => {
- throw new Error(SUBMIT_CHANGES_COMMIT_ERROR);
- });
-};
-
-const createMergeRequest = (projectId, title, description, sourceBranch, targetBranch) => {
- Tracking.event(document.body.dataset.page, TRACKING_ACTION_CREATE_MERGE_REQUEST);
- Api.trackRedisCounterEvent(SERVICE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST);
-
- return Api.createProjectMergeRequest(
- projectId,
- convertObjectPropsToSnakeCase({
- title,
- description,
- sourceBranch,
- targetBranch,
- }),
- ).catch(() => {
- throw new Error(SUBMIT_CHANGES_MERGE_REQUEST_ERROR);
- });
-};
-
-const submitContentChanges = ({
- username,
- projectId,
- sourcePath,
- targetBranch,
- content,
- images,
- mergeRequestMeta,
- formattedMarkdown,
-}) => {
- const branch = generateBranchName(username, targetBranch);
- const { title: mergeRequestTitle, description: mergeRequestDescription } = mergeRequestMeta;
- const meta = {};
-
- return createBranch(projectId, branch, targetBranch)
- .then(({ data: { web_url: url } }) => {
- const message = `${DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE}\n\n${DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION}`;
-
- Object.assign(meta, { branch: { label: branch, url } });
-
- return formattedMarkdown
- ? commit(
- projectId,
- message,
- branch,
- createUpdateSourceFileAction(sourcePath, formattedMarkdown),
- )
- : meta;
- })
- .then(() =>
- commit(projectId, mergeRequestTitle, branch, [
- ...createUpdateSourceFileAction(sourcePath, content),
- ...createImageActions(images, content),
- ]),
- )
- .then(({ data: { short_id: label, web_url: url } }) => {
- Object.assign(meta, { commit: { label, url } });
-
- return createMergeRequest(
- projectId,
- mergeRequestTitle,
- mergeRequestDescription,
- branch,
- targetBranch,
- );
- })
- .then(({ data: { iid: label, web_url: url } }) => {
- Object.assign(meta, { mergeRequest: { label: label.toString(), url } });
-
- return meta;
- });
-};
-
-export default submitContentChanges;
diff --git a/app/assets/javascripts/static_site_editor/services/templater.js b/app/assets/javascripts/static_site_editor/services/templater.js
deleted file mode 100644
index 47fc36c3d18..00000000000
--- a/app/assets/javascripts/static_site_editor/services/templater.js
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * The purpose of this file is to modify Markdown source such that templated code (embedded ruby currently) can be temporarily wrapped and unwrapped in codeblocks:
- * 1. `wrap()`: temporarily wrap in codeblocks (useful for a WYSIWYG editing experience)
- * 2. `unwrap()`: undo the temporarily wrapped codeblocks (useful for Markdown editing experience and saving edits)
- *
- * Without this `templater`, the templated code is otherwise interpreted as Markdown content resulting in loss of spacing, indentation, escape characters, etc.
- *
- */
-
-const ticks = '```';
-const marker = 'sse';
-const wrapPrefix = `${ticks} ${marker}\n`; // Space intentional due to https://github.com/nhn/tui.editor/blob/6bcec75c69028570d93d973aa7533090257eaae0/libs/to-mark/src/renderer.gfm.js#L26
-const wrapPostfix = `\n${ticks}`;
-const markPrefix = `${marker}-${Date.now()}`;
-
-const reHelpers = {
- template: `.| |\\t|\\n(?!(\\n|${markPrefix}))`,
- openTag: '<(?!figure|iframe)[a-zA-Z]+.*?>',
- closeTag: '</.+>',
-};
-const reTemplated = new RegExp(`(^${wrapPrefix}(${reHelpers.template})+?${wrapPostfix}$)`, 'gm');
-const rePreexistingCodeBlocks = new RegExp(`(^${ticks}.*\\n(.|\\s)+?${ticks}$)`, 'gm');
-const reHtmlMarkup = new RegExp(
- `^((${reHelpers.openTag}){1}(${reHelpers.template})*(${reHelpers.closeTag}){1})$`,
- 'gm',
-);
-const reEmbeddedRubyBlock = new RegExp(`(^<%(${reHelpers.template})+%>$)`, 'gm');
-const reEmbeddedRubyInline = new RegExp(`(^.*[<|&lt;]%(${reHelpers.template})+$)`, 'gm');
-
-const patternGroups = {
- ignore: [rePreexistingCodeBlocks],
- // Order is intentional (general to specific) where HTML markup is marked first, then ERB blocks, then inline ERB
- // Order in combo with the `mark()` algorithm is used to mitigate potential duplicate pattern matches (ERB nested in HTML for example)
- allow: [reHtmlMarkup, reEmbeddedRubyBlock, reEmbeddedRubyInline],
-};
-
-const mark = (source, groups) => {
- let text = source;
- let id = 0;
- const hash = {};
-
- Object.entries(groups).forEach(([groupKey, group]) => {
- group.forEach((pattern) => {
- const matches = text.match(pattern);
- if (matches) {
- matches.forEach((match) => {
- const key = `${markPrefix}-${groupKey}-${id}`;
- text = text.replace(match, key);
- hash[key] = match;
- id += 1;
- });
- }
- });
- });
-
- return { text, hash };
-};
-
-const unmark = (text, hash) => {
- let source = text;
-
- Object.entries(hash).forEach(([key, value]) => {
- const newVal = key.includes('ignore') ? value : `${wrapPrefix}${value}${wrapPostfix}`;
- source = source.replace(key, newVal);
- });
-
- return source;
-};
-
-const unwrap = (source) => {
- let text = source;
- const matches = text.match(reTemplated);
-
- if (matches) {
- matches.forEach((match) => {
- const initial = match.replace(`${wrapPrefix}`, '').replace(`${wrapPostfix}`, '');
- text = text.replace(match, initial);
- });
- }
-
- return text;
-};
-
-const wrap = (source) => {
- const { text, hash } = mark(unwrap(source), patternGroups);
- return unmark(text, hash);
-};
-
-export default { wrap, unwrap };
diff --git a/app/assets/javascripts/tags/components/delete_tag_modal.vue b/app/assets/javascripts/tags/components/delete_tag_modal.vue
new file mode 100644
index 00000000000..e3b666ec968
--- /dev/null
+++ b/app/assets/javascripts/tags/components/delete_tag_modal.vue
@@ -0,0 +1,192 @@
+<script>
+import { GlButton, GlFormInput, GlModal, GlSprintf, GlAlert } from '@gitlab/ui';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import csrf from '~/lib/utils/csrf';
+import { sprintf, s__ } from '~/locale';
+import eventHub from '../event_hub';
+
+export default {
+ csrf,
+ components: {
+ GlModal,
+ GlButton,
+ GlFormInput,
+ GlSprintf,
+ GlAlert,
+ },
+ data() {
+ return {
+ isProtected: false,
+ tagName: '',
+ path: '',
+ enteredTagName: '',
+ modalId: 'delete-tag-modal',
+ };
+ },
+ computed: {
+ title() {
+ const modalTitle = this.isProtected
+ ? this.$options.i18n.modalTitleProtectedTag
+ : this.$options.i18n.modalTitle;
+
+ return sprintf(modalTitle, { tagName: this.tagName });
+ },
+ message() {
+ const modalMessage = this.isProtected
+ ? this.$options.i18n.modalMessageProtectedTag
+ : this.$options.i18n.modalMessage;
+
+ return sprintf(modalMessage, { tagName: this.tagName });
+ },
+ undoneWarning() {
+ return sprintf(this.$options.i18n.undoneWarning, {
+ buttonText: this.buttonText,
+ });
+ },
+ confirmationText() {
+ return sprintf(this.$options.i18n.confirmationText, {
+ tagName: this.tagName,
+ });
+ },
+ buttonText() {
+ return this.isProtected
+ ? this.$options.i18n.deleteButtonTextProtectedTag
+ : this.$options.i18n.deleteButtonText;
+ },
+ tagNameConfirmed() {
+ return this.enteredTagName === this.tagName;
+ },
+ deleteButtonDisabled() {
+ return this.isProtected && !this.tagNameConfirmed;
+ },
+ },
+ mounted() {
+ eventHub.$on('openModal', this.openModal);
+ for (const btn of document.querySelectorAll('.js-delete-tag-button')) {
+ btn.addEventListener('click', this.deleteTagBtnListener.bind(this, btn));
+ }
+ },
+ destroyed() {
+ eventHub.$off('openModal', this.openModal);
+ for (const btn of document.querySelectorAll('.js-delete-tag-button')) {
+ btn.removeEventListener('click', this.deleteTagBtnListener.bind(this, btn));
+ }
+ },
+ methods: {
+ deleteTagBtnListener(btn) {
+ return this.openModal({
+ ...btn.dataset,
+ isProtected: parseBoolean(btn.dataset.isProtected),
+ });
+ },
+ openModal({ isProtected, tagName, path }) {
+ this.enteredTagName = '';
+ this.isProtected = isProtected;
+ this.tagName = tagName;
+ this.path = path;
+
+ this.$refs.modal.show();
+ },
+ submitForm() {
+ this.$refs.form.submit();
+ },
+ closeModal() {
+ this.$refs.modal.hide();
+ },
+ },
+ i18n: {
+ modalTitle: s__('TagsPage|Delete tag. Are you ABSOLUTELY SURE?'),
+ modalTitleProtectedTag: s__('TagsPage|Delete protected tag. Are you ABSOLUTELY SURE?'),
+ modalMessage: s__(
+ "TagsPage|You're about to permanently delete the tag %{strongStart}%{tagName}.%{strongEnd}",
+ ),
+ modalMessageProtectedTag: s__(
+ "TagsPage|You're about to permanently delete the protected tag %{strongStart}%{tagName}.%{strongEnd}",
+ ),
+ undoneWarning: s__(
+ 'TagsPage|After you confirm and select %{strongStart}%{buttonText},%{strongEnd} you cannot recover this tag.',
+ ),
+ cancelButtonText: s__('TagsPage|Cancel, keep tag'),
+ confirmationText: s__(
+ 'TagsPage|Deleting the %{strongStart}%{tagName}%{strongEnd} tag cannot be undone. Are you sure?',
+ ),
+ confirmationTextProtectedTag: s__('TagsPage|Please type the following to confirm:'),
+ deleteButtonText: s__('TagsPage|Yes, delete tag'),
+ deleteButtonTextProtectedTag: s__('TagsPage|Yes, delete protected tag'),
+ },
+};
+</script>
+
+<template>
+ <gl-modal ref="modal" size="sm" :modal-id="modalId" :title="title">
+ <gl-alert class="gl-mb-5" variant="danger" :dismissible="false">
+ <div data-testid="modal-message">
+ <gl-sprintf :message="message">
+ <template #strong="{ content }">
+ <strong> {{ content }} </strong>
+ </template>
+ </gl-sprintf>
+ </div>
+ </gl-alert>
+
+ <form ref="form" :action="path" method="post">
+ <div v-if="isProtected" class="gl-mt-4">
+ <p>
+ <gl-sprintf :message="undoneWarning">
+ <template #strong="{ content }">
+ <strong> {{ content }} </strong>
+ </template>
+ </gl-sprintf>
+ </p>
+ <p>
+ <gl-sprintf :message="$options.i18n.confirmationTextProtectedTag">
+ <template #strong="{ content }">
+ {{ content }}
+ </template>
+ </gl-sprintf>
+ <code class="gl-white-space-pre-wrap"> {{ tagName }} </code>
+ <gl-form-input
+ v-model="enteredTagName"
+ name="delete_tag_input"
+ type="text"
+ class="gl-mt-4"
+ aria-labelledby="input-label"
+ autocomplete="off"
+ />
+ </p>
+ </div>
+ <div v-else>
+ <p class="gl-mt-4">
+ <gl-sprintf :message="confirmationText">
+ <template #strong="{ content }">
+ <strong>
+ {{ content }}
+ </strong>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+
+ <input ref="method" type="hidden" name="_method" value="delete" />
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ </form>
+
+ <template #modal-footer>
+ <div class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0">
+ <gl-button data-testid="delete-tag-cancel-button" @click="closeModal">
+ {{ $options.i18n.cancelButtonText }}
+ </gl-button>
+ <div class="gl-mr-3"></div>
+ <gl-button
+ ref="deleteTagButton"
+ :disabled="deleteButtonDisabled"
+ variant="danger"
+ data-qa-selector="delete_tag_confirmation_button"
+ data-testid="delete-tag-confirmation-button"
+ @click="submitForm"
+ >{{ buttonText }}</gl-button
+ >
+ </div>
+ </template>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/tags/event_hub.js b/app/assets/javascripts/tags/event_hub.js
new file mode 100644
index 00000000000..e31806ad199
--- /dev/null
+++ b/app/assets/javascripts/tags/event_hub.js
@@ -0,0 +1,3 @@
+import createEventHub from '~/helpers/event_hub_factory';
+
+export default createEventHub();
diff --git a/app/assets/javascripts/tags/init_delete_tag_modal.js b/app/assets/javascripts/tags/init_delete_tag_modal.js
new file mode 100644
index 00000000000..03e3d393c14
--- /dev/null
+++ b/app/assets/javascripts/tags/init_delete_tag_modal.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import DeleteTagModal from '~/tags/components/delete_tag_modal.vue';
+
+export default function initDeleteTagModal() {
+ const el = document.querySelector('.js-delete-tag-modal');
+ if (!el) return false;
+
+ return new Vue({
+ el,
+ render(createComponent) {
+ return createComponent(DeleteTagModal);
+ },
+ });
+}
diff --git a/app/assets/javascripts/terraform/components/states_table.vue b/app/assets/javascripts/terraform/components/states_table.vue
index efc2991f40f..b19f92aaeb4 100644
--- a/app/assets/javascripts/terraform/components/states_table.vue
+++ b/app/assets/javascripts/terraform/components/states_table.vue
@@ -1,7 +1,16 @@
<script>
-import { GlAlert, GlBadge, GlLink, GlLoadingIcon, GlSprintf, GlTable, GlTooltip } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlBadge,
+ GlLink,
+ GlLoadingIcon,
+ GlSprintf,
+ GlTable,
+ GlTooltip,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { s__ } from '~/locale';
+import { s__, sprintf } from '~/locale';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@@ -20,6 +29,9 @@ export default {
StateActions,
TimeAgoTooltip,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
mixins: [timeagoMixin],
props: {
states: {
@@ -68,6 +80,8 @@ export default {
locked: s__('Terraform|Locked'),
lockedByUser: s__('Terraform|Locked by %{user} %{timeAgo}'),
lockingState: s__('Terraform|Locking state'),
+ deleting: s__('Terraform|Removed'),
+ deletionInProgress: s__('Terraform|Deletion in progress'),
name: s__('Terraform|Name'),
pipeline: s__('Terraform|Pipeline'),
removing: s__('Terraform|Removing'),
@@ -85,6 +99,12 @@ export default {
lockedByUserName(item) {
return item.lockedByUser?.name || this.$options.i18n.unknownUser;
},
+ lockedByUserText(item) {
+ return sprintf(this.$options.i18n.lockedByUser, {
+ user: this.lockedByUserName(item),
+ timeAgo: this.timeFormatted(item.lockedAt),
+ });
+ },
pipelineDetailedStatus(item) {
return item.latestVersion?.job?.detailedStatus;
},
@@ -142,29 +162,27 @@ export default {
</div>
<div
+ v-else-if="item.deletedAt"
+ v-gl-tooltip.right
+ class="gl-mx-3"
+ :title="$options.i18n.deletionInProgress"
+ :data-testid="`state-badge-${item.name}`"
+ >
+ <gl-badge icon="remove">
+ {{ $options.i18n.deleting }}
+ </gl-badge>
+ </div>
+
+ <div
v-else-if="item.lockedAt"
- :id="`terraformLockedBadgeContainer${item.name}`"
+ v-gl-tooltip.right
class="gl-mx-3"
+ :title="lockedByUserText(item)"
+ :data-testid="`state-badge-${item.name}`"
>
- <gl-badge :id="`terraformLockedBadge${item.name}`" icon="lock">
+ <gl-badge icon="lock">
{{ $options.i18n.locked }}
</gl-badge>
-
- <gl-tooltip
- :container="`terraformLockedBadgeContainer${item.name}`"
- :target="`terraformLockedBadge${item.name}`"
- placement="right"
- >
- <gl-sprintf :message="$options.i18n.lockedByUser">
- <template #user>
- {{ lockedByUserName(item) }}
- </template>
-
- <template #timeAgo>
- {{ timeFormatted(item.lockedAt) }}
- </template>
- </gl-sprintf>
- </gl-tooltip>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/terraform/components/states_table_actions.vue b/app/assets/javascripts/terraform/components/states_table_actions.vue
index 1970d6d7949..773ecf1d5d5 100644
--- a/app/assets/javascripts/terraform/components/states_table_actions.vue
+++ b/app/assets/javascripts/terraform/components/states_table_actions.vue
@@ -33,6 +33,7 @@ export default {
directives: {
GlModalDirective,
},
+ inject: ['projectPath'],
props: {
state: {
required: true,
@@ -149,7 +150,14 @@ export default {
variables: {
stateID: this.state.id,
},
- refetchQueries: () => [{ query: getStatesQuery }],
+ refetchQueries: () => [
+ {
+ query: getStatesQuery,
+ variables: {
+ projectPath: this.projectPath,
+ },
+ },
+ ],
awaitRefetchQueries: true,
notifyOnNetworkStatusChange: true,
})
diff --git a/app/assets/javascripts/terraform/components/terraform_list.vue b/app/assets/javascripts/terraform/components/terraform_list.vue
index 7eb79120fb8..f098b447d10 100644
--- a/app/assets/javascripts/terraform/components/terraform_list.vue
+++ b/app/assets/javascripts/terraform/components/terraform_list.vue
@@ -31,15 +31,12 @@ export default {
GlTabs,
StatesTable,
},
+ inject: ['projectPath'],
props: {
emptyStateImage: {
required: true,
type: String,
},
- projectPath: {
- required: true,
- type: String,
- },
terraformAdmin: {
required: false,
type: Boolean,
@@ -105,7 +102,7 @@ export default {
</p>
</template>
- <gl-loading-icon v-if="isLoading" size="md" class="gl-mt-3" />
+ <gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3" />
<div v-else-if="statesList">
<div v-if="statesCount">
diff --git a/app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql b/app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql
index fb823336411..ee3d5f474e2 100644
--- a/app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql
+++ b/app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql
@@ -11,6 +11,7 @@ fragment State on TerraformState {
name
lockedAt
updatedAt
+ deletedAt
lockedByUser {
...User
diff --git a/app/assets/javascripts/terraform/index.js b/app/assets/javascripts/terraform/index.js
index 34261f3c4db..2d70ccfac4d 100644
--- a/app/assets/javascripts/terraform/index.js
+++ b/app/assets/javascripts/terraform/index.js
@@ -30,6 +30,7 @@ export default () => {
el,
apolloProvider: new VueApollo({ defaultClient }),
provide: {
+ projectPath,
accessTokensPath,
terraformApiUrl,
username,
@@ -38,8 +39,7 @@ export default () => {
return createElement(TerraformList, {
props: {
emptyStateImage,
- projectPath,
- terraformAdmin: el.hasAttribute('data-terraform-admin'),
+ terraformAdmin: Object.prototype.hasOwnProperty.call(el.dataset, 'terraformAdmin'),
},
});
},
diff --git a/app/assets/javascripts/token_access/components/token_access.vue b/app/assets/javascripts/token_access/components/token_access.vue
index e739ec37739..de8cd856bf7 100644
--- a/app/assets/javascripts/token_access/components/token_access.vue
+++ b/app/assets/javascripts/token_access/components/token_access.vue
@@ -167,7 +167,7 @@ export default {
</script>
<template>
<div>
- <gl-loading-icon v-if="$apollo.loading" size="md" class="gl-mt-5" />
+ <gl-loading-icon v-if="$apollo.loading" size="lg" class="gl-mt-5" />
<template v-else>
<gl-toggle
v-model="jobTokenScopeEnabled"
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 b6c9330c754..82ef3371d91 100644
--- a/app/assets/javascripts/token_access/components/token_projects_table.vue
+++ b/app/assets/javascripts/token_access/components/token_projects_table.vue
@@ -2,10 +2,6 @@
import { GlButton, GlTable } from '@gitlab/ui';
import { __, s__ } from '~/locale';
-const defaultTableClasses = {
- thClass: 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!',
-};
-
export default {
i18n: {
emptyText: s__('CI/CD|No projects have been added to the scope'),
@@ -15,14 +11,12 @@ export default {
key: 'project',
label: __('Projects that can be accessed'),
tdClass: 'gl-p-5!',
- ...defaultTableClasses,
columnClass: 'gl-w-85p',
},
{
key: 'actions',
label: '',
tdClass: 'gl-p-5! gl-text-right',
- ...defaultTableClasses,
columnClass: 'gl-w-15p',
},
],
diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js
index 438ae2bc1bc..a3615eab26f 100644
--- a/app/assets/javascripts/user_popovers.js
+++ b/app/assets/javascripts/user_popovers.js
@@ -1,6 +1,8 @@
import Vue from 'vue';
+import { debounce } from 'lodash';
import UsersCache from './lib/utils/users_cache';
import UserPopover from './vue_shared/components/user_popover/user_popover.vue';
+import { USER_POPOVER_DELAY } from './vue_shared/components/user_popover/constants';
const removeTitle = (el) => {
// Removing titles so its not showing tooltips also
@@ -59,87 +61,78 @@ const populateUserInfo = (user) => {
);
};
-const initializedPopovers = new Map();
-let domObservedForChanges = false;
+function createPopover(el, user) {
+ removeTitle(el);
+ const preloadedUserInfo = getPreloadedUserInfo(el.dataset);
-const addPopoversToModifiedTree = new MutationObserver(() => {
- const userLinks = document?.querySelectorAll('.js-user-link, .gfm-project_member');
+ Object.assign(user, preloadedUserInfo);
- if (userLinks) {
- addPopovers(userLinks); /* eslint-disable-line no-use-before-define */
+ if (preloadedUserInfo.userId) {
+ populateUserInfo(user);
}
-});
+ const UserPopoverComponent = Vue.extend(UserPopover);
+ return new UserPopoverComponent({
+ propsData: {
+ target: el,
+ user,
+ show: true,
+ placement: el.dataset.placement || 'top',
+ },
+ });
+}
-function observeBody() {
- if (!domObservedForChanges) {
- addPopoversToModifiedTree.observe(document.body, {
- subtree: true,
- childList: true,
- });
+function launchPopover(el, mountPopover) {
+ if (el.user) return;
- domObservedForChanges = true;
- }
+ const emptyUser = {
+ location: null,
+ bio: null,
+ workInformation: null,
+ status: null,
+ isFollowed: false,
+ loaded: false,
+ };
+ el.user = emptyUser;
+ el.addEventListener(
+ 'mouseleave',
+ ({ target }) => {
+ target.removeAttribute('aria-describedby');
+ },
+ { once: true },
+ );
+ const popoverInstance = createPopover(el, emptyUser);
+
+ const { userId } = el.dataset;
+
+ popoverInstance.$on('follow', () => {
+ UsersCache.updateById(userId, { is_followed: true });
+ el.user.isFollowed = true;
+ });
+
+ popoverInstance.$on('unfollow', () => {
+ UsersCache.updateById(userId, { is_followed: false });
+ el.user.isFollowed = false;
+ });
+
+ mountPopover(popoverInstance);
}
-export default function addPopovers(elements = document.querySelectorAll('.js-user-link')) {
- const userLinks = Array.from(elements);
- const UserPopoverComponent = Vue.extend(UserPopover);
+const userLinkSelector = 'a.js-user-link, a.gfm-project_member';
- observeBody();
+const getUserLinkNode = (node) => node.closest(userLinkSelector);
- return userLinks
- .filter(({ dataset }) => dataset.user || dataset.userId)
- .map((el) => {
- if (initializedPopovers.has(el)) {
- return initializedPopovers.get(el);
- }
+const lazyLaunchPopover = debounce((mountPopover, event) => {
+ const userLink = getUserLinkNode(event.target);
+ if (userLink) {
+ launchPopover(userLink, mountPopover);
+ }
+}, USER_POPOVER_DELAY);
- const user = {
- location: null,
- bio: null,
- workInformation: null,
- status: null,
- isFollowed: false,
- loaded: false,
- };
- const renderedPopover = new UserPopoverComponent({
- propsData: {
- target: el,
- user,
- placement: el.dataset.placement || 'top',
- },
- });
-
- const { userId } = el.dataset;
-
- renderedPopover.$on('follow', () => {
- UsersCache.updateById(userId, { is_followed: true });
- user.isFollowed = true;
- });
-
- renderedPopover.$on('unfollow', () => {
- UsersCache.updateById(userId, { is_followed: false });
- user.isFollowed = false;
- });
-
- initializedPopovers.set(el, renderedPopover);
-
- renderedPopover.$mount();
-
- el.addEventListener('mouseenter', ({ target }) => {
- removeTitle(target);
- const preloadedUserInfo = getPreloadedUserInfo(target.dataset);
-
- Object.assign(user, preloadedUserInfo);
-
- if (preloadedUserInfo.userId) {
- populateUserInfo(user);
- }
- });
- el.addEventListener('mouseleave', ({ target }) => {
- target.removeAttribute('aria-describedby');
- });
-
- return renderedPopover;
- });
+let hasAddedLazyPopovers = false;
+
+export default function addPopovers(mountPopover = (instance) => instance.$mount()) {
+ if (!hasAddedLazyPopovers) {
+ document.addEventListener('mouseover', (event) => lazyLaunchPopover(mountPopover, event));
+ hasAddedLazyPopovers = true;
+ }
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
index e7d5e4086bc..4163d195e0f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlSprintf, GlLink } from '@gitlab/ui';
import createFlash from '~/flash';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -11,9 +11,11 @@ import eventHub from '../../event_hub';
import approvalsMixin from '../../mixins/approvals';
import MrWidgetContainer from '../mr_widget_container.vue';
import MrWidgetIcon from '../mr_widget_icon.vue';
+import { INVALID_RULES_DOCS_PATH } from '../../constants';
import ApprovalsSummary from './approvals_summary.vue';
import ApprovalsSummaryOptional from './approvals_summary_optional.vue';
import { FETCH_LOADING, FETCH_ERROR, APPROVE_ERROR, UNAPPROVE_ERROR } from './messages';
+import { humanizeInvalidApproversRules } from './humanized_text';
export default {
name: 'MRWidgetApprovals',
@@ -23,6 +25,8 @@ export default {
ApprovalsSummary,
ApprovalsSummaryOptional,
GlButton,
+ GlSprintf,
+ GlLink,
},
mixins: [approvalsMixin, glFeatureFlagsMixin()],
props: {
@@ -78,6 +82,15 @@ export default {
approvals() {
return this.mr.approvals || {};
},
+ invalidRules() {
+ return this.approvals.invalid_approvers_rules || [];
+ },
+ hasInvalidRules() {
+ return this.approvals.merge_request_approvers_available && this.invalidRules.length;
+ },
+ invalidRulesText() {
+ return humanizeInvalidApproversRules(this.invalidRules);
+ },
approvedBy() {
return this.approvals.approved_by ? this.approvals.approved_by.map((x) => x.user) : [];
},
@@ -104,20 +117,24 @@ export default {
return {
text: this.approvalText,
category: this.isApproved ? 'secondary' : 'primary',
- variant: 'info',
+ variant: 'confirm',
action: () => this.approve(),
};
} else if (this.showUnapprove) {
return {
text: s__('mrWidget|Revoke approval'),
- variant: 'warning',
- category: 'secondary',
+ variant: 'default',
action: () => this.unapprove(),
};
}
return null;
},
+ pluralizedRuleText() {
+ return this.invalidRules.length > 1
+ ? this.$options.i18n.invalidRulesPlural
+ : this.$options.i18n.invalidRuleSingular;
+ },
},
created() {
this.refreshApprovals()
@@ -194,6 +211,16 @@ export default {
},
},
FETCH_LOADING,
+ linkToInvalidRules: INVALID_RULES_DOCS_PATH,
+ i18n: {
+ invalidRuleSingular: s__(
+ 'mrWidget|Approval rule %{rules} is invalid. GitLab has approved this rule automatically to unblock the merge request. %{link}',
+ ),
+ invalidRulesPlural: s__(
+ 'mrWidget|Approval rules %{rules} are invalid. GitLab has approved these rules automatically to unblock the merge request. %{link}',
+ ),
+ learnMore: __('Learn more.'),
+ },
};
</script>
<template>
@@ -202,29 +229,45 @@ export default {
<mr-widget-icon name="approval" />
<div v-if="fetchingApprovals">{{ $options.FETCH_LOADING }}</div>
<template v-else>
- <gl-button
- v-if="action"
- :variant="action.variant"
- :category="action.category"
- :loading="isApproving"
- class="mr-3"
- data-qa-selector="approve_button"
- @click="action.action"
- >
- {{ action.text }}
- </gl-button>
- <approvals-summary-optional
- v-if="isOptional"
- :can-approve="hasAction"
- :help-path="mr.approvalsHelpPath"
- />
- <approvals-summary
- v-else
- :approved="isApproved"
- :approvals-left="approvals.approvals_left || 0"
- :rules-left="approvals.approvalRuleNamesLeft"
- :approvers="approvedBy"
- />
+ <div class="gl-display-flex gl-flex-direction-column">
+ <div class="gl-display-flex gl-flex-direction-row gl-align-items-center">
+ <gl-button
+ v-if="action"
+ :variant="action.variant"
+ :category="action.category"
+ :loading="isApproving"
+ class="gl-mr-5"
+ data-qa-selector="approve_button"
+ @click="action.action"
+ >
+ {{ action.text }}
+ </gl-button>
+ <approvals-summary-optional
+ v-if="isOptional"
+ :can-approve="hasAction"
+ :help-path="mr.approvalsHelpPath"
+ />
+ <approvals-summary
+ v-else
+ :approved="isApproved"
+ :approvals-left="approvals.approvals_left || 0"
+ :rules-left="approvals.approvalRuleNamesLeft"
+ :approvers="approvedBy"
+ />
+ </div>
+ <div v-if="hasInvalidRules" class="gl-text-gray-400 gl-mt-2" data-testid="invalid-rules">
+ <gl-sprintf :message="pluralizedRuleText">
+ <template #rules>
+ {{ invalidRulesText }}
+ </template>
+ <template #link>
+ <gl-link :href="$options.linkToInvalidRules" target="_blank">
+ {{ $options.i18n.learnMore }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ </div>
<slot
:is-approving="isApproving"
:approve-with-auth="approveWithAuth"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
index 0e31f97b9db..b1c4f7c5a7c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
@@ -98,10 +98,10 @@ export default {
<template>
<div data-qa-selector="approvals_summary_content">
- <strong>{{ approvalLeftMessage }}</strong>
+ <span class="gl-font-weight-bold">{{ approvalLeftMessage }}</span>
<template v-if="hasApprovers">
<span v-if="approvalLeftMessage">{{ message }}</span>
- <strong v-else>{{ message }}</strong>
+ <span v-else class="gl-font-weight-bold">{{ message }}</span>
<user-avatar-list
class="gl-display-inline-block gl-vertical-align-middle"
:img-size="24"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/humanized_text.js b/app/assets/javascripts/vue_merge_request_widget/components/approvals/humanized_text.js
new file mode 100644
index 00000000000..6689d070053
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/humanized_text.js
@@ -0,0 +1,23 @@
+import { __ } from '~/locale';
+
+const humanizeRules = (invalidRules) => {
+ if (invalidRules.length > 1) {
+ return invalidRules.reduce((rules, { name }, index) => {
+ if (index === invalidRules.length - 1) {
+ return `${rules}${__(' and ')}"${name}"`;
+ }
+ return rules ? `${rules}, "${name}"` : `"${name}"`;
+ }, '');
+ }
+ return `"${invalidRules[0].name}"`;
+};
+
+export const humanizeInvalidApproversRules = (invalidRules) => {
+ const ruleCount = invalidRules.length;
+
+ if (!ruleCount) {
+ return '';
+ }
+
+ return humanizeRules(invalidRules);
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue
index d878a1fa2e0..655ceb5f700 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue
@@ -26,6 +26,7 @@ export default {
},
methods: {
onClickAction(action) {
+ this.$emit('clickedAction', action);
if (action.onClick) {
action.onClick();
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index 0bc17de638b..4ba620da00a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -6,16 +6,16 @@ import {
GlTooltipDirective,
GlIntersectionObserver,
} from '@gitlab/ui';
-import { once } from 'lodash';
import * as Sentry from '@sentry/browser';
import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
-import api from '~/api';
import { sprintf, s__, __ } from '~/locale';
import Poll from '~/lib/utils/poll';
+import { normalizeHeaders } from '~/lib/utils/common_utils';
import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants';
import StatusIcon from './status_icon.vue';
import Actions from './actions.vue';
import ChildContent from './child_content.vue';
+import { createTelemetryHub } from './telemetry';
import { generateText } from './utils';
export const LOADING_STATES = {
@@ -26,6 +26,7 @@ export const LOADING_STATES = {
};
export default {
+ telemetry: true,
components: {
GlButton,
GlLoadingIcon,
@@ -49,6 +50,7 @@ export default {
showFade: false,
modalData: undefined,
modalName: undefined,
+ telemetry: null,
};
},
computed: {
@@ -131,50 +133,85 @@ export default {
}
},
},
+ created() {
+ if (this.$options.telemetry) {
+ this.telemetry = createTelemetryHub(this.$options.name);
+ }
+ },
mounted() {
this.loadCollapsedData();
+
+ this.telemetry?.viewed();
},
methods: {
- triggerRedisTracking: once(function triggerRedisTracking() {
- if (this.$options.expandEvent) {
- api.trackRedisHllUserEvent(this.$options.expandEvent);
- }
- }),
toggleCollapsed(e) {
if (this.isCollapsible && !e?.target?.closest('.btn:not(.btn-icon),a')) {
- this.isCollapsed = !this.isCollapsed;
+ if (this.isCollapsed) {
+ this.telemetry?.expanded({ type: this.statusIconName });
+ }
- this.triggerRedisTracking();
+ this.isCollapsed = !this.isCollapsed;
}
},
+ initExtensionMultiPolling() {
+ const allData = [];
+ const requests = this.fetchMultiData();
+
+ requests.forEach((request) => {
+ const poll = new Poll({
+ resource: {
+ fetchData: () => request(this),
+ },
+ method: 'fetchData',
+ successCallback: (response) => {
+ this.headerCheck(response, (data) => allData.push(data));
+
+ if (allData.length === requests.length) {
+ this.setCollapsedData(allData);
+ }
+ },
+ errorCallback: (e) => {
+ this.setCollapsedError(e);
+ },
+ });
+
+ poll.makeRequest();
+ });
+ },
initExtensionPolling() {
const poll = new Poll({
resource: {
- fetchData: () => this.fetchCollapsedData(this.$props),
+ fetchData: () => this.fetchCollapsedData(this),
},
method: 'fetchData',
- successCallback: ({ data }) => {
- if (Object.keys(data).length > 0) {
- poll.stop();
- this.setCollapsedData(data);
- }
+ successCallback: (response) => {
+ this.headerCheck(response, (data) => this.setCollapsedData(data));
},
errorCallback: (e) => {
- poll.stop();
-
this.setCollapsedError(e);
},
});
poll.makeRequest();
},
+ headerCheck(response, callback) {
+ const headers = normalizeHeaders(response.headers);
+
+ if (!headers['POLL-INTERVAL']) {
+ callback(response.data);
+ }
+ },
loadCollapsedData() {
this.loadingState = LOADING_STATES.collapsedLoading;
if (this.$options.enablePolling) {
- this.initExtensionPolling();
+ if (this.fetchMultiData) {
+ this.initExtensionMultiPolling();
+ } else {
+ this.initExtensionPolling();
+ }
} else {
- this.fetchCollapsedData(this.$props)
+ this.fetchCollapsedData(this)
.then((data) => {
this.setCollapsedData(data);
})
@@ -197,7 +234,7 @@ export default {
this.loadingState = LOADING_STATES.expandedLoading;
- this.fetchFullData(this.$props)
+ this.fetchFullData(this)
.then((data) => {
this.loadingState = null;
this.fullData = data.map((x, i) => ({ id: i, ...x }));
@@ -231,6 +268,11 @@ export default {
this.toggleCollapsed(e);
}
},
+ onClickedAction(action) {
+ if (action.fullReport) {
+ this.telemetry?.fullReportClicked();
+ }
+ },
generateText,
},
EXTENSION_ICON_CLASS,
@@ -268,6 +310,7 @@ export default {
<actions
:widget="$options.label || $options.name"
:tertiary-buttons="tertiaryActionsButtons"
+ @clickedAction="onClickedAction"
/>
<div
v-if="isCollapsible"
@@ -324,6 +367,7 @@ export default {
:widget-label="widgetLabel"
:modal-id="modalId"
:level="2"
+ @clickedAction="onClickedAction"
/>
</gl-intersection-observer>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
index 0ca4c92a5ae..38f83a61b30 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
@@ -39,6 +39,9 @@ export default {
isArray(arr) {
return Array.isArray(arr);
},
+ onClickedAction(action) {
+ this.$emit('clickedAction', action);
+ },
generateText,
},
};
@@ -63,14 +66,14 @@ export default {
<div class="gl-w-full">
<div class="gl-display-flex gl-flex-nowrap">
<div class="gl-flex-wrap gl-display-flex gl-w-full">
- <div class="gl-mr-4 gl-display-flex gl-align-items-center">
+ <div class="gl-display-flex gl-align-items-center">
<p v-safe-html="generateText(data.text)" class="gl-m-0"></p>
</div>
- <div v-if="data.link">
+ <div v-if="data.link" class="gl-pr-2">
<gl-link :href="data.link.href">{{ data.link.text }}</gl-link>
</div>
- <div v-if="data.modal">
- <gl-link v-gl-modal="modalId" @click="data.modal.onClick">
+ <div v-if="data.modal" class="gl-pr-2">
+ <gl-link v-gl-modal="modalId" data-testid="modal-link" @click="data.modal.onClick">
{{ data.modal.text }}
</gl-link>
</div>
@@ -81,7 +84,12 @@ export default {
{{ data.badge.text }}
</gl-badge>
</div>
- <actions :widget="widgetLabel" :tertiary-buttons="data.actions" class="gl-ml-auto" />
+ <actions
+ :widget="widgetLabel"
+ :tertiary-buttons="data.actions"
+ class="gl-ml-auto gl-pl-3"
+ @clickedAction="onClickedAction"
+ />
</div>
<p
v-if="data.subtext"
@@ -101,6 +109,7 @@ export default {
:modal-id="modalId"
:level="3"
data-testid="child-content"
+ @clickedAction="onClickedAction"
/>
</li>
</ul>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js
index b9dfd3bd41e..a58d524b9ed 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js
@@ -14,7 +14,7 @@ export default {
if (extensions.length === 0) return null;
return h(
- 'div',
+ 'section',
{
attrs: {
role: 'region',
@@ -34,13 +34,7 @@ export default {
{ ...extension },
{
props: {
- ...extension.props.reduce(
- (acc, key) => ({
- ...acc,
- [key]: this.mr[key],
- }),
- {},
- ),
+ mr: this.mr,
},
},
),
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
index 65273678fb9..f4fcf4c9571 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
@@ -10,12 +10,27 @@ export const registerExtension = (extension) => {
registeredExtensions.extensions.push({
extends: ExtensionBase,
name: extension.name,
- props: extension.props,
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
+ telemetry: extension.telemetry,
i18n: extension.i18n,
expandEvent: extension.expandEvent,
enablePolling: extension.enablePolling,
modalComponent: extension.modalComponent,
computed: {
+ ...extension.props.reduce(
+ (acc, propKey) => ({
+ ...acc,
+ [propKey]() {
+ return this.mr[propKey];
+ },
+ }),
+ {},
+ ),
...Object.keys(extension.computed).reduce(
(acc, computedKey) => ({
...acc,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue
index 456a1f17aae..bb626c9adba 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue
@@ -49,7 +49,7 @@ export default {
]"
class="gl-rounded-full gl-mr-3 gl-relative gl-p-2"
>
- <gl-loading-icon v-if="isLoading" size="lg" inline class="gl-display-block" />
+ <gl-loading-icon v-if="isLoading" size="sm" inline class="gl-display-block" />
<gl-icon
v-else
:name="$options.EXTENSION_ICON_NAMES[iconName]"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
new file mode 100644
index 00000000000..aec3a35f37c
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
@@ -0,0 +1,207 @@
+import api from '~/api';
+import createEventHub from '~/helpers/event_hub_factory';
+import {
+ TELEMETRY_WIDGET_VIEWED,
+ TELEMETRY_WIDGET_EXPANDED,
+ TELEMETRY_WIDGET_FULL_REPORT_CLICKED,
+} from '../../constants';
+
+/*
+ * Additional events to send beyond the defaults for certain widget extensions
+ */
+const nonStandardEvents = {
+ codeQuality: {
+ uniqueUser: {
+ expand: ['i_testing_code_quality_widget_total'],
+ },
+ counter: {},
+ },
+ terraform: {
+ uniqueUser: {
+ expand: ['i_testing_terraform_widget_total'],
+ },
+ counter: {},
+ },
+ issues: {
+ uniqueUser: {
+ expand: ['i_testing_load_performance_widget_total'],
+ },
+ counter: {},
+ },
+ testReport: {
+ uniqueUser: {
+ expand: ['i_testing_summary_widget_total'],
+ },
+ counter: {},
+ },
+};
+
+function combineDeepArray(path, ...objects) {
+ const parts = path.split('.');
+ const allEntries = objects.reduce((entries, currentObject) => {
+ let expandedEntries = entries;
+ let traversed = currentObject;
+
+ parts.forEach((part) => {
+ traversed = traversed?.[part];
+ });
+
+ if (traversed) {
+ expandedEntries = [...entries, ...traversed];
+ }
+
+ return expandedEntries;
+ }, []);
+
+ return Array.from(new Set(allEntries));
+}
+
+function simplifyWidgetName(componentName) {
+ const noWidget = componentName.replace(/^Widget/, '');
+
+ return noWidget.charAt(0).toLowerCase() + noWidget.slice(1);
+}
+
+function baseRedisEventName(extensionName) {
+ const redisEventName = extensionName.replace(/([A-Z])/g, '_$1').toLowerCase();
+
+ return `i_merge_request_widget_${redisEventName}`;
+}
+
+function whenable(bus) {
+ return function when(event) {
+ return ({ execute, track, special }) => {
+ bus.$on(event, (busEvent) => {
+ track.forEach((redisEvent) => {
+ execute(redisEvent);
+ });
+
+ special?.({ event, execute, track, bus, busEvent });
+ });
+ };
+ };
+}
+
+function defaultBehaviorEvents({ bus, config }) {
+ const when = whenable(bus);
+ const isViewed = when(TELEMETRY_WIDGET_VIEWED);
+ const isExpanded = when(TELEMETRY_WIDGET_EXPANDED);
+ const fullReportIsClicked = when(TELEMETRY_WIDGET_FULL_REPORT_CLICKED);
+ const toHll = config?.uniqueUser || {};
+ const toCounts = config?.counter || {};
+ const user = api.trackRedisHllUserEvent.bind(api);
+ const count = api.trackRedisCounterEvent.bind(api);
+
+ if (toHll.view) {
+ isViewed({ execute: user, track: toHll.view });
+ }
+ if (toCounts.view) {
+ isViewed({ execute: count, track: toCounts.view });
+ }
+
+ if (toHll.expand) {
+ isExpanded({
+ execute: user,
+ track: toHll.expand,
+ special: ({ execute, track, busEvent }) => {
+ if (busEvent.type) {
+ track.forEach((event) => {
+ execute(`${event}_${busEvent.type}`);
+ });
+ }
+ },
+ });
+ }
+ if (toCounts.expand) {
+ isExpanded({
+ execute: count,
+ track: toCounts.expand,
+ special: ({ execute, track, busEvent }) => {
+ if (busEvent.type) {
+ track.forEach((event) => {
+ execute(`${event}_${busEvent.type}`);
+ });
+ }
+ },
+ });
+ }
+
+ if (toHll.clickFullReport) {
+ fullReportIsClicked({ execute: user, track: toHll.clickFullReport });
+ }
+ if (toCounts.clickFullReport) {
+ fullReportIsClicked({ execute: count, track: toCounts.clickFullReport });
+ }
+}
+
+function baseTelemetry(componentName) {
+ const simpleExtensionName = simplifyWidgetName(componentName);
+ const additionalNonStandard = nonStandardEvents[simpleExtensionName] || {};
+ /*
+ * Telemetry config format is:
+ * {
+ * TELEMETRY_TYPE: {
+ * BEHAVIOR: [ EVENT_NAME, ... ]
+ * }
+ * }
+ *
+ * Right now, there are currently configurations for these telemetry types:
+ * - uniqueUser is sent to RedisHLL
+ * - counter is sent to a regular Redis counter
+ */
+ const defaultTelemetry = {
+ uniqueUser: {
+ view: [`${baseRedisEventName(simpleExtensionName)}_view`],
+ expand: [`${baseRedisEventName(simpleExtensionName)}_expand`],
+ clickFullReport: [`${baseRedisEventName(simpleExtensionName)}_click_full_report`],
+ },
+ counter: {
+ view: [`${baseRedisEventName(simpleExtensionName)}_count_view`],
+ expand: [`${baseRedisEventName(simpleExtensionName)}_count_expand`],
+ clickFullReport: [`${baseRedisEventName(simpleExtensionName)}_count_click_full_report`],
+ },
+ };
+
+ return {
+ uniqueUser: {
+ view: combineDeepArray('uniqueUser.view', defaultTelemetry, additionalNonStandard),
+ expand: combineDeepArray('uniqueUser.expand', defaultTelemetry, additionalNonStandard),
+ clickFullReport: combineDeepArray(
+ 'uniqueUser.clickFullReport',
+ defaultTelemetry,
+ additionalNonStandard,
+ ),
+ },
+ counter: {
+ view: combineDeepArray('counter.view', defaultTelemetry, additionalNonStandard),
+ expand: combineDeepArray('counter.expand', defaultTelemetry, additionalNonStandard),
+ clickFullReport: combineDeepArray(
+ 'counter.clickFullReport',
+ defaultTelemetry,
+ additionalNonStandard,
+ ),
+ },
+ };
+}
+
+export function createTelemetryHub(componentName) {
+ const bus = createEventHub();
+ const config = baseTelemetry(componentName);
+
+ defaultBehaviorEvents({ bus, config });
+
+ return {
+ viewed() {
+ bus.$emit(TELEMETRY_WIDGET_VIEWED);
+ },
+ expanded({ type }) {
+ bus.$emit(TELEMETRY_WIDGET_EXPANDED, { type });
+ },
+ fullReportClicked() {
+ bus.$emit(TELEMETRY_WIDGET_FULL_REPORT_CLICKED);
+ },
+ /* I want a Record here: #{ ...config } // and then the comment would be: This is for debugging only, changing your reference to it does nothing. 😘 */
+ config: Object.freeze({ ...config }), // This is *intended* to be for debugging only, but it's pretty mutable, and it has references to live data in child props
+ bus,
+ };
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
index f5667aee15b..f8d2732b385 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
@@ -32,7 +32,7 @@ export default {
computed: {
arrowIconName() {
- return this.isCollapsed ? 'angle-right' : 'angle-down';
+ return this.isCollapsed ? 'chevron-lg-right' : 'chevron-lg-down';
},
ariaLabel() {
return this.isCollapsed ? __('Expand') : __('Collapse');
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
deleted file mode 100644
index e1d88099580..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ /dev/null
@@ -1,100 +0,0 @@
-<script>
-import {
- GlLink,
- GlTooltipDirective,
- GlModalDirective,
- GlSafeHtmlDirective as SafeHtml,
- GlSprintf,
-} from '@gitlab/ui';
-import { constructWebIDEPath } 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/tooltip_on_truncate.vue';
-import MrWidgetIcon from './mr_widget_icon.vue';
-
-export default {
- name: 'MRWidgetHeader',
- components: {
- clipboardButton,
- TooltipOnTruncate,
- MrWidgetIcon,
- GlLink,
- GlSprintf,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- GlModalDirective,
- SafeHtml,
- },
- props: {
- mr: {
- type: Object,
- required: true,
- },
- },
- computed: {
- shouldShowCommitsBehindText() {
- return this.mr.divergedCommitsCount > 0;
- },
- branchNameClipboardData() {
- // This supports code in app/assets/javascripts/copy_to_clipboard.js that
- // works around ClipboardJS limitations to allow the context-specific
- // copy/pasting of plain text or GFM.
- return JSON.stringify({
- text: this.mr.sourceBranch,
- gfm: `\`${this.mr.sourceBranch}\``,
- });
- },
- webIdePath() {
- return constructWebIDEPath(this.mr);
- },
- isFork() {
- return this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath;
- },
- },
- i18n: {
- webIdeText: s__('mrWidget|Open in Web IDE'),
- gitpodText: s__('mrWidget|Open in Gitpod'),
- },
-};
-</script>
-<template>
- <div class="gl-display-flex mr-source-target">
- <mr-widget-icon name="git-merge" />
- <div class="git-merge-container d-flex">
- <div class="normal">
- <strong>
- {{ s__('mrWidget|Request to merge') }}
- <tooltip-on-truncate
- v-safe-html="mr.sourceBranchLink"
- :title="mr.sourceBranch"
- truncate-target="child"
- class="label-branch label-truncate js-source-branch"
- /><clipboard-button
- data-testid="mr-widget-copy-clipboard"
- :text="branchNameClipboardData"
- :title="__('Copy branch name')"
- category="tertiary"
- />
- {{ s__('mrWidget|into') }}
- <tooltip-on-truncate
- :title="mr.targetBranch"
- truncate-target="child"
- class="label-branch label-truncate"
- >
- <a :href="mr.targetBranchTreePath" class="js-target-branch"> {{ mr.targetBranch }} </a>
- </tooltip-on-truncate>
- </strong>
- <div v-if="shouldShowCommitsBehindText" class="diverged-commits-count">
- <gl-sprintf :message="s__('mrWidget|The source branch is %{link} the target branch')">
- <template #link>
- <gl-link :href="mr.targetBranchPath">{{
- n__('%d commit behind', '%d commits behind', mr.divergedCommitsCount)
- }}</gl-link>
- </template>
- </gl-sprintf>
- </div>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
index 472df8e3110..437342bf438 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
@@ -13,7 +13,7 @@ export default {
</script>
<template>
- <div class="circle-icon-container gl-mr-3 align-self-start align-self-lg-center">
+ <div class="circle-icon-container gl-mr-3 align-self-start">
<gl-icon :name="name" :size="24" />
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 3b3b46e9772..1e1a2049414 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -172,7 +172,7 @@ export default {
</p>
</template>
<template v-else-if="!hasPipeline">
- <gl-loading-icon size="md" />
+ <gl-loading-icon size="lg" />
<p
class="gl-flex-grow-1 gl-display-flex gl-ml-3 gl-mb-0"
data-testid="monitoring-pipeline-message"
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 b8a1f89d232..913aa0e1e34 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
@@ -93,9 +93,7 @@ export default {
</span>
</p>
<div
- v-if="
- divergedCommitsCount > 0 && glFeatures.updatedMrHeader && !glFeatures.restructuredMrWidget
- "
+ v-if="divergedCommitsCount > 0 && !glFeatures.restructuredMrWidget"
class="diverged-commits-count"
>
<gl-sprintf :message="s__('mrWidget|The source branch is %{link} the target branch')">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
index 8b410926c46..45958d7fb8d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
@@ -113,7 +113,7 @@ export default {
data-testid="ok"
category="primary"
class="gl-mt-2"
- variant="info"
+ variant="confirm"
:href="pipelinePath"
:data-track-property="humanAccess"
:data-track-value="$options.SP_SHOW_TRACK_VALUE"
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 4fb95fe635c..cf482410bef 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
@@ -417,6 +417,7 @@ export default {
}
this.isMakingRequest = true;
+ this.editCommitMessage = false;
if (!useAutoMerge) {
this.mr.transitionStateMachine({ transition: MERGE });
@@ -663,7 +664,11 @@ export default {
<gl-sprintf v-else :message="mergeDisabledText" />
</div>
<template v-if="glFeatures.restructuredMrWidget">
- <div v-show="editCommitMessage" class="gl-w-full gl-order-n1">
+ <div
+ v-if="editCommitMessage"
+ class="gl-w-full gl-order-n1"
+ data-testid="edit_commit_message"
+ >
<ul
:class="{
'content-list': !glFeatures.restructuredMrWidget,
@@ -711,15 +716,13 @@ export default {
<div
v-if="!restructuredWidgetShowMergeButtons"
class="gl-w-full gl-order-n1 gl-text-gray-500"
+ data-qa-selector="merged_status_content"
>
<strong v-if="mr.state !== 'closed'">
{{ __('Merge details') }}
</strong>
<ul class="gl-pl-4 gl-m-0">
- <li
- v-if="mr.divergedCommitsCount > 0 && glFeatures.updatedMrHeader"
- class="gl-line-height-normal"
- >
+ <li v-if="mr.divergedCommitsCount > 0" class="gl-line-height-normal">
<gl-sprintf
:message="s__('mrWidget|The source branch is %{link} the target branch')"
>
@@ -788,11 +791,7 @@ export default {
</div>
</div>
<template v-if="shouldShowMergeControls && !glFeatures.restructuredMrWidget">
- <div
- v-if="!shouldShowMergeEdit"
- class="mr-fast-forward-message"
- data-qa-selector="fast_forward_message_content"
- >
+ <div v-if="!shouldShowMergeEdit" class="mr-fast-forward-message">
{{ __('Fast-forward merge without a merge commit') }}
</div>
<commits-header
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index e0e19094c40..5bd7745d704 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -126,6 +126,7 @@ export default {
}) => {
toast(__('Marked as ready. Merging is now allowed.'));
$('.merge-request .detail-page-description .title').text(title);
+ eventHub.$emit('MRWidgetUpdateRequested');
},
)
.catch(() =>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
index 2ba945a3ecf..18fdb29ba54 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
+import { GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
import { n__ } from '~/locale';
@@ -9,7 +9,7 @@ import TerraformPlan from './terraform_plan.vue';
export default {
name: 'MRWidgetTerraformContainer',
components: {
- GlSkeletonLoading,
+ GlSkeletonLoader,
GlSprintf,
MrWidgetExpanableSection,
TerraformPlan,
@@ -100,7 +100,7 @@ export default {
<template>
<section class="mr-widget-section">
<div v-if="loading" class="mr-widget-body">
- <gl-skeleton-loading />
+ <gl-skeleton-loader />
</div>
<mr-widget-expanable-section v-else>
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index 533bb38a88c..c148a35209f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -1,4 +1,5 @@
import { s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
import { stateToComponentMap as classStateMap, stateKey } from './stores/state_maps';
export const SUCCESS = 'success';
@@ -165,4 +166,15 @@ export const EXTENSION_ICON_CLASS = {
export const EXTENSION_SUMMARY_FAILED_CLASS = 'gl-text-red-500';
export const EXTENSION_SUMMARY_NEUTRAL_CLASS = 'gl-text-gray-700';
+export const TELEMETRY_WIDGET_VIEWED = 'WIDGET_VIEWED';
+export const TELEMETRY_WIDGET_EXPANDED = 'WIDGET_EXPANDED';
+export const TELEMETRY_WIDGET_FULL_REPORT_CLICKED = 'WIDGET_FULL_REPORT_CLICKED';
+
export { STATE_MACHINE };
+
+export const INVALID_RULES_DOCS_PATH = helpPagePath(
+ 'user/project/merge_requests/approvals/index.md',
+ {
+ anchor: 'invalid-rules',
+ },
+);
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.js
index 168f10bd148..f14e80d0be6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.js
@@ -6,6 +6,7 @@ import { EXTENSION_ICONS } from '../../constants';
export default {
name: 'WidgetAccessibility',
enablePolling: true,
+ telemetry: false,
i18n: {
loading: s__('Reports|Accessibility scanning results are being parsed'),
error: s__('Reports|Accessibility scanning failed loading results'),
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
index cea8df2484b..2477429af5b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
@@ -13,7 +13,6 @@ export default {
loading: s__('ciReport|Code Quality test metrics results are being parsed'),
error: s__('ciReport|Code Quality failed loading results'),
},
- expandEvent: 'i_testing_code_quality_widget_total',
computed: {
summary() {
const { newErrors, resolvedErrors, errorSummary } = this.collapsedData;
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
index 6ca0ea9c4e7..a7aaa2f4476 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
@@ -12,7 +12,6 @@ export default {
label: 'Issues',
loading: 'Loading issues...',
},
- expandEvent: 'i_testing_load_performance_widget_total',
// Add an array of props
// These then get mapped to values stored in the MR Widget store
props: ['targetProjectFullPath', 'conflictsDocsPath'],
@@ -45,7 +44,7 @@ export default {
console.log('Hello world');
},
},
- { text: 'Full report', href: this.conflictsDocsPath, target: '_blank' },
+ { text: 'Full report', href: this.conflictsDocsPath, target: '_blank', fullReport: true },
];
},
shouldCollapse() {
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/index.js
new file mode 100644
index 00000000000..ce35ae033de
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/index.js
@@ -0,0 +1,10 @@
+// This is here because ee_else_ce requires both ce and ee versions of the
+// file to be present.
+// TODO: implement me
+export default {
+ name: 'WidgetSecurityReportsCE',
+ data() {
+ return {};
+ },
+ props: ['securityReportPaths'],
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js
index 8fcc4f818ec..6611aedcb07 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js
@@ -23,7 +23,6 @@ export default {
reportErrored: s__('Terraform|Generating the report caused an error.'),
fullLog: __('Full log'),
},
- expandEvent: 'i_testing_terraform_widget_total',
props: ['terraformReportsPath'],
computed: {
// Extension computed props
@@ -113,6 +112,7 @@ export default {
href: report.job_path,
text: this.$options.i18n.fullLog,
target: '_blank',
+ fullReport: true,
};
actions.push(action);
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js
index 577b2cbfc5c..164bda33b95 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js
@@ -1,5 +1,6 @@
import { uniqueId } from 'lodash';
import axios from '~/lib/utils/axios_utils';
+import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
import { EXTENSION_ICONS } from '../../constants';
import {
summaryTextBuilder,
@@ -7,6 +8,7 @@ import {
reportSubTextBuilder,
countRecentlyFailedTests,
recentFailuresTextBuilder,
+ formatFilePath,
} from './utils';
import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants';
@@ -14,8 +16,8 @@ export default {
name: 'WidgetTestSummary',
enablePolling: true,
i18n,
- expandEvent: 'i_testing_summary_widget_total',
props: ['testResultsPath', 'headBlobPath', 'pipeline'],
+ modalComponent: TestCaseDetails,
computed: {
summary(data) {
if (data.parsingInProgress) {
@@ -47,14 +49,18 @@ export default {
text: this.$options.i18n.fullReport,
href: `${this.pipeline.path}/test_report`,
target: '_blank',
+ fullReport: true,
},
];
},
},
methods: {
fetchCollapsedData() {
- return axios.get(this.testResultsPath).then(({ data = {}, status }) => {
+ return axios.get(this.testResultsPath).then((res) => {
+ const { data = {}, status } = res;
+
return {
+ ...res,
data: {
hasSuiteError: data.suites?.some((suite) => suite.status === ERROR_STATUS),
parsingInProgress: status === 204,
@@ -94,8 +100,18 @@ export default {
return {
id: uniqueId('test-'),
header: this.testHeader(test, sectionHeader, index),
+ modal: {
+ text: test.name,
+ onClick: () => {
+ this.modalData = {
+ testCase: {
+ filePath: test.file && `${this.headBlobPath}/${formatFilePath(test.file)}`,
+ ...test,
+ },
+ };
+ },
+ },
icon: { name: iconName },
- text: test.name,
};
};
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
index 9e4b0ac581c..7bbcb0cd04a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
@@ -82,3 +82,12 @@ export const countRecentlyFailedTests = (subject) => {
})
.reduce((total, count) => total + count, 0);
};
+
+/**
+ * Removes `./` from the beginning of a file path so it can be appended onto a blob path
+ * @param {String} file
+ * @returns {String} - formatted value
+ */
+export const formatFilePath = (file) => {
+ return file.replace(/^\.?\/*/, '');
+};
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 8ebb7f6f159..c68437b9879 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
@@ -1,6 +1,7 @@
<script>
import { GlSafeHtmlDirective } from '@gitlab/ui';
import { isEmpty } from 'lodash';
+import securityReportExtension from 'ee_else_ce/vue_merge_request_widget/extensions/security_reports';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/approvals/approvals.vue';
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
@@ -15,7 +16,6 @@ import SmartInterval from '~/smart_interval';
import { setFaviconOverlay } from '../lib/utils/favicon';
import Loading from './components/loading.vue';
import MrWidgetAlertMessage from './components/mr_widget_alert_message.vue';
-import WidgetHeader from './components/mr_widget_header.vue';
import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue';
import WidgetRelatedLinks from './components/mr_widget_related_links.vue';
import WidgetSuggestPipeline from './components/mr_widget_suggest_pipeline.vue';
@@ -59,7 +59,6 @@ export default {
components: {
Loading,
ExtensionsContainer,
- 'mr-widget-header': WidgetHeader,
'mr-widget-suggest-pipeline': WidgetSuggestPipeline,
MrWidgetPipelineContainer,
'mr-widget-related-links': WidgetRelatedLinks,
@@ -190,7 +189,7 @@ export default {
);
},
shouldRenderSecurityReport() {
- return Boolean(this.mr.pipeline.id);
+ return Boolean(this.mr?.pipeline?.id);
},
shouldRenderTerraformPlans() {
return Boolean(this.mr?.terraformReportsPath);
@@ -231,12 +230,15 @@ export default {
window.gon?.features?.refactorMrWidgetsExtensionsUser
);
},
+ shouldShowSecurityExtension() {
+ return window.gon?.features?.refactorSecurityExtension;
+ },
+ shouldShowCodeQualityExtension() {
+ return window.gon?.features?.refactorCodeQualityExtension;
+ },
isRestructuredMrWidgetEnabled() {
return window.gon?.features?.restructuredMrWidget;
},
- isUpdatedHeaderEnabled() {
- return window.gon?.features?.updatedMrHeader;
- },
},
watch: {
'mr.machineValue': {
@@ -270,6 +272,11 @@ export default {
this.registerTestReportExtension();
}
},
+ shouldRenderSecurityReport(newVal) {
+ if (newVal) {
+ this.registerSecurityReportExtension();
+ }
+ },
},
mounted() {
MRWidgetService.fetchInitialData()
@@ -516,7 +523,7 @@ export default {
}
},
registerCodeQualityExtension() {
- if (this.shouldRenderCodeQuality && this.shouldShowExtension) {
+ if (this.shouldRenderCodeQuality && this.shouldShowCodeQualityExtension) {
registerExtension(codeQualityExtension);
}
},
@@ -525,20 +532,23 @@ export default {
registerExtension(testReportExtension);
}
},
+ registerSecurityReportExtension() {
+ if (this.shouldRenderSecurityReport && this.shouldShowSecurityExtension) {
+ registerExtension(securityReportExtension);
+ }
+ },
},
};
</script>
<template>
<div v-if="isLoaded" class="mr-state-widget gl-mt-3">
<header
- v-if="shouldRenderCollaborationStatus || !isUpdatedHeaderEnabled"
- :class="{ 'mr-widget-workflow gl-mt-0!': isUpdatedHeaderEnabled }"
- class="gl-rounded-base gl-border-solid gl-border-1 gl-border-gray-100 gl-overflow-hidden"
+ v-if="shouldRenderCollaborationStatus"
+ class="gl-rounded-base gl-border-solid gl-border-1 gl-border-gray-100 gl-overflow-hidden mr-widget-workflow gl-mt-0!"
>
<mr-widget-alert-message v-if="shouldRenderCollaborationStatus" type="info">
{{ s__('mrWidget|Members who can merge are allowed to add commits.') }}
</mr-widget-alert-message>
- <mr-widget-header v-if="!isUpdatedHeaderEnabled" :mr="mr" />
</header>
<mr-widget-suggest-pipeline
v-if="shouldSuggestPipelines"
@@ -584,7 +594,7 @@ export default {
</div>
<extensions-container :mr="mr" />
<grouped-codequality-reports-app
- v-if="shouldRenderCodeQuality && !shouldShowExtension"
+ v-if="shouldRenderCodeQuality && !shouldShowCodeQualityExtension"
:head-blob-path="mr.headBlobPath"
:base-blob-path="mr.baseBlobPath"
:codequality-reports-path="mr.codequalityReportsPath"
@@ -592,7 +602,7 @@ export default {
/>
<security-reports-app
- v-if="shouldRenderSecurityReport"
+ v-if="shouldRenderSecurityReport && !shouldShowSecurityExtension"
:pipeline-id="mr.pipeline.id"
:project-id="mr.sourceProjectId"
:security-reports-docs-path="mr.securityReportsDocsPath"
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 2ae4f4da2f3..18d955652ba 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
@@ -3,8 +3,6 @@ import { stateKey } from './state_maps';
export default function deviseState() {
if (!this.commitsCount) {
return stateKey.nothingToMerge;
- } else if (this.hasMergeChecksFailed && !this.autoMergeEnabled) {
- return stateKey.mergeChecksFailed;
} else if (this.projectArchived) {
return stateKey.archived;
} else if (this.branchMissing) {
@@ -15,6 +13,8 @@ export default function deviseState() {
return stateKey.conflicts;
} else if (this.shouldBeRebased) {
return stateKey.rebase;
+ } else if (this.hasMergeChecksFailed && !this.autoMergeEnabled) {
+ return stateKey.mergeChecksFailed;
} else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
return stateKey.pipelineFailed;
} else if (this.draft) {
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 146cf7e11a7..03c9a01cc7a 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
@@ -33,6 +33,7 @@ export default class MergeRequestStore {
this.setData(data);
this.initCodeQualityReport(data);
+ this.initSecurityReport(data);
this.setGitpodData(data);
}
@@ -41,6 +42,19 @@ export default class MergeRequestStore {
this.codeQuality = data.codequality_reports_path;
}
+ initSecurityReport(data) {
+ // TODO: check if gl.mrWidgetData can be safely removed after we migrate to the
+ // widget extension.
+ this.securityReportPaths = {
+ apiFuzzingReportPath: data.api_fuzzing_comparison_path,
+ coverageFuzzingReportPath: data.coverage_fuzzing_comparison_path,
+ sastReportPath: data.sast_comparison_path,
+ dastReportPath: data.dast_comparison_path,
+ secretDetectionReportPath: data.secret_detection_comparison_path,
+ dependencyScanningReportPath: data.dependency_scanning_comparison_path,
+ };
+ }
+
setData(data, isRebased) {
this.initApprovals();
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
index c93f620995f..f2ea55df63d 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
@@ -18,7 +18,6 @@ import { toggleContainerClasses } from '~/lib/utils/dom_utils';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
-import initUserPopovers from '~/user_popovers';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import MetricImagesTab from '~/vue_shared/components/metric_images/metric_images_tab.vue';
@@ -83,9 +82,6 @@ export default {
alertId: {
default: '',
},
- isThreatMonitoringPage: {
- default: false,
- },
projectId: {
default: '',
},
@@ -175,7 +171,6 @@ export default {
updated() {
this.$nextTick(() => {
highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member'));
- initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
});
},
methods: {
@@ -225,9 +220,7 @@ export default {
});
},
incidentPath(issueId) {
- return this.isThreatMonitoringPage
- ? joinPaths(this.projectIssuesPath, issueId)
- : joinPaths(this.projectIssuesPath, 'incident', issueId);
+ return joinPaths(this.projectIssuesPath, 'incident', issueId);
},
trackPageViews() {
const { category, action } = this.trackAlertsDetailsViewsOptions;
@@ -374,7 +367,6 @@ export default {
</gl-tab>
<metric-images-tab
- v-if="!isThreatMonitoringPage"
:data-testid="$options.tabsConfig[1].id"
:title="$options.tabsConfig[1].title"
/>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
index 489d4afa41f..72dcc16b57a 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
@@ -302,9 +302,11 @@ export default {
<span v-else class="gl-display-flex gl-align-items-center gl-line-height-normal">
{{ __('None') }} -
<gl-button
- class="gl-ml-2"
+ class="gl-ml-2 gl-reset-color!"
href="#"
+ category="tertiary"
variant="link"
+ size="small"
data-testid="unassigned-users"
@click="updateAlertAssignees(currentUser)"
>
diff --git a/app/assets/javascripts/vue_shared/alert_details/constants.js b/app/assets/javascripts/vue_shared/alert_details/constants.js
index 6cc70739eaa..d106f545c61 100644
--- a/app/assets/javascripts/vue_shared/alert_details/constants.js
+++ b/app/assets/javascripts/vue_shared/alert_details/constants.js
@@ -30,13 +30,4 @@ export const PAGE_CONFIG = {
label: 'Status',
},
},
- THREAT_MONITORING: {
- TITLE: 'THREAT_MONITORING',
- STATUSES: {
- TRIGGERED: s__('ThreatMonitoring|Unreviewed'),
- ACKNOWLEDGED: s__('ThreatMonitoring|In review'),
- RESOLVED: s__('ThreatMonitoring|Resolved'),
- IGNORED: s__('ThreatMonitoring|Dismissed'),
- },
- },
};
diff --git a/app/assets/javascripts/vue_shared/alert_details/index.js b/app/assets/javascripts/vue_shared/alert_details/index.js
index 614748fa80d..5793069440c 100644
--- a/app/assets/javascripts/vue_shared/alert_details/index.js
+++ b/app/assets/javascripts/vue_shared/alert_details/index.js
@@ -65,16 +65,12 @@ export default (selector) => {
const opsProperties = {};
- if (page === PAGE_CONFIG.OPERATIONS.TITLE) {
- const { TRACK_ALERTS_DETAILS_VIEWS_OPTIONS, TRACK_ALERT_STATUS_UPDATE_OPTIONS } = PAGE_CONFIG[
- page
- ];
- provide.trackAlertsDetailsViewsOptions = TRACK_ALERTS_DETAILS_VIEWS_OPTIONS;
- provide.trackAlertStatusUpdateOptions = TRACK_ALERT_STATUS_UPDATE_OPTIONS;
- opsProperties.store = createStore({}, service);
- } else if (page === PAGE_CONFIG.THREAT_MONITORING.TITLE) {
- provide.isThreatMonitoringPage = true;
- }
+ const { TRACK_ALERTS_DETAILS_VIEWS_OPTIONS, TRACK_ALERT_STATUS_UPDATE_OPTIONS } = PAGE_CONFIG[
+ page
+ ];
+ provide.trackAlertsDetailsViewsOptions = TRACK_ALERTS_DETAILS_VIEWS_OPTIONS;
+ provide.trackAlertStatusUpdateOptions = TRACK_ALERT_STATUS_UPDATE_OPTIONS;
+ opsProperties.store = createStore({}, service);
// eslint-disable-next-line no-new
new Vue({
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index 9bccc49e894..8bffc2479a1 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -45,7 +45,12 @@ export default {
return validSizes.includes(value);
},
},
- borderless: {
+ isActive: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isBorderless: {
type: Boolean,
required: false,
default: false,
@@ -67,15 +72,19 @@ export default {
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status} gl-rounded-full gl-justify-content-center`;
},
icon() {
- return this.borderless ? `${this.status.icon}_borderless` : this.status.icon;
+ return this.isBorderless ? `${this.status.icon}_borderless` : this.status.icon;
},
},
};
</script>
<template>
<span
- :class="[wrapperStyleClasses, { interactive: isInteractive }]"
+ :class="[
+ wrapperStyleClasses,
+ { interactive: isInteractive, active: isActive, borderless: isBorderless },
+ ]"
:style="{ height: `${size}px`, width: `${size}px` }"
+ data-testid="ci-icon-wrapper"
>
<gl-icon :name="icon" :size="size" :class="cssClasses" :aria-label="status.icon" />
</span>
diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
index f14e1992901..dd6923d9fcd 100644
--- a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
@@ -45,7 +45,7 @@ export default {
};
</script>
<template>
- <gl-dropdown right :text="$options.labels.defaultLabel" category="primary" variant="info">
+ <gl-dropdown right :text="$options.labels.defaultLabel" category="primary" variant="confirm">
<div class="pb-2 mx-1">
<template v-if="sshLink">
<gl-dropdown-section-header>{{ $options.labels.ssh }}</gl-dropdown-section-header>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue
new file mode 100644
index 00000000000..92817d5fa70
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue
@@ -0,0 +1,25 @@
+<script>
+export default {
+ props: {
+ color: {
+ type: String,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <span
+ class="dropdown-label-box gl-flex-shrink-0 gl-top-1 gl-mr-0"
+ data-testid="color-item"
+ :style="{ backgroundColor: color }"
+ ></span>
+ <span class="hide-collapsed">{{ title }}</span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue
new file mode 100644
index 00000000000..6b79883d76b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue
@@ -0,0 +1,214 @@
+<script>
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
+import { DEFAULT_COLOR, COLOR_WIDGET_COLOR, DROPDOWN_VARIANT, ISSUABLE_COLORS } from './constants';
+import DropdownContents from './dropdown_contents.vue';
+import DropdownValue from './dropdown_value.vue';
+import { isDropdownVariantSidebar, isDropdownVariantEmbedded } from './utils';
+import epicColorQuery from './graphql/epic_color.query.graphql';
+import updateEpicColorMutation from './graphql/epic_update_color.mutation.graphql';
+
+export default {
+ i18n: {
+ assignColor: s__('ColorWidget|Assign epic color'),
+ dropdownButtonText: COLOR_WIDGET_COLOR,
+ fetchingError: s__('ColorWidget|Error fetching epic color.'),
+ updatingError: s__('ColorWidget|An error occurred while updating color.'),
+ widgetTitle: COLOR_WIDGET_COLOR,
+ },
+ components: {
+ DropdownValue,
+ DropdownContents,
+ SidebarEditableItem,
+ },
+ props: {
+ allowEdit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ iid: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ variant: {
+ type: String,
+ required: false,
+ default: DROPDOWN_VARIANT.Sidebar,
+ },
+ dropdownButtonText: {
+ type: String,
+ required: false,
+ default: COLOR_WIDGET_COLOR,
+ },
+ dropdownTitle: {
+ type: String,
+ required: false,
+ default: s__('ColorWidget|Assign epic color'),
+ },
+ },
+ data() {
+ return {
+ issuableColor: {
+ color: '',
+ title: '',
+ },
+ colorUpdateInProgress: false,
+ oldIid: null,
+ sidebarExpandedOnClick: false,
+ };
+ },
+ apollo: {
+ issuableColor: {
+ query: epicColorQuery,
+ skip() {
+ return !isDropdownVariantSidebar(this.variant);
+ },
+ variables() {
+ return {
+ iid: this.iid,
+ fullPath: this.fullPath,
+ };
+ },
+ update(data) {
+ const issuableColor = data.workspace?.issuable?.color;
+
+ if (issuableColor) {
+ return ISSUABLE_COLORS.find((color) => color.color === issuableColor) ?? DEFAULT_COLOR;
+ }
+
+ return DEFAULT_COLOR;
+ },
+ error() {
+ createFlash({
+ message: this.$options.i18n.fetchingError,
+ captureError: true,
+ });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.colorUpdateInProgress || this.$apollo.queries.issuableColor.loading;
+ },
+ },
+ watch: {
+ iid(_, oldVal) {
+ this.oldIid = oldVal;
+ },
+ },
+ methods: {
+ handleDropdownClose(color) {
+ if (this.iid !== '') {
+ this.updateSelectedColor(this.getUpdateVariables(color));
+ } else {
+ this.$emit('updateSelectedColor', color);
+ }
+
+ this.collapseEditableItem();
+ },
+ collapseEditableItem() {
+ this.$refs.editable?.collapse();
+ if (this.sidebarExpandedOnClick) {
+ this.sidebarExpandedOnClick = false;
+ this.$emit('toggleCollapse');
+ }
+ },
+ getUpdateVariables(color) {
+ const currentIid = this.oldIid || this.iid;
+
+ return {
+ iid: currentIid,
+ groupPath: this.fullPath,
+ color: color.color,
+ };
+ },
+ updateSelectedColor(inputVariables) {
+ this.colorUpdateInProgress = true;
+
+ this.$apollo
+ .mutate({
+ mutation: updateEpicColorMutation,
+ variables: { input: inputVariables },
+ })
+ .then(({ data }) => {
+ if (data.updateIssuableColor?.errors?.length) {
+ throw new Error();
+ }
+
+ this.$emit('updateSelectedColor', {
+ id: data.updateIssuableColor?.issuable?.id,
+ color: data.updateIssuableColor?.issuable?.color,
+ });
+ })
+ .catch((error) =>
+ createFlash({
+ message: this.$options.i18n.updatingError,
+ captureError: true,
+ error,
+ }),
+ )
+ .finally(() => {
+ this.colorUpdateInProgress = false;
+ });
+ },
+ isDropdownVariantSidebar,
+ isDropdownVariantEmbedded,
+ },
+};
+</script>
+
+<template>
+ <div
+ class="labels-select-wrapper gl-relative"
+ :class="{
+ 'is-embedded': isDropdownVariantEmbedded(variant),
+ }"
+ >
+ <template v-if="isDropdownVariantSidebar(variant)">
+ <sidebar-editable-item
+ ref="editable"
+ :title="$options.i18n.widgetTitle"
+ :loading="isLoading"
+ :can-edit="allowEdit"
+ @open="oldIid = null"
+ >
+ <template #collapsed>
+ <dropdown-value :selected-color="issuableColor">
+ <slot></slot>
+ </dropdown-value>
+ </template>
+ <template #default="{ edit }">
+ <dropdown-value :selected-color="issuableColor" class="gl-mb-2">
+ <slot></slot>
+ </dropdown-value>
+ <dropdown-contents
+ ref="dropdownContents"
+ :dropdown-button-text="dropdownButtonText"
+ :dropdown-title="dropdownTitle"
+ :selected-color="issuableColor"
+ :variant="variant"
+ :is-visible="edit"
+ @setColor="handleDropdownClose"
+ @closeDropdown="collapseEditableItem"
+ />
+ </template>
+ </sidebar-editable-item>
+ </template>
+ <dropdown-contents
+ v-else
+ ref="dropdownContents"
+ :dropdown-button-text="dropdownButtonText"
+ :dropdown-title="dropdownTitle"
+ :selected-color="issuableColor"
+ :variant="variant"
+ @setColor="handleDropdownClose"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js b/app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js
new file mode 100644
index 00000000000..c70785abd1e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js
@@ -0,0 +1,30 @@
+import { __, s__ } from '~/locale';
+
+export const COLOR_WIDGET_COLOR = s__('ColorWidget|Color');
+
+export const DROPDOWN_VARIANT = {
+ Sidebar: 'sidebar',
+ Embedded: 'embedded',
+};
+
+export const DEFAULT_COLOR = { title: __('SuggestedColors|Blue'), color: '#1068bf' };
+
+export const ISSUABLE_COLORS = [
+ DEFAULT_COLOR,
+ {
+ title: s__('SuggestedColors|Green'),
+ color: '#217645',
+ },
+ {
+ title: s__('SuggestedColors|Red'),
+ color: '#c91c00',
+ },
+ {
+ title: s__('SuggestedColors|Orange'),
+ color: '#9e5400',
+ },
+ {
+ title: s__('SuggestedColors|Purple'),
+ color: '#694cc0',
+ },
+];
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue
new file mode 100644
index 00000000000..4eb1d3d08ca
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue
@@ -0,0 +1,109 @@
+<script>
+import { GlDropdown } from '@gitlab/ui';
+import DropdownContentsColorView from './dropdown_contents_color_view.vue';
+import DropdownHeader from './dropdown_header.vue';
+import { isDropdownVariantSidebar } from './utils';
+
+export default {
+ components: {
+ DropdownContentsColorView,
+ DropdownHeader,
+ GlDropdown,
+ },
+ props: {
+ dropdownTitle: {
+ type: String,
+ required: true,
+ },
+ selectedColor: {
+ type: Object,
+ required: true,
+ },
+ dropdownButtonText: {
+ type: String,
+ required: true,
+ },
+ variant: {
+ type: String,
+ required: true,
+ },
+ isVisible: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ showDropdownContentsCreateView: false,
+ localSelectedColor: this.selectedColor,
+ isDirty: false,
+ };
+ },
+ computed: {
+ buttonText() {
+ if (!this.localSelectedColor?.title) {
+ return this.dropdownButtonText;
+ }
+
+ return this.localSelectedColor.title;
+ },
+ },
+ watch: {
+ localSelectedColor: {
+ handler() {
+ this.isDirty = true;
+ },
+ deep: true,
+ },
+ isVisible(newVal) {
+ if (newVal) {
+ this.$refs.dropdown.show();
+ this.isDirty = false;
+ this.localSelectedColor = this.selectedColor;
+ } else {
+ this.$refs.dropdown.hide();
+ this.setColor();
+ }
+ },
+ selectedColor(newVal) {
+ if (!this.isDirty) {
+ this.localSelectedColor = newVal;
+ }
+ },
+ },
+ methods: {
+ setColor() {
+ if (!this.isDirty) {
+ return;
+ }
+ this.$emit('setColor', this.localSelectedColor);
+ },
+ handleDropdownHide() {
+ this.$emit('closeDropdown');
+ if (!isDropdownVariantSidebar(this.variant)) {
+ this.setColor();
+ }
+ this.$refs.dropdown.hide();
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown ref="dropdown" :text="buttonText" class="gl-w-full" @hide="handleDropdownHide">
+ <template #header>
+ <dropdown-header
+ ref="header"
+ :dropdown-title="dropdownTitle"
+ @closeDropdown="handleDropdownHide"
+ />
+ </template>
+ <template #default>
+ <dropdown-contents-color-view
+ v-model="localSelectedColor"
+ @closeDropdown="handleDropdownHide"
+ />
+ </template>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue
new file mode 100644
index 00000000000..62f4cf59c14
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue
@@ -0,0 +1,53 @@
+<script>
+import { GlDropdownForm, GlDropdownItem } from '@gitlab/ui';
+import ColorItem from './color_item.vue';
+import { ISSUABLE_COLORS } from './constants';
+
+export default {
+ components: {
+ GlDropdownForm,
+ GlDropdownItem,
+ ColorItem,
+ },
+ model: {
+ prop: 'selectedColor',
+ },
+ props: {
+ selectedColor: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ colors: ISSUABLE_COLORS,
+ };
+ },
+ methods: {
+ isColorSelected(color) {
+ return this.selectedColor.color === color.color;
+ },
+ handleColorClick(color) {
+ this.$emit('input', color);
+ this.$emit('closeDropdown', this.selectedColor);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown-form>
+ <div>
+ <gl-dropdown-item
+ v-for="color in colors"
+ :key="color.color"
+ :is-checked="isColorSelected(color)"
+ :is-check-centered="true"
+ :is-check-item="true"
+ @click.native.capture.stop="handleColorClick(color)"
+ >
+ <color-item :color="color.color" :title="color.title" />
+ </gl-dropdown-item>
+ </div>
+ </gl-dropdown-form>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue
new file mode 100644
index 00000000000..a32b1570f5f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue
@@ -0,0 +1,31 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlButton,
+ },
+ props: {
+ dropdownTitle: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!">
+ <span class="gl-flex-grow-1">{{ dropdownTitle }}</span>
+ <gl-button
+ :aria-label="__('Close')"
+ variant="link"
+ size="small"
+ class="dropdown-header-button gl-p-0!"
+ icon="close"
+ @click="$emit('closeDropdown')"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue
new file mode 100644
index 00000000000..4cba66eefd2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue
@@ -0,0 +1,43 @@
+<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { COLOR_WIDGET_COLOR } from './constants';
+import ColorItem from './color_item.vue';
+
+export default {
+ i18n: {
+ dropdownTitle: COLOR_WIDGET_COLOR,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ GlIcon,
+ ColorItem,
+ },
+ props: {
+ selectedColor: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="value js-value">
+ <div
+ v-gl-tooltip.left.viewport
+ :title="$options.i18n.dropdownTitle"
+ class="sidebar-collapsed-icon"
+ >
+ <gl-icon name="appearance" />
+ <color-item
+ :color="selectedColor.color"
+ :title="selectedColor.title"
+ class="gl-font-base gl-line-height-24"
+ />
+ </div>
+
+ <color-item class="hide-collapsed" :color="selectedColor.color" :title="selectedColor.title" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql
new file mode 100644
index 00000000000..959e0f8c1a5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql
@@ -0,0 +1,9 @@
+query epicColor($fullPath: ID!, $iid: ID) {
+ workspace: group(fullPath: $fullPath) {
+ id
+ issuable: epic(iid: $iid) {
+ id
+ color
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql
new file mode 100644
index 00000000000..2975b42253f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql
@@ -0,0 +1,9 @@
+mutation updateEpicColor($input: UpdateEpicInput!) {
+ updateIssuableColor: updateEpic(input: $input) {
+ issuable: epic {
+ id
+ color
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js b/app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js
new file mode 100644
index 00000000000..46196e793b3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js
@@ -0,0 +1,15 @@
+import { DROPDOWN_VARIANT } from './constants';
+
+/**
+ * Returns boolean representing whether dropdown variant
+ * is `sidebar`
+ * @param {string} variant
+ */
+export const isDropdownVariantSidebar = (variant) => variant === DROPDOWN_VARIANT.Sidebar;
+
+/**
+ * Returns boolean representing whether dropdown variant
+ * is `embedded`
+ * @param {string} variant
+ */
+export const isDropdownVariantEmbedded = (variant) => variant === DROPDOWN_VARIANT.Embedded;
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index 9cf8638f3cb..3ecfac10f9c 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -1,8 +1,5 @@
<script>
-import {
- GlDeprecatedSkeletonLoading as GlSkeletonLoading,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlSkeletonLoader, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { forEach, escape } from 'lodash';
@@ -14,7 +11,7 @@ let axiosSource;
export default {
components: {
- GlSkeletonLoading,
+ GlSkeletonLoader,
},
directives: {
SafeHtml,
@@ -115,7 +112,7 @@ export default {
<template>
<div ref="markdownPreview" class="md-previewer" data-testid="md-previewer">
- <gl-skeleton-loading v-if="isLoading" />
+ <gl-skeleton-loader v-if="isLoading" />
<div
v-else
v-safe-html:[$options.safeHtmlConfig]="previewContent"
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 d7a84798e47..5d7f4ae2a01 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -1,4 +1,4 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
export const DEBOUNCE_DELAY = 500;
export const MAX_RECENT_TOKENS_SIZE = 3;
@@ -46,11 +46,13 @@ export const SortDirection = {
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_AUTHOR = __('Author');
+export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential');
+export const TOKEN_TITLE_CONTACT = s__('Crm|Contact');
export const TOKEN_TITLE_LABEL = __('Label');
-export const TOKEN_TITLE_TYPE = __('Type');
-export const TOKEN_TITLE_RELEASE = __('Release');
+export const TOKEN_TITLE_MILESTONE = __('Milestone');
export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
-export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential');
+export const TOKEN_TITLE_ORGANIZATION = s__('Crm|Organization');
+export const TOKEN_TITLE_RELEASE = __('Release');
+export const TOKEN_TITLE_TYPE = __('Type');
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 c3a0a97a7ba..6a4ff07c999 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
@@ -168,7 +168,7 @@ export default {
if (data || operator) {
this.searchKey = data;
- if (!this.suggestionsLoading && !this.activeTokenValue) {
+ if (!this.activeTokenValue) {
let search = this.searchTerm ? this.searchTerm : data;
if (search.startsWith('"') && search.endsWith('"')) {
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 6f24955814c..178c57a5666 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
@@ -43,9 +43,7 @@ export default {
},
methods: {
getActiveLabel(labels, data) {
- return labels.find(
- (label) => this.getLabelName(label).toLowerCase() === stripQuotes(data).toLowerCase(),
- );
+ return labels.find((label) => this.getLabelName(label) === stripQuotes(data));
},
/**
* There's an inconsistency between private and public API
@@ -128,7 +126,7 @@ export default {
<div class="gl-display-flex gl-align-items-center">
<span
:style="{ backgroundColor: label.color }"
- class="gl-display-inline-block mr-2 p-2"
+ class="gl-display-inline-block gl-mr-3 gl-p-3"
></span>
<div>{{ getLabelName(label) }}</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
index 69548f0e7a8..15d858b99b9 100644
--- a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
+++ b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
@@ -1,5 +1,11 @@
<script>
-import { GlFormInputGroup, GlFormGroup, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlFormInputGroup,
+ GlFormInput,
+ GlFormGroup,
+ GlButton,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import { __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -12,6 +18,7 @@ export default {
},
components: {
GlFormInputGroup,
+ GlFormInput,
GlFormGroup,
GlButton,
ClipboardButton,
@@ -80,10 +87,15 @@ export default {
this.$emit('visibility-change', this.valueIsVisible);
},
+ handleClick() {
+ this.$refs.input.$el.select();
+ },
handleCopyButtonClick() {
this.$emit('copy');
},
handleFormInputCopy(event) {
+ this.handleCopyButtonClick();
+
if (this.computedValueIsVisible) {
return;
}
@@ -96,14 +108,21 @@ export default {
</script>
<template>
<gl-form-group v-bind="$attrs">
- <gl-form-input-group
- :value="displayedValue"
- input-class="gl-font-monospace! gl-cursor-default!"
- select-on-click
- readonly
- v-bind="formInputGroupProps"
- @copy="handleFormInputCopy"
- >
+ <gl-form-input-group>
+ <gl-form-input
+ ref="input"
+ readonly
+ class="gl-font-monospace! gl-cursor-default!"
+ v-bind="formInputGroupProps"
+ :value="displayedValue"
+ @copy="handleFormInputCopy"
+ @click="handleClick"
+ />
+
+ <!--
+ This v-if is necessary to avoid an issue with border radius.
+ See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88059#note_969812649
+ -->
<template v-if="showToggleVisibilityButton || showCopyButton" #append>
<gl-button
v-if="showToggleVisibilityButton"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index ba2b5eaa4f9..4fdf7f45643 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -266,7 +266,7 @@ export default {
}}
</p>
<gl-button
- variant="info"
+ variant="confirm"
category="primary"
size="small"
@click="handleSuggestDismissed"
diff --git a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
index 7a7074da084..78a7fed6293 100644
--- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
@@ -98,13 +98,15 @@ export default {
<span v-else-if="isConfidential" ref="confidential">
{{ confidentialContextText }}
{{ __('People without permission will never get a notification.') }}
- <gl-link :href="confidentialNoteableDocsPath" target="_blank">{{ __('Learn more') }}</gl-link>
+ <gl-link :href="confidentialNoteableDocsPath" target="_blank">{{
+ __('Learn more.')
+ }}</gl-link>
</span>
<span v-else-if="isLocked" ref="locked">
{{ lockedContextText }}
{{ __('Only project members can comment.') }}
- <gl-link :href="lockedNoteableDocsPath" target="_blank">{{ __('Learn more') }}</gl-link>
+ <gl-link :href="lockedNoteableDocsPath" target="_blank">{{ __('Learn more.') }}</gl-link>
</span>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index 3aca068c074..2206ae98c73 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -1,11 +1,11 @@
<script>
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlSkeletonLoader } from '@gitlab/ui';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
export default {
name: 'SkeletonNote',
components: {
- GlSkeletonLoading,
+ GlSkeletonLoader,
TimelineEntryItem,
},
};
@@ -16,7 +16,7 @@ export default {
<div class="timeline-icon"></div>
<div class="timeline-content">
<div class="note-header"></div>
- <div class="note-body"><gl-skeleton-loading /></div>
+ <div class="note-body"><gl-skeleton-loader /></div>
</div>
</timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index dd7a851b1be..3593ea16968 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -18,7 +18,7 @@
*/
import {
GlButton,
- GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+ GlSkeletonLoader,
GlTooltipDirective,
GlIcon,
GlSafeHtmlDirective as SafeHtml,
@@ -26,9 +26,9 @@ import {
import $ from 'jquery';
import { mapGetters, mapActions, mapState } from 'vuex';
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
+import '~/behaviors/markdown/render_gfm';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
-import initMRPopovers from '~/mr_popover/';
import noteHeader from '~/notes/components/note_header.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { spriteIcon } from '~/lib/utils/common_utils';
@@ -46,7 +46,7 @@ export default {
noteHeader,
TimelineEntryItem,
GlButton,
- GlSkeletonLoading,
+ GlSkeletonLoader,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -94,7 +94,7 @@ export default {
},
},
mounted() {
- initMRPopovers(this.$el.querySelectorAll('.gfm-merge_request'));
+ $(this.$refs['gfm-content']).renderGFM();
},
methods: {
...mapActions(['fetchDescriptionVersion', 'softDeleteDescriptionVersion']),
@@ -130,7 +130,7 @@ export default {
<div class="timeline-content">
<div class="note-header">
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
- <span v-safe-html="actionTextHtml"></span>
+ <span ref="gfm-content" v-safe-html="actionTextHtml"></span>
<template
v-if="canSeeDescriptionVersion || note.outdated_line_change_path"
#extra-controls
@@ -172,7 +172,7 @@ export default {
</div>
<div v-if="shouldShowDescriptionVersion" class="description-version pt-2">
<pre v-if="isLoadingDescriptionVersion" class="loading-state">
- <gl-skeleton-loading />
+ <gl-skeleton-loader />
</pre>
<pre v-else v-safe-html="descriptionVersion" class="wrapper mt-2"></pre>
<gl-button
@@ -218,7 +218,9 @@ export default {
</tr>
</table>
</div>
- <gl-skeleton-loading v-else-if="showLines" class="gl-mt-4" />
+ <div v-else-if="showLines" class="mt-4">
+ <gl-skeleton-loader />
+ </div>
</div>
</div>
</timeline-entry-item>
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 f21092af501..67ad7769c7c 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
@@ -130,16 +130,19 @@ export default {
<span data-testid="legend-text">{{ legendText }}</span>
</template>
</gl-infinite-scroll>
- <div v-if="showNoResultsMessage" class="text-muted ml-2 js-no-results-message">
+ <div v-if="showNoResultsMessage" class="gl-text-gray-600 gl-ml-3 js-no-results-message">
{{ __('Sorry, no projects matched your search') }}
</div>
<div
v-if="showMinimumSearchQueryMessage"
- class="text-muted ml-2 js-minimum-search-query-message"
+ class="gl-text-gray-600 gl-ml-3 js-minimum-search-query-message"
>
{{ __('Enter at least three characters to search') }}
</div>
- <div v-if="showSearchErrorMessage" class="text-danger ml-2 js-search-error-message">
+ <div
+ v-if="showSearchErrorMessage"
+ class="gl-text-red-500 gl-font-weight-bold gl-ml-3 js-search-error-message"
+ >
{{ __('Something went wrong, unable to search projects') }}
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
index da68fe961a6..1948a6778f4 100644
--- a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
@@ -12,7 +12,7 @@ export default {
GlFilteredSearch,
},
props: {
- filter: {
+ filters: {
type: Array,
required: true,
},
@@ -33,7 +33,7 @@ export default {
computed: {
internalFilter: {
get() {
- return this.filter;
+ return this.filters;
},
set(value) {
this.$emit('filter:changed', value);
@@ -71,7 +71,7 @@ export default {
const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ORDER;
const newQueryString = this.generateQueryData({
sorting: { ...this.sorting, sort },
- filter: this.filter,
+ filter: this.filters,
});
this.$emit('sorting:changed', { sort });
this.$emit('query:changed', newQueryString);
@@ -79,7 +79,7 @@ export default {
onSortItemClick(item) {
const newQueryString = this.generateQueryData({
sorting: { ...this.sorting, orderBy: item },
- filter: this.filter,
+ filter: this.filters,
});
this.$emit('sorting:changed', { orderBy: item });
this.$emit('query:changed', newQueryString);
@@ -87,7 +87,7 @@ export default {
submitSearch() {
const newQueryString = this.generateQueryData({
sorting: this.sorting,
- filter: this.filter,
+ filter: this.filters,
});
this.$emit('filter:submit');
this.$emit('query:changed', newQueryString);
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
index 34845e3d9e4..c97e191b630 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
@@ -1,7 +1,5 @@
import { s__ } from '~/locale';
-export const PLATFORMS_WITHOUT_ARCHITECTURES = ['docker', 'kubernetes'];
-
export const REGISTRATION_TOKEN_PLACEHOLDER = '$REGISTRATION_TOKEN';
export const INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES = {
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue
index 5d144c0d699..06852f511bf 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue
@@ -9,35 +9,19 @@ export default {
RunnerInstructionsModal,
},
directives: {
- GlModalDirective,
+ GlModal: GlModalDirective,
},
modalId: 'runner-instructions-modal',
i18n: {
buttonText: s__('Runners|Show runner installation instructions'),
},
- data() {
- return {
- opened: false,
- };
- },
- methods: {
- onClick() {
- // lazily mount modal to prevent premature instructions requests
- this.opened = true;
- },
- },
};
</script>
<template>
<div>
- <gl-button
- v-gl-modal-directive="$options.modalId"
- class="gl-mt-4"
- data-testid="show-modal-button"
- @click="onClick"
- >
+ <gl-button v-gl-modal="$options.modalId" class="gl-mt-4" data-testid="show-modal-button">
{{ $options.i18n.buttonText }}
</gl-button>
- <runner-instructions-modal v-if="opened" :modal-id="$options.modalId" />
+ <runner-instructions-modal :modal-id="$options.modalId" />
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
index 9eaaf7d1c18..bfaf3b92c34 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
@@ -17,7 +17,6 @@ import { __, s__ } from '~/locale';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import {
INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES,
- PLATFORMS_WITHOUT_ARCHITECTURES,
REGISTRATION_TOKEN_PLACEHOLDER,
} from './constants';
import getRunnerPlatformsQuery from './graphql/queries/get_runner_platforms.query.graphql';
@@ -59,21 +58,25 @@ export default {
apollo: {
platforms: {
query: getRunnerPlatformsQuery,
+ skip() {
+ // Only load instructions once the modal is shown
+ return !this.shown;
+ },
update(data) {
- return data?.runnerPlatforms?.nodes.map(({ name, humanReadableName, architectures }) => {
- return {
- name,
- humanReadableName,
- architectures: architectures?.nodes || [],
- };
- });
+ return (
+ data?.runnerPlatforms?.nodes.map(({ name, humanReadableName, architectures }) => {
+ return {
+ name,
+ humanReadableName,
+ architectures: architectures?.nodes || [],
+ };
+ }) ?? []
+ );
},
result() {
- if (this.platforms.length) {
- // If it is set and available, select the defaultSelectedPlatform.
- // Otherwise, select the first available platform
- this.selectPlatform(this.defaultPlatform() || this.platforms[0]);
- }
+ // If it is set and available, select the defaultSelectedPlatform.
+ // Otherwise, select the first available platform
+ this.selectPlatform(this.defaultPlatformName || this.platforms?.[0].name);
},
error() {
this.toggleAlert(true);
@@ -82,12 +85,12 @@ export default {
instructions: {
query: getRunnerSetupInstructionsQuery,
skip() {
- return !this.selectedPlatform;
+ return !this.shown || !this.selectedPlatform;
},
variables() {
return {
- platform: this.selectedPlatformName,
- architecture: this.selectedArchitectureName || '',
+ platform: this.selectedPlatform,
+ architecture: this.selectedArchitecture || '',
};
},
update(data) {
@@ -100,6 +103,7 @@ export default {
},
data() {
return {
+ shown: false,
platforms: [],
selectedPlatform: null,
selectedArchitecture: null,
@@ -109,55 +113,63 @@ export default {
};
},
computed: {
- platformsEmpty() {
- return isEmpty(this.platforms);
- },
instructionsEmpty() {
return isEmpty(this.instructions);
},
- selectedPlatformName() {
- return this.selectedPlatform?.name;
- },
- selectedArchitectureName() {
- return this.selectedArchitecture?.name;
+ architectures() {
+ return this.platforms.find(({ name }) => name === this.selectedPlatform)?.architectures || [];
},
- hasArchitecureList() {
- return !PLATFORMS_WITHOUT_ARCHITECTURES.includes(this.selectedPlatformName);
+ binaryUrl() {
+ return this.architectures.find(({ name }) => name === this.selectedArchitecture)
+ ?.downloadLocation;
},
instructionsWithoutArchitecture() {
- return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatformName]?.instructions;
+ return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform]?.instructions;
},
runnerInstallationLink() {
- return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatformName]?.link;
+ return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform]?.link;
},
registerInstructionsWithToken() {
const { registerInstructions } = this.instructions || {};
if (this.registrationToken) {
- return registerInstructions.replace(REGISTRATION_TOKEN_PLACEHOLDER, this.registrationToken);
+ return registerInstructions?.replace(
+ REGISTRATION_TOKEN_PLACEHOLDER,
+ this.registrationToken,
+ );
}
-
return registerInstructions;
},
},
+ updated() {
+ // Refocus on dom changes, after loading data
+ this.refocusSelectedPlatformButton();
+ },
methods: {
show() {
this.$refs.modal.show();
},
- focusSelected() {
- // By default the first platform always gets the focus, but when the `defaultPlatformName`
- // property is present, any other platform might actually be selected.
- this.$refs[this.selectedPlatformName]?.[0].$el.focus();
+ onShown() {
+ this.shown = true;
+ this.refocusSelectedPlatformButton();
},
- defaultPlatform() {
- return this.platforms.find((platform) => platform.name === this.defaultPlatformName);
+ refocusSelectedPlatformButton() {
+ // On modal opening, the first focusable element is auto-focused by bootstrap-vue
+ // This can be confusing for users, because the wrong platform button can
+ // get focused when setting a `defaultPlatformName`.
+ // This method refocuses the expected button.
+ // See more about this auto-focus: https://bootstrap-vue.org/docs/components/modal#auto-focus-on-open
+ this.$refs[this.selectedPlatform]?.[0].$el.focus();
},
- selectPlatform(platform) {
- this.selectedPlatform = platform;
+ selectPlatform(platformName) {
+ this.selectedPlatform = platformName;
- if (!platform.architectures?.some(({ name }) => name === this.selectedArchitectureName)) {
- // Select first architecture when current value is not available
- this.selectArchitecture(platform.architectures[0]);
+ // Update architecture when platform changes
+ const arch = this.architectures.find(({ name }) => name === this.selectedArchitecture);
+ if (arch) {
+ this.selectArchitecture(arch.name);
+ } else {
+ this.selectArchitecture(this.architectures[0]?.name);
}
},
selectArchitecture(architecture) {
@@ -175,6 +187,7 @@ export default {
},
},
i18n: {
+ environment: __('Environment'),
installARunner: s__('Runners|Install a runner'),
architecture: s__('Runners|Architecture'),
downloadInstallBinary: s__('Runners|Download and install binary'),
@@ -182,6 +195,7 @@ export default {
registerRunnerCommand: s__('Runners|Command to register runner'),
fetchError: s__('Runners|An error has occurred fetching instructions'),
copyInstructions: s__('Runners|Copy instructions'),
+ viewInstallationInstructions: s__('Runners|View installation instructions'),
},
closeButton: {
text: __('Close'),
@@ -197,17 +211,17 @@ export default {
:action-secondary="$options.closeButton"
v-bind="$attrs"
v-on="$listeners"
- @shown="focusSelected"
+ @shown="onShown"
>
<gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)">
{{ $options.i18n.fetchError }}
</gl-alert>
- <gl-skeleton-loader v-if="platformsEmpty && $apollo.loading" />
+ <gl-skeleton-loader v-if="!platforms.length && $apollo.loading" />
- <template v-if="!platformsEmpty">
+ <template v-if="platforms.length">
<h5>
- {{ __('Environment') }}
+ {{ $options.i18n.environment }}
</h5>
<div v-gl-resize-observer="onPlatformsButtonResize">
<gl-button-group
@@ -220,29 +234,29 @@ export default {
v-for="platform in platforms"
:key="platform.name"
:ref="platform.name"
- :selected="selectedPlatform && selectedPlatform.name === platform.name"
- @click="selectPlatform(platform)"
+ :selected="selectedPlatform === platform.name"
+ @click="selectPlatform(platform.name)"
>
{{ platform.humanReadableName }}
</gl-button>
</gl-button-group>
</div>
</template>
- <template v-if="hasArchitecureList">
+ <template v-if="architectures.length">
<template v-if="selectedPlatform">
<h5>
{{ $options.i18n.architecture }}
<gl-loading-icon v-if="$apollo.loading" size="sm" inline />
</h5>
- <gl-dropdown class="gl-mb-3" :text="selectedArchitectureName">
+ <gl-dropdown class="gl-mb-3" :text="selectedArchitecture">
<gl-dropdown-item
- v-for="architecture in selectedPlatform.architectures"
+ v-for="architecture in architectures"
:key="architecture.name"
:is-check-item="true"
- :is-checked="selectedArchitectureName === architecture.name"
+ :is-checked="selectedArchitecture === architecture.name"
data-testid="architecture-dropdown-item"
- @click="selectArchitecture(architecture)"
+ @click="selectArchitecture(architecture.name)"
>
{{ architecture.name }}
</gl-dropdown-item>
@@ -250,8 +264,9 @@ export default {
<div class="gl-sm-display-flex gl-align-items-center gl-mb-3">
<h5>{{ $options.i18n.downloadInstallBinary }}</h5>
<gl-button
+ v-if="binaryUrl"
class="gl-ml-auto"
- :href="selectedArchitecture.downloadLocation"
+ :href="binaryUrl"
download
icon="download"
data-testid="binary-download-button"
@@ -298,7 +313,7 @@ export default {
<p>{{ instructionsWithoutArchitecture }}</p>
<gl-button :href="runnerInstallationLink">
<gl-icon name="external-link" />
- {{ s__('Runners|View installation instructions') }}
+ {{ $options.i18n.viewInstallationInstructions }}
</gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
index 60111210f5d..9388ef4ba45 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
@@ -2,6 +2,9 @@
import { GlButton, GlIcon } from '@gitlab/ui';
import { mapActions, mapGetters } from 'vuex';
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget` instead.
export default {
components: {
GlButton,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
index 399db978b60..1064cbc26e3 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
@@ -4,6 +4,9 @@ import { mapGetters, mapState } from 'vuex';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue` instead.
export default {
components: {
DropdownContentsLabelsView,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
index 2cccb8325f4..3ff3755de46 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
@@ -2,6 +2,9 @@
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue` instead.
export default {
components: {
GlButton,
@@ -51,10 +54,10 @@ export default {
<template>
<div class="labels-select-contents-create js-labels-create">
- <div class="dropdown-title d-flex align-items-center pt-0 pb-2">
+ <div class="dropdown-title d-flex align-items-center pt-0 pb-2 gl-mb-0">
<gl-button
:aria-label="__('Go back')"
- variant="link"
+ category="tertiary"
size="small"
class="js-btn-back dropdown-header-button p-0"
icon="arrow-left"
@@ -63,7 +66,7 @@ export default {
<span class="flex-grow-1">{{ labelsCreateTitle }}</span>
<gl-button
:aria-label="__('Close')"
- variant="link"
+ category="tertiary"
size="small"
class="dropdown-header-button p-0"
icon="close"
@@ -95,7 +98,7 @@ export default {
></span>
<gl-form-input
v-model.trim="selectedColor"
- class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
+ class="gl-rounded-top-left-none gl-rounded-bottom-left-none gl-mb-2"
:placeholder="__('Use custom color #FF0000')"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
index 134575b7a27..e235bfde394 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
@@ -13,6 +13,9 @@ import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/
import LabelItem from './label_item.vue';
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue` instead.
export default {
components: {
GlIntersectionObserver,
@@ -166,7 +169,7 @@ export default {
<span class="flex-grow-1">{{ labelsListTitle }}</span>
<gl-button
:aria-label="__('Close')"
- variant="link"
+ category="tertiary"
size="small"
class="dropdown-header-button gl-p-0!"
icon="close"
@@ -193,6 +196,7 @@ export default {
:key="label.id"
:label="label"
:is-label-set="label.set"
+ :is-label-indeterminate="label.indeterminate"
:highlight="index === currentHighlightItem"
@clickLabel="handleLabelClick(label)"
/>
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 e91a0489ef1..e4325492334 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
@@ -2,6 +2,9 @@
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue` instead.
export default {
components: {
GlButton,
@@ -23,7 +26,9 @@ export default {
</script>
<template>
- <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-font-weight-bold">
+ <div
+ class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-font-weight-bold gl-mb-0"
+ >
{{ __('Labels') }}
<template v-if="allowLabelEdit">
<gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline />
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
index 35ac9ef8565..e59d150dd43 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
@@ -5,6 +5,9 @@ import { mapState } from 'vuex';
import { isScopedLabel } from '~/lib/utils/common_utils';
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue` instead.
export default {
components: {
GlLabel,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue
index 8a26c4a6618..5966c78aa51 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue
@@ -2,6 +2,9 @@
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget` instead.
export default {
directives: {
GlTooltip: GlTooltipDirective,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
index dd40add6376..154e3013acd 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
@@ -1,6 +1,10 @@
<script>
import { GlLink, GlIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue` instead.
export default {
functional: true,
props: {
@@ -12,6 +16,11 @@ export default {
type: Boolean,
required: true,
},
+ isLabelIndeterminate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
highlight: {
type: Boolean,
required: false,
@@ -19,7 +28,7 @@ export default {
},
},
render(h, { props, listeners }) {
- const { label, highlight, isLabelSet } = props;
+ const { label, highlight, isLabelSet, isLabelIndeterminate } = props;
const labelColorBox = h('span', {
class: 'dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3',
@@ -33,18 +42,36 @@ export default {
const checkedIcon = h(GlIcon, {
class: {
- 'gl-mr-3 gl-flex-shrink-0': true,
+ 'gl-mr-3 gl-flex-shrink-0 has-tooltip': true,
hidden: !isLabelSet,
},
+ attrs: {
+ title: __('Selected for all items.'),
+ 'data-testid': 'checked-icon',
+ },
props: {
name: 'mobile-issue-close',
},
});
+ const indeterminateIcon = h(GlIcon, {
+ class: {
+ 'gl-mr-3 gl-flex-shrink-0 has-tooltip': true,
+ hidden: !isLabelIndeterminate,
+ },
+ attrs: {
+ title: __('Selected for some items.'),
+ 'data-testid': 'indeterminate-icon',
+ },
+ props: {
+ name: 'dash',
+ },
+ });
+
const noIcon = h('span', {
class: {
'gl-mr-5 gl-pr-3': true,
- hidden: isLabelSet,
+ hidden: isLabelSet || isLabelIndeterminate,
},
attrs: {
'data-testid': 'no-icon',
@@ -63,7 +90,7 @@ export default {
},
},
},
- [noIcon, checkedIcon, labelColorBox, labelTitle],
+ [noIcon, checkedIcon, indeterminateIcon, labelColorBox, labelTitle],
);
return h(
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 7e259cb8b96..b61996cdcdb 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
@@ -15,6 +15,9 @@ import labelsSelectModule from './store';
Vue.use(Vuex);
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue` instead.
export default {
store: new Vuex.Store(labelsSelectModule()),
components: {
@@ -198,11 +201,12 @@ export default {
!state.showDropdownButton &&
!state.showDropdownContents
) {
- let filterFn = (label) => label.touched;
- if (this.isDropdownVariantEmbedded) {
- filterFn = (label) => label.set;
- }
- this.handleDropdownClose(state.labels.filter(filterFn));
+ const filterTouchedLabelsFn = (label) => label.touched;
+ const filterSetLabelsFn = (label) => label.set;
+ const labels = this.isDropdownVariantEmbedded
+ ? state.labels.filter(filterSetLabelsFn)
+ : state.labels.filter(filterTouchedLabelsFn);
+ this.handleDropdownClose(labels, state.labels.filter(filterTouchedLabelsFn));
}
},
/**
@@ -265,11 +269,11 @@ export default {
isInDropdownContents
);
},
- handleDropdownClose(labels) {
- // Only emit label updates if there are any labels to update
- // on UI.
+ handleDropdownClose(labels, touchedLabels) {
+ // Only emit label updates if there are any
+ // labels to update on UI.
if (labels.length) this.$emit('updateSelectedLabels', labels);
- this.$emit('onDropdownClose');
+ this.$emit('onDropdownClose', touchedLabels);
},
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
index d14f96720b7..ef3eedd9bb2 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
@@ -8,9 +8,10 @@ import { DropdownVariant } from '../constants';
* @param {object} state
*/
export const dropdownButtonText = (state, getters) => {
- const selectedLabels = getters.isDropdownVariantSidebar
- ? state.labels.filter((label) => label.set)
- : state.selectedLabels;
+ const selectedLabels =
+ getters.isDropdownVariantSidebar || getters.isDropdownVariantEmbedded
+ ? state.labels.filter((label) => label.set || label.indeterminate)
+ : state.selectedLabels;
if (!selectedLabels.length) {
return state.dropdownButtonText || __('Label');
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
index 9e64f03fe84..43b23994cdf 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
@@ -2,8 +2,39 @@ import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils';
import { DropdownVariant } from '../constants';
import * as types from './mutation_types';
+const transformLabels = (labels, selectedLabels) =>
+ labels.map((label) => {
+ const selectedLabel = selectedLabels.find(({ id }) => id === label.id);
+
+ return {
+ ...label,
+ set: Boolean(selectedLabel?.set),
+ indeterminate: Boolean(selectedLabel?.indeterminate),
+ };
+ });
+
export default {
[types.SET_INITIAL_STATE](state, props) {
+ // We need to ensure that selectedLabels have
+ // `set` & `indeterminate` properties defined.
+ if (props.selectedLabels?.length) {
+ props.selectedLabels.forEach((label) => {
+ /* eslint-disable no-param-reassign */
+ if (label.set === undefined && label.indeterminate === undefined) {
+ label.set = true;
+ label.indeterminate = false;
+ } else if (label.set === undefined && label.indeterminate !== undefined) {
+ label.set = false;
+ } else if (label.set !== undefined && label.indeterminate === undefined) {
+ label.indeterminate = false;
+ } else {
+ label.set = false;
+ label.indeterminate = false;
+ }
+ /* eslint-enable no-param-reassign */
+ });
+ }
+
Object.assign(state, { ...props });
},
@@ -36,10 +67,7 @@ export default {
// selectedLabels array.
state.labelsFetchInProgress = false;
state.labelsFetched = true;
- state.labels = labels.map((label) => ({
- ...label,
- set: state.selectedLabels.some((selectedLabel) => selectedLabel.id === label.id),
- }));
+ state.labels = transformLabels(labels, state.selectedLabels);
},
[types.RECEIVE_SET_LABELS_FAILURE](state) {
state.labelsFetchInProgress = false;
@@ -62,7 +90,8 @@ export default {
const candidateLabel = state.labels.find((label) => labelId === label.id);
if (candidateLabel) {
candidateLabel.touched = true;
- candidateLabel.set = !candidateLabel.set;
+ candidateLabel.set = candidateLabel.indeterminate ? true : !candidateLabel.set;
+ candidateLabel.indeterminate = false;
}
if (isScopedLabel(candidateLabel)) {
@@ -80,9 +109,6 @@ export default {
},
[types.UPDATE_LABELS_SET_STATE](state) {
- state.labels = state.labels.map((label) => ({
- ...label,
- set: state.selectedLabels.some((selectedLabel) => selectedLabel.id === label.id),
- }));
+ state.labels = transformLabels(state.labels, state.selectedLabels);
},
};
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 0fa64a29b3a..5471cda0cc5 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
@@ -169,6 +169,9 @@ export default {
setFocus() {
this.$refs.header.focusInput();
},
+ hideDropdown() {
+ this.$refs.dropdown.hide();
+ },
showDropdown() {
this.$refs.dropdown.show();
},
@@ -205,7 +208,7 @@ export default {
:show-dropdown-contents-create-view="showDropdownContentsCreateView"
:is-standalone="isStandalone"
@toggleDropdownContentsCreateView="toggleDropdownContent"
- @closeDropdown="$emit('closeDropdown')"
+ @closeDropdown="hideDropdown"
@input="debouncedSearchKeyUpdate"
@searchEnter="selectFirstItem"
/>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
index 090bf9493bf..5f344ae4214 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
@@ -140,18 +140,19 @@ export default {
<template>
<div class="labels-select-contents-create js-labels-create">
<div class="dropdown-input">
- <gl-alert v-if="error" variant="danger" :dismissible="false" class="gl-mb-3">
+ <gl-alert v-if="error" variant="danger" :dismissible="false" class="gl-mt-3">
{{ error }}
</gl-alert>
<gl-form-input
v-model.trim="labelTitle"
+ class="gl-mt-3"
:placeholder="__('Name new label')"
:autofocus="true"
data-testid="label-title-input"
/>
</div>
<div class="dropdown-content gl-px-3">
- <div class="suggest-colors suggest-colors-dropdown gl-mt-0! gl-mb-3!">
+ <div class="suggest-colors suggest-colors-dropdown gl-mt-0! gl-mb-3! gl-mb-0">
<gl-link
v-for="(color, index) in suggestedColors"
:key="index"
@@ -169,7 +170,7 @@ export default {
></span>
<gl-form-input
v-model.trim="selectedColor"
- class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
+ class="gl-rounded-top-left-none gl-rounded-bottom-left-none gl-mb-2"
:placeholder="__('Use custom color #FF0000')"
data-testid="selected-color-text"
/>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
index faad69732dd..aaddab43e2a 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
@@ -51,7 +51,7 @@ export default {
<div data-testid="dropdown-header">
<div
v-if="!isStandalone"
- class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
+ class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3! gl-mb-0"
data-testid="dropdown-header-title"
>
<gl-button
@@ -72,6 +72,7 @@ export default {
class="dropdown-header-button gl-p-0!"
icon="close"
data-testid="close-button"
+ data-qa-selector="close_labels_dropdown_button"
@click="$emit('closeDropdown')"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
index 57ee816c4c7..57e3ee4aaa5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
@@ -92,7 +92,9 @@ export default {
@click="handleCollapsedClick"
>
<gl-icon name="labels" />
- <span class="gl-font-base gl-line-height-24">{{ selectedLabels.length }}</span>
+ <span class="collapse-truncated-title gl-pt-2 gl-px-3 gl-font-sm">{{
+ selectedLabels.length
+ }}</span>
</div>
<span
v-if="!selectedLabels.length"
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
index c30ca5369ee..7b62f0cdb7d 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
@@ -72,7 +72,7 @@ export default {
</div>
<pre
- class="gl-p-0! gl-w-full gl-overflow-visible! gl-ml-11! gl-border-none! code highlight"
+ class="gl-p-0! gl-w-full gl-overflow-visible! gl-ml-11! gl-border-none! code highlight gl-line-height-normal"
:class="firstLineClass"
><code><span :id="`LC${number}`" v-safe-html="formattedContent" :lang="language" class="line" data-testid="content"></span></code></pre>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
index bed6dd4d5c6..0d78530d878 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
@@ -134,3 +134,7 @@ export const BIDI_CHARS_CLASS_LIST = 'unicode-bidi has-tooltip';
export const BIDI_CHAR_TOOLTIP = __(
'Potentially unwanted character detected: Unicode BiDi Control',
);
+
+export const HLJS_COMMENT_SELECTOR = 'hljs-comment';
+
+export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight';
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
new file mode 100644
index 00000000000..c9f7e5508be
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
@@ -0,0 +1,13 @@
+import { HLJS_ON_AFTER_HIGHLIGHT } from '../constants';
+import wrapComments from './wrap_comments';
+
+/**
+ * Registers our plugins for Highlight.js
+ *
+ * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst
+ *
+ * @param {Object} hljs - the Highlight.js instance.
+ */
+export const registerPlugins = (hljs) => {
+ hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments });
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js
new file mode 100644
index 00000000000..5be92af5b55
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js
@@ -0,0 +1,39 @@
+import { HLJS_COMMENT_SELECTOR } from '../constants';
+
+const createWrapper = (content) => {
+ const span = document.createElement('span');
+ span.className = HLJS_COMMENT_SELECTOR;
+ span.innerHTML = content;
+ return span.outerHTML;
+};
+
+/**
+ * Highlight.js plugin for wrapping multi-line comments in the `hljs-comment` class.
+ * This ensures that multi-line comments are rendered correctly in the GitLab UI.
+ *
+ * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst
+ *
+ * @param {Object} Result - an object that represents the highlighted result from Highlight.js
+ */
+export default (result) => {
+ if (!result.value.includes(HLJS_COMMENT_SELECTOR)) return;
+
+ let wrapComment = false;
+
+ // eslint-disable-next-line no-param-reassign
+ result.value = result.value // Highlight.js expects the result param to be mutated for plugins to work
+ .split('\n')
+ .map((lineContent) => {
+ const includesClosingTag = lineContent.includes('</span>');
+ if (lineContent.includes(HLJS_COMMENT_SELECTOR) && !includesClosingTag) {
+ wrapComment = true;
+ return lineContent;
+ }
+ const line = wrapComment ? createWrapper(lineContent) : lineContent;
+ if (includesClosingTag) {
+ wrapComment = false;
+ }
+ return line;
+ })
+ .join('\n');
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
index ed87a202b15..f819a9e5be2 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
@@ -5,6 +5,7 @@ import eventHub from '~/notes/event_hub';
import languageLoader from '~/content_editor/services/highlight_js_language_loader';
import { ROUGE_TO_HLJS_LANGUAGE_MAP, LINES_PER_CHUNK } from './constants';
import Chunk from './components/chunk.vue';
+import { registerPlugins } from './plugins/index';
/*
* This component is optimized to handle source code with many lines of code by splitting source code into chunks of 70 lines of code,
@@ -111,6 +112,7 @@ export default {
let detectedLanguage = language;
let highlightedContent;
if (this.hljs) {
+ registerPlugins(this.hljs);
if (!detectedLanguage) {
const hljsHighlightAuto = this.hljs.highlightAuto(content);
highlightedContent = hljsHighlightAuto.value;
diff --git a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
index f62bf686f85..424cab20c7e 100644
--- a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
+++ b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
@@ -149,7 +149,7 @@ export default {
>
<slot>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4"
type="button"
@click="openFileUpload"
>
@@ -192,7 +192,7 @@ export default {
<transition name="upload-dropzone-fade">
<div
v-show="dragging && !enableDragBehavior"
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
>
<div v-show="!isDragDataValid" class="mw-50 gl-text-center">
<slot name="invalid-drag-data-slot">
diff --git a/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue b/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue
index bc5e0cf10dd..20a666509a4 100644
--- a/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue
+++ b/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue
@@ -34,7 +34,7 @@ export default {
<div class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1">
<div
v-if="$slots['left-primary-text']"
- class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0 gl-mb-4"
+ class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0"
>
<slot name="left-primary-text"></slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/constants.js b/app/assets/javascripts/vue_shared/components/user_popover/constants.js
new file mode 100644
index 00000000000..1d49aefd297
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/user_popover/constants.js
@@ -0,0 +1 @@
+export const USER_POPOVER_DELAY = 200;
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index ec7a7cd72ae..768cd005727 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -14,12 +14,14 @@ import { glEmojiTag } from '~/emoji';
import createFlash from '~/flash';
import { followUser, unfollowUser } from '~/rest_api';
import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
+import { USER_POPOVER_DELAY } from './constants';
const MAX_SKELETON_LINES = 4;
export default {
name: 'UserPopover',
maxSkeletonLines: MAX_SKELETON_LINES,
+ USER_POPOVER_DELAY,
components: {
GlIcon,
GlLink,
@@ -48,6 +50,11 @@ export default {
required: false,
default: 'top',
},
+ show: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -133,25 +140,21 @@ export default {
</script>
<template>
- <!-- 200ms delay so not every mouseover triggers Popover -->
- <gl-popover :target="target" :delay="200" :placement="placement" boundary="viewport">
- <div class="gl-p-3 gl-line-height-normal gl-display-flex" data-testid="user-popover">
- <div
- class="gl-p-2 flex-shrink-1 gl-display-flex gl-flex-direction-column align-items-center gl-w-70p"
- >
+ <!-- Delayed so not every mouseover triggers Popover -->
+ <gl-popover
+ :css-classes="['gl-max-w-48']"
+ :show="show"
+ :target="target"
+ :delay="$options.USER_POPOVER_DELAY"
+ :placement="placement"
+ boundary="viewport"
+ triggers="hover focus manual"
+ >
+ <div class="gl-py-3 gl-line-height-normal gl-display-flex" data-testid="user-popover">
+ <div class="gl-mr-4 gl-flex-shrink-0">
<user-avatar-image :img-src="user.avatarUrl" :size="64" css-classes="gl-m-0!" />
- <div v-if="shouldRenderToggleFollowButton" class="gl-mt-3">
- <gl-button
- :variant="toggleFollowButtonVariant"
- :loading="toggleFollowLoading"
- size="small"
- data-testid="toggle-follow-button"
- @click="toggleFollow"
- >{{ toggleFollowButtonText }}</gl-button
- >
- </div>
</div>
- <div class="gl-w-full gl-min-w-0 gl-word-break-word">
+ <div class="gl-w-full gl-word-break-word gl-display-flex gl-align-items-center">
<template v-if="userIsLoading">
<gl-skeleton-loader
:lines="$options.maxSkeletonLines"
@@ -161,7 +164,7 @@ export default {
/>
</template>
<template v-else>
- <div class="gl-mb-3">
+ <div>
<h5 class="gl-m-0">
<user-name-with-status
:name="user.name"
@@ -170,42 +173,64 @@ export default {
/>
</h5>
<span class="gl-text-gray-500">@{{ user.username }}</span>
- </div>
- <div class="gl-text-gray-500">
- <div v-if="user.bio" class="gl-display-flex gl-mb-2">
- <gl-icon name="profile" class="gl-flex-shrink-0" />
- <span ref="bio" class="gl-ml-2">{{ user.bio }}</span>
+ <div v-if="shouldRenderToggleFollowButton" class="gl-mt-3">
+ <gl-button
+ :variant="toggleFollowButtonVariant"
+ :loading="toggleFollowLoading"
+ size="small"
+ data-testid="toggle-follow-button"
+ @click="toggleFollow"
+ >{{ toggleFollowButtonText }}</gl-button
+ >
</div>
- <div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
- <gl-icon name="work" class="gl-flex-shrink-0" />
- <span ref="workInformation" class="gl-ml-2">{{ user.workInformation }}</span>
- </div>
- <div v-if="user.location" class="gl-display-flex gl-mb-2">
- <gl-icon name="location" class="gl-flex-shrink-0" />
- <span class="gl-ml-2">{{ user.location }}</span>
- </div>
- <div
- v-if="user.localTime && !user.bot"
- class="gl-display-flex gl-mb-2"
- data-testid="user-popover-local-time"
- >
- <gl-icon name="clock" class="gl-flex-shrink-0" />
- <span class="gl-ml-2">{{ user.localTime }}</span>
- </div>
- </div>
- <div v-if="statusHtml" class="gl-mb-2" data-testid="user-popover-status">
- <span v-safe-html:[$options.safeHtmlConfig]="statusHtml"></span>
- </div>
- <div v-if="user.bot && user.websiteUrl" class="gl-text-blue-500">
- <gl-icon name="question" />
- <gl-link data-testid="user-popover-bot-docs-link" :href="user.websiteUrl">
- <gl-sprintf :message="__('Learn more about %{username}')">
- <template #username>{{ user.name }}</template>
- </gl-sprintf>
- </gl-link>
</div>
</template>
</div>
</div>
+ <div class="gl-mt-2 gl-w-full gl-word-break-word">
+ <template v-if="userIsLoading">
+ <gl-skeleton-loader
+ :lines="$options.maxSkeletonLines"
+ preserve-aspect-ratio="none"
+ equal-width-lines
+ :height="24"
+ />
+ </template>
+ <template v-else>
+ <div class="gl-text-gray-500">
+ <div v-if="user.bio" class="gl-display-flex gl-mb-2">
+ <gl-icon name="profile" class="gl-flex-shrink-0" />
+ <span ref="bio" class="gl-ml-2">{{ user.bio }}</span>
+ </div>
+ <div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
+ <gl-icon name="work" class="gl-flex-shrink-0" />
+ <span ref="workInformation" class="gl-ml-2">{{ user.workInformation }}</span>
+ </div>
+ <div v-if="user.location" class="gl-display-flex gl-mb-2">
+ <gl-icon name="location" class="gl-flex-shrink-0" />
+ <span class="gl-ml-2">{{ user.location }}</span>
+ </div>
+ <div
+ v-if="user.localTime && !user.bot"
+ class="gl-display-flex gl-mb-2"
+ data-testid="user-popover-local-time"
+ >
+ <gl-icon name="clock" class="gl-flex-shrink-0" />
+ <span class="gl-ml-2">{{ user.localTime }}</span>
+ </div>
+ </div>
+ <div v-if="statusHtml" class="gl-mb-2" data-testid="user-popover-status">
+ <span v-safe-html:[$options.safeHtmlConfig]="statusHtml"></span>
+ </div>
+ <div v-if="user.bot && user.websiteUrl" class="gl-text-blue-500">
+ <gl-icon name="question" />
+ <gl-link data-testid="user-popover-bot-docs-link" :href="user.websiteUrl">
+ <gl-sprintf :message="__('Learn more about %{username}')">
+ <template #username>{{ user.name }}</template>
+ </gl-sprintf>
+ </gl-link>
+ </div>
+ </template>
+ </div>
</gl-popover>
</template>
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 15f84e48179..cac0d5a45c9 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -307,7 +307,7 @@ export default {
<actions-button
:actions="actions"
:selected-key="selection"
- :variant="isBlob ? 'info' : 'default'"
+ :variant="isBlob ? 'confirm' : 'default'"
:category="isBlob ? 'primary' : 'secondary'"
@select="select"
/>
diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js
index 3ebeec4a50b..14328b1f25f 100644
--- a/app/assets/javascripts/vue_shared/constants.js
+++ b/app/assets/javascripts/vue_shared/constants.js
@@ -71,10 +71,14 @@ export const AVATAR_SHAPE_OPTION_RECT = 'rect';
export const confidentialityInfoText = (workspaceType, issuableType) =>
sprintf(
__(
- 'Only %{workspaceType} members with at least Reporter role can view or be notified about this %{issuableType}.',
+ 'Only %{workspaceType} members with %{permissions} can view or be notified about this %{issuableType}.',
),
{
workspaceType: workspaceType === WorkspaceType.project ? __('project') : __('group'),
issuableType: issuableType === IssuableType.Issue ? __('issue') : __('epic'),
+ permissions:
+ issuableType === IssuableType.Issue
+ ? __('at least the Reporter role, the author, and assignees')
+ : __('at least the Reporter role'),
},
);
diff --git a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue
index f4cbaba9313..033bb8c3885 100644
--- a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue
@@ -29,7 +29,6 @@ export default {
<template>
<div class="issuable-create-container">
<slot name="title"></slot>
- <hr class="gl-mt-0" />
<issuable-form
:description-preview-path="descriptionPreviewPath"
:description-help-path="descriptionHelpPath"
diff --git a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
index 0758cb507e9..89eecea5239 100644
--- a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
@@ -51,9 +51,9 @@ export default {
<template>
<gl-form class="common-note-form gfm-form" @submit.stop.prevent>
- <div data-testid="issuable-title" class="form-group row">
- <label for="issuable-title" class="col-form-label col-sm-2">{{ __('Title') }}</label>
- <div class="col-sm-10">
+ <div data-testid="issuable-title" class="row">
+ <label for="issuable-title" class="col-12 gl-mb-0">{{ __('Title') }}</label>
+ <div class="col-12">
<gl-form-group :description="__('Maximum of 255 characters')">
<gl-form-input
id="issuable-title"
@@ -66,10 +66,8 @@ export default {
</div>
</div>
<div data-testid="issuable-description" class="form-group row">
- <label for="issuable-description" class="col-form-label col-sm-2">{{
- __('Description')
- }}</label>
- <div class="col-sm-10">
+ <label for="issuable-description" class="col-12">{{ __('Description') }}</label>
+ <div class="col-12">
<markdown-field
:markdown-preview-path="descriptionPreviewPath"
:markdown-docs-path="descriptionHelpPath"
@@ -91,37 +89,28 @@ export default {
</markdown-field>
</div>
</div>
- <div class="row">
- <div class="col-lg-6">
- <div data-testid="issuable-labels" class="form-group row">
- <label for="issuable-labels" class="col-form-label col-md-2 col-lg-4">{{
- __('Labels')
- }}</label>
- <div class="col-md-8 col-sm-10">
- <div class="issuable-form-select-holder">
- <labels-select
- :allow-label-edit="true"
- :allow-label-create="true"
- :allow-multiselect="true"
- :allow-scoped-labels="true"
- :labels-fetch-path="labelsFetchPath"
- :labels-manage-path="labelsManagePath"
- :selected-labels="selectedLabels"
- :labels-list-title="__('Select label')"
- :footer-create-label-title="__('Create project label')"
- :footer-manage-label-title="__('Manage project labels')"
- :variant="$options.LabelSelectVariant.Embedded"
- @updateSelectedLabels="handleUpdateSelectedLabels"
- />
- </div>
- </div>
+ <div data-testid="issuable-labels" class="form-group row">
+ <label for="issuable-labels" class="col-12">{{ __('Labels') }}</label>
+ <div class="col-12">
+ <div class="issuable-form-select-holder">
+ <labels-select
+ :allow-label-edit="true"
+ :allow-label-create="true"
+ :allow-multiselect="true"
+ :allow-scoped-labels="true"
+ :labels-fetch-path="labelsFetchPath"
+ :labels-manage-path="labelsManagePath"
+ :selected-labels="selectedLabels"
+ :labels-list-title="__('Select label')"
+ :footer-create-label-title="__('Create project label')"
+ :footer-manage-label-title="__('Manage project labels')"
+ :variant="$options.LabelSelectVariant.Embedded"
+ @updateSelectedLabels="handleUpdateSelectedLabels"
+ />
</div>
</div>
</div>
- <div
- data-testid="issuable-create-actions"
- class="footer-block row-content-block gl-display-flex"
- >
+ <div data-testid="issuable-create-actions" class="footer-block gl-display-flex gl-mt-6">
<slot
name="actions"
:issuable-title="issuableTitle"
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
index 6453290f6ea..a9f8caa3e1f 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
@@ -23,6 +23,11 @@ export default {
},
mixins: [timeagoMixin],
props: {
+ hasScopedLabelsFeature: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
issuableSymbol: {
type: String,
required: true,
@@ -132,7 +137,7 @@ export default {
return Boolean(this.$slots[slotName]);
},
scopedLabel(label) {
- return isScopedLabel(label);
+ return this.hasScopedLabelsFeature && isScopedLabel(label);
},
labelTitle(label) {
return label.title || label.name;
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
index 8b293b2e9f6..8fbf0bb10a0 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
@@ -1,10 +1,5 @@
<script>
-import {
- GlAlert,
- GlKeysetPagination,
- GlDeprecatedSkeletonLoading as GlSkeletonLoading,
- GlPagination,
-} from '@gitlab/ui';
+import { GlAlert, GlKeysetPagination, GlSkeletonLoader, GlPagination } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
@@ -27,7 +22,7 @@ export default {
components: {
GlAlert,
GlKeysetPagination,
- GlSkeletonLoading,
+ GlSkeletonLoader,
IssuableTabs,
FilteredSearchBar,
IssuableItem,
@@ -138,6 +133,11 @@ export default {
required: false,
default: 2,
},
+ hasScopedLabelsFeature: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
labelFilterParam: {
type: String,
required: false,
@@ -307,7 +307,7 @@ export default {
</issuable-bulk-edit-sidebar>
<ul v-if="issuablesLoading" class="content-list">
<li v-for="n in skeletonItemCount" :key="n" class="issue gl-px-5! gl-py-5!">
- <gl-skeleton-loading />
+ <gl-skeleton-loader />
</li>
</ul>
<template v-else>
@@ -325,6 +325,7 @@ export default {
:class="{ 'gl-cursor-grab': isManualOrdering }"
data-qa-selector="issuable_container"
:data-qa-issuable-title="issuable.title"
+ :has-scoped-labels-feature="hasScopedLabelsFeature"
:issuable-symbol="issuableSymbol"
:issuable="issuable"
:label-filter-param="labelFilterParam"
diff --git a/app/assets/javascripts/vue_shared/issuable/list/constants.js b/app/assets/javascripts/vue_shared/issuable/list/constants.js
index c6dce6a51c2..be9afc0610d 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/constants.js
+++ b/app/assets/javascripts/vue_shared/issuable/list/constants.js
@@ -46,6 +46,13 @@ export const AvailableSortOptions = [
},
];
+export const IssuableTypes = {
+ Issue: 'ISSUE',
+ Incident: 'INCIDENT',
+ TestCase: 'TEST_CASE',
+ Requirement: 'REQUIREMENT',
+};
+
export const DEFAULT_PAGE_SIZE = 20;
export const DEFAULT_SKELETON_COUNT = 5;
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
index 05dc1650379..5eb3da3c62e 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
@@ -22,10 +22,6 @@ export default {
type: Object,
required: true,
},
- statusBadgeClass: {
- type: String,
- required: true,
- },
statusIcon: {
type: String,
required: true,
@@ -162,7 +158,6 @@ export default {
<template v-else>
<issuable-title
:issuable="issuable"
- :status-badge-class="statusBadgeClass"
:status-icon="statusIcon"
:enable-edit="enableEdit"
@edit-issuable="$emit('edit-issuable', $event)"
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
index 649dbd6576b..f035795a045 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
@@ -40,11 +40,6 @@ export default {
required: false,
default: '',
},
- statusBadgeClass: {
- type: String,
- required: false,
- default: '',
- },
statusIcon: {
type: String,
required: false,
@@ -113,12 +108,7 @@ export default {
<template>
<div class="detail-page-header">
<div class="detail-page-header-body">
- <gl-badge
- data-testid="status"
- class="issuable-status-badge gl-mr-3"
- :class="statusBadgeClass"
- :variant="badgeVariant"
- >
+ <gl-badge class="issuable-status-badge gl-mr-3" :variant="badgeVariant">
<gl-icon v-if="statusIcon" :name="statusIcon" :class="statusIconClass" />
<span class="gl-display-none gl-sm-display-block"><slot name="status-badge"></slot></span>
</gl-badge>
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
index c165ee91c59..7ed93c042f8 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
@@ -17,11 +17,6 @@ export default {
type: Object,
required: true,
},
- statusBadgeClass: {
- type: String,
- required: false,
- default: '',
- },
statusIcon: {
type: String,
required: false,
@@ -108,7 +103,6 @@ export default {
<div class="issuable-show-container" data-qa-selector="issuable_show_container">
<issuable-header
:issuable-state="issuable.state"
- :status-badge-class="statusBadgeClass"
:status-icon="statusIcon"
:status-icon-class="statusIconClass"
:blocked="issuable.blocked"
@@ -127,7 +121,6 @@ export default {
<issuable-body
:issuable="issuable"
- :status-badge-class="statusBadgeClass"
:status-icon="statusIcon"
:status-icon-class="statusIconClass"
:enable-edit="enableEdit"
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
index 47f05a2cee2..3d7c71ce974 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
@@ -1,12 +1,14 @@
<script>
import {
GlIcon,
+ GlBadge,
GlButton,
GlIntersectionObserver,
GlTooltipDirective,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import { __ } from '~/locale';
+import { IssuableStates } from '~/vue_shared/issuable/list/constants';
export default {
i18n: {
@@ -14,6 +16,7 @@ export default {
},
components: {
GlIcon,
+ GlBadge,
GlButton,
GlIntersectionObserver,
},
@@ -26,10 +29,6 @@ export default {
type: Object,
required: true,
},
- statusBadgeClass: {
- type: String,
- required: true,
- },
statusIcon: {
type: String,
required: true,
@@ -44,6 +43,11 @@ export default {
stickyTitleVisible: false,
};
},
+ computed: {
+ badgeVariant() {
+ return this.issuable.state === IssuableStates.Opened ? 'success' : 'info';
+ },
+ },
methods: {
handleTitleAppear() {
this.stickyTitleVisible = false;
@@ -60,7 +64,7 @@ export default {
<div class="title-container">
<h1
v-safe-html="issuable.titleHtml || issuable.title"
- class="title qa-title"
+ class="title qa-title gl-font-size-h-display"
dir="auto"
data-testid="title"
></h1>
@@ -84,14 +88,12 @@ export default {
<div
class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-mx-auto gl-px-5"
>
- <p
- data-testid="status"
- class="issuable-status-box status-box gl-white-space-nowrap gl-my-0"
- :class="statusBadgeClass"
- >
- <gl-icon :name="statusIcon" class="gl-display-block d-sm-none gl-h-6!" />
- <span class="gl-display-none d-sm-block"><slot name="status-badge"></slot></span>
- </p>
+ <gl-badge class="gl-white-space-nowrap gl-mr-3" :variant="badgeVariant">
+ <gl-icon v-if="statusIcon" class="gl-sm-display-none" :name="statusIcon" />
+ <span class="gl-display-none gl-sm-display-block">
+ <slot name="status-badge"></slot>
+ </span>
+ </gl-badge>
<p
class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0"
:title="issuable.title"
diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
index 1f3cc663848..8e9b8ef3e6f 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
@@ -130,11 +130,7 @@ export default {
<slot name="extra-description"></slot>
</div>
<div class="col-lg-9">
- <gl-breadcrumb v-if="breadcrumbs" :items="breadcrumbs">
- <template #separator>
- <gl-icon name="chevron-right" :size="8" />
- </template>
- </gl-breadcrumb>
+ <gl-breadcrumb v-if="breadcrumbs" :items="breadcrumbs" />
<legacy-container :key="activePanel.name" :selector="activePanel.selector" />
</div>
</div>
diff --git a/app/assets/javascripts/whats_new/components/app.vue b/app/assets/javascripts/whats_new/components/app.vue
index b74dba686ad..0c55cc2f8a6 100644
--- a/app/assets/javascripts/whats_new/components/app.vue
+++ b/app/assets/javascripts/whats_new/components/app.vue
@@ -33,7 +33,7 @@ export default {
this.fetchFreshItems();
const body = document.querySelector('body');
- const namespaceId = body.getAttribute('data-namespace-id');
+ const { namespaceId } = body.dataset;
this.track('click_whats_new_drawer', { label: 'namespace_id', value: namespaceId });
},
diff --git a/app/assets/javascripts/whats_new/store/actions.js b/app/assets/javascripts/whats_new/store/actions.js
index f209f145884..d3da988e3ed 100644
--- a/app/assets/javascripts/whats_new/store/actions.js
+++ b/app/assets/javascripts/whats_new/store/actions.js
@@ -1,5 +1,6 @@
import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
+import { joinPaths } from '~/lib/utils/url_utility';
import { STORAGE_KEY } from '../utils/notification';
import * as types from './mutation_types';
@@ -23,7 +24,7 @@ export default {
const v = versionDigest;
return axios
- .get('/-/whats_new', {
+ .get(joinPaths('/', gon.relative_url_root || '', '/-/whats_new'), {
params: {
page,
v,
diff --git a/app/assets/javascripts/whats_new/utils/notification.js b/app/assets/javascripts/whats_new/utils/notification.js
index 66ee3b1a971..41aff202f48 100644
--- a/app/assets/javascripts/whats_new/utils/notification.js
+++ b/app/assets/javascripts/whats_new/utils/notification.js
@@ -1,6 +1,6 @@
export const STORAGE_KEY = 'display-whats-new-notification';
-export const getVersionDigest = (appEl) => appEl.getAttribute('data-version-digest');
+export const getVersionDigest = (appEl) => appEl.dataset.versionDigest;
export const setNotification = (appEl) => {
const versionDigest = getVersionDigest(appEl);
diff --git a/app/assets/javascripts/work_items/components/item_state.vue b/app/assets/javascripts/work_items/components/item_state.vue
index 0b6c1a75bb2..69670d3471c 100644
--- a/app/assets/javascripts/work_items/components/item_state.vue
+++ b/app/assets/javascripts/work_items/components/item_state.vue
@@ -49,14 +49,28 @@ export default {
</script>
<template>
- <gl-form-group :label="$options.i18n.status" :label-for="$options.labelId">
+ <gl-form-group
+ :label="$options.i18n.status"
+ :label-for="$options.labelId"
+ label-cols="3"
+ label-cols-lg="2"
+ label-class="gl-pb-0!"
+ class="gl-align-items-center"
+ >
<gl-form-select
:id="$options.labelId"
:value="state"
:options="$options.states"
:disabled="loading"
- class="gl-w-auto"
+ class="gl-w-auto hide-select-decoration"
@change="setState"
/>
</gl-form-group>
</template>
+
+<style>
+.hide-select-decoration:not(:focus, :hover) {
+ background-image: none;
+ box-shadow: none;
+}
+</style>
diff --git a/app/assets/javascripts/work_items/components/item_title.vue b/app/assets/javascripts/work_items/components/item_title.vue
index 232510b108d..ce2fa158596 100644
--- a/app/assets/javascripts/work_items/components/item_title.vue
+++ b/app/assets/javascripts/work_items/components/item_title.vue
@@ -40,18 +40,18 @@ export default {
<template>
<h2
- class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5 gl-display-inline-block"
+ class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5 gl-w-full"
:class="{ 'gl-cursor-not-allowed': disabled }"
aria-labelledby="item-title"
>
- <span
+ <div
id="item-title"
ref="titleEl"
role="textbox"
:aria-label="__('Title')"
:data-placeholder="placeholder"
:contenteditable="!disabled"
- class="gl-pseudo-placeholder"
+ class="gl-pseudo-placeholder gl-px-4 gl-py-3 gl-ml-n4 gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base"
@blur="handleBlur"
@keyup="handleInput"
@keydown.enter.exact="handleSubmit"
@@ -59,7 +59,8 @@ export default {
@keydown.meta.u.prevent
@keydown.ctrl.b.prevent
@keydown.meta.b.prevent
- >{{ title }}</span
>
+ {{ title }}
+ </div>
</h2>
</template>
diff --git a/app/assets/javascripts/work_items/components/update_work_item.js b/app/assets/javascripts/work_items/components/update_work_item.js
new file mode 100644
index 00000000000..fc395fa5be3
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/update_work_item.js
@@ -0,0 +1,23 @@
+import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
+import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
+
+export function getUpdateWorkItemMutation({ input, workItemParentId }) {
+ let mutation = updateWorkItemMutation;
+
+ const variables = {
+ input,
+ };
+
+ if (workItemParentId) {
+ mutation = updateWorkItemTaskMutation;
+ variables.input = {
+ id: workItemParentId,
+ taskData: input,
+ };
+ }
+
+ return {
+ mutation,
+ variables,
+ };
+}
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees.vue
new file mode 100644
index 00000000000..4d1c171772e
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -0,0 +1,111 @@
+<script>
+import { GlTokenSelector, GlIcon, GlAvatar, GlLink } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
+
+function isClosingIcon(el) {
+ return el?.classList.contains('gl-token-close');
+}
+
+export default {
+ components: {
+ GlTokenSelector,
+ GlIcon,
+ GlAvatar,
+ GlLink,
+ },
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ assignees: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isEditing: false,
+ localAssignees: this.assignees.map((assignee) => ({
+ ...assignee,
+ class: 'gl-bg-transparent!',
+ })),
+ };
+ },
+ computed: {
+ assigneeIds() {
+ return this.localAssignees.map((assignee) => assignee.id);
+ },
+ assigneeListEmpty() {
+ return this.assignees.length === 0;
+ },
+ containerClass() {
+ return !this.isEditing ? 'gl-shadow-none! gl-bg-transparent!' : '';
+ },
+ },
+ methods: {
+ getUserId(id) {
+ return getIdFromGraphQLId(id);
+ },
+ setAssignees(e) {
+ if (isClosingIcon(e.relatedTarget) || !this.isEditing) return;
+ this.isEditing = false;
+ this.$apollo.mutate({
+ mutation: localUpdateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ assigneeIds: this.assigneeIds,
+ },
+ },
+ });
+ },
+ async focusTokenSelector() {
+ this.isEditing = true;
+ await this.$nextTick();
+ this.$refs.tokenSelector.focusTextInput();
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-mb-4 work-item-assignees gl-relative">
+ <span class="gl-font-weight-bold gl-w-15 gl-pt-2" data-testid="assignees-title">{{
+ __('Assignee(s)')
+ }}</span>
+ <gl-token-selector
+ ref="tokenSelector"
+ v-model="localAssignees"
+ hide-dropdown-with-no-items
+ :container-class="containerClass"
+ class="gl-w-full gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base"
+ @token-remove="focusTokenSelector"
+ @focus="isEditing = true"
+ @blur="setAssignees"
+ >
+ <template #empty-placeholder>
+ <div
+ class="add-assignees gl-min-w-fit-content gl-display-flex gl-align-items-center gl-text-gray-300 gl-pr-4 gl-top-2"
+ data-testid="empty-state"
+ >
+ <gl-icon name="profile" />
+ <span class="gl-ml-2">{{ __('Add assignees') }}</span>
+ </div>
+ </template>
+ <template #token-content="{ token }">
+ <gl-link
+ :href="token.webUrl"
+ :title="token.name"
+ :data-user-id="getUserId(token.id)"
+ data-placement="top"
+ class="gl-text-decoration-none! gl-text-body! gl-display-flex gl-md-display-inline-flex! gl-align-items-center js-user-link"
+ >
+ <gl-avatar :size="24" :src="token.avatarUrl" />
+ <span class="gl-pl-2">{{ token.name }}</span>
+ </gl-link>
+ </template>
+ </gl-token-selector>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
new file mode 100644
index 00000000000..5a85fcdd7ac
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -0,0 +1,234 @@
+<script>
+import { GlButton, GlFormGroup, GlSafeHtmlDirective } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import { __, s__ } from '~/locale';
+import Tracking from '~/tracking';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import workItemQuery from '../graphql/work_item.query.graphql';
+import updateWorkItemWidgetsMutation from '../graphql/update_work_item_widgets.mutation.graphql';
+import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_DESCRIPTION } from '../constants';
+
+export default {
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
+ components: {
+ GlButton,
+ GlFormGroup,
+ MarkdownField,
+ },
+ mixins: [Tracking.mixin()],
+ inject: ['fullPath'],
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ },
+ markdownDocsPath: helpPagePath('user/markdown'),
+ data() {
+ return {
+ workItem: {},
+ isEditing: false,
+ isSubmitting: false,
+ isSubmittingWithKeydown: false,
+ desc: '',
+ };
+ },
+ apollo: {
+ workItem: {
+ query: workItemQuery,
+ variables() {
+ return {
+ id: this.workItemId,
+ };
+ },
+ skip() {
+ return !this.workItemId;
+ },
+ error() {
+ this.error = i18n.fetchError;
+ },
+ },
+ },
+ computed: {
+ autosaveKey() {
+ return this.workItemId;
+ },
+ canEdit() {
+ return this.workItem?.userPermissions?.updateWorkItem;
+ },
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_description',
+ property: `type_${this.workItemType}`,
+ };
+ },
+ descriptionHtml() {
+ return this.workItemDescription?.descriptionHtml;
+ },
+ descriptionText: {
+ get() {
+ return this.desc;
+ },
+ set(desc) {
+ this.desc = desc;
+ },
+ },
+ workItemDescription() {
+ return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESCRIPTION);
+ },
+ workItemType() {
+ return this.workItem?.workItemType?.name;
+ },
+ markdownPreviewPath() {
+ return `${gon.relative_url_root || ''}/${this.fullPath}/preview_markdown?target_type=${
+ this.workItemType
+ }`;
+ },
+ },
+ methods: {
+ async startEditing() {
+ this.isEditing = true;
+
+ this.desc = getDraft(this.autosaveKey) || this.workItemDescription?.description || '';
+
+ await this.$nextTick();
+
+ this.$refs.textarea.focus();
+ },
+ async cancelEditing() {
+ const isDirty = this.desc !== this.workItemDescription?.description;
+
+ if (isDirty) {
+ const msg = s__('WorkItem|Are you sure you want to cancel editing?');
+
+ const confirmed = await confirmAction(msg, {
+ primaryBtnText: __('Discard changes'),
+ cancelBtnText: __('Continue editing'),
+ });
+
+ if (!confirmed) {
+ return;
+ }
+ }
+
+ this.isEditing = false;
+ clearDraft(this.autosaveKey);
+ },
+ onInput() {
+ if (this.isSubmittingWithKeydown) {
+ return;
+ }
+
+ updateDraft(this.autosaveKey, this.desc);
+ },
+ async updateWorkItem(event) {
+ if (event.key) {
+ this.isSubmittingWithKeydown = true;
+ }
+
+ this.isSubmitting = true;
+
+ try {
+ this.track('updated_description');
+
+ const {
+ data: { workItemUpdateWidgets },
+ } = await this.$apollo.mutate({
+ mutation: updateWorkItemWidgetsMutation,
+ variables: {
+ input: {
+ id: this.workItem.id,
+ descriptionWidget: {
+ description: this.descriptionText,
+ },
+ },
+ },
+ });
+
+ if (workItemUpdateWidgets.errors?.length) {
+ throw new Error(workItemUpdateWidgets.errors[0]);
+ }
+
+ this.isEditing = false;
+ clearDraft(this.autosaveKey);
+ } catch (error) {
+ this.$emit('error', error.message);
+ Sentry.captureException(error);
+ }
+
+ this.isSubmitting = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group
+ v-if="isEditing"
+ class="gl-pt-5 gl-mb-5 gl-mt-0! gl-border-t! gl-border-b"
+ :label="__('Description')"
+ label-for="work-item-description"
+ label-class="gl-float-left"
+ >
+ <div class="gl-display-flex gl-justify-content-flex-end">
+ <gl-button class="gl-ml-auto" data-testid="cancel" @click="cancelEditing">{{
+ __('Cancel')
+ }}</gl-button>
+ <gl-button
+ class="js-no-auto-disable gl-ml-4"
+ category="primary"
+ variant="confirm"
+ :loading="isSubmitting"
+ data-testid="save-description"
+ @click="updateWorkItem"
+ >{{ __('Save') }}</gl-button
+ >
+ </div>
+ <markdown-field
+ can-attach-file
+ :textarea-value="descriptionText"
+ :is-submitting="isSubmitting"
+ :markdown-preview-path="markdownPreviewPath"
+ :markdown-docs-path="$options.markdownDocsPath"
+ class="gl-p-3 bordered-box"
+ >
+ <template #textarea>
+ <textarea
+ id="work-item-description"
+ ref="textarea"
+ v-model="descriptionText"
+ :disabled="isSubmitting"
+ class="note-textarea js-gfm-input js-autosize markdown-area"
+ dir="auto"
+ data-supports-quick-actions="false"
+ :aria-label="__('Description')"
+ :placeholder="__('Write a comment or drag your files here…')"
+ @keydown.meta.enter="updateWorkItem"
+ @keydown.ctrl.enter="updateWorkItem"
+ @keydown.exact.esc.stop="cancelEditing"
+ @input="onInput"
+ ></textarea>
+ </template>
+ </markdown-field>
+ </gl-form-group>
+ <div v-else class="gl-pt-5 gl-mb-5 gl-border-t gl-border-b">
+ <div class="gl-display-flex">
+ <h3 class="gl-font-base gl-mt-0">{{ __('Description') }}</h3>
+ <gl-button
+ v-if="canEdit"
+ class="gl-ml-auto"
+ icon="pencil"
+ data-testid="edit-description"
+ @click="startEditing"
+ >{{ __('Edit') }}</gl-button
+ >
+ </div>
+ <div v-safe-html="descriptionHtml" class="md gl-mb-5"></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 4222ffe42fe..5272df2d53f 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -1,27 +1,45 @@
<script>
import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
-import { i18n } from '../constants';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import {
+ i18n,
+ WIDGET_TYPE_ASSIGNEE,
+ WIDGET_TYPE_DESCRIPTION,
+ WIDGET_TYPE_WEIGHT,
+} from '../constants';
import workItemQuery from '../graphql/work_item.query.graphql';
import workItemTitleSubscription from '../graphql/work_item_title.subscription.graphql';
import WorkItemActions from './work_item_actions.vue';
import WorkItemState from './work_item_state.vue';
import WorkItemTitle from './work_item_title.vue';
+import WorkItemDescription from './work_item_description.vue';
+import WorkItemAssignees from './work_item_assignees.vue';
+import WorkItemWeight from './work_item_weight.vue';
export default {
i18n,
components: {
GlAlert,
GlSkeletonLoader,
+ WorkItemAssignees,
WorkItemActions,
+ WorkItemDescription,
WorkItemTitle,
WorkItemState,
+ WorkItemWeight,
},
+ mixins: [glFeatureFlagMixin()],
props: {
workItemId: {
type: String,
required: false,
default: null,
},
+ workItemParentId: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -66,6 +84,18 @@ export default {
canDelete() {
return this.workItem?.userPermissions?.deleteWorkItem;
},
+ workItemsMvc2Enabled() {
+ return this.glFeatures.workItemsMvc2;
+ },
+ hasDescriptionWidget() {
+ return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESCRIPTION);
+ },
+ workItemAssignees() {
+ return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_ASSIGNEE);
+ },
+ workItemWeight() {
+ return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_WEIGHT);
+ },
},
};
</script>
@@ -83,27 +113,40 @@ export default {
</gl-skeleton-loader>
</div>
<template v-else>
- <div class="gl-display-flex">
+ <div class="gl-display-flex gl-align-items-start">
<work-item-title
:work-item-id="workItem.id"
:work-item-title="workItem.title"
:work-item-type="workItemType"
+ :work-item-parent-id="workItemParentId"
class="gl-mr-5"
@error="error = $event"
- @updated="$emit('workItemUpdated')"
/>
<work-item-actions
:work-item-id="workItem.id"
:can-delete="canDelete"
- class="gl-ml-auto gl-mt-5"
+ class="gl-ml-auto gl-mt-6"
@deleteWorkItem="$emit('deleteWorkItem')"
@error="error = $event"
/>
</div>
+ <template v-if="workItemsMvc2Enabled">
+ <work-item-assignees
+ v-if="workItemAssignees"
+ :work-item-id="workItem.id"
+ :assignees="workItemAssignees.nodes"
+ />
+ <work-item-weight v-if="workItemWeight" :weight="workItemWeight.weight" />
+ </template>
<work-item-state
:work-item="workItem"
+ :work-item-parent-id="workItemParentId"
+ @error="error = $event"
+ />
+ <work-item-description
+ v-if="hasDescriptionWidget"
+ :work-item-id="workItem.id"
@error="error = $event"
- @updated="$emit('workItemUpdated')"
/>
</template>
</section>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
index 172a40a6e56..d1c8022ac57 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
@@ -37,7 +37,7 @@ export default {
default: null,
},
},
- emits: ['workItemDeleted', 'workItemUpdated', 'close'],
+ emits: ['workItemDeleted', 'close'],
data() {
return {
error: undefined,
@@ -98,15 +98,24 @@ export default {
</script>
<template>
- <gl-modal ref="modal" hide-footer size="lg" modal-id="work-item-detail-modal" @hide="closeModal">
+ <gl-modal
+ ref="modal"
+ hide-footer
+ size="lg"
+ modal-id="work-item-detail-modal"
+ header-class="gl-p-0 gl-pb-2!"
+ body-class="gl-pb-6!"
+ @hide="closeModal"
+ >
<gl-alert v-if="error" variant="danger" @dismiss="error = false">
{{ error }}
</gl-alert>
<work-item-detail
+ :work-item-parent-id="issueGid"
:work-item-id="workItemId"
+ class="gl-p-5 gl-mt-n3"
@deleteWorkItem="deleteWorkItem"
- @workItemUpdated="$emit('workItemUpdated')"
/>
</gl-modal>
</template>
@@ -114,7 +123,7 @@ export default {
<style>
/* hide the existing modal header
*/
-#work-item-detail-modal .modal-header {
+#work-item-detail-modal .modal-header * {
display: none;
}
</style>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/index.js b/app/assets/javascripts/work_items/components/work_item_links/index.js
new file mode 100644
index 00000000000..320a4a213e3
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_links/index.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import WorkItemLinks from './work_item_links.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export default function initWorkItemLinks() {
+ if (!window.gon.features.workItemsHierarchy) {
+ return;
+ }
+
+ const workItemLinksRoot = document.querySelector('.js-work-item-links-root');
+
+ if (!workItemLinksRoot) {
+ return;
+ }
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: workItemLinksRoot,
+ name: 'WorkItemLinksRoot',
+ apolloProvider,
+ components: {
+ workItemLinks: WorkItemLinks,
+ },
+ render: (createElement) =>
+ createElement('work-item-links', {
+ props: {
+ issuableId: parseInt(workItemLinksRoot.dataset.issuableId, 10),
+ },
+ }),
+ });
+}
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
new file mode 100644
index 00000000000..bdfff100333
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
@@ -0,0 +1,165 @@
+<script>
+import { GlButton, GlBadge, GlIcon, GlLoadingIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
+import {
+ STATE_OPEN,
+ WIDGET_ICONS,
+ WORK_ITEM_STATUS_TEXT,
+ WIDGET_TYPE_HIERARCHY,
+} from '../../constants';
+import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql';
+import WorkItemLinksForm from './work_item_links_form.vue';
+
+export default {
+ components: {
+ GlButton,
+ GlBadge,
+ GlIcon,
+ GlLoadingIcon,
+ WorkItemLinksForm,
+ },
+ props: {
+ workItemId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ issuableId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ apollo: {
+ children: {
+ query: getWorkItemLinksQuery,
+ variables() {
+ return {
+ id: this.issuableGid,
+ };
+ },
+ update(data) {
+ return (
+ data.workItem.widgets.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)?.children
+ .nodes ?? []
+ );
+ },
+ skip() {
+ return !this.issuableId;
+ },
+ },
+ },
+ data() {
+ return {
+ isShownAddForm: false,
+ isOpen: true,
+ children: [],
+ };
+ },
+ computed: {
+ // Only used for children for now but should be extended later to support parents and siblings
+ isChildrenEmpty() {
+ return this.children?.length === 0;
+ },
+ toggleIcon() {
+ return this.isOpen ? 'chevron-lg-up' : 'chevron-lg-down';
+ },
+ toggleLabel() {
+ return this.isOpen
+ ? s__('WorkItem|Collapse child items')
+ : s__('WorkItem|Expand child items');
+ },
+ issuableGid() {
+ return this.issuableId ? convertToGraphQLId(TYPE_WORK_ITEM, this.issuableId) : null;
+ },
+ isLoading() {
+ return this.$apollo.queries.children.loading;
+ },
+ },
+ methods: {
+ badgeVariant(state) {
+ return state === STATE_OPEN ? 'success' : 'info';
+ },
+ toggle() {
+ this.isOpen = !this.isOpen;
+ },
+ toggleAddForm() {
+ this.isShownAddForm = !this.isShownAddForm;
+ },
+ },
+ i18n: {
+ title: s__('WorkItem|Child items'),
+ emptyStateMessage: s__(
+ 'WorkItem|No child items are currently assigned. Use child items to prioritize tasks that your team should complete in order to accomplish your goals!',
+ ),
+ addChildButtonLabel: s__('WorkItem|Add a child'),
+ },
+ WIDGET_TYPE_TASK_ICON: WIDGET_ICONS.TASK,
+ WORK_ITEM_STATUS_TEXT,
+};
+</script>
+
+<template>
+ <div class="gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-bg-gray-10">
+ <div
+ class="gl-p-4 gl-display-flex gl-justify-content-space-between"
+ :class="{ 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100': isOpen }"
+ >
+ <h5 class="gl-m-0 gl-line-height-32">{{ $options.i18n.title }}</h5>
+ <div class="gl-border-l-1 gl-border-l-solid gl-border-l-gray-50 gl-pl-4">
+ <gl-button
+ category="tertiary"
+ :icon="toggleIcon"
+ :aria-label="toggleLabel"
+ data-testid="toggle-links"
+ @click="toggle"
+ />
+ </div>
+ </div>
+ <div
+ v-if="isOpen"
+ class="gl-bg-gray-10 gl-p-4 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
+ data-testid="links-body"
+ >
+ <gl-loading-icon v-if="isLoading" color="dark" class="gl-my-3" />
+
+ <template v-else>
+ <div v-if="isChildrenEmpty" class="gl-px-8" data-testid="links-empty">
+ <p>
+ {{ $options.i18n.emptyStateMessage }}
+ </p>
+ <gl-button
+ v-if="!isShownAddForm"
+ category="secondary"
+ variant="confirm"
+ data-testid="toggle-add-form"
+ @click="toggleAddForm"
+ >
+ {{ $options.i18n.addChildButtonLabel }}
+ </gl-button>
+ <work-item-links-form v-else data-testid="add-links-form" @cancel="toggleAddForm" />
+ </div>
+ <div
+ v-for="child in children"
+ :key="child.id"
+ class="gl-relative gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base"
+ data-testid="links-child"
+ >
+ <div>
+ <gl-icon :name="$options.WIDGET_TYPE_TASK_ICON" class="gl-mr-3 gl-text-gray-700" />
+ <span class="gl-word-break-all">{{ child.title }}</span>
+ </div>
+ <div class="gl-ml-0 gl-sm-ml-auto! gl-mt-3 gl-sm-mt-0">
+ <gl-badge :variant="badgeVariant(child.state)">
+ <span class="gl-sm-display-block">{{
+ $options.WORK_ITEM_STATUS_TEXT[child.state]
+ }}</span>
+ </gl-badge>
+ </div>
+ </div>
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
new file mode 100644
index 00000000000..22728f58026
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
@@ -0,0 +1,28 @@
+<script>
+import { GlForm, GlFormInput, GlButton } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlForm,
+ GlFormInput,
+ GlButton,
+ },
+ data() {
+ return {
+ relatedWorkItem: '',
+ };
+ },
+};
+</script>
+
+<template>
+ <gl-form @submit.prevent>
+ <gl-form-input v-model="relatedWorkItem" class="gl-mb-4" />
+ <gl-button type="submit" category="secondary" variant="confirm">
+ {{ s__('WorkItem|Add') }}
+ </gl-button>
+ <gl-button category="tertiary" class="gl-float-right" @click="$emit('cancel')">
+ {{ s__('WorkItem|Cancel') }}
+ </gl-button>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_state.vue b/app/assets/javascripts/work_items/components/work_item_state.vue
index 51db4c804eb..87f4a8822b1 100644
--- a/app/assets/javascripts/work_items/components/work_item_state.vue
+++ b/app/assets/javascripts/work_items/components/work_item_state.vue
@@ -7,8 +7,9 @@ import {
STATE_CLOSED,
STATE_EVENT_CLOSE,
STATE_EVENT_REOPEN,
+ TRACKING_CATEGORY_SHOW,
} from '../constants';
-import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
+import { getUpdateWorkItemMutation } from './update_work_item';
import ItemState from './item_state.vue';
export default {
@@ -21,6 +22,11 @@ export default {
type: Object,
required: true,
},
+ workItemParentId: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -33,14 +39,14 @@ export default {
},
tracking() {
return {
- category: 'workItems:show',
+ category: TRACKING_CATEGORY_SHOW,
label: 'item_state',
property: `type_${this.workItemType}`,
};
},
},
methods: {
- async updateWorkItemState(newState) {
+ updateWorkItemState(newState) {
const stateEventMap = {
[STATE_OPEN]: STATE_EVENT_REOPEN,
[STATE_CLOSED]: STATE_EVENT_CLOSE,
@@ -48,35 +54,39 @@ export default {
const stateEvent = stateEventMap[newState];
- await this.updateWorkItem(stateEvent);
+ this.updateWorkItem(stateEvent);
},
+
async updateWorkItem(updatedState) {
if (!updatedState) {
return;
}
+ const input = {
+ id: this.workItem.id,
+ stateEvent: updatedState,
+ };
+
this.updateInProgress = true;
try {
this.track('updated_state');
- const {
- data: { workItemUpdate },
- } = await this.$apollo.mutate({
- mutation: updateWorkItemMutation,
- variables: {
- input: {
- id: this.workItem.id,
- stateEvent: updatedState,
- },
- },
+ const { mutation, variables } = getUpdateWorkItemMutation({
+ workItemParentId: this.workItemParentId,
+ input,
});
- if (workItemUpdate?.errors?.length) {
- throw new Error(workItemUpdate.errors[0]);
- }
+ const { data } = await this.$apollo.mutate({
+ mutation,
+ variables,
+ });
- this.$emit('updated');
+ const errors = data.workItemUpdate?.errors;
+
+ if (errors?.length) {
+ throw new Error(errors[0]);
+ }
} catch (error) {
this.$emit('error', i18n.updateError);
Sentry.captureException(error);
diff --git a/app/assets/javascripts/work_items/components/work_item_title.vue b/app/assets/javascripts/work_items/components/work_item_title.vue
index d2e6d3c0bbf..b4c13037038 100644
--- a/app/assets/javascripts/work_items/components/work_item_title.vue
+++ b/app/assets/javascripts/work_items/components/work_item_title.vue
@@ -1,7 +1,8 @@
<script>
+import * as Sentry from '@sentry/browser';
import Tracking from '~/tracking';
-import { i18n } from '../constants';
-import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
+import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
+import { getUpdateWorkItemMutation } from './update_work_item';
import ItemTitle from './item_title.vue';
export default {
@@ -25,11 +26,16 @@ export default {
required: false,
default: '',
},
+ workItemParentId: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
tracking() {
return {
- category: 'workItems:show',
+ category: TRACKING_CATEGORY_SHOW,
label: 'item_title',
property: `type_${this.workItemType}`,
};
@@ -41,21 +47,37 @@ export default {
return;
}
+ const input = {
+ id: this.workItemId,
+ title: updatedTitle,
+ };
+
+ this.updateInProgress = true;
+
try {
- await this.$apollo.mutate({
- mutation: updateWorkItemMutation,
- variables: {
- input: {
- id: this.workItemId,
- title: updatedTitle,
- },
- },
- });
this.track('updated_title');
- this.$emit('updated');
- } catch {
+
+ const { mutation, variables } = getUpdateWorkItemMutation({
+ workItemParentId: this.workItemParentId,
+ input,
+ });
+
+ const { data } = await this.$apollo.mutate({
+ mutation,
+ variables,
+ });
+
+ const errors = data.workItemUpdate?.errors;
+
+ if (errors?.length) {
+ throw new Error(errors[0]);
+ }
+ } catch (error) {
this.$emit('error', i18n.updateError);
+ Sentry.captureException(error);
}
+
+ this.updateInProgress = false;
},
},
};
diff --git a/app/assets/javascripts/work_items/components/work_item_weight.vue b/app/assets/javascripts/work_items/components/work_item_weight.vue
new file mode 100644
index 00000000000..b0f2b3aa14a
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_weight.vue
@@ -0,0 +1,26 @@
+<script>
+import { __ } from '~/locale';
+
+export default {
+ inject: ['hasIssueWeightsFeature'],
+ props: {
+ weight: {
+ type: Number,
+ required: false,
+ default: undefined,
+ },
+ },
+ computed: {
+ weightText() {
+ return this.weight ?? __('None');
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="hasIssueWeightsFeature" class="gl-mb-5">
+ <span class="gl-display-inline-block gl-font-weight-bold gl-w-15">{{ __('Weight') }}</span>
+ {{ weightText }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index e914500108f..2df4978a319 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -6,9 +6,27 @@ export const STATE_CLOSED = 'CLOSED';
export const STATE_EVENT_REOPEN = 'REOPEN';
export const STATE_EVENT_CLOSE = 'CLOSE';
+export const TRACKING_CATEGORY_SHOW = 'workItems:show';
+
export const i18n = {
fetchError: s__('WorkItem|Something went wrong when fetching the work item. Please try again.'),
updateError: s__('WorkItem|Something went wrong while updating the work item. Please try again.'),
};
export const DEFAULT_MODAL_TYPE = 'Task';
+
+export const WIDGET_TYPE_ASSIGNEE = 'ASSIGNEES';
+export const WIDGET_TYPE_DESCRIPTION = 'DESCRIPTION';
+export const WIDGET_TYPE_WEIGHT = 'WEIGHT';
+export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY';
+
+export const WIDGET_TYPE_TASK_ICON = 'task-done';
+
+export const WIDGET_ICONS = {
+ TASK: 'task-done',
+};
+
+export const WORK_ITEM_STATUS_TEXT = {
+ CLOSED: s__('WorkItem|Closed'),
+ OPEN: s__('WorkItem|Open'),
+};
diff --git a/app/assets/javascripts/work_items/graphql/local_update_work_item.mutation.graphql b/app/assets/javascripts/work_items/graphql/local_update_work_item.mutation.graphql
new file mode 100644
index 00000000000..0d31ecef6f8
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/local_update_work_item.mutation.graphql
@@ -0,0 +1,9 @@
+#import "./work_item.fragment.graphql"
+
+mutation localUpdateWorkItem($input: LocalWorkItemAssigneesInput) {
+ localUpdateWorkItem(input: $input) @client {
+ workItem {
+ ...WorkItem
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/provider.js b/app/assets/javascripts/work_items/graphql/provider.js
index 3c2955ce1e2..09d929faae2 100644
--- a/app/assets/javascripts/work_items/graphql/provider.js
+++ b/app/assets/javascripts/work_items/graphql/provider.js
@@ -1,11 +1,93 @@
+import produce from 'immer';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import { WIDGET_TYPE_ASSIGNEE } from '../constants';
+import typeDefs from './typedefs.graphql';
+import workItemQuery from './work_item.query.graphql';
+
+export const temporaryConfig = {
+ typeDefs,
+ cacheConfig: {
+ possibleTypes: {
+ LocalWorkItemWidget: ['LocalWorkItemAssignees'],
+ },
+ typePolicies: {
+ WorkItem: {
+ fields: {
+ mockWidgets: {
+ read(widgets) {
+ return (
+ widgets || [
+ {
+ __typename: 'LocalWorkItemAssignees',
+ type: 'ASSIGNEES',
+ nodes: [
+ {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/1',
+ avatarUrl: '',
+ webUrl: '',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ name: 'John Doe',
+ username: 'doe_I',
+ },
+ {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/2',
+ avatarUrl: '',
+ webUrl: '',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ name: 'Marcus Rutherford',
+ username: 'ruthfull',
+ },
+ ],
+ },
+ {
+ __typename: 'LocalWorkItemWeight',
+ type: 'WEIGHT',
+ weight: 0,
+ },
+ ]
+ );
+ },
+ },
+ },
+ },
+ },
+ },
+};
+
+export const resolvers = {
+ Mutation: {
+ localUpdateWorkItem(_, { input }, { cache }) {
+ const sourceData = cache.readQuery({
+ query: workItemQuery,
+ variables: { id: input.id },
+ });
+
+ const data = produce(sourceData, (draftData) => {
+ const assigneesWidget = draftData.workItem.mockWidgets.find(
+ (widget) => widget.type === WIDGET_TYPE_ASSIGNEE,
+ );
+ assigneesWidget.nodes = assigneesWidget.nodes.filter((assignee) =>
+ input.assigneeIds.includes(assignee.id),
+ );
+ });
+
+ cache.writeQuery({
+ query: workItemQuery,
+ variables: { id: input.id },
+ data,
+ });
+ },
+ },
+};
export function createApolloProvider() {
Vue.use(VueApollo);
- const defaultClient = createDefaultClient();
+ const defaultClient = createDefaultClient(resolvers, temporaryConfig);
return new VueApollo({
defaultClient,
diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql
new file mode 100644
index 00000000000..bfe2f0fe0ce
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql
@@ -0,0 +1,36 @@
+enum LocalWidgetType {
+ ASSIGNEES
+ WEIGHT
+}
+
+interface LocalWorkItemWidget {
+ type: LocalWidgetType!
+}
+
+type LocalWorkItemAssignees implements LocalWorkItemWidget {
+ type: LocalWidgetType!
+ nodes: [UserCore]
+}
+
+type LocalWorkItemWeight implements LocalWorkItemWidget {
+ type: LocalWidgetType!
+ weight: Int
+}
+
+extend type WorkItem {
+ mockWidgets: [LocalWorkItemWidget]
+}
+
+type LocalWorkItemAssigneesInput {
+ id: WorkItemID!
+ assigneeIds: [ID!]
+}
+
+type LocalWorkItemPayload {
+ workItem: WorkItem!
+ errors: [String!]
+}
+
+extend type Mutation {
+ localUpdateWorkItem(input: LocalWorkItemAssigneesInput!): LocalWorkItemPayload
+}
diff --git a/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql b/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql
new file mode 100644
index 00000000000..470de060ee3
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql
@@ -0,0 +1,8 @@
+mutation workItemUpdateTask($input: WorkItemUpdateTaskInput!) {
+ workItemUpdate: workItemUpdateTask(input: $input) {
+ workItem {
+ id
+ descriptionHtml
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/update_work_item_widgets.mutation.graphql b/app/assets/javascripts/work_items/graphql/update_work_item_widgets.mutation.graphql
new file mode 100644
index 00000000000..148b340b439
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/update_work_item_widgets.mutation.graphql
@@ -0,0 +1,10 @@
+#import "./work_item.fragment.graphql"
+
+mutation workItemUpdateWidgets($input: WorkItemUpdateWidgetsInput!) {
+ workItemUpdateWidgets(input: $input) {
+ workItem {
+ ...WorkItem
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
index e25fd102699..04701f6899e 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -11,4 +11,11 @@ fragment WorkItem on WorkItem {
deleteWorkItem
updateWorkItem
}
+ widgets {
+ ... on WorkItemWidgetDescription {
+ type
+ description
+ descriptionHtml
+ }
+ }
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item.query.graphql b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
index 3b46fed97ec..30bc61f5c59 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
@@ -3,5 +3,21 @@
query workItem($id: WorkItemID!) {
workItem(id: $id) {
...WorkItem
+ mockWidgets @client {
+ ... on LocalWorkItemAssignees {
+ type
+ nodes {
+ id
+ avatarUrl
+ name
+ username
+ webUrl
+ }
+ }
+ ... on LocalWorkItemWeight {
+ type
+ weight
+ }
+ }
}
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql
new file mode 100644
index 00000000000..c2496f53cc8
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql
@@ -0,0 +1,28 @@
+query workItemQuery($id: WorkItemID!) {
+ workItem(id: $id) {
+ id
+ workItemType {
+ id
+ }
+ title
+ widgets {
+ type
+ ... on WorkItemWidgetHierarchy {
+ type
+ parent {
+ id
+ }
+ children {
+ nodes {
+ id
+ workItemType {
+ id
+ }
+ title
+ state
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index e39b0d6a353..33e28831b54 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -1,11 +1,12 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import App from './components/app.vue';
import { createRouter } from './router';
import { createApolloProvider } from './graphql/provider';
export const initWorkItemsRoot = () => {
const el = document.querySelector('#js-work-items');
- const { fullPath, issuesListPath } = el.dataset;
+ const { fullPath, hasIssueWeightsFeature, issuesListPath } = el.dataset;
return new Vue({
el,
@@ -13,6 +14,7 @@ export const initWorkItemsRoot = () => {
apolloProvider: createApolloProvider(),
provide: {
fullPath,
+ hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
issuesListPath,
},
render(createElement) {
diff --git a/app/assets/javascripts/work_items/pages/work_item_root.vue b/app/assets/javascripts/work_items/pages/work_item_root.vue
index 6dc3dc3b3c9..e9840889bdb 100644
--- a/app/assets/javascripts/work_items/pages/work_item_root.vue
+++ b/app/assets/javascripts/work_items/pages/work_item_root.vue
@@ -4,6 +4,7 @@ import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
+import ZenMode from '~/zen_mode';
import WorkItemDetail from '../components/work_item_detail.vue';
import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql';
@@ -29,6 +30,9 @@ export default {
return convertToGraphQLId(TYPE_WORK_ITEM, this.id);
},
},
+ mounted() {
+ this.ZenMode = new ZenMode();
+ },
methods: {
deleteWorkItem() {
this.$apollo
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index 24549a170bd..092cf643e0f 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -17,7 +17,6 @@
@import './pages/note_form';
@import './pages/notes';
@import './pages/notifications';
-@import './pages/pages';
@import './pages/pipelines';
@import './pages/profile';
@import './pages/profiles/preferences';
diff --git a/app/assets/stylesheets/components/avatar.scss b/app/assets/stylesheets/components/avatar.scss
index 3885134e276..ceac5da7f80 100644
--- a/app/assets/stylesheets/components/avatar.scss
+++ b/app/assets/stylesheets/components/avatar.scss
@@ -113,10 +113,6 @@ $avatar-sizes: (
border-radius: 0;
border: 0;
}
-
- &.avatar-placeholder {
- border: 0;
- }
}
.identicon {
diff --git a/app/assets/stylesheets/components/feature_highlight.scss b/app/assets/stylesheets/components/feature_highlight.scss
index 54123e74675..4d301cc5617 100644
--- a/app/assets/stylesheets/components/feature_highlight.scss
+++ b/app/assets/stylesheets/components/feature_highlight.scss
@@ -1,29 +1,3 @@
-.gl-badge.feature-highlight-badge {
- background-color: $purple-light;
- color: $purple;
-
- &,
- &.sm {
- padding: 0.25rem;
- }
-}
-
-.gl-order-1 {
- order: 1;
-}
-
-.gl-sm-order-init {
- @media (min-width: $breakpoint-sm) {
- order: initial;
- }
-}
-
-.gl-xs-ml-3 {
- @media (max-width: $breakpoint-sm) {
- @include gl-ml-3;
- }
-}
-
.gl-sm-mr-3 {
@media (min-width: $breakpoint-sm) {
@include gl-mr-3;
diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss
index 7c668666d70..3bb889b6ba0 100644
--- a/app/assets/stylesheets/components/related_items_list.scss
+++ b/app/assets/stylesheets/components/related_items_list.scss
@@ -80,11 +80,6 @@ $item-remove-button-space: 42px;
.health-label-short {
max-width: 0;
}
-
- .bullet-separator {
- font-size: 9px;
- color: $gray-200;
- }
}
.card-header {
@@ -198,7 +193,6 @@ $item-remove-button-space: 42px;
}
}
-.mr-status-wrapper,
.mr-ci-status {
line-height: 0;
}
diff --git a/app/assets/stylesheets/components/upload_dropzone/upload_dropzone.scss b/app/assets/stylesheets/components/upload_dropzone/upload_dropzone.scss
index 2bc6eba3342..f6be241d644 100644
--- a/app/assets/stylesheets/components/upload_dropzone/upload_dropzone.scss
+++ b/app/assets/stylesheets/components/upload_dropzone/upload_dropzone.scss
@@ -1,25 +1,50 @@
+@function encodecolor($string) {
+ @if type-of($string) == 'color' {
+ $hex: str-slice(ie-hex-str($string), 4);
+ $string: unquote('#{$hex}');
+ }
+ $string: '%23' + $string;
+ @return $string;
+}
+
+@mixin dropzone-background($stroke-color, $stroke-width: 4, $stroke-linecap: 'butt') {
+ background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='#{encodecolor($stroke-color)}' stroke-width='#{$stroke-width}' stroke-dasharray='6%2c4' stroke-dashoffset='0' stroke-linecap='#{encodecolor($stroke-linecap)}'/%3e%3c/svg%3e");
+}
+
.upload-dropzone-border {
- border: 2px dashed $gray-100;
+ border: 0;
+ @include dropzone-background($gray-400, 2, 'round');
+ border-radius: 8px;
}
.upload-dropzone-card {
- transition: border $gl-transition-duration-medium $general-hover-transition-curve;
+ transition: background $gl-transition-duration-medium $general-hover-transition-curve,
+ border $gl-transition-duration-medium $general-hover-transition-curve;
color: $gl-text-color;
+ &:hover,
&:focus,
+ &:focus-within,
&:active {
outline: none;
- border: 2px dashed $purple;
+ @include dropzone-background($blue-500);
color: $gl-text-color;
}
+ &:focus,
+ &:focus-within,
+ &:active {
+ @include gl-focus;
+ }
+
&:hover {
border-color: $gray-300;
}
}
.upload-dropzone-overlay {
- border: 2px dashed $purple;
+ background-color: $blue-50;
+ @include dropzone-background($blue-500);
top: 0;
left: 0;
pointer-events: none;
diff --git a/app/assets/stylesheets/errors.scss b/app/assets/stylesheets/errors.scss
index 00cc3409fa7..3ef6452b706 100644
--- a/app/assets/stylesheets/errors.scss
+++ b/app/assets/stylesheets/errors.scss
@@ -9,6 +9,10 @@
@import 'bootstrap/scss/buttons';
@import 'bootstrap/scss/forms';
+@import '@gitlab/ui/src/scss/variables';
+@import '@gitlab/ui/src/scss/utility-mixins/index';
+@import '@gitlab/ui/src/components/base/button/button';
+
$body-color: #666;
$header-color: #456;
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 8e43a9b1b0d..e977fb92928 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -24,7 +24,6 @@
@import 'framework/kbd';
@import 'framework/header';
@import 'framework/highlight';
-@import 'framework/issue_box';
@import 'framework/lists';
@import 'framework/logo';
@import 'framework/job_log';
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 1192c51b9aa..56ec61ffd84 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -6,6 +6,12 @@
width: 20px;
height: 20px;
}
+
+ // Show active state.
+ .gl-button.selected {
+ background-color: $blue-50;
+ box-shadow: inset 0 0 0 2px $blue-500;
+ }
}
.emoji-menu {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 33522c66024..5fa1923af7c 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -106,15 +106,15 @@
}
@mixin btn-blue {
- @include btn-color($blue-500, $blue-600, $blue-600, $blue-700, $blue-700, $blue-800, $white);
+ @include btn-color($blue-500, $blue-600, $blue-600, $blue-700, $blue-700, $blue-800, $white-contrast);
}
@mixin btn-orange {
- @include btn-color($orange-500, $orange-600, $orange-500, $orange-600, $orange-600, $orange-800, $white);
+ @include btn-color($orange-500, $orange-600, $orange-500, $orange-600, $orange-600, $orange-800, $white-contrast);
}
@mixin btn-red {
- @include btn-color($red-500, $red-600, $red-600, $red-700, $red-700, $red-800, $white);
+ @include btn-color($red-500, $red-600, $red-600, $red-700, $red-700, $red-800, $white-contrast);
}
@mixin btn-white {
@@ -122,7 +122,7 @@
}
@mixin btn-purple {
- @include btn-color($purple-700, $purple-800, $purple-800, $purple-900, $purple-900, $purple-950, $white);
+ @include btn-color($purple-700, $purple-800, $purple-800, $purple-900, $purple-900, $purple-950, $white-contrast);
}
@mixin btn-with-margin {
@@ -175,10 +175,6 @@
@include btn-outline($white, $red-500, $red-500, $red-100, $red-700, $red-500, $red-200, $red-600, $red-800);
}
- &.btn-warning {
- @include btn-outline($white, $orange-500, $orange-500, $orange-50, $orange-600, $orange-600, $orange-100, $orange-700, $orange-700);
- }
-
&.btn-primary,
&.btn-info {
@include btn-outline($white, $blue-500, $blue-500, $blue-100, $blue-700, $blue-500, $blue-200, $blue-600, $blue-800);
@@ -190,10 +186,6 @@
@include btn-blue;
}
- &.btn-warning {
- @include btn-orange;
- }
-
&.btn-danger {
@include btn-red;
}
@@ -357,25 +349,6 @@
}
}
-.btn-link {
- padding: 0;
- background-color: transparent;
- color: $blue-600;
- font-weight: normal;
- border-radius: 0;
- border-color: transparent;
- border-width: 0;
-
- &:hover,
- &:active,
- &:focus {
- color: $blue-800;
- text-decoration: underline;
- background-color: transparent;
- border-color: transparent;
- }
-}
-
// The .btn-svg class is available for legacy icon buttons to
// preserve a 34px height and have 16x16 icons at the same time.
// Once a button is migrated (to the current 32px height)
@@ -417,16 +390,6 @@ fieldset[disabled] .btn,
cursor: default;
}
-// This class helps convert `.gl-button` children so that they consistently
-// match the style of `.btn` elements which might be around them. Ideally we
-// wouldn't need this class.
-//
-// Remove by upgrading all buttons in a container to use the new `.gl-button` style.
-.gl-button-deprecated-adapter .gl-button {
- box-shadow: none;
- border-width: 1px;
-}
-
copy-code {
@include gl-absolute;
@include gl-transition-medium;
@@ -440,3 +403,22 @@ copy-code {
@include gl-opacity-10;
}
}
+
+.btn-link {
+ padding: 0;
+ background-color: transparent;
+ color: $blue-600;
+ font-weight: normal;
+ border-radius: 0;
+ border-color: transparent;
+ border-width: 0;
+
+ &:hover,
+ &:active,
+ &:focus {
+ color: $blue-800;
+ text-decoration: underline;
+ background-color: transparent;
+ border-color: transparent;
+ }
+}
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index e06c71dccf0..036cec15935 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -27,7 +27,8 @@
}
.toggle-sidebar-button {
- width: $contextual-sidebar-collapsed-width;
+ width: #{$contextual-sidebar-collapsed-width - 1px};
+ padding: 0 21px;
.collapse-text {
display: none;
@@ -81,7 +82,7 @@
@include gl-px-0;
@include gl-pb-2;
@include gl-pt-0;
- background-color: $gray-10;
+ @include gl-bg-gray-10;
box-shadow: 0 $gl-spacing-scale-2 $gl-spacing-scale-5 $t-gray-a-24, 0 0 $gl-spacing-scale-1 $t-gray-a-24;
border-style: none;
border-radius: $border-radius-default;
@@ -128,7 +129,7 @@
@include gl-p-2;
@include gl-mb-2;
- @include gl-mt-0;
+ @include gl-mt-1;
.avatar-container {
@include gl-font-weight-normal;
@@ -246,7 +247,8 @@
z-index: 600;
width: $contextual-sidebar-width;
top: $header-height;
- background-color: $gray-50;
+ background-color: $contextual-sidebar-bg-color;
+ border-right: 1px solid $contextual-sidebar-border-color;
transform: translate3d(0, 0, 0);
&.sidebar-collapsed-desktop {
@@ -352,7 +354,6 @@
}
.sidebar-top-level-items {
- @include gl-mt-2;
margin-bottom: 60px;
.context-header a {
@@ -410,11 +411,10 @@
.toggle-sidebar-button,
.close-nav-button {
@include side-panel-toggle;
- background-color: $gray-50;
- border-top: 1px solid $border-color;
+ background-color: $contextual-sidebar-bg-color;
position: fixed;
bottom: 0;
- width: $contextual-sidebar-width;
+ width: #{$contextual-sidebar-width - 1px};
.collapse-text,
.icon-chevron-double-lg-left,
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index 7a77256398e..904d041fdc9 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -456,19 +456,6 @@ table.code {
table-layout: fixed;
border-radius: 0 0 $border-radius-default $border-radius-default;
- .diff-tr:first-of-type.line_expansion > .diff-td,
- tr:first-of-type.line_expansion > td {
- border-top: 0;
- }
-
- .diff-tr:nth-last-of-type(2).line_expansion > .diff-td,
- tr:nth-last-of-type(2).line_expansion,
- tr:last-of-type.line_expansion {
- > td {
- border-bottom: 0;
- }
- }
-
.diff-tr.line_holder .diff-td,
tr.line_holder td {
line-height: $code-line-height;
@@ -557,6 +544,10 @@ table.code {
left: 0;
}
}
+
+ img {
+ max-width: 100%;
+ }
}
}
@@ -607,10 +598,6 @@ table.code {
grid-template-columns: 50px 8px 0 1fr;
}
- .diff-grid-3-col {
- grid-template-columns: 50px 1fr !important;
- }
-
.diff-grid-2-col {
grid-template-columns: 100px 1fr !important;
@@ -619,10 +606,6 @@ table.code {
}
}
- &.inline-diff-view .diff-grid-3-col {
- grid-template-columns: 50px 50px 1fr !important;
- }
-
.diff-grid-comments {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -839,18 +822,6 @@ table.code {
}
}
-.commits-container {
- .diff-files-changed {
- @include media-breakpoint-up(sm) {
- top: $header-height;
-
- .with-performance-bar & {
- top: calc(#{$header-height} + #{$performance-bar-height});
- }
- }
- }
-}
-
.diff-files-changed {
background-color: $body-bg;
@@ -861,11 +832,11 @@ table.code {
@include media-breakpoint-up(sm) {
@include gl-sticky;
- top: calc(#{$header-height} + #{$mr-tabs-height});
+ top: $header-height;
z-index: 200;
.with-performance-bar & {
- top: calc(#{$header-height} + #{$mr-tabs-height} + #{$performance-bar-height});
+ top: calc(#{$header-height} + #{$performance-bar-height});
}
&.is-stuck {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 5c6d9266f7c..43e14a63f9d 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -70,6 +70,15 @@
}
}
+.dropdown-toggle,
+.dropdown-menu-toggle,
+.dropdown-menu-close {
+ &:active,
+ &:focus {
+ @include gl-focus;
+ }
+}
+
// Get search dropdown to line up with other nav dropdowns
.search-input-container .dropdown-menu {
margin-top: 11px;
@@ -206,6 +215,13 @@
text-decoration: none;
}
+ &:active,
+ &:focus,
+ &:focus:active,
+ &.is-focused {
+ @include gl-focus($inset: true);
+ }
+
&.dropdown-menu-user-link {
line-height: 16px;
padding-top: 10px;
@@ -271,7 +287,6 @@
display: block;
text-align: left;
list-style: none;
- padding: 0 1px;
> a,
button,
@@ -839,6 +854,15 @@
color: $red-700;
}
+ .frequent-items-list-item-container .gl-button {
+ &:active,
+ &:focus,
+ &:focus:active,
+ &.is-focused {
+ @include gl-focus($inset: true);
+ }
+ }
+
.frequent-items-list-item-container a {
display: flex;
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 50783433c3d..bba995a6de3 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -108,6 +108,11 @@ label {
width: $input-short-md-width;
}
}
+
+ &:focus {
+ border-color: $gray-400;
+ @include gl-focus;
+ }
}
.select-control {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index f76a0cbbae8..ced62926218 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -52,8 +52,13 @@
display: flex;
align-items: center;
padding: 2px 8px;
- margin: 4px 2px 4px -12px;
+ margin: 4px 2px 4px -8px;
border-radius: $border-radius-default;
+
+ &:active,
+ &:focus {
+ @include gl-focus($focus-ring: $focus-ring-dark);
+ }
}
.canary-badge {
@@ -214,6 +219,11 @@
outline: 0;
color: $white;
}
+
+ &:active,
+ &:focus {
+ @include gl-focus($focus-ring: $focus-ring-dark);
+ }
}
.top-nav-toggle,
@@ -234,6 +244,8 @@
.navbar-sub-nav {
display: flex;
+ align-items: center;
+ height: 100%;
margin: 0 0 0 6px;
.dropdown-chevron {
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index a80643e695b..1c43212f501 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -11,7 +11,6 @@
border-radius: 0 0 $border-radius-default;
font-family: $monospace-font;
font-size: $code-font-size;
- line-height: 1.5;
margin: 0;
overflow: auto;
overflow-y: hidden;
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index ca0240b6a65..a8e740525e2 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -1,94 +1,55 @@
-.ci-status-icon-success,
-.ci-status-icon-passed {
- svg {
- fill: $green-500;
+@mixin icon-styles($primary-color, $svg-color) {
+ svg,
+ .gl-icon {
+ fill: $primary-color;
+ }
+
+ // For the pipeline mini graph, we pass a custom 'gl-border' so that we can enforce
+ // a border of 1px instead of the thicker svg borders to adhere to design standards.
+ // If we implement the component with 'isBorderless' and also pass that border,
+ // this css is to dynamically apply the correct border color for those specific icons.
+ &.borderless {
+ border-color: $primary-color;
}
&.interactive {
&:hover {
- background: $green-500;
+ background: $svg-color;
+ }
- svg {
- --svg-status-bg: #{$green-100};
- box-shadow: 0 0 0 1px $green-500;
- }
+ &:hover,
+ &.active {
+ box-shadow: 0 0 0 1px $primary-color;
}
}
}
+.ci-status-icon-success,
+.ci-status-icon-passed {
+ @include icon-styles($green-500, $green-100);
+}
+
.ci-status-icon-error,
.ci-status-icon-failed {
- svg {
- fill: $red-500;
- }
-
- &.interactive {
- &:hover {
- background: $red-500;
-
- svg {
- --svg-status-bg: #{$red-100};
- box-shadow: 0 0 0 1px $red-500;
- }
- }
- }
+ @include icon-styles($red-500, $red-100);
}
.ci-status-icon-pending,
.ci-status-icon-waiting-for-resource,
.ci-status-icon-failed-with-warnings,
.ci-status-icon-success-with-warnings {
- svg {
- fill: $orange-500;
- }
-
- &.interactive {
- &:hover {
- background: $orange-500;
-
- svg {
- --svg-status-bg: #{$orange-100};
- box-shadow: 0 0 0 1px $orange-500;
- }
- }
- }
+ @include icon-styles($orange-500, $orange-100);
}
.ci-status-icon-running {
- svg {
- fill: $blue-500;
- }
-
- &.interactive {
- &:hover {
- background: $blue-500;
-
- svg {
- --svg-status-bg: #{$blue-100};
- box-shadow: 0 0 0 1px $blue-500;
- }
- }
- }
+ @include icon-styles($blue-500, $blue-100);
}
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-scheduled,
.ci-status-icon-manual {
- svg {
- fill: $gray-900;
- }
-
- &.interactive {
- &:hover {
- background: $gray-900;
-
- svg {
- --svg-status-bg: #{$gray-100};
- box-shadow: 0 0 0 1px $gray-900;
- }
- }
- }
+ @include icon-styles($gray-900, $gray-100);
}
.ci-status-icon-notification,
@@ -96,20 +57,7 @@
.ci-status-icon-created,
.ci-status-icon-skipped,
.ci-status-icon-notfound {
- svg {
- fill: $gray-500;
- }
-
- &.interactive {
- &:hover {
- background: $gray-500;
-
- svg {
- --svg-status-bg: #{$gray-100};
- box-shadow: 0 0 0 1px $gray-500;
- }
- }
- }
+ @include icon-styles($gray-500, $gray-100);
}
.icon-link {
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
deleted file mode 100644
index 8baf70da0c6..00000000000
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Issue box for showing Open/Closed state:
- * Used for Issue#show page, MergeRequest#show page etc
- *
- */
-
-.status-box {
- padding: 0 $gl-btn-padding;
-
- border-radius: $border-radius-default;
- display: block;
- float: left;
- margin-right: $gl-padding-8;
- color: $white;
- font-size: $gl-font-size;
- line-height: $gl-line-height-24;
-
- &.status-box-closed,
- &.status-box-mr-closed {
- background-color: $red-500;
- }
-
- &.status-box-issue-closed,
- &.status-box-alert-resolved,
- &.status-box-mr-merged {
- background-color: $blue-500;
- }
-
- &.status-box-open {
- background-color: $green-500;
- }
-
- &.status-box-expired {
- background-color: $orange-500;
- }
-
- &.status-box-upcoming {
- background: $gl-text-color-secondary;
- }
-}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 1caf02937d5..2cea3b96ff7 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -358,8 +358,8 @@
line-height: 1;
padding: 0;
min-width: 16px;
- color: $gray-400;
- fill: $gray-400;
+ color: $gray-500;
+ fill: $gray-500;
svg {
@include btn-svg;
diff --git a/app/assets/stylesheets/framework/page_title.scss b/app/assets/stylesheets/framework/page_title.scss
index c77e2be8e5a..5ed5b1e1445 100644
--- a/app/assets/stylesheets/framework/page_title.scss
+++ b/app/assets/stylesheets/framework/page_title.scss
@@ -3,8 +3,6 @@
.page-title {
margin: $gl-padding 0;
- font-size: 1.75em;
- font-weight: $gl-font-weight-bold;
color: $gl-text-color;
}
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index 549b61aedae..74aed1bd984 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -273,6 +273,18 @@
@include scrolling-links();
}
+ .fade-left::after,
+ .fade-right::after {
+ content: '';
+ pointer-events: none;
+ z-index: -1;
+ display: block;
+ width: 16px;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ }
+
.fade-right {
@include fade(left, $gray-light);
right: -5px;
@@ -280,6 +292,11 @@
svg {
right: -7px;
}
+
+ &::after {
+ right: 0;
+ background: linear-gradient(270deg, $white, transparent);
+ }
}
.fade-left {
@@ -290,6 +307,11 @@
svg {
left: -7px;
}
+
+ &::after {
+ left: 0;
+ background: linear-gradient(90deg, $white, transparent);
+ }
}
}
@@ -316,7 +338,6 @@
.fade-right,
.fade-left {
- bottom: $gl-padding;
top: auto;
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index dd9581c4692..13201d43fd0 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -24,15 +24,11 @@
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter-collapsed-width;
}
-
- .merge-request-tabs-holder.affix {
- right: $gutter-collapsed-width;
- }
}
}
&.is-merge-request {
- @include media-breakpoint-up(md) {
+ @include media-breakpoint-up(lg) {
.content-wrapper {
padding-right: $gutter-collapsed-width;
}
@@ -60,7 +56,7 @@
z-index: $zindex-dropdown-menu;
&.right-sidebar-merge-requests {
- width: 270px;
+ width: 300px;
@include media-breakpoint-up(md) {
z-index: auto;
@@ -77,17 +73,11 @@
}
}
- @include media-breakpoint-up(md) {
- .content-wrapper {
- padding-right: $gutter-width;
- }
-
- &:not(.with-overlay) .merge-request-tabs-holder.affix {
- right: $gutter-width;
- }
-
- &.with-overlay .merge-request-tabs-holder.affix {
- right: $gutter-collapsed-width;
+ &:not(.is-merge-request) {
+ @include media-breakpoint-up(md) {
+ .content-wrapper {
+ padding-right: $gutter-width;
+ }
}
}
}
@@ -96,7 +86,7 @@
border-left: 1px solid $gray-100;
&.right-sidebar-merge-requests {
- @include media-breakpoint-up(md) {
+ @include media-breakpoint-up(lg) {
border-left: 0;
}
}
@@ -110,10 +100,6 @@
}
}
-.with-performance-bar .right-sidebar.affix {
- top: calc(#{$header-height} + #{$performance-bar-height});
-}
-
@mixin maintain-sidebar-dimensions {
display: block;
width: $gutter-width;
@@ -284,6 +270,10 @@
.reviewer-grid {
[data-css-area='attention'] {
grid-area: attention;
+
+ button.selected svg {
+ fill: $orange-500;
+ }
}
[data-css-area='user'] {
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 9b38e842635..086b83b13e0 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -49,7 +49,7 @@
}
img.avatar {
- margin-right: $gl-padding;
+ margin-right: $gl-padding-12;
@include media-breakpoint-down(sm) {
width: $gl-spacing-scale-6;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index b1e44a81267..b5e0dcd875a 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -135,11 +135,13 @@
blockquote,
.blockquote {
- color: $gl-grayish-blue;
font-size: inherit;
- padding: 8px 24px;
- margin: 16px 0;
- border-left: 3px solid $white-dark;
+ @include gl-text-gray-700;
+ @include gl-py-3;
+ @include gl-pl-6;
+ @include gl-my-3;
+ @include gl-mx-0;
+ @include gl-inset-border-l-4-gray-100;
&:dir(rtl) {
border-left: 0;
@@ -147,9 +149,8 @@
}
p {
- color: $gl-grayish-blue !important;
- font-size: inherit;
line-height: 1.5;
+ @include gl-reset-color;
&:last-child {
margin: 0;
@@ -592,6 +593,14 @@
}
/**
+ * Links
+ *
+ */
+a:focus-visible {
+ @include gl-focus($outline: true, $outline-offset: $outline-width);
+}
+
+/**
* Headers
*
*/
@@ -602,8 +611,6 @@ body {
.page-title {
margin: #{2 * $grid-size} 0;
line-height: 1.3;
- font-size: 1.25em;
- font-weight: $gl-font-weight-bold;
&.with-button {
line-height: 34px;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index bc649b6407d..eeffc4fc21b 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -8,8 +8,8 @@ $gutter-inner-width: 250px;
$sidebar-transition-duration: 0.3s;
$sidebar-breakpoint: 1024px;
$default-transition-duration: 0.15s;
-$contextual-sidebar-width: 220px;
-$contextual-sidebar-collapsed-width: 48px;
+$contextual-sidebar-width: 256px;
+$contextual-sidebar-collapsed-width: 56px;
$toggle-sidebar-height: 48px;
/**
@@ -169,7 +169,7 @@ $purple-800: #453894 !default;
$purple-900: #2f2a6b !default;
$purple-950: #232150 !default;
-$gray-10: #fafafa !default;
+$gray-10: #f5f5f5 !default;
$gray-50: #f0f0f0 !default;
$gray-100: #dbdbdb !default;
$gray-200: #bfbfbf !default;
@@ -357,6 +357,8 @@ $border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor);
/*
* UI elements
*/
+$contextual-sidebar-bg-color: #f5f5f5;
+$contextual-sidebar-border-color: #e9e9e9;
$border-color: $gray-100;
$shadow-color: $t-gray-a-08;
$well-expand-item: #e8f2f7 !default;
@@ -426,8 +428,6 @@ $gl-padding-12: 12px;
$gl-padding: 16px;
$gl-padding-24: 24px;
$gl-padding-32: 32px;
-$gl-padding-50: 50px;
-$gl-col-padding: 15px;
$gl-input-padding: 10px;
$gl-vert-padding: 6px;
$gl-padding-top: 10px;
@@ -439,7 +439,7 @@ $browser-scrollbar-size: 10px;
/*
* Misc
*/
-$header-height: var(--header-height, 40px);
+$header-height: var(--header-height, 48px);
$header-zindex: 1000;
$zindex-dropdown-menu: 300;
$suggestion-header-height: 46px;
@@ -580,7 +580,7 @@ $sidebar-toggle-height: 60px;
$sidebar-toggle-width: 40px;
$sidebar-milestone-toggle-bottom-margin: 10px;
$sidebar-avatar-size: 32px;
-$sidebar-top-item-lr-margin: 4px;
+$sidebar-top-item-lr-margin: 8px;
$sidebar-top-item-tb-margin: 1px;
/*
diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss
index 433141ae690..fcbd05141b9 100644
--- a/app/assets/stylesheets/highlight/common.scss
+++ b/app/assets/stylesheets/highlight/common.scss
@@ -29,36 +29,6 @@
}
}
-@mixin old-diff-expansion($background, $border, $link) {
- background-color: $background;
-
- .diff-td,
- td {
- border-top: 1px solid $border;
- border-bottom: 1px solid $border;
- }
-
- button {
- color: $link;
- border: 0;
- background: transparent;
-
- &[disabled] {
- color: desaturate($link, 100%);
- opacity: 0.5;
- cursor: default;
- }
-
- &:hover:not([disabled]) {
- text-decoration: underline;
- }
-
- &:not(:focus-visible) {
- outline: 0;
- }
- }
-}
-
@mixin dark-diff-expansion-line {
&.expansion .diff-td {
@@ -156,3 +126,11 @@
@include gl-display-inline-block;
}
}
+
+@mixin hljs-override($suffix, $color) {
+ &.blob-viewer {
+ .hljs-#{$suffix} {
+ color: $color;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/highlight/themes/dark.scss b/app/assets/stylesheets/highlight/themes/dark.scss
index 0eeebdb2e7a..f4d9909d81f 100644
--- a/app/assets/stylesheets/highlight/themes/dark.scss
+++ b/app/assets/stylesheets/highlight/themes/dark.scss
@@ -154,10 +154,6 @@ $dark-il: #de935f;
color: $dark-line-color;
}
- .old-line_expansion {
- @include old-diff-expansion($dark-main-bg, $dark-border, $dark-na);
- }
-
.diff-line-expand-button {
@include diff-expansion($gray-600, $gray-200, $gray-300, $white);
}
diff --git a/app/assets/stylesheets/highlight/themes/monokai.scss b/app/assets/stylesheets/highlight/themes/monokai.scss
index b8cd97d6504..dfa32d4b773 100644
--- a/app/assets/stylesheets/highlight/themes/monokai.scss
+++ b/app/assets/stylesheets/highlight/themes/monokai.scss
@@ -96,6 +96,25 @@ $monokai-gh: #75715e;
}
.code.monokai {
+ // Highlight.js theme overrides (https://gitlab.com/gitlab-org/gitlab/-/issues/365167)
+ // We should be able to remove the overrides once the upstream issue is fixed (https://github.com/sourcegraph/sourcegraph/issues/23251)
+ @include hljs-override('string', $monokai-s);
+ @include hljs-override('attr', $monokai-na);
+ @include hljs-override('keyword', $monokai-k);
+ @include hljs-override('variable', $monokai-nv);
+ @include hljs-override('variable.language_', $monokai-k);
+ @include hljs-override('title', $monokai-nf);
+ @include hljs-override('name', $monokai-k);
+ @include hljs-override('tag', $monokai-nt);
+ @include hljs-override('type', $monokai-nc);
+ @include hljs-override('number', $monokai-mf);
+ @include hljs-override('literal', $monokai-kc);
+ @include hljs-override('built_in', $monokai-n);
+ @include hljs-override('section', $monokai-gh);
+ @include hljs-override('bullet', $monokai-n);
+ @include hljs-override('subst', $monokai-p);
+ @include hljs-override('symbol', $monokai-ni);
+
// Line numbers
.file-line-num {
@include line-number-link($monokai-line-num-color);
@@ -125,10 +144,6 @@ $monokai-gh: #75715e;
color: $monokai-text-color;
}
- .old-line_expansion {
- @include old-diff-expansion($monokai-bg, $monokai-border, $monokai-k);
- }
-
.diff-line-expand-button {
@include diff-expansion($gray-600, $gray-200, $gray-300, $white);
}
diff --git a/app/assets/stylesheets/highlight/themes/none.scss b/app/assets/stylesheets/highlight/themes/none.scss
index 99a3de23c26..f70c53c9eaa 100644
--- a/app/assets/stylesheets/highlight/themes/none.scss
+++ b/app/assets/stylesheets/highlight/themes/none.scss
@@ -15,6 +15,14 @@
}
.code.none {
+ // Highlight.js theme overrides (https://gitlab.com/gitlab-org/gitlab/-/issues/365167)
+ // We should be able to remove the overrides once the upstream issue is fixed (https://github.com/sourcegraph/sourcegraph/issues/23251)
+ &.blob-viewer {
+ [class^="hljs-"] {
+ color: $gl-text-color;
+ }
+ }
+
// Line numbers
.file-line-num {
@include line-number-link($black-transparent);
@@ -44,10 +52,6 @@
color: $gl-text-color;
}
- .old-line_expansion {
- @include old-diff-expansion($gray-light, $white-normal, $gl-text-color);
- }
-
.diff-line-expand-button {
@include diff-expansion($gray-100, $gray-700, $gray-200, $gray-800);
}
diff --git a/app/assets/stylesheets/highlight/themes/solarized-dark.scss b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
index 55d17b8f1d2..73aa6275d17 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-dark.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
@@ -99,6 +99,25 @@ $solarized-dark-il: #2aa198;
}
.code.solarized-dark {
+ // Highlight.js theme overrides (https://gitlab.com/gitlab-org/gitlab/-/issues/365167)
+ // We should be able to remove the overrides once the upstream issue is fixed (https://github.com/sourcegraph/sourcegraph/issues/23251)
+ @include hljs-override('string', $solarized-dark-s);
+ @include hljs-override('attr', $solarized-dark-na);
+ @include hljs-override('keyword', $solarized-dark-k);
+ @include hljs-override('variable', $solarized-dark-nv);
+ @include hljs-override('variable.language_', $solarized-dark-k);
+ @include hljs-override('title', $solarized-dark-nf);
+ @include hljs-override('name', $solarized-dark-k);
+ @include hljs-override('tag', $solarized-dark-nt);
+ @include hljs-override('type', $solarized-dark-nc);
+ @include hljs-override('number', $solarized-dark-mf);
+ @include hljs-override('literal', $solarized-dark-kc);
+ @include hljs-override('built_in', $solarized-dark-n);
+ @include hljs-override('section', $solarized-dark-gh);
+ @include hljs-override('bullet', $solarized-dark-n);
+ @include hljs-override('subst', $solarized-dark-p);
+ @include hljs-override('symbol', $solarized-dark-ni);
+
// Line numbers
.file-line-num {
@include line-number-link($solarized-dark-line-color);
@@ -128,10 +147,6 @@ $solarized-dark-il: #2aa198;
color: $solarized-dark-pre-color;
}
- .old-line_expansion {
- @include old-diff-expansion($solarized-dark-line-bg, $solarized-dark-border, $solarized-dark-kd);
- }
-
.diff-line-expand-button {
@include diff-expansion(lighten($solarized-dark-pre-bg, 10%), $gray-200, lighten($solarized-dark-pre-bg, 20%), $white);
}
diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss
index 72b961097e4..74448317270 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-light.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss
@@ -134,10 +134,6 @@ $solarized-light-il: #2aa198;
background-color: $solarized-light-pre-bg;
color: $solarized-light-pre-color;
}
-
- .old-line_expansion {
- @include old-diff-expansion($solarized-light-line-bg, $solarized-light-border, $solarized-light-kd);
- }
.diff-line-expand-button {
@include diff-expansion($gray-100, $gray-700, $gray-200, $gray-800);
diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss
index b984c194033..aac8ccde96e 100644
--- a/app/assets/stylesheets/highlight/white_base.scss
+++ b/app/assets/stylesheets/highlight/white_base.scss
@@ -133,19 +133,6 @@ pre.code,
color: $white-code-color;
}
-.old-line_expansion {
- @include old-diff-expansion($gray-light, $border-color, $blue-600);
-
- &.diff-tr:last-child {
- border-bottom-right-radius: 4px;
- border-bottom-left-radius: 4px;
-
- .diff-td {
- border-bottom: 0;
- }
- }
-}
-
.diff-line-expand-button {
@include diff-expansion($gray-100, $gray-700, $gray-200, $gray-800);
}
diff --git a/app/assets/stylesheets/notify_enhanced.scss b/app/assets/stylesheets/notify_enhanced.scss
index a366498ea03..b331d997a97 100644
--- a/app/assets/stylesheets/notify_enhanced.scss
+++ b/app/assets/stylesheets/notify_enhanced.scss
@@ -32,10 +32,6 @@ body {
font-size: inherit;
}
-a {
- text-decoration: none;
-}
-
.gl-mb-5 {
@include gl-mb-5;
}
diff --git a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
index 3327f8da632..613d27a2f39 100644
--- a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
+++ b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
@@ -52,7 +52,6 @@
width: $ci-action-dropdown-svg-size;
height: $ci-action-dropdown-svg-size;
position: relative;
- top: 1px;
vertical-align: initial;
}
}
diff --git a/app/assets/stylesheets/page_bundles/build.scss b/app/assets/stylesheets/page_bundles/build.scss
index d55d6b27576..d40c03b7fd1 100644
--- a/app/assets/stylesheets/page_bundles/build.scss
+++ b/app/assets/stylesheets/page_bundles/build.scss
@@ -44,13 +44,6 @@
}
}
- &.affix-top {
- position: absolute;
- right: 0;
- left: 0;
- top: 0;
- }
-
.controllers {
@include build-controllers(15px, center, false, 0, inline, 0);
}
@@ -177,12 +170,6 @@
width: 289px;
overflow: auto;
- svg {
- margin-right: 3px;
- height: 14px;
- width: 14px;
- }
-
a {
padding: $gl-padding 10px $gl-padding 40px;
width: 270px;
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 6c270852e53..a4a82fdcef3 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -563,24 +563,11 @@ $ide-commit-header-height: 48px;
}
.ide-commit-options {
- label {
- font-weight: normal;
-
- &.is-disabled {
- .ide-option-label {
- text-decoration: line-through;
- }
+ .is-disabled {
+ .ide-option-label {
+ text-decoration: line-through;
}
}
-
- .form-text.text-muted {
- margin-top: 0;
- line-height: 0;
- }
-}
-
-.ide-commit-new-branch {
- margin-left: 25px;
}
.ide-sidebar-link {
diff --git a/app/assets/stylesheets/page_bundles/issues_show.scss b/app/assets/stylesheets/page_bundles/issues_show.scss
index ade649faaae..c664e0a734e 100644
--- a/app/assets/stylesheets/page_bundles/issues_show.scss
+++ b/app/assets/stylesheets/page_bundles/issues_show.scss
@@ -24,8 +24,9 @@
/* The inside bullet aligns itself to the bottom, which we see when text to the right of
* a multi-line list item wraps. We fix this by aligning it to the top, and excluding
- * other elements adversely affected by this. Targeting ::marker doesn't seem to work. */
- > *:not(code):not(input):not(.gl-label) {
+ * other elements. Targeting ::marker doesn't seem to work, instead we exclude custom elements
+ * or anything with a class */
+ > *:not(gl-emoji, code, [class]) {
vertical-align: top;
}
@@ -75,29 +76,6 @@
}
}
-.description.work-items-enabled {
- ul.task-list {
- > li.task-list-item {
- .js-add-task {
- svg {
- visibility: hidden;
- }
-
- &:focus svg {
- visibility: visible;
- }
- }
-
- &:hover,
- &:focus-within {
- .js-add-task svg {
- visibility: visible;
- }
- }
- }
- }
-}
-
.is-ghost {
opacity: 0.3;
pointer-events: none;
diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss
index f153569f99b..0a2b3175aa9 100644
--- a/app/assets/stylesheets/page_bundles/jira_connect.scss
+++ b/app/assets/stylesheets/page_bundles/jira_connect.scss
@@ -33,12 +33,6 @@ $header-height: 40px;
right: 0;
}
-.jira-connect-user {
- position: fixed;
- top: 10px;
- right: 20px;
-}
-
.jira-connect-app {
margin-top: $header-height;
height: calc(100% - #{$header-height});
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index f04cdfba0e4..14873c54cd7 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -315,7 +315,7 @@ $tabs-holder-z-index: 250;
}
.mr-fast-forward-message {
- padding-left: $gl-padding-50;
+ padding-left: $gl-spacing-scale-9;
padding-bottom: $gl-padding;
}
@@ -610,10 +610,10 @@ $tabs-holder-z-index: 250;
.mr-widget-extension {
border-top: 1px solid var(--border-color, $border-color);
- background-color: var(--gray-50, $gray-50);
+ background-color: var(--gray-10, $gray-10);
&.clickable:hover {
- background-color: var(--gray-100, $gray-100);
+ background-color: var(--gray-50, $gray-50);
cursor: pointer;
}
}
@@ -737,9 +737,70 @@ $tabs-holder-z-index: 250;
}
.merge-request-overview {
- @include media-breakpoint-up(md) {
+ @include media-breakpoint-up(lg) {
display: grid;
- grid-template-columns: 1fr 270px;
+ grid-template-columns: calc(95% - 285px) auto;
grid-gap: 5%;
}
}
+
+.container-fluid:not(.container-limited) {
+ .detail-page-header,
+ .detail-page-description,
+ .merge-request-tabs-container {
+ &.is-merge-request {
+ @include gl-mx-auto;
+ max-width: $fixed-layout-width - ($gl-padding * 2);
+ }
+ }
+}
+
+.submit-review-dropdown {
+ &.show .dropdown-menu {
+ width: calc(100vw - 20px);
+ max-width: 650px;
+
+ .gl-new-dropdown-inner {
+ max-height: none !important;
+ }
+
+ .md-header {
+ .gl-tab-nav-item {
+ @include gl-text-gray-900;
+ @include gl-pb-5;
+
+ &:hover {
+ @include gl-bg-none;
+ @include gl-text-gray-900;
+
+ &:not(.gl-tab-nav-item-active) {
+ @include gl-inset-border-b-2-gray-200;
+ }
+ }
+ }
+
+ .gl-tab-nav-item-active {
+ @include gl-font-weight-bold;
+ @include gl-text-gray-900;
+ @include gl-inset-border-b-2-theme-accent;
+
+ &:active,
+ &:focus,
+ &:focus:active {
+ box-shadow: inset 0 -#{$gl-border-size-2} 0 0 var(--gl-theme-accent, $theme-indigo-500),
+ $focus-ring;
+ @include gl-outline-none;
+ }
+ }
+ }
+ }
+
+ .gl-new-dropdown-contents {
+ padding: $gl-spacing-scale-4 !important;
+ }
+
+ .md-preview-holder {
+ max-height: 180px;
+ height: 180px;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/pipelines.scss b/app/assets/stylesheets/page_bundles/pipelines.scss
index a225a0f0061..4946bbbebe5 100644
--- a/app/assets/stylesheets/page_bundles/pipelines.scss
+++ b/app/assets/stylesheets/page_bundles/pipelines.scss
@@ -74,11 +74,8 @@
.stage-cell {
.stage-container {
- align-items: center;
- display: inline-flex;
-
- + .stage-container {
- margin-left: 4px;
+ &:last-child {
+ margin-right: 0;
}
// Hack to show a button tooltip inline
@@ -94,10 +91,11 @@
&:not(:last-child) {
&::after {
content: '';
- width: 4px;
+ border-bottom: 2px solid $gray-200;
position: absolute;
right: -4px;
- border-bottom: 2px solid $gray-200;
+ top: 11px;
+ width: 4px;
}
}
}
diff --git a/app/assets/stylesheets/page_bundles/wiki.scss b/app/assets/stylesheets/page_bundles/wiki.scss
index c64e159c648..9bbea48d2c0 100644
--- a/app/assets/stylesheets/page_bundles/wiki.scss
+++ b/app/assets/stylesheets/page_bundles/wiki.scss
@@ -1,5 +1,4 @@
@import 'mixins_and_variables_and_functions';
-@import 'highlight.js/scss/a11y-light';
@import 'components/content_editor';
.title .edit-wiki-header {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index afe57bb26e6..80b9e378252 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -56,7 +56,7 @@
position: relative;
font-family: $monospace-font;
$left: 12px;
- overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-foss/issues/13987
+
.max-width-marker {
width: 72ch;
color: $commit-max-width-marker-color;
@@ -95,6 +95,10 @@
}
}
+.commits-row + .commits-row {
+ border-top: 1px solid $white-normal;
+}
+
.text-expander {
display: inline-flex;
background: $white;
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index e0319952adb..909de9d57f2 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -5,15 +5,6 @@
line-height: 34px;
display: flex;
- a {
- color: $gl-text-color;
-
- &.link,
- &.gl-link {
- color: $blue-600;
- }
- }
-
.author-link {
white-space: nowrap;
}
@@ -23,6 +14,15 @@
}
}
+.detail-page-header a {
+ color: $gl-text-color;
+}
+
+.detail-page-header a.link,
+.detail-page-header .title a {
+ color: $blue-600;
+}
+
.detail-page-header-body {
position: relative;
display: flex;
@@ -58,7 +58,6 @@
.detail-page-description {
.title {
margin: 0 0 16px;
- font-size: 2em;
color: $gl-text-color;
padding: 0 0 0.3em;
border-bottom: 1px solid $white-dark;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 7ac3ef2221f..96ca9fbcb43 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -6,15 +6,8 @@
.groups-list {
@include basic-list;
- display: flex;
- flex-direction: column;
- margin: 0;
li {
- .title {
- font-weight: 600;
- }
-
a {
text-decoration: none;
@@ -40,8 +33,8 @@
}
.save-group-loader {
- margin-top: $gl-padding-50;
- margin-bottom: $gl-padding-50;
+ margin-top: $gl-spacing-scale-9;
+ margin-bottom: $gl-spacing-scale-9;
color: $gray-700;
}
@@ -81,17 +74,13 @@ table.pipeline-project-metrics tr td {
}
.explore-groups.landing {
- .inner-content {
- padding: 0;
-
- p {
- margin: 7px 0 0;
- max-width: 480px;
- padding: 0 $gl-padding;
+ .inner-content p {
+ margin: 7px 0 0;
+ max-width: 480px;
+ padding: 0 $gl-padding;
- @include media-breakpoint-down(sm) {
- margin: 0 auto;
- }
+ @include media-breakpoint-down(sm) {
+ margin: 0 auto;
}
}
@@ -113,13 +102,6 @@ table.pipeline-project-metrics tr td {
}
.groups-list-tree-container {
- .has-no-search-results {
- text-align: center;
- padding: $gl-padding;
- font-style: italic;
- color: $well-light-text-color;
- }
-
> .group-list-tree > .group-row.has-children:first-child {
border-top: 0;
}
@@ -135,16 +117,6 @@ table.pipeline-project-metrics tr td {
}
}
- .folder-caret,
- .item-type-icon {
- display: inline-block;
- color: $gl-text-color-secondary;
- }
-
- .folder-caret {
- width: $gl-font-size-large;
- }
-
.item-type-icon {
margin-top: 2px;
width: 20px;
@@ -288,10 +260,3 @@ table.pipeline-project-metrics tr td {
}
}
}
-
-.js-groups-list-holder {
- .groups-list-loading {
- font-size: 34px;
- text-align: center;
- }
-}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 086abcf3f86..f3182af3047 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -1,3 +1,14 @@
+.status-box {
+ padding: 0 $gl-btn-padding;
+ border-radius: $border-radius-default;
+ display: block;
+ float: left;
+ margin-right: $gl-padding-8;
+ color: $white;
+ font-size: $gl-font-size;
+ line-height: $gl-line-height-24;
+}
+
.issuable-warning-icon {
background-color: $orange-50;
border-radius: $border-radius-default;
@@ -134,8 +145,9 @@
}
&.right-sidebar-merge-requests {
- @include media-breakpoint-down(sm) {
+ @include media-breakpoint-down(md) {
@include right-sidebar;
+ z-index: 251;
}
}
@@ -186,7 +198,7 @@
}
.block {
- @include media-breakpoint-up(md) {
+ @include media-breakpoint-up(lg) {
padding: $gl-spacing-scale-5 0;
}
}
@@ -263,10 +275,6 @@
}
}
- &.affix-top .issuable-sidebar {
- height: 100%;
- }
-
&.right-sidebar-expanded {
&:not(.right-sidebar-merge-requests) {
width: $gutter-width;
@@ -280,8 +288,33 @@
padding: 0 20px;
&.is-merge-request {
- @include media-breakpoint-up(md) {
+ @include media-breakpoint-up(lg) {
padding: 0;
+
+ form {
+ --initial-top: calc(#{$header-height} + #{$mr-tabs-height});
+ --top: var(--initial-top);
+
+ @include gl-sticky;
+ @include gl-overflow-auto;
+
+ top: var(--top);
+ height: calc(100vh - var(--top));
+ padding: 0 15px;
+ margin-bottom: calc(var(--top) * -1);
+
+ .with-performance-bar & {
+ --top: calc(var(--initial-top) + #{$performance-bar-height});
+ }
+
+ .with-system-header & {
+ --top: calc(var(--initial-top) + #{$system-header-height});
+ }
+
+ .with-performance-bar.with-system-header & {
+ --top: calc(var(--initial-top) + #{$system-header-height} + #{$performance-bar-height});
+ }
+ }
}
}
}
@@ -334,7 +367,7 @@
}
&.right-sidebar-merge-requests {
- @include media-breakpoint-up(md) {
+ @include media-breakpoint-up(lg) {
display: block;
}
}
@@ -617,21 +650,6 @@
}
}
-.issuable-status-box {
- align-self: stretch;
- display: flex;
- justify-content: center;
- align-items: center;
- margin-top: 0;
- padding: 0 $gl-padding-8;
-
- @include media-breakpoint-up(sm) {
- display: inline-block;
- height: auto;
- align-self: center;
- }
-}
-
.issuable-gutter-toggle {
@include media-breakpoint-down(sm) {
margin-left: $btn-side-margin;
@@ -919,3 +937,58 @@
margin-right: -7px;
z-index: 1;
}
+
+.issuable-discussion.incident-timeline-events {
+ .main-notes-list::before {
+ content: none;
+ }
+
+ .timeline-event-note {
+ p {
+ margin-bottom: 0;
+ }
+ }
+}
+
+/**
+ * We have a very specific design proposal where we cannot
+ * use `vertical-line` mixin as it is and have to use
+ * custom styles, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81284#note_904867444
+ */
+.timeline-entry-vertical-line {
+ &::before,
+ &::after {
+ content: '';
+ border-left: 2px solid $gray-50;
+ position: absolute;
+ left: 39px;
+ height: 80%;
+ }
+
+ &:first-child::before,
+ &:last-child::after {
+ content: none;
+ }
+
+ &:first-child {
+ &::after {
+ top: 50%;
+ }
+ }
+
+ &:last-child {
+ &::before {
+ bottom: 50%;
+ }
+ }
+
+ &:not(:first-child):not(:last-child) {
+ &::before {
+ top: -10%;
+ }
+
+ &::after {
+ bottom: -10%;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 82216b8d5c5..bd66319d78f 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -74,21 +74,11 @@
> li:not(.empty-message):not(.no-border) {
background-color: $white;
- margin-bottom: 5px;
display: flex;
justify-content: space-between;
- padding: $gl-padding;
- border-radius: $border-radius-default;
- border: 1px solid $gray-50;
-
- &:last-child {
- margin-bottom: 0;
- }
.prioritized-labels:not(.is-not-draggable) & {
- box-shadow: 0 1px 2px $issue-boards-card-shadow;
cursor: grab;
- border: 0;
&:active {
cursor: grabbing;
@@ -111,11 +101,6 @@
width: 109px;
}
-.labels-container {
- border-radius: $border-radius-default;
- padding: $gl-padding $gl-padding-8;
-}
-
.label-actions-list {
list-style: none;
flex-shrink: 0;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 0d3ed0e7c71..a3fbedd87a9 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -3,6 +3,7 @@
*
*/
$tabs-holder-z-index: 250;
+$comparison-empty-state-height: 62px;
.space-children {
@include clearfix;
@@ -70,6 +71,10 @@ $tabs-holder-z-index: 250;
}
}
+.compare-commit-empty {
+ min-height: $comparison-empty-state-height;
+}
+
.commits-empty {
text-align: center;
@@ -200,11 +205,6 @@ $tabs-holder-z-index: 250;
}
}
-.assign-to-me-link {
- padding-left: 12px;
- white-space: nowrap;
-}
-
.table-holder {
.ci-table {
th {
@@ -214,14 +214,9 @@ $tabs-holder-z-index: 250;
}
}
-.merge-request-tabs-holder,
-.epic-tabs-holder {
+.merge-request-tabs-holder {
top: $header-height;
z-index: $tabs-holder-z-index;
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- padding-left: $gl-padding;
- padding-right: $gl-padding;
background-color: $body-bg;
border-bottom: 1px solid $border-color;
@@ -251,30 +246,14 @@ $tabs-holder-z-index: 250;
}
}
-.merge-request-tabs-holder.affix .merge-request-tabs-container,
-.epic-tabs-holder.affix .epic-tabs-container {
- padding-left: $gl-padding;
- padding-right: $gl-padding;
-}
-
.with-performance-bar {
- .merge-request-tabs-holder,
- .epic-tabs-holder {
+ .merge-request-tabs-holder {
top: calc(#{$header-height} + #{$performance-bar-height});
}
}
-.merge-request-tabs,
-.epic-tabs {
- display: flex;
- flex-wrap: nowrap;
- margin-bottom: 0;
- padding: 0;
-}
-
.limit-container-width {
- .merge-request-tabs-container,
- .epic-tabs-container {
+ .merge-request-tabs-container {
max-width: $limited-layout-width;
margin-left: auto;
margin-right: auto;
@@ -287,11 +266,7 @@ $tabs-holder-z-index: 250;
}
}
-.merge-request-tabs-container,
-.epic-tabs-container {
- display: flex;
- justify-content: space-between;
-
+.merge-request-tabs-container {
@include media-breakpoint-down(xs) {
.discussion-filter-container {
margin-bottom: $gl-padding-4;
@@ -318,16 +293,14 @@ $tabs-holder-z-index: 250;
// Wrap MR tabs/buttons so you don't have to scroll on desktop
@include media-breakpoint-down(md) {
- .merge-request-tabs-container,
- .epic-tabs-container {
+ .merge-request-tabs-container {
flex-direction: column-reverse;
}
}
@include media-breakpoint-down(lg) {
.right-sidebar-expanded {
- .merge-request-tabs-container,
- .epic-tabs-container {
+ .merge-request-tabs-container {
flex-direction: column-reverse;
align-items: flex-start;
}
@@ -335,8 +308,7 @@ $tabs-holder-z-index: 250;
}
.limit-container-width:not(.container-limited) {
- .merge-request-tabs-holder:not(.affix) .merge-request-tabs-container,
- .epic-tabs-holder:not(.affix) .epic-tabs-container {
+ .merge-request-tabs-holder .merge-request-tabs-container {
max-width: $limited-layout-width - ($gl-padding * 2);
}
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 1c408f6d985..645f145328b 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -55,16 +55,6 @@
box-shadow ease-in-out 0.15s;
background-color: $white;
- &.is-focused {
- border-color: $input-focus-border-color;
- box-shadow: $input-focus-box-shadow;
-
- .comment-toolbar,
- .nav-links {
- border-color: $blue-300;
- }
- }
-
&.is-dropzone-hover {
border-color: $green-500;
box-shadow: 0 0 2px $black-transparent,
@@ -75,9 +65,22 @@
border-color: $green-500;
}
}
+
+ // Disable inner focus
+ textarea:focus {
+ @include gl-shadow-none;
+ }
+ }
+
+ .comment-warning-wrapper:focus-within {
+ @include gl-focus;
}
}
+.md-area:focus-within {
+ @include gl-focus;
+}
+
.md-header .nav-links {
display: flex;
flex-flow: row wrap;
@@ -428,7 +431,11 @@ table {
}
.comment-warning-wrapper {
+ transition: border-color ease-in-out 0.15s,
+ box-shadow ease-in-out 0.15s;
+
.md-area {
border: 0;
+ box-shadow: none;
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 1949603b416..82e96dee4c6 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -20,7 +20,7 @@ $system-note-svg-size: 16px;
}
.note-wrapper {
- padding: $gl-padding;
+ padding: $gl-padding $gl-padding-8 $gl-padding $gl-padding;
&.outlined {
@include outline-comment();
@@ -199,6 +199,7 @@ $system-note-svg-size: 16px;
}
.note-body {
+ padding: $gl-padding-4;
overflow-x: auto;
overflow-y: hidden;
@@ -564,7 +565,6 @@ $system-note-svg-size: 16px;
}
.discussion-header {
- min-height: $line-height-base * 2em;
box-sizing: content-box;
.note-header-info {
@@ -579,6 +579,7 @@ $system-note-svg-size: 16px;
&.note-wrapper {
display: flex;
align-items: center;
+ padding-right: $gl-padding;
}
}
@@ -615,6 +616,7 @@ $system-note-svg-size: 16px;
.note-header-info {
min-width: 0;
+ padding-left: $gl-padding-4;
&.discussion {
padding-bottom: 0;
@@ -623,7 +625,7 @@ $system-note-svg-size: 16px;
.note-header-info,
.note-actions {
- padding-bottom: $gl-padding-8;
+ padding-bottom: $gl-padding-4;
}
.system-note .note-header-info {
diff --git a/app/assets/stylesheets/pages/pages.scss b/app/assets/stylesheets/pages/pages.scss
deleted file mode 100644
index 2de33f20595..00000000000
--- a/app/assets/stylesheets/pages/pages.scss
+++ /dev/null
@@ -1,55 +0,0 @@
-.pages-domain-list {
- &-item {
- align-items: center;
-
- .domain-status {
- display: inline-flex;
- left: $gl-padding;
- position: absolute;
- }
-
- .domain-name {
- flex-grow: 1;
- }
-
- }
-
- &.has-verification-status > li {
- padding-left: 3 * $gl-padding;
- }
-
-}
-
-.status-badge {
-
- display: inline-flex;
- margin-bottom: $gl-padding-8;
-
- // Most of the following settings "stolen" from btn-sm
- // Border radius is overwritten for both
- .label,
- .btn {
- padding: $gl-padding-4 $gl-padding-8;
- font-size: $gl-font-size;
- line-height: $gl-btn-line-height;
- border-radius: 0;
- display: flex;
- align-items: center;
- }
-
- .btn svg {
- top: auto;
- }
-
- :first-child {
- line-height: $gl-line-height;
- }
-
- :not(:first-child) {
- border-left: 0;
- }
-
- :last-child {
- border-radius: $border-radius-default;
- }
-}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index f76a8030e5b..812cc6ab4e6 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -363,10 +363,6 @@ table.u2f-registrations {
color: $gl-text-color-secondary;
}
-.gitlab-slack-body {
- max-width: 420px;
-}
-
.gitlab-slack-slack-logo {
transform: scale(200%); // Slack logo SVG is scaled down 50% and has empty space around it
}
diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss
index b583d40de79..518ec181e5e 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/pages/profiles/preferences.scss
@@ -1,6 +1,6 @@
.application-theme {
- $ui-dark-bg: #2e2e2e;
- $ui-light-bg: #dfdfdf;
+ $ui-gray-bg: #2e2e2e;
+ $ui-light-gray-bg: #dfdfdf;
$ui-dark-mode-bg: #1f1f1f;
.preview {
@@ -42,13 +42,13 @@
background-color: $theme-light-red-700;
}
- &.ui-dark {
- background-color: $ui-dark-bg;
+ &.ui-gray {
+ background-color: $ui-gray-bg;
border: solid 1px $border-color;
}
- &.ui-light {
- background-color: $ui-light-bg;
+ &.ui-light-gray {
+ background-color: $ui-light-gray-bg;
}
&.gl-dark {
diff --git a/app/assets/stylesheets/snippets.scss b/app/assets/stylesheets/snippets.scss
index f79237eee3d..e1f540c0f5f 100644
--- a/app/assets/stylesheets/snippets.scss
+++ b/app/assets/stylesheets/snippets.scss
@@ -26,6 +26,7 @@
&.gl-snippet-icon-doc-code { background-position: 0 0; }
&.gl-snippet-icon-doc-text { background-position: 0 -16px; }
&.gl-snippet-icon-download { background-position: 0 -32px; }
+ &.gl-snippet-icon-copy-to-clipboard { background-position: 0 -48px; }
}
.blob-viewer {
@@ -109,6 +110,7 @@
.file-header-content {
max-width: 75%;
+ align-self: center;
.file-title-name {
font-weight: $gl-font-weight-bold;
@@ -143,6 +145,7 @@
}
.btn-group {
+ button.btn,
a.btn {
background-color: $white;
text-decoration: none;
@@ -165,5 +168,9 @@
border-right: $border-style;
}
}
+
+ button.btn {
+ padding: 9px 9px 8px;
+ }
}
}
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index 001431e517b..4cefa60b12a 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -42,10 +42,6 @@ body {
text-align: left;
background-color: #1f1f1f;
}
-h1 {
- margin-top: 0;
- margin-bottom: 0.25rem;
-}
ul {
margin-top: 0;
margin-bottom: 1rem;
@@ -105,15 +101,6 @@ button::-moz-focus-inner,
[type="search"] {
outline-offset: -2px;
}
-h1 {
- margin-bottom: 0.25rem;
- font-weight: 600;
- line-height: 1.2;
- color: #fafafa;
-}
-h1 {
- font-size: 2.1875rem;
-}
.list-unstyled {
padding-left: 0;
list-style: none;
@@ -363,9 +350,6 @@ h1 {
white-space: nowrap;
border: 0;
}
-.m-auto {
- margin: auto !important;
-}
.gl-badge {
display: inline-flex;
align-items: center;
@@ -547,10 +531,6 @@ html [type="button"],
[role="button"] {
cursor: pointer;
}
-h1 {
- margin-top: 20px;
- margin-bottom: 10px;
-}
strong {
font-weight: bold;
}
@@ -634,6 +614,10 @@ body {
.dropdown {
position: relative;
}
+.dropdown-menu-toggle:active {
+ box-shadow: 0 0 0 1px #333, 0 0 0 3px #1f75cb;
+ outline: none;
+}
.search-input-container .dropdown-menu {
margin-top: 11px;
}
@@ -684,7 +668,6 @@ body {
display: block;
text-align: left;
list-style: none;
- padding: 0 1px;
}
.dropdown-menu li > a,
.dropdown-menu li button {
@@ -710,6 +693,12 @@ body {
outline: 0;
text-decoration: none;
}
+.dropdown-menu li > a:active,
+.dropdown-menu li button:active {
+ box-shadow: inset 0 0 0 2px #1f75cb, inset 0 0 0 3px #333,
+ inset 0 0 0 1px #333;
+ outline: none;
+}
.dropdown-menu .divider {
height: 1px;
margin: 0.25rem 0;
@@ -755,7 +744,7 @@ input {
padding: 0 16px;
z-index: 1000;
margin-bottom: 0;
- min-height: var(--header-height, 40px);
+ min-height: var(--header-height, 48px);
border: 0;
position: fixed;
top: 0;
@@ -771,7 +760,7 @@ input {
display: flex;
justify-content: space-between;
position: relative;
- min-height: var(--header-height, 40px);
+ min-height: var(--header-height, 48px);
padding-left: 0;
}
.navbar-gitlab .header-content .title {
@@ -787,16 +776,17 @@ input {
.navbar-gitlab .header-content .title img {
height: 24px;
}
-.navbar-gitlab .header-content .title img + .logo-text {
- margin-left: 8px;
-}
.navbar-gitlab .header-content .title a {
display: flex;
align-items: center;
padding: 2px 8px;
- margin: 4px 2px 4px -12px;
+ margin: 4px 2px 4px -8px;
border-radius: 4px;
}
+.navbar-gitlab .header-content .title a:active {
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6), 0 0 0 3px #1068bf;
+ outline: none;
+}
.navbar-gitlab .header-content .title .canary-badge {
margin-left: -8px;
}
@@ -902,6 +892,13 @@ input {
height: 32px;
font-weight: 600;
}
+.navbar-sub-nav > li > a:active,
+.navbar-sub-nav > li > button:active,
+.navbar-nav > li > a:active,
+.navbar-nav > li > button:active {
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6), 0 0 0 3px #1068bf;
+ outline: none;
+}
.navbar-sub-nav > li .top-nav-toggle,
.navbar-sub-nav > li > button,
.navbar-nav > li .top-nav-toggle,
@@ -915,6 +912,8 @@ input {
}
.navbar-sub-nav {
display: flex;
+ align-items: center;
+ height: 100%;
margin: 0 0 0 6px;
}
.caret-down,
@@ -990,7 +989,7 @@ input {
.context-header {
position: relative;
margin-right: 2px;
- width: 220px;
+ width: 256px;
}
.context-header > a,
.context-header > button {
@@ -1015,17 +1014,17 @@ input {
}
@media (min-width: 768px) {
.page-with-contextual-sidebar {
- padding-left: 48px;
+ padding-left: 56px;
}
}
@media (min-width: 1200px) {
.page-with-contextual-sidebar {
- padding-left: 220px;
+ padding-left: 256px;
}
}
@media (min-width: 768px) {
.page-with-icon-sidebar {
- padding-left: 48px;
+ padding-left: 56px;
}
}
.nav-sidebar {
@@ -1033,13 +1032,14 @@ input {
bottom: 0;
left: 0;
z-index: 600;
- width: 220px;
- top: var(--header-height, 40px);
- background-color: #303030;
+ width: 256px;
+ top: var(--header-height, 48px);
+ background-color: #f5f5f5;
+ border-right: 1px solid #e9e9e9;
transform: translate3d(0, 0, 0);
}
.nav-sidebar.sidebar-collapsed-desktop {
- width: 48px;
+ width: 56px;
}
.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll {
overflow-x: hidden;
@@ -1092,7 +1092,7 @@ input {
border-radius: 0.25rem;
width: auto;
line-height: 1rem;
- margin: 1px 4px;
+ margin: 1px 8px;
}
.nav-sidebar li.active > a {
font-weight: 600;
@@ -1106,7 +1106,7 @@ input {
}
@media (max-width: 767.98px) {
.nav-sidebar {
- left: -220px;
+ left: -256px;
}
}
.nav-sidebar .nav-icon-container {
@@ -1228,7 +1228,7 @@ input {
}
@media (min-width: 768px) and (max-width: 1199px) {
.nav-sidebar:not(.sidebar-expanded-mobile) {
- width: 48px;
+ width: 56px;
}
.nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll {
overflow-x: hidden;
@@ -1263,7 +1263,7 @@ input {
}
.nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
height: 60px;
- width: 48px;
+ width: 56px;
}
.nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
padding: 10px 4px;
@@ -1295,7 +1295,8 @@ input {
margin-right: 0;
}
.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button {
- width: 48px;
+ width: 55px;
+ padding: 0 21px;
}
.nav-sidebar:not(.sidebar-expanded-mobile)
.toggle-sidebar-button
@@ -1328,10 +1329,10 @@ input {
border-radius: 0.25rem;
width: auto;
line-height: 1rem;
- margin: 1px 4px;
+ margin: 1px 8px;
padding: 0.25rem;
margin-bottom: 0.25rem;
- margin-top: 0;
+ margin-top: 0.125rem;
}
.nav-sidebar-inner-scroll > div.context-header a .avatar-container {
font-weight: 400;
@@ -1350,13 +1351,12 @@ input {
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items {
- margin-top: 0.25rem;
margin-bottom: 60px;
}
.sidebar-top-level-items .context-header a {
padding: 0.25rem;
margin-bottom: 0.25rem;
- margin-top: 0;
+ margin-top: 0.125rem;
}
.sidebar-top-level-items .context-header a .avatar-container {
font-weight: 400;
@@ -1402,11 +1402,10 @@ input {
color: #999;
display: flex;
align-items: center;
- background-color: #303030;
- border-top: 1px solid #404040;
+ background-color: #f5f5f5;
position: fixed;
bottom: 0;
- width: 220px;
+ width: 255px;
}
.toggle-sidebar-button .collapse-text,
.toggle-sidebar-button .icon-chevron-double-lg-left,
@@ -1420,7 +1419,7 @@ input {
}
.sidebar-collapsed-desktop .context-header {
height: 60px;
- width: 48px;
+ width: 56px;
}
.sidebar-collapsed-desktop .context-header a {
padding: 10px 4px;
@@ -1452,7 +1451,8 @@ input {
margin-right: 0;
}
.sidebar-collapsed-desktop .toggle-sidebar-button {
- width: 48px;
+ width: 55px;
+ padding: 0 21px;
}
.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text {
display: none;
@@ -1788,6 +1788,12 @@ body.gl-dark {
--svg-status-bg: #333;
--nav-active-bg: rgba(255, 255, 255, 0.08);
}
+.nav-sidebar,
+.toggle-sidebar-button,
+.close-nav-button {
+ background-color: #262626;
+ border-right: 1px solid #303030;
+}
.nav-sidebar li a {
color: var(--gray-600);
}
@@ -2044,19 +2050,9 @@ body.gl-dark {
.gl-display-none {
display: none;
}
-@media (min-width: 992px) {
- .gl-lg-display-none\! {
- display: none !important;
- }
-}
.gl-display-flex {
display: flex;
}
-@media (min-width: 992px) {
- .gl-lg-display-flex {
- display: flex;
- }
-}
@media (min-width: 576px) {
.gl-sm-display-block {
display: block;
@@ -2070,12 +2066,18 @@ body.gl-dark {
.gl-display-inline-block\! {
display: inline-block !important;
}
+.gl-align-items-center {
+ align-items: center;
+}
.gl-align-items-stretch {
align-items: stretch;
}
.gl-flex-grow-1 {
flex-grow: 1;
}
+.gl-justify-content-end {
+ justify-content: flex-end;
+}
.gl-relative {
position: relative;
}
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
index c42b5554d8d..cb3c97f18a3 100644
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -27,10 +27,6 @@ body {
text-align: left;
background-color: #fff;
}
-h1 {
- margin-top: 0;
- margin-bottom: 0.25rem;
-}
ul {
margin-top: 0;
margin-bottom: 1rem;
@@ -90,15 +86,6 @@ button::-moz-focus-inner,
[type="search"] {
outline-offset: -2px;
}
-h1 {
- margin-bottom: 0.25rem;
- font-weight: 600;
- line-height: 1.2;
- color: #303030;
-}
-h1 {
- font-size: 2.1875rem;
-}
.list-unstyled {
padding-left: 0;
list-style: none;
@@ -348,9 +335,6 @@ h1 {
white-space: nowrap;
border: 0;
}
-.m-auto {
- margin: auto !important;
-}
.gl-badge {
display: inline-flex;
align-items: center;
@@ -431,7 +415,7 @@ a.gl-badge.badge-warning:active {
.gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only,
.gl-form-input.form-control:disabled,
.gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only {
- background-color: #fafafa;
+ background-color: #f5f5f5;
box-shadow: inset 0 0 0 1px #dbdbdb;
}
.gl-form-input:disabled,
@@ -532,10 +516,6 @@ html [type="button"],
[role="button"] {
cursor: pointer;
}
-h1 {
- margin-top: 20px;
- margin-bottom: 10px;
-}
strong {
font-weight: bold;
}
@@ -619,6 +599,10 @@ body {
.dropdown {
position: relative;
}
+.dropdown-menu-toggle:active {
+ box-shadow: 0 0 0 1px #fff, 0 0 0 3px #428fdc;
+ outline: none;
+}
.search-input-container .dropdown-menu {
margin-top: 11px;
}
@@ -669,7 +653,6 @@ body {
display: block;
text-align: left;
list-style: none;
- padding: 0 1px;
}
.dropdown-menu li > a,
.dropdown-menu li button {
@@ -695,6 +678,12 @@ body {
outline: 0;
text-decoration: none;
}
+.dropdown-menu li > a:active,
+.dropdown-menu li button:active {
+ box-shadow: inset 0 0 0 2px #428fdc, inset 0 0 0 3px #fff,
+ inset 0 0 0 1px #fff;
+ outline: none;
+}
.dropdown-menu .divider {
height: 1px;
margin: 0.25rem 0;
@@ -740,7 +729,7 @@ input {
padding: 0 16px;
z-index: 1000;
margin-bottom: 0;
- min-height: var(--header-height, 40px);
+ min-height: var(--header-height, 48px);
border: 0;
position: fixed;
top: 0;
@@ -756,7 +745,7 @@ input {
display: flex;
justify-content: space-between;
position: relative;
- min-height: var(--header-height, 40px);
+ min-height: var(--header-height, 48px);
padding-left: 0;
}
.navbar-gitlab .header-content .title {
@@ -772,16 +761,17 @@ input {
.navbar-gitlab .header-content .title img {
height: 24px;
}
-.navbar-gitlab .header-content .title img + .logo-text {
- margin-left: 8px;
-}
.navbar-gitlab .header-content .title a {
display: flex;
align-items: center;
padding: 2px 8px;
- margin: 4px 2px 4px -12px;
+ margin: 4px 2px 4px -8px;
border-radius: 4px;
}
+.navbar-gitlab .header-content .title a:active {
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 0 0 3px #63a6e9;
+ outline: none;
+}
.navbar-gitlab .header-content .title .canary-badge {
margin-left: -8px;
}
@@ -887,6 +877,13 @@ input {
height: 32px;
font-weight: 600;
}
+.navbar-sub-nav > li > a:active,
+.navbar-sub-nav > li > button:active,
+.navbar-nav > li > a:active,
+.navbar-nav > li > button:active {
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 0 0 3px #63a6e9;
+ outline: none;
+}
.navbar-sub-nav > li .top-nav-toggle,
.navbar-sub-nav > li > button,
.navbar-nav > li .top-nav-toggle,
@@ -900,6 +897,8 @@ input {
}
.navbar-sub-nav {
display: flex;
+ align-items: center;
+ height: 100%;
margin: 0 0 0 6px;
}
.caret-down,
@@ -975,7 +974,7 @@ input {
.context-header {
position: relative;
margin-right: 2px;
- width: 220px;
+ width: 256px;
}
.context-header > a,
.context-header > button {
@@ -1000,17 +999,17 @@ input {
}
@media (min-width: 768px) {
.page-with-contextual-sidebar {
- padding-left: 48px;
+ padding-left: 56px;
}
}
@media (min-width: 1200px) {
.page-with-contextual-sidebar {
- padding-left: 220px;
+ padding-left: 256px;
}
}
@media (min-width: 768px) {
.page-with-icon-sidebar {
- padding-left: 48px;
+ padding-left: 56px;
}
}
.nav-sidebar {
@@ -1018,13 +1017,14 @@ input {
bottom: 0;
left: 0;
z-index: 600;
- width: 220px;
- top: var(--header-height, 40px);
- background-color: #f0f0f0;
+ width: 256px;
+ top: var(--header-height, 48px);
+ background-color: #f5f5f5;
+ border-right: 1px solid #e9e9e9;
transform: translate3d(0, 0, 0);
}
.nav-sidebar.sidebar-collapsed-desktop {
- width: 48px;
+ width: 56px;
}
.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll {
overflow-x: hidden;
@@ -1077,7 +1077,7 @@ input {
border-radius: 0.25rem;
width: auto;
line-height: 1rem;
- margin: 1px 4px;
+ margin: 1px 8px;
}
.nav-sidebar li.active > a {
font-weight: 600;
@@ -1091,7 +1091,7 @@ input {
}
@media (max-width: 767.98px) {
.nav-sidebar {
- left: -220px;
+ left: -256px;
}
}
.nav-sidebar .nav-icon-container {
@@ -1213,7 +1213,7 @@ input {
}
@media (min-width: 768px) and (max-width: 1199px) {
.nav-sidebar:not(.sidebar-expanded-mobile) {
- width: 48px;
+ width: 56px;
}
.nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll {
overflow-x: hidden;
@@ -1248,7 +1248,7 @@ input {
}
.nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
height: 60px;
- width: 48px;
+ width: 56px;
}
.nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
padding: 10px 4px;
@@ -1280,7 +1280,8 @@ input {
margin-right: 0;
}
.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button {
- width: 48px;
+ width: 55px;
+ padding: 0 21px;
}
.nav-sidebar:not(.sidebar-expanded-mobile)
.toggle-sidebar-button
@@ -1313,10 +1314,10 @@ input {
border-radius: 0.25rem;
width: auto;
line-height: 1rem;
- margin: 1px 4px;
+ margin: 1px 8px;
padding: 0.25rem;
margin-bottom: 0.25rem;
- margin-top: 0;
+ margin-top: 0.125rem;
}
.nav-sidebar-inner-scroll > div.context-header a .avatar-container {
font-weight: 400;
@@ -1335,13 +1336,12 @@ input {
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items {
- margin-top: 0.25rem;
margin-bottom: 60px;
}
.sidebar-top-level-items .context-header a {
padding: 0.25rem;
margin-bottom: 0.25rem;
- margin-top: 0;
+ margin-top: 0.125rem;
}
.sidebar-top-level-items .context-header a .avatar-container {
font-weight: 400;
@@ -1387,11 +1387,10 @@ input {
color: #666;
display: flex;
align-items: center;
- background-color: #f0f0f0;
- border-top: 1px solid #dbdbdb;
+ background-color: #f5f5f5;
position: fixed;
bottom: 0;
- width: 220px;
+ width: 255px;
}
.toggle-sidebar-button .collapse-text,
.toggle-sidebar-button .icon-chevron-double-lg-left,
@@ -1405,7 +1404,7 @@ input {
}
.sidebar-collapsed-desktop .context-header {
height: 60px;
- width: 48px;
+ width: 56px;
}
.sidebar-collapsed-desktop .context-header a {
padding: 10px 4px;
@@ -1437,7 +1436,8 @@ input {
margin-right: 0;
}
.sidebar-collapsed-desktop .toggle-sidebar-button {
- width: 48px;
+ width: 55px;
+ padding: 0 21px;
}
.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text {
display: none;
@@ -1704,19 +1704,9 @@ svg.s16 {
.gl-display-none {
display: none;
}
-@media (min-width: 992px) {
- .gl-lg-display-none\! {
- display: none !important;
- }
-}
.gl-display-flex {
display: flex;
}
-@media (min-width: 992px) {
- .gl-lg-display-flex {
- display: flex;
- }
-}
@media (min-width: 576px) {
.gl-sm-display-block {
display: block;
@@ -1730,12 +1720,18 @@ svg.s16 {
.gl-display-inline-block\! {
display: inline-block !important;
}
+.gl-align-items-center {
+ align-items: center;
+}
.gl-align-items-stretch {
align-items: stretch;
}
.gl-flex-grow-1 {
flex-grow: 1;
}
+.gl-justify-content-end {
+ justify-content: flex-end;
+}
.gl-relative {
position: relative;
}
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
index 020ed9c040b..3090edfb123 100644
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -297,7 +297,7 @@ fieldset:disabled a.btn {
.gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only,
.gl-form-input.form-control:disabled,
.gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only {
- background-color: #fafafa;
+ background-color: #f5f5f5;
box-shadow: inset 0 0 0 1px #dbdbdb;
}
.gl-form-input:disabled,
@@ -419,7 +419,7 @@ body.navless {
}
}
.navless-container {
- margin-top: var(--header-height, 40px);
+ margin-top: var(--header-height, 48px);
padding-top: 32px;
}
.btn {
@@ -506,7 +506,7 @@ label.label-bold {
}
.navbar-empty {
justify-content: center;
- height: var(--header-height, 40px);
+ height: var(--header-height, 48px);
background: #fff;
border-bottom: 1px solid #dbdbdb;
}
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index 6a9e96c3ac5..fe8a5aec1b3 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -209,7 +209,6 @@ body.gl-dark {
&.btn-info,
&.btn-success,
&.btn-danger,
- &.btn-warning,
&.btn-confirm {
&-tertiary {
mix-blend-mode: screen;
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index dbb961fe71f..34bb4925249 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -41,6 +41,13 @@
border-color: $gray-800;
}
+.nav-sidebar,
+.toggle-sidebar-button,
+.close-nav-button {
+ background-color: darken($gray-50, 4%);
+ border-right: 1px solid $gray-50;
+}
+
.nav-sidebar {
li {
a {
@@ -68,6 +75,11 @@
}
}
+aside.right-sidebar:not(.right-sidebar-merge-requests) {
+ background-color: $gray-10;
+ border-left-color: $gray-50;
+}
+
body.gl-dark {
@include gitlab-theme($gray-900, $gray-400, $gray-500, $gray-900, $gray-900, $white);
diff --git a/app/assets/stylesheets/themes/theme_dark.scss b/app/assets/stylesheets/themes/theme_gray.scss
index 4c52cdc30df..75b111f90c7 100644
--- a/app/assets/stylesheets/themes/theme_dark.scss
+++ b/app/assets/stylesheets/themes/theme_gray.scss
@@ -1,7 +1,7 @@
@import './theme_helper';
body {
- &.ui-dark {
+ &.ui-gray {
@include gitlab-theme(
$gray-200,
$gray-300,
diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss
index 234010074aa..ad352f0022b 100644
--- a/app/assets/stylesheets/themes/theme_helper.scss
+++ b/app/assets/stylesheets/themes/theme_helper.scss
@@ -97,8 +97,7 @@
.notification-dot {
will-change: border-color, background-color;
- // stylelint-disable-next-line
- border-color: $nav-svg-color + 33;
+ border-color: adjust-color($nav-svg-color, $red: 33, $green: 33, $blue: 33);
}
&.header-help-dropdown-toggle .notification-dot {
diff --git a/app/assets/stylesheets/themes/theme_light.scss b/app/assets/stylesheets/themes/theme_light_gray.scss
index 66b2b3c3437..ad19438d79a 100644
--- a/app/assets/stylesheets/themes/theme_light.scss
+++ b/app/assets/stylesheets/themes/theme_light_gray.scss
@@ -1,7 +1,7 @@
@import './theme_helper';
body {
- &.ui-light {
+ &.ui-light-gray {
@include gitlab-theme(
$gray-500,
$gray-700,
@@ -33,6 +33,14 @@ body {
&.active > button {
color: $white;
}
+
+ > a,
+ > button {
+ &:active,
+ &:focus {
+ @include gl-focus;
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index fd85ff894a7..27fcade548f 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -368,28 +368,49 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
}
/*
- * The below style will be moved to @gitlab/ui by
- * https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1792
+ * The styles from here to END-#1825 will be moved to @gitlab/ui by
+ * https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1825
*/
-.gl-text-purple-800 {
- color: $purple-800;
+.gl-lg-mx-12 {
+ @include media-breakpoint-up(lg) {
+ margin-left: $gl-spacing-scale-12;
+ margin-right: $gl-spacing-scale-12;
+ }
}
-.gl-bg-theme-indigo-800 {
- background-color: $theme-indigo-800;
+.gl-lg-ml-12 {
+ @include media-breakpoint-up(lg) {
+ margin-left: $gl-spacing-scale-12;
+ }
}
-.gl-border-indigo-700 {
- border-color: $theme-indigo-700;
+.gl-lg-mr-12 {
+ @include media-breakpoint-up(lg) {
+ margin-right: $gl-spacing-scale-12;
+ }
}
-.gl-border-gray-75 {
- border-color: $gl-text-color-quaternary;
+.gl-lg-ml-10 {
+ @include media-breakpoint-up(lg) {
+ margin-left: $gl-spacing-scale-10;
+ }
}
-.gl-min-h-8 {
- min-height: $gl-spacing-scale-8;
+.gl-lg-mr-10 {
+ @include media-breakpoint-up(lg) {
+ margin-right: $gl-spacing-scale-10;
+ }
}
-/* End gitlab-ui#1751 */
+.gl-lg-w-30p {
+ @include gl-media-breakpoint-up(lg) {
+ width: 30%;
+ }
+}
+.gl-lg-w-40p {
+ @include gl-media-breakpoint-up(lg) {
+ width: 40%;
+ }
+}
+/* END-#1825 */
diff --git a/app/components/diffs/overflow_warning_component.html.haml b/app/components/diffs/overflow_warning_component.html.haml
index b184fa1d527..b334bfbcd89 100644
--- a/app/components/diffs/overflow_warning_component.html.haml
+++ b/app/components/diffs/overflow_warning_component.html.haml
@@ -1,6 +1,6 @@
= render Pajamas::AlertComponent.new(title: _('Too many changes to show.'),
variant: :warning,
- alert_class: 'gl-mb-5') do |c|
+ alert_options: { class: 'gl-mb-5' }) do |c|
= c.body do
= message
diff --git a/app/components/diffs/overflow_warning_component.rb b/app/components/diffs/overflow_warning_component.rb
index 0d0e225beb4..5123809cfdc 100644
--- a/app/components/diffs/overflow_warning_component.rb
+++ b/app/components/diffs/overflow_warning_component.rb
@@ -2,12 +2,6 @@
module Diffs
class OverflowWarningComponent < BaseComponent
- # Skipping coverage because of https://gitlab.com/gitlab-org/gitlab/-/issues/357381
- #
- # This is fully tested by the output in the view part of this component,
- # but undercoverage doesn't understand the relationship between the two parts.
- #
- # :nocov:
def initialize(diffs:, diff_files:, project:, commit: nil, merge_request: nil)
@diffs = diffs
@diff_files = diff_files
@@ -68,6 +62,5 @@ module Diffs
def button_classes
"btn gl-alert-action btn-default gl-button btn-default-secondary"
end
- # :nocov:
end
end
diff --git a/app/components/diffs/stats_component.rb b/app/components/diffs/stats_component.rb
index 55589c7b015..74788133aa2 100644
--- a/app/components/diffs/stats_component.rb
+++ b/app/components/diffs/stats_component.rb
@@ -28,13 +28,6 @@ module Diffs
Gitlab::Json.dump(diffs_map)
end
- # Disabled undercoverage reports for this method
- # as it returns a false positive on the last line,
- # which is covered in the tests
- #
- # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/357381
- #
- # :nocov:
def diff_file_path_text(diff_file, max: 60)
path = diff_file.new_path
@@ -42,7 +35,6 @@ module Diffs
"...#{path[-(max - 3)..]}"
end
- # :nocov:
private
diff --git a/app/components/pajamas/alert_component.html.haml b/app/components/pajamas/alert_component.html.haml
index 782ac8b9ca2..13c458f05e9 100644
--- a/app/components/pajamas/alert_component.html.haml
+++ b/app/components/pajamas/alert_component.html.haml
@@ -1,11 +1,10 @@
-.gl-alert{ role: 'alert', class: [base_class, @alert_class], data: @alert_data }
+.gl-alert{ @alert_options, role: 'alert', class: base_class }
- if @show_icon
= sprite_icon(icon, css_class: icon_classes)
- if @dismissible
- %button.btn.gl-dismiss-btn.btn-default.btn-sm.gl-button.btn-default-tertiary.btn-icon.js-close{ type: 'button',
- aria: { label: _('Dismiss') },
- class: @close_button_class,
- data: @close_button_data }
+ %button.btn.gl-dismiss-btn.btn-default.btn-sm.gl-button.btn-default-tertiary.btn-icon.js-close{ @close_button_options,
+ type: 'button',
+ aria: { label: _('Dismiss') } }
= sprite_icon('close')
.gl-alert-content{ role: 'alert' }
- if @title
diff --git a/app/components/pajamas/alert_component.rb b/app/components/pajamas/alert_component.rb
index c1b2132da29..cfab34f537e 100644
--- a/app/components/pajamas/alert_component.rb
+++ b/app/components/pajamas/alert_component.rb
@@ -7,21 +7,17 @@ module Pajamas
# @param [Symbol] variant
# @param [Boolean] dismissible
# @param [Boolean] show_icon
- # @param [String] alert_class
- # @param [Hash] alert_data
- # @param [String] close_button_class
- # @param [Hash] close_button_data
+ # @param [Hash] alert_options
+ # @param [Hash] close_button_options
def initialize(
title: nil, variant: :info, dismissible: true, show_icon: true,
- alert_class: nil, alert_data: {}, close_button_class: nil, close_button_data: {})
+ alert_options: {}, close_button_options: {})
@title = title
@variant = variant
@dismissible = dismissible
@show_icon = show_icon
- @alert_class = alert_class
- @alert_data = alert_data
- @close_button_class = close_button_class
- @close_button_data = close_button_data
+ @alert_options = alert_options
+ @close_button_options = close_button_options
end
def base_class
diff --git a/app/components/pajamas/banner_component.html.haml b/app/components/pajamas/banner_component.html.haml
new file mode 100644
index 00000000000..4fa2ed09cd3
--- /dev/null
+++ b/app/components/pajamas/banner_component.html.haml
@@ -0,0 +1,23 @@
+%section.gl-banner{ @banner_options, class: banner_class }
+ - if illustration?
+ .gl-banner-illustration
+ = illustration
+ - elsif @svg_path.present?
+ .gl-banner-illustration
+ = image_tag @svg_path, alt: ""
+
+ .gl-banner-content
+ %h1.gl-banner-title= title
+
+ = content
+
+ - if primary_action?
+ = primary_action
+ - else
+ = link_to @button_text, @button_link, { **@button_options, class: 'btn btn-md btn-confirm gl-button js-close-callout' }
+
+ - actions.each do |action|
+ = action
+
+ %button.gl-button.gl-banner-close.btn-sm.btn-icon.js-close{ @close_options, class: close_class, type: 'button' }
+ = sprite_icon('close', size: 16, css_class: 'dismiss-icon')
diff --git a/app/components/pajamas/banner_component.rb b/app/components/pajamas/banner_component.rb
new file mode 100644
index 00000000000..9b6343b47c9
--- /dev/null
+++ b/app/components/pajamas/banner_component.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Pajamas
+ class BannerComponent < Pajamas::Component
+ # @param [String] button_text
+ # @param [String] button_link
+ # @param [Boolean] embedded
+ # @param [Symbol] variant
+ # @param [String] svg_path
+ # @param [Hash] banner_options
+ # @param [Hash] button_options
+ # @param [Hash] close_options
+ def initialize(
+ button_text: 'OK',
+ button_link: '#',
+ embedded: false,
+ variant: :promotion,
+ svg_path: nil,
+ banner_options: {},
+ button_options: {},
+ close_options: {}
+ )
+ @button_text = button_text
+ @button_link = button_link
+ @embedded = embedded
+ @variant = variant.to_sym
+ @svg_path = svg_path.to_s
+ @banner_options = banner_options
+ @button_options = button_options
+ @close_options = close_options
+ end
+
+ private
+
+ def banner_class
+ classes = []
+ classes.push('gl-border-none') if @embedded
+ classes.push('gl-banner-introduction') if introduction?
+ classes.join(' ')
+ end
+
+ def close_class
+ if introduction?
+ 'btn-confirm btn-confirm-tertiary'
+ else
+ 'btn-default btn-default-tertiary'
+ end
+ end
+
+ delegate :sprite_icon, to: :helpers
+
+ renders_one :title
+ renders_one :illustration
+ renders_one :primary_action
+ renders_many :actions
+
+ def introduction?
+ @variant == :introduction
+ end
+ end
+end
diff --git a/app/components/pajamas/button_component.html.haml b/app/components/pajamas/button_component.html.haml
new file mode 100644
index 00000000000..8ce7d9e0315
--- /dev/null
+++ b/app/components/pajamas/button_component.html.haml
@@ -0,0 +1,8 @@
+= content_tag tag, {**@button_options, **base_attributes, class: button_class, href: @href, target: @target } do
+ - if @loading
+ = gl_loading_icon(inline: true, css_class: 'gl-button-icon gl-button-loading-indicator')
+ - if @icon && (!@loading || content)
+ = sprite_icon(@icon, css_class: "gl-icon gl-button-icon #{@icon_classes}")
+ - if content
+ %span.gl-button-text{ class: @button_text_classes }
+ = content
diff --git a/app/components/pajamas/button_component.rb b/app/components/pajamas/button_component.rb
new file mode 100644
index 00000000000..c6193d1ae05
--- /dev/null
+++ b/app/components/pajamas/button_component.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+module Pajamas
+ class ButtonComponent < Pajamas::Component
+ # @param [Symbol] category
+ # @param [Symbol] variant
+ # @param [Symbol] size
+ # @param [Symbol] type
+ # @param [Boolean] disabled
+ # @param [Boolean] loading
+ # @param [Boolean] block
+ # @param [Boolean] selected
+ # @param [String] icon
+ # @param [String] href
+ # @param [String] target
+ # @param [Hash] button_options
+ # @param [String] button_text_classes
+ # @param [String] icon_classes
+ def initialize(
+ category: :primary,
+ variant: :default,
+ size: :medium,
+ type: :button,
+ disabled: false,
+ loading: false,
+ block: false,
+ selected: false,
+ icon: nil,
+ href: nil,
+ target: nil,
+ button_options: {},
+ button_text_classes: nil,
+ icon_classes: nil
+ )
+ @category = filter_attribute(category.to_sym, CATEGORY_OPTIONS)
+ @variant = filter_attribute(variant.to_sym, VARIANT_OPTIONS)
+ @size = filter_attribute(size.to_sym, SIZE_OPTIONS)
+ @type = filter_attribute(type.to_sym, TYPE_OPTIONS, default: :button)
+ @disabled = disabled
+ @loading = loading
+ @block = block
+ @selected = selected
+ @icon = icon
+ @href = href
+ @target = filter_attribute(target, TARGET_OPTIONS)
+ @button_options = button_options
+ @button_text_classes = button_text_classes
+ @icon_classes = icon_classes
+ end
+
+ private
+
+ def button_class
+ classes = ['gl-button btn']
+ classes.push('disabled') if @disabled || @loading
+ classes.push('selected') if @selected
+ classes.push('btn-block') if @block
+ classes.push('btn-icon') if @icon && !content
+
+ classes.push(SIZE_CLASSES[@size])
+
+ classes.push(VARIANT_CLASSES[@variant])
+
+ unless NON_CATEGORY_VARIANTS.include?(@variant) || @category == :primary
+ classes.push(VARIANT_CLASSES[@variant] + '-' + CATEGORY_CLASSES[@category])
+ end
+
+ classes.push(@button_options[:class])
+
+ classes.join(' ')
+ end
+
+ CATEGORY_OPTIONS = [:primary, :secondary, :tertiary].freeze
+ VARIANT_OPTIONS = [:default, :confirm, :danger, :dashed, :link, :reset].freeze
+ SIZE_OPTIONS = [:small, :medium].freeze
+ TYPE_OPTIONS = [:button, :reset, :submit].freeze
+ TARGET_OPTIONS = %w[_self _blank _parent _top].freeze
+
+ CATEGORY_CLASSES = {
+ primary: '',
+ secondary: 'secondary',
+ tertiary: 'tertiary'
+ }.freeze
+
+ VARIANT_CLASSES = {
+ default: 'btn-default',
+ confirm: 'btn-confirm',
+ danger: 'btn-danger',
+ dashed: 'btn-dashed',
+ link: 'btn-link',
+ reset: 'btn-gl-reset'
+ }.freeze
+
+ NON_CATEGORY_VARIANTS = [:dashed, :link, :reset].freeze
+
+ SIZE_CLASSES = {
+ small: 'btn-sm',
+ medium: 'btn-md'
+ }.freeze
+
+ delegate :sprite_icon, to: :helpers
+ delegate :gl_loading_icon, to: :helpers
+
+ def tag
+ @href ? 'a' : 'button'
+ end
+
+ def base_attributes
+ attributes = {}
+
+ attributes['disabled'] = '' if @disabled || @loading
+ attributes['aria-disabled'] = true if @disabled || @loading
+ attributes['type'] = @type unless @href
+
+ attributes
+ end
+ end
+end
diff --git a/app/components/pajamas/card_component.html.haml b/app/components/pajamas/card_component.html.haml
new file mode 100644
index 00000000000..007229cc69f
--- /dev/null
+++ b/app/components/pajamas/card_component.html.haml
@@ -0,0 +1,9 @@
+.gl-card{ @card_options }
+ - if header?
+ .gl-card-header{ @header_options }
+ = header
+ .gl-card-body{ @body_options }
+ = body
+ - if footer?
+ .gl-card-footer{ @footer_options }
+ = footer
diff --git a/app/components/pajamas/card_component.rb b/app/components/pajamas/card_component.rb
new file mode 100644
index 00000000000..bcc71db1c34
--- /dev/null
+++ b/app/components/pajamas/card_component.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# Renders a GlCard root element
+module Pajamas
+ class CardComponent < Pajamas::Component
+ # @param [Hash] card_options
+ # @param [Hash] header_options
+ # @param [Hash] body_options
+ # @param [Hash] footer_options
+ def initialize(card_options: {}, header_options: {}, body_options: {}, footer_options: {})
+ @card_options = card_options
+ @header_options = header_options
+ @body_options = body_options
+ @footer_options = footer_options
+ end
+
+ renders_one :header
+ renders_one :body
+ renders_one :footer
+ end
+end
diff --git a/app/components/pajamas/checkbox_component.html.haml b/app/components/pajamas/checkbox_component.html.haml
new file mode 100644
index 00000000000..9e3d4e68a42
--- /dev/null
+++ b/app/components/pajamas/checkbox_component.html.haml
@@ -0,0 +1,6 @@
+.gl-form-checkbox.custom-control.custom-checkbox
+ = form.check_box(method,
+ formatted_input_options,
+ checked_value,
+ unchecked_value)
+ = render_label_with_help_text
diff --git a/app/components/pajamas/checkbox_component.rb b/app/components/pajamas/checkbox_component.rb
new file mode 100644
index 00000000000..ae78d0453f8
--- /dev/null
+++ b/app/components/pajamas/checkbox_component.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+# Renders a Pajamas compliant checkbox element
+# Must be used in an instance of `ActionView::Helpers::FormBuilder`
+module Pajamas
+ class CheckboxComponent < Pajamas::Component
+ include Pajamas::Concerns::CheckboxRadioLabelWithHelpText
+ include Pajamas::Concerns::CheckboxRadioOptions
+
+ renders_one :label
+ renders_one :help_text
+
+ def initialize(
+ form:,
+ method:,
+ label: nil,
+ help_text: nil,
+ label_options: {},
+ checkbox_options: {},
+ checked_value: '1',
+ unchecked_value: '0'
+ )
+ @form = form
+ @method = method
+ @label_argument = label
+ @help_text_argument = help_text
+ @label_options = label_options
+ @input_options = checkbox_options
+ @checked_value = checked_value
+ @unchecked_value = unchecked_value
+ @value = checked_value if checkbox_options[:multiple]
+ end
+
+ attr_reader(
+ :form,
+ :method,
+ :label_argument,
+ :help_text_argument,
+ :label_options,
+ :input_options,
+ :checked_value,
+ :unchecked_value,
+ :value
+ )
+
+ private
+
+ def label_content
+ label? ? label : label_argument
+ end
+
+ def help_text_content
+ help_text? ? help_text : help_text_argument
+ end
+ end
+end
diff --git a/app/components/pajamas/component.rb b/app/components/pajamas/component.rb
index b05d93b680e..3b1826a646c 100644
--- a/app/components/pajamas/component.rb
+++ b/app/components/pajamas/component.rb
@@ -4,8 +4,6 @@ module Pajamas
class Component < ViewComponent::Base
private
- # :nocov:
-
# Filter a given a value against a list of allowed values
# If no value is given or value is not allowed return default one
#
@@ -18,6 +16,14 @@ module Pajamas
default
end
- # :nocov:
+
+ # Add CSS classes and additional options to an existing options hash
+ #
+ # @param [Hash] options
+ # @param [Array] css_classes
+ # @param [Hash] additional_option
+ def format_options(options:, css_classes: [], additional_options: {})
+ options.merge({ class: [*css_classes, options[:class]].flatten.compact }, additional_options)
+ end
end
end
diff --git a/app/components/pajamas/concerns/checkbox_radio_label_with_help_text.rb b/app/components/pajamas/concerns/checkbox_radio_label_with_help_text.rb
new file mode 100644
index 00000000000..4ece904fb85
--- /dev/null
+++ b/app/components/pajamas/concerns/checkbox_radio_label_with_help_text.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Pajamas
+ module Concerns
+ module CheckboxRadioLabelWithHelpText
+ def render_label_with_help_text
+ form.label(method, formatted_label_options) { label_entry }
+ end
+
+ private
+
+ def label_entry
+ if help_text_content
+ content_tag(:span, label_content) +
+ content_tag(:p, help_text_content, class: 'help-text', data: { testid: 'pajamas-component-help-text' })
+ else
+ content_tag(:span, label_content)
+ end
+ end
+
+ def formatted_label_options
+ format_options(
+ options: label_options,
+ css_classes: ['custom-control-label'],
+ additional_options: { value: value }
+ )
+ end
+ end
+ end
+end
diff --git a/app/components/pajamas/concerns/checkbox_radio_options.rb b/app/components/pajamas/concerns/checkbox_radio_options.rb
new file mode 100644
index 00000000000..e79fdb7b601
--- /dev/null
+++ b/app/components/pajamas/concerns/checkbox_radio_options.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Pajamas
+ module Concerns
+ module CheckboxRadioOptions
+ def formatted_input_options
+ format_options(options: input_options, css_classes: ['custom-control-input'])
+ end
+ end
+ end
+end
diff --git a/app/components/pajamas/radio_component.html.haml b/app/components/pajamas/radio_component.html.haml
new file mode 100644
index 00000000000..6bf57b0b187
--- /dev/null
+++ b/app/components/pajamas/radio_component.html.haml
@@ -0,0 +1,5 @@
+.gl-form-radio.custom-control.custom-radio
+ = form.radio_button(method,
+ value,
+ formatted_input_options)
+ = render_label_with_help_text
diff --git a/app/components/pajamas/radio_component.rb b/app/components/pajamas/radio_component.rb
new file mode 100644
index 00000000000..52a761b9d7d
--- /dev/null
+++ b/app/components/pajamas/radio_component.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+# Renders a Pajamas compliant radio button element
+# Must be used in an instance of `ActionView::Helpers::FormBuilder`
+module Pajamas
+ class RadioComponent < Pajamas::Component
+ include Pajamas::Concerns::CheckboxRadioLabelWithHelpText
+ include Pajamas::Concerns::CheckboxRadioOptions
+
+ renders_one :label
+ renders_one :help_text
+
+ def initialize(
+ form:,
+ method:,
+ label: nil,
+ help_text: nil,
+ label_options: {},
+ radio_options: {},
+ value: nil
+ )
+ @form = form
+ @method = method
+ @label_argument = label
+ @help_text_argument = help_text
+ @label_options = label_options
+ @input_options = radio_options
+ @value = value
+ end
+
+ attr_reader(
+ :form,
+ :method,
+ :label_argument,
+ :help_text_argument,
+ :label_options,
+ :input_options,
+ :value
+ )
+
+ private
+
+ def label_content
+ label? ? label : label_argument
+ end
+
+ def help_text_content
+ help_text? ? help_text : help_text_argument
+ end
+ end
+end
diff --git a/app/controllers/admin/application_settings/appearances_controller.rb b/app/controllers/admin/application_settings/appearances_controller.rb
index 47b2356a60f..cf765c96a8f 100644
--- a/app/controllers/admin/application_settings/appearances_controller.rb
+++ b/app/controllers/admin/application_settings/appearances_controller.rb
@@ -4,6 +4,7 @@ class Admin::ApplicationSettings::AppearancesController < Admin::ApplicationCont
before_action :set_appearance, except: :create
feature_category :navigation
+ urgency :low
def show
end
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 253fca0a253..7f95b136e4e 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -24,6 +24,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:delete_self_monitoring_project,
:status_delete_self_monitoring_project
]
+ urgency :low, [
+ :create_self_monitoring_project,
+ :status_create_self_monitoring_project,
+ :delete_self_monitoring_project,
+ :status_delete_self_monitoring_project
+ ]
feature_category :source_code_management, [:repository, :clear_repository_check_states]
feature_category :continuous_integration, [:ci_cd, :reset_registration_token]
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index 8b672929f88..865af244773 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -6,6 +6,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
before_action :finder, only: [:edit, :update, :destroy]
feature_category :navigation
+ urgency :low
# rubocop: disable CodeReuse/ActiveRecord
def index
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index 20e36e5fd84..8fe106249c3 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -7,7 +7,6 @@ class Admin::DashboardController < Admin::ApplicationController
feature_category :not_owned # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
- # rubocop: disable CodeReuse/ActiveRecord
def index
@counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS)
@projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@@ -24,7 +23,6 @@ class Admin::DashboardController < Admin::ApplicationController
Gitlab::Redis::Sessions
].map(&:version).uniq
end
- # rubocop: enable CodeReuse/ActiveRecord
def stats
@users_statistics = UsersStatistics.latest
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 4d163824ef6..2ae0442c005 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -108,6 +108,7 @@ class Admin::GroupsController < Admin::ApplicationController
:visibility_level,
:require_two_factor_authentication,
:two_factor_grace_period,
+ :enabled_git_access_protocol,
:project_creation_level,
:subgroup_creation_level,
admin_note_attributes: [
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index 02e33baaf07..24d7bd9ca7b 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -98,3 +98,5 @@ class Admin::RunnersController < Admin::ApplicationController
end
# rubocop: enable CodeReuse/ActiveRecord
end
+
+Admin::RunnersController.prepend_mod
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 6b11b8eda5c..874eb8985fb 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -9,7 +9,7 @@ class Admin::UsersController < Admin::ApplicationController
before_action :ensure_destroy_prerequisites_met, only: [:destroy]
before_action :check_ban_user_feature_flag, only: [:ban]
- feature_category :users
+ feature_category :user_management
PAGINATION_WITH_COUNT_LIMIT = 1000
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 4fc96752507..30760d472a4 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -25,6 +25,7 @@ class ApplicationController < ActionController::Base
include FlocOptOut
include CheckRateLimit
+ before_action :limit_session_time, if: -> { !current_user }
before_action :authenticate_user!, except: [:route_not_found]
before_action :enforce_terms!, if: :should_enforce_terms?
before_action :validate_user_service_ticket!
@@ -43,7 +44,6 @@ class ApplicationController < ActionController::Base
# Make sure the `auth_user` is memoized so it can be logged, we do this after
# all other before filters that could have set the user.
before_action :auth_user
- before_action :limit_session_time, if: -> { !current_user }
prepend_around_action :set_current_context
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index f84d2ed320d..32d1ddf920e 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -62,7 +62,9 @@ class AutocompleteController < ApplicationController
def deploy_keys_with_owners
deploy_keys = DeployKey.with_write_access_for_project(project)
- render json: DeployKeySerializer.new.represent(deploy_keys, { with_owner: true, user: current_user })
+ render json: DeployKeys::BasicDeployKeySerializer.new.represent(
+ deploy_keys, { with_owner: true, user: current_user }
+ )
end
private
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index ae3b6125bde..a04fd09aa22 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -8,7 +8,7 @@ class Clusters::ClustersController < Clusters::BaseController
before_action :cluster, only: [:cluster_status, :show, :update, :destroy, :clear_cache]
before_action :user_cluster, only: [:connect]
before_action :authorize_read_cluster!, only: [:show, :index]
- before_action :authorize_create_cluster!, only: [:connect, :authorize_aws_role]
+ before_action :authorize_create_cluster!, only: [:connect]
before_action :authorize_update_cluster!, only: [:update]
before_action :update_applications_status, only: [:cluster_status]
before_action :ensure_feature_enabled!, except: [:index, :new_cluster_docs]
@@ -16,15 +16,6 @@ class Clusters::ClustersController < Clusters::BaseController
helper_method :token_in_session
STATUS_POLLING_INTERVAL = 10_000
- AWS_CSP_DOMAINS = %w[https://ec2.ap-east-1.amazonaws.com https://ec2.ap-northeast-1.amazonaws.com https://ec2.ap-northeast-2.amazonaws.com https://ec2.ap-northeast-3.amazonaws.com https://ec2.ap-south-1.amazonaws.com https://ec2.ap-southeast-1.amazonaws.com https://ec2.ap-southeast-2.amazonaws.com https://ec2.ca-central-1.amazonaws.com https://ec2.eu-central-1.amazonaws.com https://ec2.eu-north-1.amazonaws.com https://ec2.eu-west-1.amazonaws.com https://ec2.eu-west-2.amazonaws.com https://ec2.eu-west-3.amazonaws.com https://ec2.me-south-1.amazonaws.com https://ec2.sa-east-1.amazonaws.com https://ec2.us-east-1.amazonaws.com https://ec2.us-east-2.amazonaws.com https://ec2.us-west-1.amazonaws.com https://ec2.us-west-2.amazonaws.com https://ec2.af-south-1.amazonaws.com https://iam.amazonaws.com].freeze
-
- content_security_policy do |p|
- next if p.directives.blank?
-
- default_connect_src = p.directives['connect-src'] || p.directives['default-src']
- connect_src_values = Array.wrap(default_connect_src) | AWS_CSP_DOMAINS
- p.connect_src(*connect_src_values)
- end
def index
@clusters = cluster_list
@@ -95,19 +86,6 @@ class Clusters::ClustersController < Clusters::BaseController
redirect_to clusterable.index_path, status: :found
end
- def create_aws
- @aws_cluster = ::Clusters::CreateService
- .new(current_user, create_aws_cluster_params)
- .execute
- .present(current_user: current_user)
-
- if @aws_cluster.persisted?
- head :created, location: @aws_cluster.show_path
- else
- render status: :unprocessable_entity, json: @aws_cluster.errors
- end
- end
-
def create_user
@user_cluster = ::Clusters::CreateService
.new(current_user, create_user_cluster_params)
@@ -117,23 +95,10 @@ class Clusters::ClustersController < Clusters::BaseController
if @user_cluster.persisted?
redirect_to @user_cluster.show_path
else
- generate_gcp_authorize_url
- validate_gcp_token
- gcp_cluster
-
render :connect
end
end
- def authorize_aws_role
- response = Clusters::Aws::AuthorizeRoleService.new(
- current_user,
- params: aws_role_params
- ).execute
-
- render json: response.body, status: response.status
- end
-
def clear_cache
cluster.delete_cached_resources!
@@ -204,27 +169,6 @@ class Clusters::ClustersController < Clusters::BaseController
end
end
- def create_aws_cluster_params
- params.require(:cluster).permit(
- *base_permitted_cluster_params,
- :name,
- provider_aws_attributes: [
- :kubernetes_version,
- :key_name,
- :role_arn,
- :region,
- :vpc_id,
- :instance_type,
- :num_nodes,
- :security_group_id,
- subnet_ids: []
- ]).merge(
- provider_type: :aws,
- platform_type: :kubernetes,
- clusterable: clusterable.__subject__
- )
- end
-
def create_user_cluster_params
params.require(:cluster).permit(
*base_permitted_cluster_params,
@@ -242,29 +186,6 @@ class Clusters::ClustersController < Clusters::BaseController
)
end
- def aws_role_params
- params.require(:cluster).permit(:role_arn, :region)
- end
-
- def generate_gcp_authorize_url
- connect_path = clusterable.connect_path().to_s
- error_path = @project ? project_clusters_path(@project) : connect_path
-
- state = generate_session_key_redirect(connect_path, error_path)
-
- @authorize_url = GoogleApi::CloudPlatform::Client.new(
- nil, callback_google_api_auth_url,
- state: state).authorize_url
- rescue GoogleApi::Auth::ConfigMissingError
- # no-op
- end
-
- def gcp_cluster
- cluster = Clusters::BuildService.new(clusterable.__subject__).execute
- cluster.build_provider_gcp
- @gcp_cluster = cluster.present(current_user: current_user)
- end
-
def proxyable
cluster.cluster
end
@@ -295,11 +216,6 @@ class Clusters::ClustersController < Clusters::BaseController
@user_cluster = cluster.present(current_user: current_user)
end
- def validate_gcp_token
- @valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
- .validate_token(expires_at_in_session)
- end
-
def token_in_session
session[GoogleApi::CloudPlatform::Client.session_key_for_token]
end
@@ -309,26 +225,6 @@ class Clusters::ClustersController < Clusters::BaseController
session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
end
- def generate_session_key_redirect(uri, error_uri)
- GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
- session[key] = uri
- session[:error_uri] = error_uri
- end
- end
-
- ##
- # Unfortunately the EC2 API doesn't provide a list of
- # possible instance types. There is a workaround, using
- # the Pricing API, but instead of requiring the
- # user to grant extra permissions for this we use the
- # values that validate the CloudFormation template.
- def load_instance_types
- stack_template = File.read(Rails.root.join('vendor', 'aws', 'cloudformation', 'eks_cluster.yaml'))
- instance_types = YAML.safe_load(stack_template).dig('Parameters', 'NodeInstanceType', 'AllowedValues')
-
- instance_types.map { |type| Hash(name: type, value: type) }
- end
-
def update_applications_status
@cluster.applications.each(&:schedule_status_update)
end
diff --git a/app/controllers/concerns/gitlab_recaptcha.rb b/app/controllers/concerns/gitlab_recaptcha.rb
index 15e856463ea..cedadba5fc7 100644
--- a/app/controllers/concerns/gitlab_recaptcha.rb
+++ b/app/controllers/concerns/gitlab_recaptcha.rb
@@ -17,6 +17,9 @@ module GitlabRecaptcha
flash.delete :recaptcha_error
self.resource = resource_class.new
+
+ add_gon_variables
+
render action: 'new'
end
end
diff --git a/app/controllers/concerns/integrations/actions.rb b/app/controllers/concerns/integrations/actions.rb
index 1f788860c8f..e0a12555e11 100644
--- a/app/controllers/concerns/integrations/actions.rb
+++ b/app/controllers/concerns/integrations/actions.rb
@@ -51,11 +51,9 @@ module Integrations::Actions
private
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
def integration
@integration ||= find_or_initialize_non_project_specific_integration(params[:id])
end
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
def success_message
if integration.active?
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 4d3eb9cd183..07850acd23d 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -184,7 +184,8 @@ module IssuableActions
def paginated_discussions
return if params[:per_page].blank?
- return unless issuable.instance_of?(Issue) && Feature.enabled?(:paginated_issue_discussions, project)
+ return if issuable.instance_of?(Issue) && Feature.disabled?(:paginated_issue_discussions, project)
+ return if issuable.instance_of?(MergeRequest) && Feature.disabled?(:paginated_mr_discussions, project)
strong_memoize(:paginated_discussions) do
issuable
diff --git a/app/controllers/concerns/issues_calendar.rb b/app/controllers/concerns/issues_calendar.rb
index 1fdfde4c869..51d6d3cf05a 100644
--- a/app/controllers/concerns/issues_calendar.rb
+++ b/app/controllers/concerns/issues_calendar.rb
@@ -4,7 +4,6 @@ module IssuesCalendar
extend ActiveSupport::Concern
# rubocop:disable Gitlab/ModuleWithInstanceVariables
- # rubocop: disable CodeReuse/ActiveRecord
def render_issues_calendar(issuables)
@issues = issuables
.non_archived
@@ -23,6 +22,5 @@ module IssuesCalendar
end
end
end
- # rubocop: enable CodeReuse/ActiveRecord
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index 0b9024dc3db..fb11bece79c 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -143,8 +143,8 @@ module MembershipActions
raise NotImplementedError
end
- def requested_relations
- case params[:with_inherited_permissions].presence
+ def requested_relations(inherited_permissions = :with_inherited_permissions)
+ case params[inherited_permissions].presence
when 'exclude'
[:direct]
when 'only'
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 55b6747fcfb..928c617471b 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -6,8 +6,7 @@ module NotesActions
extend ActiveSupport::Concern
# last_fetched_at is an integer number of microseconds, which is the same
- # precision as PostgreSQL "timestamp" fields. It's important for them to have
- # identical precision for accurate pagination
+ # precision as PostgreSQL "timestamp" fields.
MICROSECOND = 1_000_000
included do
@@ -23,7 +22,7 @@ module NotesActions
end
def index
- notes, meta = gather_notes
+ notes, meta = gather_all_notes
notes = prepare_notes_for_rendering(notes)
notes = notes.select { |n| n.readable_by?(current_user) }
notes =
@@ -33,11 +32,7 @@ module NotesActions
notes.map { |note| note_json(note) }
end
- # We know there's more data, so tell the frontend to poll again after 1ms
- set_polling_interval_header(interval: 1) if meta[:more]
-
- # Only present an ETag for the empty response to ensure pagination works
- # as expected
+ # Only present an ETag for the empty response
::Gitlab::EtagCaching::Middleware.skip!(response) if notes.present?
render json: meta.merge(notes: notes)
@@ -105,17 +100,6 @@ module NotesActions
private
- # Lower bound (last_fetched_at as specified in the request) is already set in
- # the finder. Here, we select between returning all notes since then, or a
- # page's worth of notes.
- def gather_notes
- if Feature.enabled?(:paginated_notes, noteable.try(:resource_parent))
- gather_some_notes
- else
- gather_all_notes
- end
- end
-
def gather_all_notes
now = Time.current
notes = merge_resource_events(notes_finder.execute.inc_relations_for_view)
@@ -123,27 +107,11 @@ module NotesActions
[notes, { last_fetched_at: (now.to_i * MICROSECOND) + now.usec }]
end
- def gather_some_notes
- paginator = ::Gitlab::UpdatedNotesPaginator.new(
- notes_finder.execute.inc_relations_for_view,
- last_fetched_at: last_fetched_at
- )
-
- notes = paginator.notes
-
- # Fetch all the synthetic notes in the same time range as the real notes.
- # Although we don't limit the number, their text is under our control so
- # should be fairly cheap to process.
- notes = merge_resource_events(notes, fetch_until: paginator.next_fetched_at)
-
- [notes, paginator.metadata]
- end
-
- def merge_resource_events(notes, fetch_until: nil)
+ def merge_resource_events(notes)
return notes if notes_filter == UserPreference::NOTES_FILTERS[:only_comments]
ResourceEvents::MergeIntoNotesService
- .new(noteable, current_user, last_fetched_at: last_fetched_at, fetch_until: fetch_until)
+ .new(noteable, current_user, last_fetched_at: last_fetched_at)
.execute(notes)
end
diff --git a/app/controllers/concerns/product_analytics_tracking.rb b/app/controllers/concerns/product_analytics_tracking.rb
index 4021ff83578..0b51b3dd380 100644
--- a/app/controllers/concerns/product_analytics_tracking.rb
+++ b/app/controllers/concerns/product_analytics_tracking.rb
@@ -20,8 +20,17 @@ module ProductAnalyticsTracking
def route_events_to(destinations, name, &block)
track_unique_redis_hll_event(name, &block) if destinations.include?(:redis_hll)
- if destinations.include?(:snowplow) && Feature.enabled?(:route_hll_to_snowplow, tracking_namespace_source)
+ if destinations.include?(:snowplow) && event_enabled?(name)
Gitlab::Tracking.event(self.class.to_s, name, namespace: tracking_namespace_source, user: current_user)
end
end
+
+ def event_enabled?(event)
+ events_to_ff = {
+ g_analytics_valuestream: :route_hll_to_snowplow,
+ i_search_paid: :route_hll_to_snowplow_phase2
+ }
+
+ Feature.enabled?(events_to_ff[event.to_sym], tracking_namespace_source)
+ end
end
diff --git a/app/controllers/concerns/project_stats_refresh_conflicts_guard.rb b/app/controllers/concerns/project_stats_refresh_conflicts_guard.rb
new file mode 100644
index 00000000000..a3349997dbd
--- /dev/null
+++ b/app/controllers/concerns/project_stats_refresh_conflicts_guard.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ProjectStatsRefreshConflictsGuard
+ extend ActiveSupport::Concern
+
+ def reject_if_build_artifacts_size_refreshing!
+ return unless project.refreshing_build_artifacts_size?
+
+ Gitlab::ProjectStatsRefreshConflictsLogger.warn_request_rejected_during_stats_refresh(project.id)
+
+ render_409('Action temporarily disabled. The project this pipeline belongs to is undergoing stats refresh.')
+ end
+end
diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb
index 0ee8d0c9307..1bb81a46e50 100644
--- a/app/controllers/concerns/snippets_actions.rb
+++ b/app/controllers/concerns/snippets_actions.rb
@@ -75,7 +75,6 @@ module SnippetsActions
private
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
def blob
@blob ||= blobs.first
end
@@ -87,7 +86,6 @@ module SnippetsActions
snippet.blobs
end
end
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
def convert_line_endings(content)
params[:line_ending] == 'raw' ? content : content.gsub(/\r\n/, "\n")
diff --git a/app/controllers/concerns/sorting_preference.rb b/app/controllers/concerns/sorting_preference.rb
index 8d8845e2f41..6278b489028 100644
--- a/app/controllers/concerns/sorting_preference.rb
+++ b/app/controllers/concerns/sorting_preference.rb
@@ -5,10 +5,12 @@ module SortingPreference
include CookiesHelper
def set_sort_order(field = sorting_field, default_order = default_sort_order)
- set_sort_order_from_user_preference(field) ||
- set_sort_order_from_cookie(field) ||
- params[:sort] ||
- default_order
+ sort_order = set_sort_order_from_user_preference(field) || set_sort_order_from_cookie(field) || params[:sort]
+
+ # some types of sorting might not be available on the dashboard
+ return default_order unless valid_sort_order?(sort_order)
+
+ sort_order
end
# Implement sorting_field method on controllers
@@ -85,4 +87,11 @@ module SortingPreference
else value
end
end
+
+ def valid_sort_order?(sort_order)
+ return false unless sort_order
+ return can_sort_by_issue_weight?(action_name == 'issues') if sort_order.include?('weight')
+
+ true
+ 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
index b254916cdd6..707c1e6c84f 100644
--- 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
@@ -32,3 +32,5 @@ module SpammableActions::CaptchaCheck::HtmlFormatActionsSupport
request.headers['X-GitLab-Spam-Log-Id'] = params[:spam_log_id] if params[:spam_log_id]
end
end
+
+SpammableActions::CaptchaCheck::HtmlFormatActionsSupport.prepend_mod
diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb
index 9fc8886aaee..83447744013 100644
--- a/app/controllers/concerns/wiki_actions.rb
+++ b/app/controllers/concerns/wiki_actions.rb
@@ -167,7 +167,7 @@ module WikiActions
render 'shared/wikis/diff'
end
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def destroy
diff --git a/app/controllers/concerns/zuora_csp.rb b/app/controllers/concerns/zuora_csp.rb
new file mode 100644
index 00000000000..5f9be11d7b9
--- /dev/null
+++ b/app/controllers/concerns/zuora_csp.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module ZuoraCSP
+ extend ActiveSupport::Concern
+
+ ZUORA_URL = 'https://*.zuora.com'
+
+ included do
+ content_security_policy do |policy|
+ next if policy.directives.blank?
+
+ default_script_src = policy.directives['script-src'] || policy.directives['default-src']
+ script_src_values = Array.wrap(default_script_src) | ["'self'", "'unsafe-eval'", ZUORA_URL]
+
+ default_frame_src = policy.directives['frame-src'] || policy.directives['default-src']
+ frame_src_values = Array.wrap(default_frame_src) | ["'self'", ZUORA_URL]
+
+ default_child_src = policy.directives['child-src'] || policy.directives['default-src']
+ child_src_values = Array.wrap(default_child_src) | ["'self'", ZUORA_URL]
+
+ policy.script_src(*script_src_values)
+ policy.frame_src(*frame_src_values)
+ policy.child_src(*child_src_values)
+ end
+ end
+end
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index dd30d688fa8..704453fbf44 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -8,7 +8,7 @@ class ConfirmationsController < Devise::ConfirmationsController
prepend_before_action :check_recaptcha, only: :create
before_action :load_recaptcha, only: :new
- feature_category :users
+ feature_category :authentication_and_authorization
def almost_there
flash[:notice] = nil
diff --git a/app/controllers/groups/autocomplete_sources_controller.rb b/app/controllers/groups/autocomplete_sources_controller.rb
index a2eb475d360..171494e66bd 100644
--- a/app/controllers/groups/autocomplete_sources_controller.rb
+++ b/app/controllers/groups/autocomplete_sources_controller.rb
@@ -5,8 +5,7 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController
feature_category :team_planning, [:issues, :labels, :milestones, :commands]
feature_category :code_review, [:merge_requests]
- urgency :low, [:issues, :labels, :milestones, :commands]
- urgency :low, [:merge_requests]
+ urgency :low, [:issues, :labels, :milestones, :commands, :merge_requests, :members]
def members
render json: ::Groups::ParticipantsService.new(@group, current_user).execute(target)
diff --git a/app/controllers/groups/email_campaigns_controller.rb b/app/controllers/groups/email_campaigns_controller.rb
index 70c8a23d918..38087e3fc11 100644
--- a/app/controllers/groups/email_campaigns_controller.rb
+++ b/app/controllers/groups/email_campaigns_controller.rb
@@ -4,6 +4,7 @@ class Groups::EmailCampaignsController < Groups::ApplicationController
EMAIL_CAMPAIGNS_SCHEMA_URL = 'iglu:com.gitlab/email_campaigns/jsonschema/1-0-0'
feature_category :navigation
+ urgency :low
before_action :check_params
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index b95d8c87a4a..f0b857ca4c9 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -25,10 +25,8 @@ class Groups::GroupMembersController < Groups::ApplicationController
urgency :low
def index
- push_frontend_feature_flag(:group_member_inherited_group, @group)
-
@sort = params[:sort].presence || sort_value_name
- @include_relations ||= requested_relations
+ @include_relations ||= requested_relations(:groups_with_inherited_permissions)
if can?(current_user, :admin_group_member, @group)
@invited_members = invited_members
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
index 8d687bf3c2c..aeb54527c69 100644
--- a/app/controllers/groups/runners_controller.rb
+++ b/app/controllers/groups/runners_controller.rb
@@ -41,3 +41,5 @@ class Groups::RunnersController < Groups::ApplicationController
params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
end
end
+
+Groups::RunnersController.prepend_mod
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index 4b75cec19f7..b1afac1f1c7 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -9,6 +9,7 @@ module Groups
before_action :authorize_update_max_artifacts_size!, only: [:update]
before_action :define_variables, only: [:show]
before_action :push_licensed_features, only: [:show]
+ before_action :assign_variables_to_gon, only: [:show]
feature_category :continuous_integration
urgency :low
@@ -81,6 +82,10 @@ module Groups
# Overridden in EE
def push_licensed_features
end
+
+ # Overridden in EE
+ def assign_variables_to_gon
+ end
end
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index d46cf899d8c..327b4832f31 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -30,10 +30,6 @@ class GroupsController < Groups::ApplicationController
before_action :user_actions, only: [:show]
- before_action do
- push_frontend_feature_flag(:vue_issues_list, @group)
- end
-
before_action :check_export_rate_limit!, only: [:export, :download_export]
before_action :track_experiment_event, only: [:new]
@@ -212,7 +208,7 @@ class GroupsController < Groups::ApplicationController
end
def issues
- return super if !html_request? || Feature.disabled?(:vue_issues_list, group)
+ return super unless html_request?
@has_issues = IssuesFinder.new(current_user, group_id: group.id, include_subgroups: true).execute
.non_archived
@@ -289,6 +285,7 @@ class GroupsController < Groups::ApplicationController
:chat_team_name,
:require_two_factor_authentication,
:two_factor_grace_period,
+ :enabled_git_access_protocol,
:project_creation_level,
:subgroup_creation_level,
:default_branch_protection,
@@ -360,6 +357,7 @@ class GroupsController < Groups::ApplicationController
flash[:alert] = _('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
flash.delete :recaptcha_error
@group = Group.new(group_params)
+ add_gon_variables
render action: 'new'
end
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 5be2d7527ff..1508531828d 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -13,7 +13,7 @@ class HelpController < ApplicationController
def index
# Remove YAML frontmatter so that it doesn't look weird
- @help_index = File.read(Rails.root.join('doc', 'index.md')).sub(YAML_FRONT_MATTER_REGEXP, '')
+ @help_index = File.read(path_to_doc('index.md')).sub(YAML_FRONT_MATTER_REGEXP, '')
# Prefix Markdown links with `help/` unless they are external links.
# '//' not necessarily part of URL, e.g., mailto:mail@example.com
@@ -24,7 +24,7 @@ class HelpController < ApplicationController
end
def show
- @path = Rack::Utils.clean_path_info(path_params[:path])
+ @path = Rack::Utils.clean_path_info(params[:path])
respond_to do |format|
format.any(:markdown, :md, :html) do
@@ -38,7 +38,7 @@ class HelpController < ApplicationController
# Allow access to specific media files in the doc folder
format.any(:png, :gif, :jpeg, :mp4, :mp3) do
# Note: We are purposefully NOT using `Rails.root.join` because of https://gitlab.com/gitlab-org/gitlab/-/issues/216028.
- path = File.join(Rails.root, 'doc', "#{@path}.#{params[:format]}")
+ path = path_to_doc("#{@path}.#{params[:format]}")
if File.exist?(path)
send_file(path, disposition: 'inline')
@@ -61,16 +61,8 @@ class HelpController < ApplicationController
private
- def path_params
- params.require(:path)
-
- params
- end
-
def redirect_to_documentation_website?
- return false unless Gitlab::UrlSanitizer.valid_web?(documentation_url)
-
- true
+ Gitlab::UrlSanitizer.valid_web?(documentation_url)
end
def documentation_url
@@ -105,18 +97,22 @@ class HelpController < ApplicationController
def render_documentation
# Note: We are purposefully NOT using `Rails.root.join` because of https://gitlab.com/gitlab-org/gitlab/-/issues/216028.
- path = File.join(Rails.root, 'doc', "#{@path}.md")
+ path = path_to_doc("#{@path}.md")
if File.exist?(path)
# Remove YAML frontmatter so that it doesn't look weird
@markdown = File.read(path).gsub(YAML_FRONT_MATTER_REGEXP, '')
- render 'show.html.haml'
+ render :show, formats: :html
else
# Force template to Haml
- render 'errors/not_found.html.haml', layout: 'errors', status: :not_found
+ render 'errors/not_found', layout: 'errors', status: :not_found, formats: :html
end
end
+
+ def path_to_doc(file_name)
+ File.join(Rails.root, 'doc', file_name)
+ end
end
::HelpController.prepend_mod
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
index 2bcbf88039b..9fcb8385312 100644
--- a/app/controllers/ide_controller.rb
+++ b/app/controllers/ide_controller.rb
@@ -22,6 +22,11 @@ class IdeController < ApplicationController
def index
Gitlab::UsageDataCounters::WebIdeCounter.increment_views_count
+
+ if project && Feature.enabled?(:route_hll_to_snowplow_phase2, project&.namespace)
+ Gitlab::Tracking.event(self.class.to_s, 'web_ide_views',
+ namespace: project&.namespace, user: current_user)
+ end
end
private
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index c223d9d211e..b949a99c250 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -44,7 +44,6 @@ class Import::FogbugzController < Import::BaseController
redirect_to status_import_fogbugz_path
end
- # rubocop: disable CodeReuse/ActiveRecord
def status
unless client.valid?
return redirect_to new_import_fogbugz_path
@@ -52,19 +51,18 @@ class Import::FogbugzController < Import::BaseController
super
end
- # rubocop: enable CodeReuse/ActiveRecord
def create
- repo = client.repo(params[:repo_id])
- fb_session = { uri: session[:fogbugz_uri], token: session[:fogbugz_token] }
+ credentials = { uri: session[:fogbugz_uri], token: session[:fogbugz_token] }
+
umap = session[:fogbugz_user_map] || client.user_map
- project = Gitlab::FogbugzImport::ProjectCreator.new(repo, fb_session, current_user.namespace, current_user, umap).execute
+ result = Import::FogbugzService.new(client, current_user, params.merge(umap: umap)).execute(credentials)
- if project.persisted?
- render json: ProjectSerializer.new.represent(project, serializer: :import)
+ if result[:status] == :success
+ render json: ProjectSerializer.new.represent(result[:project], serializer: :import)
else
- render json: { errors: project_save_error(project) }, status: :unprocessable_entity
+ render json: { errors: result[:message] }, status: result[:http_status]
end
end
diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb
index 4b4ac07b389..399a92c59e0 100644
--- a/app/controllers/import/gitea_controller.rb
+++ b/app/controllers/import/gitea_controller.rb
@@ -7,7 +7,7 @@ class Import::GiteaController < Import::GithubController
def new
if session[access_token_key].present? && provider_url.present?
- redirect_to status_import_url
+ redirect_to status_import_url(namespace_id: params[:namespace_id])
end
end
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 9bd8f893614..8dd40b6254e 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -76,12 +76,10 @@ class Import::GithubController < Import::BaseController
protected
- # rubocop: disable CodeReuse/ActiveRecord
override :importable_repos
def importable_repos
client_repos.to_a
end
- # rubocop: enable CodeReuse/ActiveRecord
override :incompatible_repos
def incompatible_repos
diff --git a/app/controllers/jira_connect/oauth_application_ids_controller.rb b/app/controllers/jira_connect/oauth_application_ids_controller.rb
new file mode 100644
index 00000000000..05c23210da2
--- /dev/null
+++ b/app/controllers/jira_connect/oauth_application_ids_controller.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module JiraConnect
+ class OauthApplicationIdsController < ::ApplicationController
+ feature_category :integrations
+
+ skip_before_action :authenticate_user!
+
+ def show
+ if Feature.enabled?(:jira_connect_oauth_self_managed) && jira_connect_application_key.present?
+ render json: { application_id: jira_connect_application_key }
+ else
+ head :not_found
+ end
+ end
+
+ private
+
+ def jira_connect_application_key
+ Gitlab::CurrentSettings.jira_connect_application_key.presence
+ end
+ end
+end
diff --git a/app/controllers/jwks_controller.rb b/app/controllers/jwks_controller.rb
index 3b0e6ca2eb1..d3a8d3dafea 100644
--- a/app/controllers/jwks_controller.rb
+++ b/app/controllers/jwks_controller.rb
@@ -13,10 +13,6 @@ class JwksController < Doorkeeper::OpenidConnect::DiscoveryController
def payload
[
- # We keep openid_connect_signing_key so that we can seamlessly
- # replace it with ci_jwt_signing_key and remove it on the next release.
- # TODO: Remove openid_connect_signing_key in 13.7
- # https://gitlab.com/gitlab-org/gitlab/-/issues/221031
Rails.application.secrets.openid_connect_signing_key,
Gitlab::CurrentSettings.ci_jwt_signing_key
].compact.map do |key_data|
diff --git a/app/controllers/mailgun/webhooks_controller.rb b/app/controllers/mailgun/webhooks_controller.rb
new file mode 100644
index 00000000000..f7cb3eaa8ee
--- /dev/null
+++ b/app/controllers/mailgun/webhooks_controller.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Mailgun
+ class WebhooksController < ApplicationController
+ respond_to :json
+
+ skip_before_action :authenticate_user!
+ skip_before_action :verify_authenticity_token
+
+ before_action :ensure_feature_enabled!
+ before_action :authenticate_signature!
+
+ feature_category :team_planning
+
+ WEBHOOK_PROCESSORS = [
+ Gitlab::Mailgun::WebhookProcessors::FailureLogger,
+ Gitlab::Mailgun::WebhookProcessors::MemberInvites
+ ].freeze
+
+ def process_webhook
+ WEBHOOK_PROCESSORS.each do |processor_class|
+ processor_class.new(params['event-data']).execute
+ end
+
+ head :ok
+ end
+
+ private
+
+ def ensure_feature_enabled!
+ render_406 unless Gitlab::CurrentSettings.mailgun_events_enabled?
+ end
+
+ def authenticate_signature!
+ access_denied! unless valid_signature?
+ end
+
+ def valid_signature?
+ return false if Gitlab::CurrentSettings.mailgun_signing_key.blank?
+
+ # per this guide: https://documentation.mailgun.com/en/latest/user_manual.html#webhooks
+ digest = OpenSSL::Digest.new('SHA256')
+ data = [params.dig(:signature, :timestamp), params.dig(:signature, :token)].join
+
+ hmac_digest = OpenSSL::HMAC.hexdigest(digest, Gitlab::CurrentSettings.mailgun_signing_key, data)
+
+ ActiveSupport::SecurityUtils.secure_compare(params.dig(:signature, :signature), hmac_digest)
+ end
+
+ def render_406
+ # failure to stop retries per https://documentation.mailgun.com/en/latest/user_manual.html#webhooks
+ head :not_acceptable
+ end
+ end
+end
diff --git a/app/controllers/members/mailgun/permanent_failures_controller.rb b/app/controllers/members/mailgun/permanent_failures_controller.rb
deleted file mode 100644
index 685faa34694..00000000000
--- a/app/controllers/members/mailgun/permanent_failures_controller.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-module Members
- module Mailgun
- class PermanentFailuresController < ApplicationController
- respond_to :json
-
- skip_before_action :authenticate_user!
- skip_before_action :verify_authenticity_token
-
- before_action :ensure_feature_enabled!
- before_action :authenticate_signature!
- before_action :validate_invite_email!
-
- feature_category :authentication_and_authorization
-
- def create
- webhook_processor.execute
-
- head :ok
- end
-
- private
-
- def ensure_feature_enabled!
- render_406 unless Gitlab::CurrentSettings.mailgun_events_enabled?
- end
-
- def authenticate_signature!
- access_denied! unless valid_signature?
- end
-
- def valid_signature?
- return false if Gitlab::CurrentSettings.mailgun_signing_key.blank?
-
- # per this guide: https://documentation.mailgun.com/en/latest/user_manual.html#webhooks
- digest = OpenSSL::Digest.new('SHA256')
- data = [params.dig(:signature, :timestamp), params.dig(:signature, :token)].join
-
- hmac_digest = OpenSSL::HMAC.hexdigest(digest, Gitlab::CurrentSettings.mailgun_signing_key, data)
-
- ActiveSupport::SecurityUtils.secure_compare(params.dig(:signature, :signature), hmac_digest)
- end
-
- def validate_invite_email!
- # permanent_failures webhook does not provide a way to filter failures, so we'll get them all on this endpoint
- # and we only care about our invite_emails
- render_406 unless payload[:tags]&.include?(::Members::Mailgun::INVITE_EMAIL_TAG)
- end
-
- def webhook_processor
- ::Members::Mailgun::ProcessWebhookService.new(payload)
- end
-
- def payload
- @payload ||= params.permit!['event-data']
- end
-
- def render_406
- # failure to stop retries per https://documentation.mailgun.com/en/latest/user_manual.html#webhooks
- head :not_acceptable
- end
- end
- end
-end
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index 0817813f967..c9c51289d3a 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -19,6 +19,9 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
session.delete(:user_return_to)
render "doorkeeper/authorizations/redirect", locals: { redirect_uri: parsed_redirect_uri }, layout: false
else
+ redirect_uri = URI(authorization.authorize.redirect_uri)
+ allow_redirect_uri_form_action(redirect_uri.scheme)
+
render "doorkeeper/authorizations/new"
end
else
@@ -28,6 +31,20 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
private
+ # Chrome blocks redirections if the form-action CSP directive is present
+ # and the redirect location's scheme isn't allow-listed
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/form-action
+ # https://github.com/w3c/webappsec-csp/issues/8
+ def allow_redirect_uri_form_action(redirect_uri_scheme)
+ return unless content_security_policy?
+
+ form_action = request.content_security_policy.form_action
+ return unless form_action
+
+ form_action.push("#{redirect_uri_scheme}:")
+ request.content_security_policy.form_action(*form_action)
+ end
+
def pre_auth_params
# Cannot be achieved with a before_action hook, due to the execution order.
downgrade_scopes! if action_name == 'new'
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 927b50245a4..45decccfc36 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -178,6 +178,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
flash[:notice] = _('Welcome back! Your account had been deactivated due to inactivity but is now reactivated.')
end
+ store_after_sign_up_path_for_user if intent_to_register?
sign_in_and_redirect(user, event: :authentication)
end
else
@@ -259,6 +260,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
(request_params['remember_me'] == '1') if request_params.present?
end
+ def intent_to_register?
+ request_params = request.env['omniauth.params']
+ (request_params['intent'] == 'register') if request_params.present?
+ end
+
def store_redirect_fragment(redirect_fragment)
key = stored_location_key_for(:user)
location = session[key]
@@ -291,6 +297,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def fail_admin_mode_invalid_credentials
redirect_to new_admin_session_path, alert: _('Invalid login or password')
end
+
+ def store_after_sign_up_path_for_user
+ store_location_for(:user, users_sign_up_welcome_path)
+ end
end
OmniauthCallbacksController.prepend_mod_with('OmniauthCallbacksController')
diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb
index 83eabbb736e..cb8b2783000 100644
--- a/app/controllers/profiles/accounts_controller.rb
+++ b/app/controllers/profiles/accounts_controller.rb
@@ -3,7 +3,7 @@
class Profiles::AccountsController < Profiles::ApplicationController
include AuthHelper
- feature_category :users
+ feature_category :authentication_and_authorization
urgency :low, [:show]
def show
diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/profiles/active_sessions_controller.rb
index aafd7c2b65b..2607ba7d404 100644
--- a/app/controllers/profiles/active_sessions_controller.rb
+++ b/app/controllers/profiles/active_sessions_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Profiles::ActiveSessionsController < Profiles::ApplicationController
- feature_category :users
+ feature_category :authentication_and_authorization
def index
@sessions = ActiveSession.list(current_user).reject(&:is_impersonated)
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index ad2e384077a..265fa505b2a 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -24,11 +24,10 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
@personal_access_token = result.payload[:personal_access_token]
if result.success?
- PersonalAccessToken.redis_store!(current_user.id, @personal_access_token.token)
- redirect_to profile_personal_access_tokens_path, notice: _("Your new personal access token has been created.")
+ render json: { new_token: @personal_access_token.token,
+ active_access_tokens: active_personal_access_tokens }, status: :ok
else
- set_index_vars
- render :index
+ render json: { errors: result.errors }, status: :unprocessable_entity
end
end
@@ -52,14 +51,11 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
def set_index_vars
@scopes = Gitlab::Auth.available_scopes_for(current_user)
-
- @inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = active_personal_access_tokens
-
- @new_personal_access_token = PersonalAccessToken.redis_getdel(current_user.id)
end
def active_personal_access_tokens
- finder(state: 'active', sort: 'expires_at_asc').execute
+ tokens = finder(state: 'active', sort: 'expires_at_asc').execute
+ ::API::Entities::PersonalAccessTokenWithDetails.represent(tokens)
end
end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index d5e7195a157..dd1ac526b89 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -14,7 +14,10 @@ class ProfilesController < Profiles::ApplicationController
push_frontend_feature_flag(:webauthn)
end
- feature_category :users
+ feature_category :users, [:show, :update, :reset_incoming_email_token, :reset_feed_token,
+ :reset_static_object_token, :update_username]
+
+ feature_category :authentication_and_authorization, [:audit_log]
urgency :low, [:show, :update]
def show
diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb
index dbf3b2051fb..85e258b62e8 100644
--- a/app/controllers/projects/ci/pipeline_editor_controller.rb
+++ b/app/controllers/projects/ci/pipeline_editor_controller.rb
@@ -4,7 +4,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController
before_action :check_can_collaborate!
before_action do
push_frontend_feature_flag(:schema_linting, @project)
- push_frontend_feature_flag(:pipeline_editor_file_tree, @project)
+ push_frontend_feature_flag(:simulate_pipeline, @project)
end
feature_category :pipeline_authoring
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 60b8e45f5be..f4125fd0a15 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -84,7 +84,7 @@ class Projects::CommitsController < Projects::ApplicationController
@commits.each(&:lazy_author) # preload authors
- @commits = @commits.with_latest_pipeline(@ref)
+ @commits = @commits.with_markdown_cache.with_latest_pipeline(@ref)
@commits = set_commits_for_rendering(@commits)
end
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 3ced5f21b24..09a06aaed8c 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -102,7 +102,11 @@ class Projects::CompareController < Projects::ApplicationController
# source == head_ref == to
def source_project
- project
+ strong_memoize(:source_project) do
+ # Eager load project's avatar url to prevent batch loading
+ # for all forked projects
+ project&.tap(&:avatar_url)
+ end
end
def compare
@@ -112,17 +116,24 @@ class Projects::CompareController < Projects::ApplicationController
end
def start_ref
- @start_ref ||= Addressable::URI.unescape(compare_params[:from])
+ @start_ref ||= Addressable::URI.unescape(compare_params[:from]).presence
end
def head_ref
return @ref if defined?(@ref)
- @ref = @head_ref = Addressable::URI.unescape(compare_params[:to])
+ @ref = @head_ref = Addressable::URI.unescape(compare_params[:to]).presence
end
def define_commits
- @commits = compare.present? ? set_commits_for_rendering(@compare.commits) : []
+ strong_memoize(:commits) do
+ if compare.present?
+ commits = compare.commits.with_markdown_cache.with_latest_pipeline(head_ref)
+ set_commits_for_rendering(commits)
+ else
+ []
+ end
+ end
end
def define_diffs
diff --git a/app/controllers/projects/environments/prometheus_api_controller.rb b/app/controllers/projects/environments/prometheus_api_controller.rb
index 94fe67b5e85..cbb16d596a0 100644
--- a/app/controllers/projects/environments/prometheus_api_controller.rb
+++ b/app/controllers/projects/environments/prometheus_api_controller.rb
@@ -6,6 +6,7 @@ class Projects::Environments::PrometheusApiController < Projects::ApplicationCon
before_action :proxyable
feature_category :metrics
+ urgency :low
private
diff --git a/app/controllers/projects/environments/sample_metrics_controller.rb b/app/controllers/projects/environments/sample_metrics_controller.rb
index 3df20810cb3..80344c83ab7 100644
--- a/app/controllers/projects/environments/sample_metrics_controller.rb
+++ b/app/controllers/projects/environments/sample_metrics_controller.rb
@@ -2,6 +2,7 @@
class Projects::Environments::SampleMetricsController < Projects::ApplicationController
feature_category :metrics
+ urgency :low
def query
result = Metrics::SampleMetricsService.new(params[:identifier], range_start: params[:start], range_end: params[:end]).query
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 1a2c0d64d19..ac3c85f3b40 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -207,7 +207,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController
private
def deployments
- environment.deployments.ordered.page(params[:page])
+ environment
+ .deployments
+ .with_environment_page_associations
+ .ordered
+ .page(params[:page])
end
def verify_api_request!
diff --git a/app/controllers/projects/error_tracking/base_controller.rb b/app/controllers/projects/error_tracking/base_controller.rb
index ffbe487d8a1..62b8b9f3c1a 100644
--- a/app/controllers/projects/error_tracking/base_controller.rb
+++ b/app/controllers/projects/error_tracking/base_controller.rb
@@ -4,6 +4,7 @@ class Projects::ErrorTracking::BaseController < Projects::ApplicationController
POLLING_INTERVAL = 1_000
feature_category :error_tracking
+ urgency :low
def set_polling_interval
Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
diff --git a/app/controllers/projects/error_tracking/projects_controller.rb b/app/controllers/projects/error_tracking/projects_controller.rb
index d59cbc25d25..531bd327e43 100644
--- a/app/controllers/projects/error_tracking/projects_controller.rb
+++ b/app/controllers/projects/error_tracking/projects_controller.rb
@@ -8,6 +8,7 @@ module Projects
before_action :authorize_read_sentry_issue!
feature_category :error_tracking
+ urgency :low
def index
service = ::ErrorTracking::ListProjectsService.new(
diff --git a/app/controllers/projects/google_cloud/base_controller.rb b/app/controllers/projects/google_cloud/base_controller.rb
index 0d65431d870..980e9bdcdad 100644
--- a/app/controllers/projects/google_cloud/base_controller.rb
+++ b/app/controllers/projects/google_cloud/base_controller.rb
@@ -2,6 +2,7 @@
class Projects::GoogleCloud::BaseController < Projects::ApplicationController
feature_category :five_minute_production_app
+ urgency :low
before_action :admin_project_google_cloud!
before_action :google_oauth2_enabled!
diff --git a/app/controllers/projects/grafana_api_controller.rb b/app/controllers/projects/grafana_api_controller.rb
index 9c5d6c8ebc3..d5099367873 100644
--- a/app/controllers/projects/grafana_api_controller.rb
+++ b/app/controllers/projects/grafana_api_controller.rb
@@ -5,6 +5,7 @@ class Projects::GrafanaApiController < Projects::ApplicationController
include MetricsDashboard
feature_category :metrics
+ urgency :low
def proxy
result = ::Grafana::ProxyService.new(
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
index fd7ba7b5460..70eab792b40 100644
--- a/app/controllers/projects/incidents_controller.rb
+++ b/app/controllers/projects/incidents_controller.rb
@@ -7,7 +7,6 @@ class Projects::IncidentsController < Projects::ApplicationController
before_action :authorize_read_issue!
before_action :load_incident, only: [:show]
before_action do
- push_frontend_feature_flag(:incident_escalations, @project)
push_frontend_feature_flag(:incident_timeline, @project)
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index b65616fdb3c..f974b16468c 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -20,10 +20,12 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :disable_query_limiting, only: [:create_merge_request, :move, :bulk_update]
before_action :check_issues_available!
before_action :issue, unless: ->(c) { ISSUES_EXCEPT_ACTIONS.include?(c.action_name.to_sym) }
+ before_action :redirect_if_task, unless: ->(c) { ISSUES_EXCEPT_ACTIONS.include?(c.action_name.to_sym) }
+
after_action :log_issue_show, unless: ->(c) { ISSUES_EXCEPT_ACTIONS.include?(c.action_name.to_sym) }
before_action :set_issuables_index, if: ->(c) {
- SET_ISSUABLES_INDEX_ONLY_ACTIONS.include?(c.action_name.to_sym) && !vue_issues_list?
+ SET_ISSUABLES_INDEX_ONLY_ACTIONS.include?(c.action_name.to_sym) && !index_html_request?
}
# Allow write(create) issue
@@ -39,7 +41,6 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_download_code!, only: [:related_branches]
before_action do
- push_frontend_feature_flag(:vue_issues_list, project&.group)
push_frontend_feature_flag(:contacts_autocomplete, project&.group)
push_frontend_feature_flag(:incident_timeline, project)
end
@@ -50,6 +51,8 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:paginated_issue_discussions, project)
push_frontend_feature_flag(:realtime_labels, project)
push_force_frontend_feature_flag(:work_items, project&.work_items_feature_flag_enabled?)
+ push_frontend_feature_flag(:work_items_mvc_2)
+ push_frontend_feature_flag(:work_items_hierarchy, project)
end
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
@@ -81,7 +84,7 @@ class Projects::IssuesController < Projects::ApplicationController
attr_accessor :vulnerability_id
def index
- if vue_issues_list?
+ if index_html_request?
set_sort_order
else
@issues = @issuables
@@ -251,16 +254,14 @@ class Projects::IssuesController < Projects::ApplicationController
end
def service_desk
- @issues = @issuables # rubocop:disable Gitlab/ModuleWithInstanceVariables
- @users.push(User.support_bot) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @issues = @issuables
+ @users.push(User.support_bot)
end
protected
- def vue_issues_list?
- action_name.to_sym == :index &&
- html_request? &&
- Feature.enabled?(:vue_issues_list, project&.group)
+ def index_html_request?
+ action_name.to_sym == :index && html_request?
end
def sorting_field
@@ -403,6 +404,13 @@ class Projects::IssuesController < Projects::ApplicationController
# Overridden in EE
def create_vulnerability_issue_feedback(issue); end
+
+ def redirect_if_task
+ return render_404 if issue.task? && !project.work_items_feature_flag_enabled?
+ return unless issue.task?
+
+ redirect_to project_work_items_path(project, issue.id, params: request.query_parameters)
+ end
end
Projects::IssuesController.prepend_mod_with('Projects::IssuesController')
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 8c9f82b9dc1..9574c5d5849 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -3,6 +3,7 @@
class Projects::JobsController < Projects::ApplicationController
include SendFileUpload
include ContinueParams
+ include ProjectStatsRefreshConflictsGuard
urgency :low, [:index, :show, :trace, :retry, :play, :cancel, :unschedule, :status, :erase, :raw]
@@ -19,6 +20,7 @@ class Projects::JobsController < Projects::ApplicationController
before_action :verify_proxy_request!, only: :proxy_websocket_authorize
before_action :push_jobs_table_vue, only: [:index]
before_action :push_jobs_table_vue_search, only: [:index]
+ before_action :reject_if_build_artifacts_size_refreshing!, only: [:erase]
before_action do
push_frontend_feature_flag(:infinitely_collapsible_sections, @project)
@@ -40,7 +42,6 @@ class Projects::JobsController < Projects::ApplicationController
@builds = @builds.page(params[:page]).per(30).without_count
end
- # rubocop: disable CodeReuse/ActiveRecord
def show
respond_to do |format|
format.html
@@ -53,7 +54,6 @@ class Projects::JobsController < Projects::ApplicationController
end
end
end
- # rubocop: enable CodeReuse/ActiveRecord
def trace
@build.trace.being_watched! if @build.running?
diff --git a/app/controllers/projects/logs_controller.rb b/app/controllers/projects/logs_controller.rb
index 63d8981ef38..0f751db2064 100644
--- a/app/controllers/projects/logs_controller.rb
+++ b/app/controllers/projects/logs_controller.rb
@@ -8,6 +8,7 @@ module Projects
before_action :ensure_deployments, only: %i(k8s elasticsearch)
feature_category :logging
+ urgency :low
def index
return render_404 unless Feature.enabled?(:monitor_logging, project)
diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb
index c4f4913a620..a4091ebdf4b 100644
--- a/app/controllers/projects/mattermosts_controller.rb
+++ b/app/controllers/projects/mattermosts_controller.rb
@@ -20,7 +20,7 @@ class Projects::MattermostsController < Projects::ApplicationController
if result
flash[:notice] = 'This service is now configured'
- redirect_to edit_project_integration_path(@project, integration)
+ redirect_to edit_project_settings_integration_path(@project, integration)
else
flash[:alert] = message || 'Failed to configure service'
redirect_to new_project_mattermost_path(@project)
diff --git a/app/controllers/projects/merge_requests/drafts_controller.rb b/app/controllers/projects/merge_requests/drafts_controller.rb
index 686d2c1dc1f..db7557674b2 100644
--- a/app/controllers/projects/merge_requests/drafts_controller.rb
+++ b/app/controllers/projects/merge_requests/drafts_controller.rb
@@ -49,6 +49,10 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
def publish
result = DraftNotes::PublishService.new(merge_request, current_user).execute(draft_note(allow_nil: true))
+ if Feature.enabled?(:mr_review_submit_comment, @project) && create_note_params[:note]
+ Notes::CreateService.new(@project, current_user, create_note_params).execute
+ end
+
if result[:status] == :success
head :ok
else
@@ -102,6 +106,15 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
end
end
+ def create_note_params
+ params.permit(
+ :note
+ ).tap do |create_params|
+ create_params[:noteable_type] = merge_request.class.name
+ create_params[:noteable_id] = merge_request.id
+ end
+ end
+
def prepare_notes_for_rendering(notes)
return [] unless notes
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 458df40ece1..d420e136316 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -35,19 +35,19 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:file_identifier_hash)
push_frontend_feature_flag(:merge_request_widget_graphql, project)
push_frontend_feature_flag(:core_security_mr_widget_counts, project)
- push_frontend_feature_flag(:paginated_notes, project)
push_frontend_feature_flag(:confidential_notes, project)
push_frontend_feature_flag(:restructured_mr_widget, project)
push_frontend_feature_flag(:refactor_mr_widgets_extensions, project)
+ push_frontend_feature_flag(:refactor_code_quality_extension, project)
push_frontend_feature_flag(:refactor_mr_widget_test_summary, project)
push_frontend_feature_flag(:rebase_without_ci_ui, project)
push_frontend_feature_flag(:issue_assignees_widget, @project)
push_frontend_feature_flag(:realtime_labels, project)
- push_frontend_feature_flag(:updated_diff_expansion_buttons, project)
+ push_frontend_feature_flag(:refactor_security_extension, @project)
push_frontend_feature_flag(:mr_attention_requests, current_user)
- push_frontend_feature_flag(:updated_mr_header, project)
- push_frontend_feature_flag(:remove_diff_header_icons, project)
push_frontend_feature_flag(:moved_mr_sidebar, project)
+ push_frontend_feature_flag(:paginated_mr_discussions, project)
+ push_frontend_feature_flag(:mr_review_submit_comment, project)
end
before_action do
@@ -299,7 +299,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def remove_wip
@merge_request = ::MergeRequests::UpdateService
- .new(project: project, current_user: current_user, params: { wip_event: 'unwip' })
+ .new(project: project, current_user: current_user, params: { wip_event: 'ready' })
.execute(@merge_request)
render json: serialize_widget(@merge_request)
diff --git a/app/controllers/projects/metrics/dashboards/builder_controller.rb b/app/controllers/projects/metrics/dashboards/builder_controller.rb
index 96ca6d89111..a6b57798923 100644
--- a/app/controllers/projects/metrics/dashboards/builder_controller.rb
+++ b/app/controllers/projects/metrics/dashboards/builder_controller.rb
@@ -7,6 +7,7 @@ module Projects
before_action :authorize_metrics_dashboard!
feature_category :metrics
+ urgency :low
def panel_preview
respond_to do |format|
diff --git a/app/controllers/projects/metrics_dashboard_controller.rb b/app/controllers/projects/metrics_dashboard_controller.rb
index e305b018293..f2f276071a0 100644
--- a/app/controllers/projects/metrics_dashboard_controller.rb
+++ b/app/controllers/projects/metrics_dashboard_controller.rb
@@ -16,6 +16,7 @@ module Projects
end
feature_category :metrics
+ urgency :low
def show
if environment
diff --git a/app/controllers/projects/performance_monitoring/dashboards_controller.rb b/app/controllers/projects/performance_monitoring/dashboards_controller.rb
index 51a07c1b7a5..8acbc17aef3 100644
--- a/app/controllers/projects/performance_monitoring/dashboards_controller.rb
+++ b/app/controllers/projects/performance_monitoring/dashboards_controller.rb
@@ -13,6 +13,7 @@ module Projects
end
feature_category :metrics
+ urgency :low
def create
result = ::Metrics::Dashboard::CloneDashboardService.new(project, current_user, dashboard_params).execute
diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb
index fa38fb209f0..a23d7fb3e6b 100644
--- a/app/controllers/projects/pipeline_schedules_controller.rb
+++ b/app/controllers/projects/pipeline_schedules_controller.rb
@@ -14,13 +14,11 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
feature_category :continuous_integration
urgency :low
- # rubocop: disable CodeReuse/ActiveRecord
def index
@scope = params[:scope]
@all_schedules = Ci::PipelineSchedulesFinder.new(@project).execute
@schedules = Ci::PipelineSchedulesFinder.new(@project).execute(scope: params[:scope])
end
- # rubocop: enable CodeReuse/ActiveRecord
def new
@schedule = project.pipeline_schedules.new
diff --git a/app/controllers/projects/pipelines/tests_controller.rb b/app/controllers/projects/pipelines/tests_controller.rb
index 8f0e20290fe..e5b2dd14f69 100644
--- a/app/controllers/projects/pipelines/tests_controller.rb
+++ b/app/controllers/projects/pipelines/tests_controller.rb
@@ -23,7 +23,7 @@ module Projects
def show
respond_to do |format|
format.json do
- if Feature.enabled?(:ci_test_report_artifacts_expired, project) && pipeline.has_expired_test_reports?
+ if pipeline.has_expired_test_reports?
render json: { errors: 'Test report artifacts have expired' }, status: :not_found
else
render json: TestSuiteSerializer
@@ -36,7 +36,6 @@ module Projects
private
- # rubocop: disable CodeReuse/ActiveRecord
def builds
@builds ||= pipeline.latest_builds.id_in(build_ids).presence || render_404
end
@@ -56,7 +55,6 @@ module Projects
suite
end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 94865024688..adc3a912a91 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -3,6 +3,8 @@
class Projects::PipelinesController < Projects::ApplicationController
include ::Gitlab::Utils::StrongMemoize
include RedisTracking
+ include ProjectStatsRefreshConflictsGuard
+ include ZuoraCSP
urgency :low, [
:index, :new, :builds, :show, :failures, :create,
@@ -19,11 +21,10 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action :ensure_pipeline, only: [:show, :downloadable_artifacts]
+ before_action :reject_if_build_artifacts_size_refreshing!, only: [:destroy]
before_action do
push_frontend_feature_flag(:pipeline_tabs_vue, @project)
- push_frontend_feature_flag(:downstream_retry_action, @project)
- push_frontend_feature_flag(:failed_jobs_tab_vue, @project)
end
# Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596
@@ -42,23 +43,6 @@ class Projects::PipelinesController < Projects::ApplicationController
POLLING_INTERVAL = 10_000
- content_security_policy do |policy|
- next if policy.directives.blank?
-
- default_script_src = policy.directives['script-src'] || policy.directives['default-src']
- script_src_values = Array.wrap(default_script_src) | ["'self'", "'unsafe-eval'", 'https://*.zuora.com']
-
- default_frame_src = policy.directives['frame-src'] || policy.directives['default-src']
- frame_src_values = Array.wrap(default_frame_src) | ["'self'", 'https://*.zuora.com']
-
- default_child_src = policy.directives['child-src'] || policy.directives['default-src']
- child_src_values = Array.wrap(default_child_src) | ["'self'", 'https://*.zuora.com']
-
- policy.script_src(*script_src_values)
- policy.frame_src(*frame_src_values)
- policy.child_src(*child_src_values)
- end
-
feature_category :continuous_integration, [
:charts, :show, :config_variables, :stage, :cancel, :retry,
:builds, :dag, :failures, :status,
diff --git a/app/controllers/projects/prometheus/alerts_controller.rb b/app/controllers/projects/prometheus/alerts_controller.rb
index 5e1b9570fa0..c3dc17694d9 100644
--- a/app/controllers/projects/prometheus/alerts_controller.rb
+++ b/app/controllers/projects/prometheus/alerts_controller.rb
@@ -14,19 +14,11 @@ module Projects
prepend_before_action :repository, :project_without_auth, only: [:notify]
before_action :authorize_read_prometheus_alerts!, except: [:notify]
- before_action :alert, only: [:show, :metrics_dashboard]
+ before_action :alert, only: [:metrics_dashboard]
feature_category :incident_management
urgency :low
- def index
- render json: serialize_as_json(alerts)
- end
-
- def show
- render json: serialize_as_json(alert)
- end
-
def notify
token = extract_alert_manager_token(request)
result = notify_service.execute(token)
diff --git a/app/controllers/projects/prometheus/metrics_controller.rb b/app/controllers/projects/prometheus/metrics_controller.rb
index c5778ba15f2..db5471ea322 100644
--- a/app/controllers/projects/prometheus/metrics_controller.rb
+++ b/app/controllers/projects/prometheus/metrics_controller.rb
@@ -7,6 +7,7 @@ module Projects
before_action :require_prometheus_metrics!
feature_category :metrics
+ urgency :low
def active_common
respond_to do |format|
@@ -66,7 +67,7 @@ module Projects
)
if @metric.persisted?
- redirect_to edit_project_integration_path(project, ::Integrations::Prometheus),
+ redirect_to edit_project_settings_integration_path(project, ::Integrations::Prometheus),
notice: _('Metric was successfully added.')
else
render 'new'
@@ -77,7 +78,7 @@ module Projects
@metric = prometheus_metric
if @metric.update(metrics_params)
- redirect_to edit_project_integration_path(project, ::Integrations::Prometheus),
+ redirect_to edit_project_settings_integration_path(project, ::Integrations::Prometheus),
notice: _('Metric was successfully updated.')
else
render 'edit'
@@ -93,7 +94,7 @@ module Projects
respond_to do |format|
format.html do
- redirect_to edit_project_integration_path(project, ::Integrations::Prometheus), status: :see_other
+ redirect_to edit_project_settings_integration_path(project, ::Integrations::Prometheus), status: :see_other
end
format.json do
head :ok
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index 1dfb71842bd..da414d068a6 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -18,11 +18,7 @@ class Projects::ReleasesController < Projects::ApplicationController
require_non_empty_project
end
format.json do
- if Feature.enabled?(:remove_sha_from_releases_json, project)
- render json: ReleaseSerializer.new.represent(releases)
- else
- render json: releases
- end
+ render json: ReleaseSerializer.new.represent(releases)
end
end
end
@@ -56,19 +52,11 @@ class Projects::ReleasesController < Projects::ApplicationController
end
def release
- @release ||= project.releases.find_by_tag!(sanitized_tag_name)
+ @release ||= project.releases.find_by_tag!(params[:tag])
end
def link
- release.links.find_by_filepath!(sanitized_filepath)
- end
-
- def sanitized_filepath
- "/#{CGI.unescape(params[:filepath])}"
- end
-
- def sanitized_tag_name
- CGI.unescape(params[:tag])
+ release.links.find_by_filepath!("/#{params[:filepath]}")
end
# Default order_by is 'released_at', which is set in ReleasesFinder.
diff --git a/app/controllers/projects/service_hook_logs_controller.rb b/app/controllers/projects/service_hook_logs_controller.rb
deleted file mode 100644
index 7b037c60321..00000000000
--- a/app/controllers/projects/service_hook_logs_controller.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-class Projects::ServiceHookLogsController < Projects::HookLogsController
- extend Gitlab::Utils::Override
-
- before_action :integration, only: [:show, :retry]
-
- def retry
- execute_hook
- redirect_to edit_project_integration_path(@project, @integration)
- end
-
- private
-
- def integration
- @integration ||= @project.find_or_initialize_integration(params[:integration_id])
- end
-
- override :hook
- def hook
- @hook ||= integration.service_hook || not_found
- end
-end
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
deleted file mode 100644
index 8f83e34411b..00000000000
--- a/app/controllers/projects/services_controller.rb
+++ /dev/null
@@ -1,122 +0,0 @@
-# frozen_string_literal: true
-
-class Projects::ServicesController < Projects::ApplicationController
- include Integrations::Params
- include InternalRedirect
-
- # Authorize
- 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]
-
- respond_to :html
-
- layout "project_settings"
-
- feature_category :integrations
- urgency :low, [:test]
-
- def edit
- end
-
- def update
- attributes = integration_params[:integration]
-
- 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
- if saved
- redirect_to redirect_path, notice: success_message
- else
- render 'edit'
- end
- end
-
- format.json do
- status = saved ? :ok : :unprocessable_entity
-
- render json: serialize_as_json, status: status
- end
- end
- end
-
- def test
- if integration.testable?
- render json: service_test_response, status: :ok
- else
- render json: {}, status: :not_found
- end
- end
-
- private
-
- def redirect_path
- safe_redirect_path(params[:redirect_to]).presence || edit_project_integration_path(project, integration)
- end
-
- def service_test_response
- unless integration.update(integration_params[:integration])
- return { error: true, message: _('Validations failed.'), service_response: integration.errors.full_messages.join(','), test_failed: false }
- end
-
- result = ::Integrations::Test::ProjectService.new(integration, current_user, params[:event]).execute
-
- unless result[:success]
- return { error: true, message: s_('Integrations|Connection failed. Please check your settings.'), service_response: result[:message].to_s, test_failed: true }
- end
-
- result[:data].presence || {}
- rescue *Gitlab::HTTP::HTTP_ERRORS => e
- { error: true, message: s_('Integrations|Connection failed. Please check your settings.'), service_response: e.message, test_failed: true }
- end
-
- def success_message
- if integration.active?
- s_('Integrations|%{integration} settings saved and active.') % { integration: integration.title }
- else
- s_('Integrations|%{integration} settings saved, but not active.') % { integration: integration.title }
- end
- end
-
- def integration
- @integration ||= project.find_or_initialize_integration(params[:id])
- 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?
-
- @web_hook_logs ||= integration.service_hook.web_hook_logs.recent.page(params[:page])
- end
-
- def ensure_service_enabled
- render_404 unless service
- end
-
- def serialize_as_json
- integration
- .as_json(only: integration.json_fields)
- .merge(errors: integration.errors.as_json)
- 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/settings/branch_rules_controller.rb b/app/controllers/projects/settings/branch_rules_controller.rb
new file mode 100644
index 00000000000..0a415b60124
--- /dev/null
+++ b/app/controllers/projects/settings/branch_rules_controller.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Projects
+ module Settings
+ class BranchRulesController < Projects::ApplicationController
+ before_action :authorize_admin_project!
+
+ feature_category :source_code_management
+
+ def index
+ render_404 unless Feature.enabled?(:branch_rules, project)
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index ee50327be8f..cda6c8abea7 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -13,6 +13,7 @@ module Projects
before_action :define_variables
before_action do
push_frontend_feature_flag(:ajax_new_deploy_token, @project)
+ push_frontend_feature_flag(:ci_variable_settings_graphql, @project)
end
helper_method :highlight_badge
@@ -27,14 +28,7 @@ module Projects
).to_json
end
- if current_user.ci_owned_runners_cross_joins_fix_enabled?
- render
- else
- # @assignable_runners is using ci_owned_runners
- ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336436') do
- render
- end
- end
+ render
end
def update
diff --git a/app/controllers/projects/settings/integration_hook_logs_controller.rb b/app/controllers/projects/settings/integration_hook_logs_controller.rb
new file mode 100644
index 00000000000..b3b5a292d42
--- /dev/null
+++ b/app/controllers/projects/settings/integration_hook_logs_controller.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Projects
+ module Settings
+ class IntegrationHookLogsController < Projects::HookLogsController
+ extend Gitlab::Utils::Override
+
+ before_action :integration, only: [:show, :retry]
+
+ def retry
+ execute_hook
+ redirect_to edit_project_settings_integration_path(@project, @integration)
+ end
+
+ private
+
+ def integration
+ @integration ||= @project.find_or_initialize_integration(params[:integration_id])
+ end
+
+ override :hook
+ def hook
+ @hook ||= integration.service_hook || not_found
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb
index c9d92d1aee9..3365da65de8 100644
--- a/app/controllers/projects/settings/integrations_controller.rb
+++ b/app/controllers/projects/settings/integrations_controller.rb
@@ -3,14 +3,142 @@
module Projects
module Settings
class IntegrationsController < Projects::ApplicationController
+ include ::Integrations::Params
+ include ::InternalRedirect
+
before_action :authorize_admin_project!
+ before_action :ensure_integration_enabled, only: [:edit, :update, :test]
+ before_action :integration, only: [:edit, :update, :test]
+ before_action :default_integration, only: [:edit, :update]
+ before_action :web_hook_logs, only: [:edit, :update]
+
+ respond_to :html
+
layout "project_settings"
feature_category :integrations
+ urgency :low, [:test]
- def show
+ def index
@integrations = @project.find_or_initialize_integrations
end
+
+ def edit
+ end
+
+ def update
+ attributes = integration_params[:integration]
+
+ 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
+ if saved
+ redirect_to redirect_path, notice: success_message
+ else
+ render 'edit'
+ end
+ end
+
+ format.json do
+ status = saved ? :ok : :unprocessable_entity
+
+ render json: serialize_as_json, status: status
+ end
+ end
+ end
+
+ def test
+ if integration.testable?
+ render json: integration_test_response, status: :ok
+ else
+ render json: {}, status: :not_found
+ end
+ end
+
+ private
+
+ def redirect_path
+ safe_redirect_path(params[:redirect_to]).presence ||
+ edit_project_settings_integration_path(project, integration)
+ end
+
+ def integration_test_response
+ unless integration.update(integration_params[:integration])
+ return {
+ error: true,
+ message: _('Validations failed.'),
+ service_response: integration.errors.full_messages.join(','),
+ test_failed: false
+ }
+ end
+
+ result = ::Integrations::Test::ProjectService.new(integration, current_user, params[:event]).execute
+
+ unless result[:success]
+ return {
+ error: true,
+ message: s_('Integrations|Connection failed. Please check your settings.'),
+ service_response: result[:message].to_s,
+ test_failed: true
+ }
+ end
+
+ result[:data].presence || {}
+ rescue *Gitlab::HTTP::HTTP_ERRORS => e
+ {
+ error: true,
+ message: s_('Integrations|Connection failed. Please check your settings.'),
+ service_response: e.message,
+ test_failed: true
+ }
+ end
+
+ def success_message
+ if integration.active?
+ format(s_('Integrations|%{integration} settings saved and active.'), integration: integration.title)
+ else
+ format(s_('Integrations|%{integration} settings saved, but not active.'), integration: integration.title)
+ end
+ end
+
+ def integration
+ @integration ||= project.find_or_initialize_integration(params[:id])
+ end
+
+ def default_integration
+ @default_integration ||= Integration.default_integration(integration.type, project)
+ end
+
+ def web_hook_logs
+ return unless integration.service_hook.present?
+
+ @web_hook_logs ||= integration.service_hook.web_hook_logs.recent.page(params[:page])
+ end
+
+ def ensure_integration_enabled
+ render_404 unless integration
+ end
+
+ def serialize_as_json
+ integration
+ .as_json(only: integration.json_fields)
+ .merge(errors: integration.errors.as_json)
+ end
+
+ def use_inherited_settings?(attributes)
+ default_integration && attributes[:inherit_from_id] == default_integration.id.to_s
+ end
end
end
end
diff --git a/app/controllers/projects/settings/packages_and_registries_controller.rb b/app/controllers/projects/settings/packages_and_registries_controller.rb
index 0cd2bfa9695..d3c08bef808 100644
--- a/app/controllers/projects/settings/packages_and_registries_controller.rb
+++ b/app/controllers/projects/settings/packages_and_registries_controller.rb
@@ -17,12 +17,7 @@ module Projects
private
def packages_and_registries_settings_enabled!
- render_404 unless can_destroy_container_registry_image?(project)
- end
-
- def can_destroy_container_registry_image?(project)
- Gitlab.config.registry.enabled &&
- can?(current_user, :destroy_container_image, project)
+ render_404 unless can?(current_user, :view_package_registry_project_settings, project)
end
end
end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index 0fd2d56229a..a178b8f7aa3 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -15,6 +15,7 @@ module Projects
urgency :low, [:show, :create_deploy_token]
def show
+ push_frontend_feature_flag(:branch_rules, @project)
render_show
end
diff --git a/app/controllers/projects/static_site_editor_controller.rb b/app/controllers/projects/static_site_editor_controller.rb
deleted file mode 100644
index fed6307514e..00000000000
--- a/app/controllers/projects/static_site_editor_controller.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# frozen_string_literal: true
-
-class Projects::StaticSiteEditorController < Projects::ApplicationController
- include ExtractsPath
- include CreatesCommit
- include BlobHelper
-
- layout 'fullscreen'
-
- content_security_policy do |policy|
- next if policy.directives.blank?
-
- frame_src_values = Array.wrap(policy.directives['frame-src']) | ['https://www.youtube.com']
- policy.frame_src(*frame_src_values)
- end
-
- prepend_before_action :authenticate_user!, only: [:show]
- before_action :assign_ref_and_path, only: [:show]
- before_action :authorize_edit_tree!, only: [:show]
-
- feature_category :static_site_editor
-
- def index
- render_404
- end
-
- def show
- redirect_to ide_edit_path(project, @ref, @path)
- end
-
- private
-
- def serialize_necessary_payload_values_to_json(payload)
- # This will convert booleans, Array-like and Hash-like objects to JSON
- payload.transform_values do |value|
- if value.is_a?(String) || value.is_a?(Integer)
- value
- elsif value.nil?
- ''
- else
- value.to_json
- end
- end
- end
-
- def assign_ref_and_path
- @ref, @path = extract_ref(params.fetch(:id))
-
- render_404 if @ref.blank? || @path.blank?
- end
-end
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index eb3579551bd..432497850f2 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -94,11 +94,10 @@ class Projects::TagsController < Projects::ApplicationController
def destroy
result = ::Tags::DestroyService.new(project, current_user).execute(params[:id])
- if result[:status] == :success
- render json: result
- else
- render json: { message: result[:message] }, status: result[:return_code]
- end
+ flash_type = result[:status] == :error ? :alert : :notice
+ flash[flash_type] = result[:message]
+
+ redirect_to project_tags_path(@project), status: :see_other
end
private
diff --git a/app/controllers/projects/tracings_controller.rb b/app/controllers/projects/tracings_controller.rb
index a4aac6aaa32..b5c1354c4a9 100644
--- a/app/controllers/projects/tracings_controller.rb
+++ b/app/controllers/projects/tracings_controller.rb
@@ -13,6 +13,7 @@ module Projects
before_action :authorize_update_environment!
feature_category :tracing
+ urgency :low
def show
render_404 unless Feature.enabled?(:monitor_tracing, @project)
diff --git a/app/controllers/projects/usage_quotas_controller.rb b/app/controllers/projects/usage_quotas_controller.rb
index f45ee265432..07a3c010f4f 100644
--- a/app/controllers/projects/usage_quotas_controller.rb
+++ b/app/controllers/projects/usage_quotas_controller.rb
@@ -6,6 +6,7 @@ class Projects::UsageQuotasController < Projects::ApplicationController
layout "project_settings"
feature_category :utilization
+ urgency :low
def index
@hide_search_settings = true
diff --git a/app/controllers/projects/work_items_controller.rb b/app/controllers/projects/work_items_controller.rb
index 27857dac2b7..ba23af41bb0 100644
--- a/app/controllers/projects/work_items_controller.rb
+++ b/app/controllers/projects/work_items_controller.rb
@@ -3,6 +3,8 @@
class Projects::WorkItemsController < Projects::ApplicationController
before_action do
push_force_frontend_feature_flag(:work_items, project&.work_items_feature_flag_enabled?)
+ push_frontend_feature_flag(:work_items_mvc_2)
+ push_frontend_feature_flag(:work_items_hierarchy, project)
end
feature_category :team_planning
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 60d30352ff8..1e0ef1ad337 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -42,6 +42,13 @@ class ProjectsController < Projects::ApplicationController
push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
push_licensed_feature(:security_orchestration_policies) if @project.present? && @project.licensed_feature_available?(:security_orchestration_policies)
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
+ push_frontend_feature_flag(:work_items_mvc_2)
+ push_frontend_feature_flag(:package_registry_access_level)
+ push_frontend_feature_flag(:work_items_hierarchy, @project)
+ end
+
+ before_action only: :edit do
+ push_frontend_feature_flag(:enforce_auth_checks_on_uploads, @project)
end
layout :determine_layout
@@ -410,6 +417,7 @@ class ProjectsController < Projects::ApplicationController
repository_access_level
snippets_access_level
wiki_access_level
+ package_registry_access_level
pages_access_level
metrics_dashboard_access_level
analytics_access_level
diff --git a/app/controllers/pwa_controller.rb b/app/controllers/pwa_controller.rb
index ea14dfb27b3..8de1b10e1f1 100644
--- a/app/controllers/pwa_controller.rb
+++ b/app/controllers/pwa_controller.rb
@@ -4,9 +4,13 @@ class PwaController < ApplicationController # rubocop:disable Gitlab/NamespacedC
layout 'errors'
feature_category :navigation
+ urgency :low
skip_before_action :authenticate_user!
+ def manifest
+ end
+
def offline
end
end
diff --git a/app/controllers/registrations/welcome_controller.rb b/app/controllers/registrations/welcome_controller.rb
index ea50099120b..a2b25acae64 100644
--- a/app/controllers/registrations/welcome_controller.rb
+++ b/app/controllers/registrations/welcome_controller.rb
@@ -45,7 +45,7 @@ module Registrations
end
def update_params
- params.require(:user).permit(:role, :other_role, :setup_for_company)
+ params.require(:user).permit(:role, :setup_for_company)
end
def requires_confirmation?(user)
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 7011bf856e3..206580d205a 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -153,6 +153,7 @@ class RegistrationsController < Devise::RegistrationsController
flash[:alert] = _('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
flash.delete :recaptcha_error
+ add_gon_variables
render action: 'new'
end
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index aab901c1008..7d251ba555c 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -4,6 +4,7 @@ class SearchController < ApplicationController
include ControllerWithCrossProjectAccessCheck
include SearchHelper
include RedisTracking
+ include ProductAnalyticsTracking
include SearchRateLimitable
RESCUE_FROM_TIMEOUT_ACTIONS = [:count, :show, :autocomplete].freeze
@@ -71,7 +72,6 @@ class SearchController < ApplicationController
render json: { count: count }
end
- # rubocop: disable CodeReuse/ActiveRecord
def autocomplete
term = params[:term]
@@ -80,7 +80,6 @@ class SearchController < ApplicationController
render json: search_autocomplete_opts(term).to_json
end
- # rubocop: enable CodeReuse/ActiveRecord
def opensearch
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 66a531b0b3b..9000e9c39de 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -127,7 +127,9 @@ class SessionsController < Devise::SessionsController
flash[:alert] = _('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
flash.delete :recaptcha_error
- redirect_to new_user_session_path
+ add_gon_variables
+
+ respond_with_navigational(resource) { render :new }
end
end
@@ -181,7 +183,6 @@ class SessionsController < Devise::SessionsController
# Handle an "initial setup" state, where there's only one user, it's an admin,
# and they require a password change.
- # rubocop: disable CodeReuse/ActiveRecord
def check_initial_setup
return unless User.limit(2).count == 1 # Count as much 2 to know if we have exactly one
@@ -196,7 +197,6 @@ class SessionsController < Devise::SessionsController
redirect_to edit_user_password_path(reset_password_token: @token),
notice: _("Please create a password for your new account.")
end
- # rubocop: enable CodeReuse/ActiveRecord
def ensure_password_authentication_enabled!
render_403 unless Gitlab::CurrentSettings.password_authentication_enabled_for_web?
diff --git a/app/controllers/users/callouts_controller.rb b/app/controllers/users/callouts_controller.rb
index fe308d9dd1e..f94ef9063c3 100644
--- a/app/controllers/users/callouts_controller.rb
+++ b/app/controllers/users/callouts_controller.rb
@@ -3,6 +3,7 @@
module Users
class CalloutsController < ApplicationController
feature_category :navigation
+ urgency :low
def create
if callout.persisted?
diff --git a/app/controllers/users/terms_controller.rb b/app/controllers/users/terms_controller.rb
index f0d95b56d33..f7eb2aad9dc 100644
--- a/app/controllers/users/terms_controller.rb
+++ b/app/controllers/users/terms_controller.rb
@@ -15,7 +15,7 @@ module Users
layout 'terms'
- feature_category :users
+ feature_category :user_management
def index
@redirect = redirect_path
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 794d60e733d..2799479d922 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -29,13 +29,14 @@ class UsersController < ApplicationController
feature_category :users, [:show, :activity, :groups, :projects, :contributed, :starred,
:followers, :following, :calendar, :calendar_activities,
- :exists, :activity, :follow, :unfollow, :ssh_keys, :gpg_keys]
+ :exists, :activity, :follow, :unfollow, :ssh_keys]
feature_category :snippets, [:snippets]
+ feature_category :source_code_management, [:gpg_keys]
# TODO: Set higher urgency after resolving https://gitlab.com/gitlab-org/gitlab/-/issues/357914
- urgency :low, [:show, :calendar_activities, :contributed, :activity, :projects, :groups]
- urgency :default, [:calendar, :followers, :following, :starred]
+ urgency :low, [:show, :calendar_activities, :contributed, :activity, :projects, :groups, :calendar]
+ urgency :default, [:followers, :following, :starred]
urgency :high, [:exists]
def show
diff --git a/app/controllers/whats_new_controller.rb b/app/controllers/whats_new_controller.rb
index 6f389aa4924..4decd7f1bee 100644
--- a/app/controllers/whats_new_controller.rb
+++ b/app/controllers/whats_new_controller.rb
@@ -9,6 +9,7 @@ class WhatsNewController < ApplicationController
before_action :check_valid_page_param, :set_pagination_headers
feature_category :navigation
+ urgency :low
def index
respond_to do |format|
diff --git a/app/events/pages/page_deleted_event.rb b/app/events/pages/page_deleted_event.rb
new file mode 100644
index 00000000000..b1ea14a6ec5
--- /dev/null
+++ b/app/events/pages/page_deleted_event.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Pages
+ class PageDeletedEvent < ::Gitlab::EventStore::Event
+ def schema
+ {
+ 'type' => 'object',
+ 'properties' => {
+ 'project_id' => { 'type' => 'integer' },
+ 'namespace_id' => { 'type' => 'integer' }
+ },
+ 'required' => %w[project_id namespace_id]
+ }
+ end
+ end
+end
diff --git a/app/experiments/application_experiment.rb b/app/experiments/application_experiment.rb
index e5b67527cb1..863a2c41d4c 100644
--- a/app/experiments/application_experiment.rb
+++ b/app/experiments/application_experiment.rb
@@ -34,12 +34,14 @@ class ApplicationExperiment < Gitlab::Experiment
#
# @deprecated
def key_for(source, seed = name)
- source = source.keys + source.values if source.is_a?(Hash)
-
- ingredients = Array(source).map { |v| identify(v) }
- ingredients.unshift(seed)
+ # If FIPS is enabled, we simply call the method available in the gem, which
+ # uses SHA2.
+ return super if Gitlab::FIPS.enabled?
- Digest::MD5.hexdigest(ingredients.join('|'))
+ # If FIPS isn't enabled, we use the legacy MD5 logic to keep existing
+ # experiment events working.
+ source = source.keys + source.values if source.is_a?(Hash)
+ Digest::MD5.hexdigest(Array(source).map { |v| identify(v) }.unshift(seed).join('|'))
end
def nest_experiment(other)
diff --git a/app/experiments/templates/new_project_readme_content/readme_advanced.md.tt b/app/experiments/templates/new_project_readme_content/readme_advanced.md.tt
index 9b950ff49e4..e1dddcb2c66 100644
--- a/app/experiments/templates/new_project_readme_content/readme_advanced.md.tt
+++ b/app/experiments/templates/new_project_readme_content/readme_advanced.md.tt
@@ -14,7 +14,7 @@ Already a pro? Just edit this README.md and make it your own. Want to make it ea
- [ ] [Create](<%= redirect("https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file") %>) or [upload](<%= redirect("https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file") %>) files
- [ ] [Add files using the command line](<%= redirect("https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line") %>) or push an existing Git repository with the following command:
-```
+```shell
cd existing_repo
git remote add origin <%= @project.http_url_to_repo %>
git branch -M <%= @project.default_branch_or_main %>
@@ -43,52 +43,64 @@ Use the built-in continuous integration in GitLab.
- [ ] [Use pull-based deployments for improved Kubernetes management](<%= redirect("https://docs.gitlab.com/ee/user/clusters/agent/") %>)
- [ ] [Set up protected environments](<%= redirect("https://docs.gitlab.com/ee/ci/environments/protected_environments.html") %>)
-***
+---
-# Editing this README
+## Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com) for this template.
-## Suggestions for a good README
+### Suggestions for a good README
+
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
-## Name
+### Name
+
Choose a self-explaining name for your project.
-## Description
+### Description
+
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
-## Badges
+### Badges
+
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
-## Visuals
+### Visuals
+
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
-## Installation
+### Installation
+
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
-## Usage
+### Usage
+
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
-## Support
+### Support
+
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
-## Roadmap
+### Roadmap
+
If you have ideas for releases in the future, it is a good idea to list them in the README.
-## Contributing
+### Contributing
+
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
-## Authors and acknowledgment
+### Authors and acknowledgment
+
Show your appreciation to those who have contributed to the project.
-## License
+### License
+
For open source projects, say how it is licensed.
-## Project status
-If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
+### Project status
+If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
diff --git a/app/finders/clusters/agents_finder.rb b/app/finders/clusters/agents_finder.rb
index 136bbf16981..d0b1240157c 100644
--- a/app/finders/clusters/agents_finder.rb
+++ b/app/finders/clusters/agents_finder.rb
@@ -2,6 +2,8 @@
module Clusters
class AgentsFinder
+ include FinderMethods
+
def initialize(project, current_user, params: {})
@project = project
@current_user = current_user
diff --git a/app/finders/crm/contacts_finder.rb b/app/finders/crm/contacts_finder.rb
index c2d44bec27b..29f3d6f0f16 100644
--- a/app/finders/crm/contacts_finder.rb
+++ b/app/finders/crm/contacts_finder.rb
@@ -6,6 +6,9 @@
# current_user - user performing the action. Must have the correct permission level for the group.
# params:
# group: Group, required
+# search: String, optional
+# state: CustomerRelations::ContactStateEnum, optional
+# ids: int[], optional
module Crm
class ContactsFinder
include Gitlab::Allowable
@@ -21,7 +24,11 @@ module Crm
def execute
return CustomerRelations::Contact.none unless root_group
- root_group.contacts
+ contacts = root_group.contacts
+ contacts = by_ids(contacts)
+ contacts = by_state(contacts)
+ contacts = by_search(contacts)
+ contacts.sort_by_name
end
private
@@ -35,5 +42,35 @@ module Crm
group
end
end
+
+ def by_search(contacts)
+ return contacts unless search?
+
+ contacts.search(params[:search])
+ end
+
+ def by_state(contacts)
+ return contacts unless state?
+
+ contacts.search_by_state(params[:state])
+ end
+
+ def by_ids(contacts)
+ return contacts unless ids?
+
+ contacts.id_in(params[:ids])
+ end
+
+ def search?
+ params[:search].present?
+ end
+
+ def state?
+ params[:state].present?
+ end
+
+ def ids?
+ params[:ids].present?
+ end
end
end
diff --git a/app/finders/crm/organizations_finder.rb b/app/finders/crm/organizations_finder.rb
new file mode 100644
index 00000000000..5a8ab148ef3
--- /dev/null
+++ b/app/finders/crm/organizations_finder.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+# Finder for retrieving organizations scoped to a group
+#
+# Arguments:
+# current_user - user performing the action. Must have the correct permission level for the group.
+# params:
+# group: Group, required
+# search: String, optional
+# state: CustomerRelations::OrganizationStateEnum, optional
+# ids: int[], optional
+module Crm
+ class OrganizationsFinder
+ include Gitlab::Allowable
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :params, :current_user
+
+ def initialize(current_user, params = {})
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ return CustomerRelations::Organization.none unless root_group
+
+ organizations = root_group.organizations
+ organizations = by_ids(organizations)
+ organizations = by_search(organizations)
+ organizations = by_state(organizations)
+ organizations.sort_by_name
+ end
+
+ private
+
+ def root_group
+ strong_memoize(:root_group) do
+ group = params[:group]&.root_ancestor
+
+ next unless can?(@current_user, :read_crm_organization, group)
+
+ group
+ end
+ end
+
+ def by_search(organizations)
+ return organizations unless search?
+
+ organizations.search(params[:search])
+ end
+
+ def by_state(organizations)
+ return organizations unless state?
+
+ organizations.search_by_state(params[:state])
+ end
+
+ def by_ids(organizations)
+ return organizations unless ids?
+
+ organizations.id_in(params[:ids])
+ end
+
+ def search?
+ params[:search].present?
+ end
+
+ def state?
+ params[:state].present?
+ end
+
+ def ids?
+ params[:ids].present?
+ end
+ end
+end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index fe07a52cbf0..6bbbc237e62 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -316,10 +316,8 @@ class IssuableFinder
# rubocop: disable CodeReuse/ActiveRecord
def by_project(items)
- if params.project?
+ if params.project? || params.projects
items.of_projects(params.projects).references_project
- elsif params.projects
- items.merge(params.projects.reorder(nil)).join_project
else
items.none
end
diff --git a/app/finders/issuable_finder/params.rb b/app/finders/issuable_finder/params.rb
index 359a56bd39b..32d50802537 100644
--- a/app/finders/issuable_finder/params.rb
+++ b/app/finders/issuable_finder/params.rb
@@ -142,7 +142,7 @@ class IssuableFinder
projects_public_or_visible_to_user
end
- projects.with_feature_available_for_user(klass, current_user).reorder(nil) # rubocop: disable CodeReuse/ActiveRecord
+ projects.with_feature_available_for_user(klass.base_class, current_user).reorder(nil) # rubocop: disable CodeReuse/ActiveRecord
end
end
@@ -175,7 +175,7 @@ class IssuableFinder
return Project.none unless group
if params[:include_subgroups]
- Project.where(namespace_id: group.self_and_descendants) # rubocop: disable CodeReuse/ActiveRecord
+ Project.where(namespace_id: group.self_and_descendant_ids) # rubocop: disable CodeReuse/ActiveRecord
else
group.projects
end
@@ -215,7 +215,7 @@ class IssuableFinder
end
def min_access_level
- ProjectFeature.required_minimum_access_level(klass)
+ ProjectFeature.required_minimum_access_level(klass.base_class)
end
def method_missing(method_name, *args, &block)
diff --git a/app/finders/issuables/label_filter.rb b/app/finders/issuables/label_filter.rb
index 9a6ca107b19..4e9c964e51c 100644
--- a/app/finders/issuables/label_filter.rb
+++ b/app/finders/issuables/label_filter.rb
@@ -27,7 +27,7 @@ module Issuables
def by_label(issuables)
return issuables unless label_names_from_params.present?
- target_model = issuables.model
+ target_model = issuables.base_class
if filter_by_no_label?
issuables.where(label_link_query(target_model).arel.exists.not)
@@ -55,7 +55,7 @@ module Issuables
# rubocop: disable CodeReuse/ActiveRecord
def issuables_with_selected_labels(issuables, label_names)
- target_model = issuables.model
+ target_model = issuables.base_class
if root_namespace
all_label_ids = find_label_ids(label_names)
@@ -77,7 +77,7 @@ module Issuables
# rubocop: disable CodeReuse/ActiveRecord
def issuables_without_selected_labels(issuables, label_names)
- target_model = issuables.model
+ target_model = issuables.base_class
if root_namespace
label_ids = find_label_ids(label_names).flatten(1)
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 7929c36906d..663dda73a6a 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -37,7 +37,7 @@ class IssuesFinder < IssuableFinder
# rubocop: disable CodeReuse/ActiveRecord
def klass
- Issue.includes(:author)
+ model_class.includes(:author)
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -47,10 +47,10 @@ class IssuesFinder < IssuableFinder
# rubocop: disable CodeReuse/ActiveRecord
def with_confidentiality_access_check
- return Issue.all if params.user_can_see_all_issues?
+ return model_class.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
+ issues = model_class.without_hidden
return issues.all if params.user_can_see_all_confidential_issues?
@@ -77,7 +77,7 @@ class IssuesFinder < IssuableFinder
def init_collection
if params.public_only?
- Issue.public_only
+ model_class.public_only
else
with_confidentiality_access_check
end
@@ -129,7 +129,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 (WorkItems::Type.base_types.keys & issue_type_params).sort == issue_type_params.sort
+ return model_class.none unless (WorkItems::Type.base_types.keys & issue_type_params).sort == issue_type_params.sort
items.with_issue_type(params[:issue_types])
end
@@ -140,6 +140,10 @@ class IssuesFinder < IssuableFinder
items.without_issue_type(issue_type_params)
end
+
+ def model_class
+ Issue
+ end
end
IssuesFinder.prepend_mod_with('IssuesFinder')
diff --git a/app/finders/packages/pypi/packages_finder.rb b/app/finders/packages/pypi/packages_finder.rb
index 47cfb59944b..17138134eb3 100644
--- a/app/finders/packages/pypi/packages_finder.rb
+++ b/app/finders/packages/pypi/packages_finder.rb
@@ -4,6 +4,8 @@ module Packages
module Pypi
class PackagesFinder < ::Packages::GroupOrProjectPackageFinder
def execute
+ return packages unless @params[:package_name]
+
packages.with_normalized_pypi_name(@params[:package_name])
end
diff --git a/app/finders/work_items/work_items_finder.rb b/app/finders/work_items/work_items_finder.rb
new file mode 100644
index 00000000000..960272fe47e
--- /dev/null
+++ b/app/finders/work_items/work_items_finder.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+# WorkItem model inherits from Issue model. It's planned to be its extension
+# with widgets support. Because WorkItems are internally Issues, WorkItemsFinder
+# can be almost identical to IssuesFinder, except it should return instances of
+# WorkItems instead of Issues
+module WorkItems
+ class WorkItemsFinder < IssuesFinder
+ def params_class
+ ::IssuesFinder::Params
+ end
+
+ private
+
+ def model_class
+ WorkItem
+ end
+ end
+end
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 9b23aa60eab..b399f0490ee 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -14,17 +14,19 @@ class GitlabSchema < GraphQL::Schema
use Gitlab::Graphql::Tracers::ApplicationContextTracer
use Gitlab::Graphql::Tracers::MetricsTracer
use Gitlab::Graphql::Tracers::LoggerTracer
- use Gitlab::Graphql::GenericTracing # Old tracer which will be removed eventually
+
+ # TODO: Old tracer which will be removed eventually
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/345396
+ use Gitlab::Graphql::GenericTracing
use Gitlab::Graphql::Tracers::TimerTracer
use GraphQL::Subscriptions::ActionCableSubscriptions
- use GraphQL::Pagination::Connections
use BatchLoader::GraphQL
use Gitlab::Graphql::Pagination::Connections
use Gitlab::Graphql::Timeout, max_seconds: Gitlab.config.gitlab.graphql_timeout
- query_analyzer Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer.new
- query_analyzer Gitlab::Graphql::QueryAnalyzers::RecursionAnalyzer.new
+ query_analyzer Gitlab::Graphql::QueryAnalyzers::AST::LoggerAnalyzer
+ query_analyzer Gitlab::Graphql::QueryAnalyzers::AST::RecursionAnalyzer
query Types::QueryType
mutation Types::MutationType
@@ -49,10 +51,10 @@ class GitlabSchema < GraphQL::Schema
super(queries, **kwargs)
end
- def get_type(type_name)
+ def get_type(type_name, context = GraphQL::Query::NullContext)
type_name = Gitlab::GlobalId::Deprecations.apply_to_graphql_name(type_name)
- super(type_name)
+ super(type_name, context)
end
def id_from_object(object, _type = nil, _ctx = nil)
@@ -77,8 +79,7 @@ class GitlabSchema < GraphQL::Schema
end
def resolve_type(type, object, ctx = :__undefined__)
- tc = type.metadata[:type_class]
- return if tc.respond_to?(:assignable?) && !tc.assignable?(object)
+ return if type.respond_to?(:assignable?) && !type.assignable?(object)
super
end
@@ -168,14 +169,3 @@ class GitlabSchema < GraphQL::Schema
end
GitlabSchema.prepend_mod_with('GitlabSchema') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
-# Force the schema to load as a workaround for intermittent errors we
-# see due to a lack of thread safety.
-#
-# TODO: We can remove this workaround when we convert the schema to use
-# the new query interpreter runtime.
-#
-# See:
-# - https://gitlab.com/gitlab-org/gitlab/-/issues/211478
-# - https://gitlab.com/gitlab-org/gitlab/-/issues/210556
-GitlabSchema.graphql_definition
diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb
index d57a097a9e2..5f98b222099 100644
--- a/app/graphql/mutations/base_mutation.rb
+++ b/app/graphql/mutations/base_mutation.rb
@@ -39,14 +39,16 @@ module Mutations
true
end
- def load_application_object(argument, lookup_as_type, id, context)
- ::Gitlab::Graphql::Lazy.new { super }.catch(::GraphQL::UnauthorizedError) do |e|
- Gitlab::ErrorTracking.track_exception(e)
- # The default behaviour is to abort processing and return nil for the
- # entire mutation field, but not set any top-level errors. We prefer to
- # at least say that something went wrong.
- raise_resource_not_available_error!
- end
+ def load_application_object(argument, id, context)
+ ::Gitlab::Graphql::Lazy.new { super }
+ end
+
+ def unauthorized_object(error)
+ # The default behavior is to abort processing and return nil for the
+ # entire mutation field, but not set any top-level errors. We prefer to
+ # at least say that something went wrong.
+ Gitlab::ErrorTracking.track_exception(error)
+ raise_resource_not_available_error!
end
def self.authorizes_object?
diff --git a/app/graphql/mutations/ci/pipeline/destroy.rb b/app/graphql/mutations/ci/pipeline/destroy.rb
index 3f933818ce1..935cf45c4ab 100644
--- a/app/graphql/mutations/ci/pipeline/destroy.rb
+++ b/app/graphql/mutations/ci/pipeline/destroy.rb
@@ -12,12 +12,25 @@ module Mutations
pipeline = authorized_find!(id: id)
project = pipeline.project
+ return undergoing_refresh_error(project) if project.refreshing_build_artifacts_size?
+
result = ::Ci::DestroyPipelineService.new(project, current_user).execute(pipeline)
{
success: result.success?,
errors: result.errors
}
end
+
+ private
+
+ def undergoing_refresh_error(project)
+ Gitlab::ProjectStatsRefreshConflictsLogger.warn_request_rejected_during_stats_refresh(project.id)
+
+ {
+ success: false,
+ errors: ['Action temporarily disabled. The project this pipeline belongs to is undergoing stats refresh.']
+ }
+ end
end
end
end
diff --git a/app/graphql/mutations/ci/runner/update.rb b/app/graphql/mutations/ci/runner/update.rb
index faccd1273e5..b6d8c20c40b 100644
--- a/app/graphql/mutations/ci/runner/update.rb
+++ b/app/graphql/mutations/ci/runner/update.rb
@@ -18,6 +18,10 @@ module Mutations
required: false,
description: 'Description of the runner.'
+ argument :maintenance_note, GraphQL::Types::String,
+ required: false,
+ description: 'Runner\'s maintenance notes.'
+
argument :maximum_timeout, GraphQL::Types::Int,
required: false,
description: 'Maximum timeout (in seconds) for jobs processed by the runner.'
diff --git a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb
new file mode 100644
index 00000000000..6a91a097a17
--- /dev/null
+++ b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Mutations
+ module WorkItems
+ module UpdateArguments
+ extend ActiveSupport::Concern
+
+ included do
+ argument :id, ::Types::GlobalIDType[::WorkItem],
+ required: true,
+ description: 'Global ID of the work item.'
+ argument :state_event, Types::WorkItems::StateEventEnum,
+ description: 'Close or reopen a work item.',
+ required: false
+ argument :title, GraphQL::Types::String,
+ required: false,
+ description: copy_field_description(Types::WorkItemType, :title)
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/incident_management/timeline_event/create.rb b/app/graphql/mutations/incident_management/timeline_event/create.rb
index cbc708a2530..1907954cada 100644
--- a/app/graphql/mutations/incident_management/timeline_event/create.rb
+++ b/app/graphql/mutations/incident_management/timeline_event/create.rb
@@ -23,7 +23,9 @@ module Mutations
authorize!(incident)
- response ::IncidentManagement::TimelineEvents::CreateService.new(incident, current_user, args).execute
+ response ::IncidentManagement::TimelineEvents::CreateService.new(
+ incident, current_user, args.merge(editable: true)
+ ).execute
end
private
diff --git a/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb b/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb
index 73a20b8a380..31ae29d896b 100644
--- a/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb
+++ b/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb
@@ -21,7 +21,8 @@ module Mutations
current_user,
promoted_from_note: note,
note: note.note,
- occurred_at: note.created_at
+ occurred_at: note.created_at,
+ editable: true
).execute
end
diff --git a/app/graphql/mutations/issues/set_crm_contacts.rb b/app/graphql/mutations/issues/set_crm_contacts.rb
index 4df65e4769c..cc718b4ae33 100644
--- a/app/graphql/mutations/issues/set_crm_contacts.rb
+++ b/app/graphql/mutations/issues/set_crm_contacts.rb
@@ -48,7 +48,7 @@ module Mutations
private
def feature_enabled?(project)
- Feature.enabled?(:customer_relations, project.group) && project.group&.crm_enabled?
+ project.group&.crm_enabled?
end
end
end
diff --git a/app/graphql/mutations/merge_requests/set_draft.rb b/app/graphql/mutations/merge_requests/set_draft.rb
index ab4ca73e5dc..f83c1a0caf4 100644
--- a/app/graphql/mutations/merge_requests/set_draft.rb
+++ b/app/graphql/mutations/merge_requests/set_draft.rb
@@ -27,8 +27,8 @@ module Mutations
private
- def wip_event(wip)
- wip ? 'wip' : 'unwip'
+ def wip_event(draft)
+ draft ? 'draft' : 'ready'
end
end
end
diff --git a/app/graphql/mutations/packages/cleanup/policy/update.rb b/app/graphql/mutations/packages/cleanup/policy/update.rb
new file mode 100644
index 00000000000..e7ab7439949
--- /dev/null
+++ b/app/graphql/mutations/packages/cleanup/policy/update.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Packages
+ module Cleanup
+ module Policy
+ class Update < Mutations::BaseMutation
+ graphql_name 'UpdatePackagesCleanupPolicy'
+
+ include FindsProject
+
+ authorize :admin_package
+
+ argument :project_path,
+ GraphQL::Types::ID,
+ required: true,
+ description: 'Project path where the packages cleanup policy is located.'
+
+ argument :keep_n_duplicated_package_files,
+ Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum,
+ required: false,
+ description: copy_field_description(
+ Types::Packages::Cleanup::PolicyType,
+ :keep_n_duplicated_package_files
+ )
+
+ field :packages_cleanup_policy,
+ Types::Packages::Cleanup::PolicyType,
+ null: true,
+ description: 'Packages cleanup policy after mutation.'
+
+ def resolve(project_path:, **args)
+ project = authorized_find!(project_path)
+
+ result = ::Packages::Cleanup::UpdatePolicyService
+ .new(project: project, current_user: current_user, params: args)
+ .execute
+
+ {
+ packages_cleanup_policy: result.payload[:packages_cleanup_policy],
+ errors: result.errors
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/packages/destroy_files.rb b/app/graphql/mutations/packages/destroy_files.rb
new file mode 100644
index 00000000000..3900a2c46ae
--- /dev/null
+++ b/app/graphql/mutations/packages/destroy_files.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Packages
+ class DestroyFiles < ::Mutations::BaseMutation
+ graphql_name 'DestroyPackageFiles'
+
+ include FindsProject
+
+ MAXIMUM_FILES = 100
+
+ authorize :destroy_package
+
+ argument :project_path,
+ GraphQL::Types::ID,
+ required: true,
+ description: 'Project path where the packages cleanup policy is located.'
+
+ argument :ids,
+ [::Types::GlobalIDType[::Packages::PackageFile]],
+ required: true,
+ description: 'IDs of the Package file.'
+
+ def resolve(project_path:, ids:)
+ project = authorized_find!(project_path)
+ raise_resource_not_available_error! "Cannot delete more than #{MAXIMUM_FILES} files" if ids.size > MAXIMUM_FILES
+
+ package_files = ::Packages::PackageFile.where(id: parse_gids(ids)) # rubocop:disable CodeReuse/ActiveRecord
+
+ ensure_file_access!(project, package_files)
+
+ result = ::Packages::MarkPackageFilesForDestructionService.new(package_files).execute
+
+ errors = result.error? ? Array.wrap(result[:message]) : []
+
+ { errors: errors }
+ end
+
+ private
+
+ def ensure_file_access!(project, package_files)
+ project_ids = package_files.map(&:project_id).uniq
+
+ unless project_ids.size == 1 && project_ids.include?(project.id)
+ raise_resource_not_available_error! 'All files must be in the requested project'
+ end
+ end
+
+ def parse_gids(gids)
+ gids.map { |gid| GitlabSchema.parse_gid(gid, expected_type: ::Packages::PackageFile).model_id }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/releases/create.rb b/app/graphql/mutations/releases/create.rb
index 037ade2589c..70a0e71c869 100644
--- a/app/graphql/mutations/releases/create.rb
+++ b/app/graphql/mutations/releases/create.rb
@@ -14,6 +14,10 @@ module Mutations
required: true, as: :tag,
description: 'Name of the tag to associate with the release.'
+ argument :tag_message, GraphQL::Types::String,
+ required: false,
+ description: 'Message to use if creating a new annotated tag.'
+
argument :ref, GraphQL::Types::String,
required: false,
description: 'Commit SHA or branch name to use if creating a new tag.'
diff --git a/app/graphql/mutations/security/ci_configuration/configure_sast.rb b/app/graphql/mutations/security/ci_configuration/configure_sast.rb
index 7ce0bf83a4b..cc3c1d6033b 100644
--- a/app/graphql/mutations/security/ci_configuration/configure_sast.rb
+++ b/app/graphql/mutations/security/ci_configuration/configure_sast.rb
@@ -16,7 +16,7 @@ module Mutations
description: 'SAST CI configuration for the project.'
def configure_analyzer(project, **args)
- ::Security::CiConfiguration::SastCreateService.new(project, current_user, args[:configuration]).execute
+ ::Security::CiConfiguration::SastCreateService.new(project, current_user, args[:configuration].to_h).execute
end
end
end
diff --git a/app/graphql/mutations/terraform/state/delete.rb b/app/graphql/mutations/terraform/state/delete.rb
index f08219cb395..f52ace07393 100644
--- a/app/graphql/mutations/terraform/state/delete.rb
+++ b/app/graphql/mutations/terraform/state/delete.rb
@@ -8,9 +8,9 @@ module Mutations
def resolve(id:)
state = authorized_find!(id: id)
- state.destroy
+ response = ::Terraform::States::TriggerDestroyService.new(state, current_user: current_user).execute
- { errors: errors_on_object(state) }
+ { errors: response.errors }
end
end
end
diff --git a/app/graphql/mutations/user_preferences/update.rb b/app/graphql/mutations/user_preferences/update.rb
index b71c952b0f2..c92c6d725b7 100644
--- a/app/graphql/mutations/user_preferences/update.rb
+++ b/app/graphql/mutations/user_preferences/update.rb
@@ -14,15 +14,6 @@ module Mutations
null: true,
description: 'User preferences after mutation.'
- def ready?(**args)
- if disabled_sort_value?(args)
- raise Gitlab::Graphql::Errors::ArgumentError,
- 'Feature flag `incident_escalations` must be enabled to use this sort order.'
- end
-
- super
- end
-
def resolve(**attributes)
user_preferences = current_user.user_preference
user_preferences.update(attributes)
@@ -32,14 +23,6 @@ module Mutations
errors: errors_on_object(user_preferences)
}
end
-
- private
-
- def disabled_sort_value?(args)
- return false unless [:escalation_status_asc, :escalation_status_desc].include?(args[:issues_sort])
-
- Feature.disabled?(:incident_escalations)
- end
end
end
end
diff --git a/app/graphql/mutations/work_items/create.rb b/app/graphql/mutations/work_items/create.rb
index 2e136d409ab..2ae26ed0e1a 100644
--- a/app/graphql/mutations/work_items/create.rb
+++ b/app/graphql/mutations/work_items/create.rb
@@ -8,8 +8,7 @@ module Mutations
include Mutations::SpamProtection
include FindsProject
- description "Creates a work item." \
- " Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice."
+ description "Creates a work item. Available only when feature flag `work_items` is enabled."
authorize :create_work_item
diff --git a/app/graphql/mutations/work_items/create_from_task.rb b/app/graphql/mutations/work_items/create_from_task.rb
index 4da709401a6..5ebe8b2c6d7 100644
--- a/app/graphql/mutations/work_items/create_from_task.rb
+++ b/app/graphql/mutations/work_items/create_from_task.rb
@@ -8,7 +8,7 @@ module Mutations
include Mutations::SpamProtection
description "Creates a work item from a task in another work item's description." \
- " Available only when feature flag `work_items` is enabled. This feature is experimental and is subject to change without notice."
+ " Available only when feature flag `work_items` is enabled."
authorize :update_work_item
diff --git a/app/graphql/mutations/work_items/delete.rb b/app/graphql/mutations/work_items/delete.rb
index 1830ab5443c..240a8b4c11e 100644
--- a/app/graphql/mutations/work_items/delete.rb
+++ b/app/graphql/mutations/work_items/delete.rb
@@ -5,7 +5,7 @@ module Mutations
class Delete < BaseMutation
graphql_name 'WorkItemDelete'
description "Deletes a work item." \
- " Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice."
+ " Available only when feature flag `work_items` is enabled."
authorize :delete_work_item
diff --git a/app/graphql/mutations/work_items/delete_task.rb b/app/graphql/mutations/work_items/delete_task.rb
index 87620a28fa1..b1bfed0cbf1 100644
--- a/app/graphql/mutations/work_items/delete_task.rb
+++ b/app/graphql/mutations/work_items/delete_task.rb
@@ -6,8 +6,7 @@ module Mutations
graphql_name 'WorkItemDeleteTask'
description "Deletes a task in a work item's description." \
- ' Available only when feature flag `work_items` is enabled. This feature is experimental and' \
- ' is subject to change without notice.'
+ ' Available only when feature flag `work_items` is enabled.'
authorize :update_work_item
diff --git a/app/graphql/mutations/work_items/update.rb b/app/graphql/mutations/work_items/update.rb
index 20319301482..c495da00f41 100644
--- a/app/graphql/mutations/work_items/update.rb
+++ b/app/graphql/mutations/work_items/update.rb
@@ -5,22 +5,13 @@ module Mutations
class Update < BaseMutation
graphql_name 'WorkItemUpdate'
description "Updates a work item by Global ID." \
- " Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice."
+ " Available only when feature flag `work_items` is enabled."
include Mutations::SpamProtection
+ include Mutations::WorkItems::UpdateArguments
authorize :update_work_item
- argument :id, ::Types::GlobalIDType[::WorkItem],
- required: true,
- description: 'Global ID of the work item.'
- argument :state_event, Types::WorkItems::StateEventEnum,
- description: 'Close or reopen a work item.',
- required: false
- argument :title, GraphQL::Types::String,
- required: false,
- description: copy_field_description(Types::WorkItemType, :title)
-
field :work_item, Types::WorkItemType,
null: true,
description: 'Updated work item.'
diff --git a/app/graphql/mutations/work_items/update_task.rb b/app/graphql/mutations/work_items/update_task.rb
new file mode 100644
index 00000000000..35fbe672b66
--- /dev/null
+++ b/app/graphql/mutations/work_items/update_task.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module Mutations
+ module WorkItems
+ class UpdateTask < BaseMutation
+ graphql_name 'WorkItemUpdateTask'
+ description "Updates a work item's task by Global ID." \
+ " Available only when feature flag `work_items` is enabled."
+
+ include Mutations::SpamProtection
+
+ authorize :read_work_item
+
+ argument :id, ::Types::GlobalIDType[::WorkItem],
+ required: true,
+ description: 'Global ID of the work item.'
+ argument :task_data, ::Types::WorkItems::UpdatedTaskInputType,
+ required: true,
+ description: 'Arguments necessary to update a task.'
+
+ field :task, Types::WorkItemType,
+ null: true,
+ description: 'Updated task.'
+ field :work_item, Types::WorkItemType,
+ null: true,
+ description: 'Updated work item.'
+
+ def resolve(id:, task_data:)
+ task_data_hash = task_data.to_h
+ work_item = authorized_find!(id: id)
+ task = authorized_find_task!(task_data_hash[:id])
+
+ unless work_item.project.work_items_feature_flag_enabled?
+ return { errors: ['`work_items` feature flag disabled for this project'] }
+ end
+
+ spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
+
+ ::WorkItems::UpdateService.new(
+ project: task.project,
+ current_user: current_user,
+ params: task_data_hash.except(:id),
+ spam_params: spam_params
+ ).execute(task)
+
+ check_spam_action_response!(task)
+
+ response = { errors: errors_on_object(task) }
+
+ if task.valid?
+ work_item.expire_etag_cache
+
+ response.merge(work_item: work_item, task: task)
+ else
+ response
+ end
+ end
+
+ private
+
+ def authorized_find_task!(task_id)
+ task = task_id.find
+
+ if current_user.can?(:update_work_item, task)
+ task
+ else
+ # Fail early if user cannot update task
+ raise_resource_not_available_error!
+ end
+ end
+
+ def find_object(id:)
+ id.find
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/work_items/update_widgets.rb b/app/graphql/mutations/work_items/update_widgets.rb
new file mode 100644
index 00000000000..d19da0abaac
--- /dev/null
+++ b/app/graphql/mutations/work_items/update_widgets.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Mutations
+ module WorkItems
+ class UpdateWidgets < BaseMutation
+ graphql_name 'WorkItemUpdateWidgets'
+ description "Updates the attributes of a work item's widgets by global ID." \
+ " Available only when feature flag `work_items` is enabled."
+
+ include Mutations::SpamProtection
+
+ authorize :update_work_item
+
+ argument :id, ::Types::GlobalIDType[::WorkItem],
+ required: true,
+ description: 'Global ID of the work item.'
+
+ argument :description_widget, ::Types::WorkItems::Widgets::DescriptionInputType,
+ required: false,
+ description: 'Input for description widget.'
+
+ field :work_item, Types::WorkItemType,
+ null: true,
+ description: 'Updated work item.'
+
+ def resolve(id:, **widget_attributes)
+ work_item = authorized_find!(id: id)
+
+ unless work_item.project.work_items_feature_flag_enabled?
+ return { errors: ['`work_items` feature flag disabled for this project'] }
+ end
+
+ spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
+
+ ::WorkItems::UpdateService.new(
+ project: work_item.project,
+ current_user: current_user,
+ # Cannot use prepare to use `.to_h` on each input due to
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87472#note_945199865
+ widget_params: widget_attributes.transform_values { |values| values.to_h },
+ spam_params: spam_params
+ ).execute(work_item)
+
+ check_spam_action_response!(work_item)
+
+ {
+ work_item: work_item.valid? ? work_item : nil,
+ errors: errors_on_object(work_item)
+ }
+ end
+
+ private
+
+ def find_object(id:)
+ GitlabSchema.find_by_gid(id)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/base_issues_resolver.rb b/app/graphql/resolvers/base_issues_resolver.rb
index a1fda976876..ec47a8996eb 100644
--- a/app/graphql/resolvers/base_issues_resolver.rb
+++ b/app/graphql/resolvers/base_issues_resolver.rb
@@ -33,13 +33,6 @@ module Resolvers
end
end
- def prepare_params(args, parent)
- return unless [:escalation_status_asc, :escalation_status_desc].include?(args[:sort])
- return if Feature.enabled?(:incident_escalations, parent)
-
- args[:sort] = :created_desc # default for sort argument
- end
-
private
def unconditional_includes
diff --git a/app/graphql/resolvers/ci/runner_owner_project_resolver.rb b/app/graphql/resolvers/ci/runner_owner_project_resolver.rb
new file mode 100644
index 00000000000..14b5f8f90eb
--- /dev/null
+++ b/app/graphql/resolvers/ci/runner_owner_project_resolver.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ class RunnerOwnerProjectResolver < BaseResolver
+ include LooksAhead
+
+ type Types::ProjectType, null: true
+
+ alias_method :runner, :object
+
+ def resolve_with_lookahead(**args)
+ resolve_owner
+ end
+
+ def preloads
+ {
+ full_path: [:route]
+ }
+ end
+
+ def filtered_preloads
+ selection = lookahead
+
+ preloads.each.flat_map do |name, requirements|
+ selection&.selects?(name) ? requirements : []
+ end
+ end
+
+ private
+
+ def resolve_owner
+ return unless runner.project_type?
+
+ BatchLoader::GraphQL.for(runner.id).batch(key: :runner_owner_projects) do |runner_ids, loader|
+ # rubocop: disable CodeReuse/ActiveRecord
+ runner_and_projects_with_row_number =
+ ::Ci::RunnerProject
+ .where(runner_id: runner_ids)
+ .select('id, runner_id, project_id, ROW_NUMBER() OVER (PARTITION BY runner_id ORDER BY id ASC)')
+ runner_and_owner_projects =
+ ::Ci::RunnerProject
+ .select(:id, :runner_id, :project_id)
+ .from("(#{runner_and_projects_with_row_number.to_sql}) temp WHERE row_number = 1")
+ owner_project_id_by_runner_id =
+ runner_and_owner_projects
+ .group_by(&:runner_id)
+ .transform_values { |runner_projects| runner_projects.first.project_id }
+ project_ids = owner_project_id_by_runner_id.values.uniq
+
+ all_preloads = unconditional_includes + filtered_preloads
+ owner_relation = Project.all
+ owner_relation = owner_relation.preload(*all_preloads) if all_preloads.any?
+ projects = owner_relation.where(id: project_ids).index_by(&:id)
+
+ runner_ids.each do |runner_id|
+ owner_project_id = owner_project_id_by_runner_id[runner_id]
+ loader.call(runner_id, projects[owner_project_id])
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/clusters/agent_tokens_resolver.rb b/app/graphql/resolvers/clusters/agent_tokens_resolver.rb
index 722fbab3bb7..9740bc6bb6a 100644
--- a/app/graphql/resolvers/clusters/agent_tokens_resolver.rb
+++ b/app/graphql/resolvers/clusters/agent_tokens_resolver.rb
@@ -16,7 +16,7 @@ module Resolvers
def resolve(**args)
return ::Clusters::AgentToken.none unless can_read_agent_tokens?
- tokens = agent.last_used_agent_tokens
+ tokens = agent.agent_tokens
tokens = tokens.with_status(args[:status]) if args[:status].present?
tokens
diff --git a/app/graphql/resolvers/clusters/agents_resolver.rb b/app/graphql/resolvers/clusters/agents_resolver.rb
index 5ad66ed7cdd..28618bef807 100644
--- a/app/graphql/resolvers/clusters/agents_resolver.rb
+++ b/app/graphql/resolvers/clusters/agents_resolver.rb
@@ -30,7 +30,7 @@ module Resolvers
def preloads
{
activity_events: { activity_events: [:user, agent_token: :agent] },
- tokens: :last_used_agent_tokens
+ tokens: :agent_tokens
}
end
end
diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
index de44dbb26d7..fe213936f55 100644
--- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
+++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
@@ -66,7 +66,6 @@ module IssueResolverArguments
description: 'Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues.'
argument :not, Types::Issues::NegatedIssueFilterInputType,
description: 'Negated arguments.',
- prepare: ->(negated_args, ctx) { negated_args.to_h },
required: false
argument :crm_contact_id, GraphQL::Types::String,
required: false,
@@ -85,12 +84,12 @@ module IssueResolverArguments
# Will need to be made group & namespace aware with
# https://gitlab.com/gitlab-org/gitlab-foss/issues/54520
+ args[:not] = args[:not].to_h if args[:not].present?
args[:iids] ||= [args.delete(:iid)].compact if args[:iid]
args[:attempt_project_search_optimizations] = true if args[:search].present?
prepare_assignee_username_params(args)
prepare_release_tag_params(args)
- prepare_params(args, parent) if defined?(prepare_params)
finder = IssuesFinder.new(current_user, args)
@@ -98,6 +97,8 @@ module IssueResolverArguments
end
def ready?(**args)
+ args[:not] = args[:not].to_h if args[:not].present?
+
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)
diff --git a/app/graphql/resolvers/concerns/resolves_groups.rb b/app/graphql/resolvers/concerns/resolves_groups.rb
index c451d4e7936..2a3dce80057 100644
--- a/app/graphql/resolvers/concerns/resolves_groups.rb
+++ b/app/graphql/resolvers/concerns/resolves_groups.rb
@@ -18,11 +18,9 @@ module ResolvesGroups
def preloads
{
- contacts: [:contacts],
container_repositories_count: [:container_repositories],
custom_emoji: [:custom_emoji],
full_path: [:route],
- organizations: [:organizations],
path: [:route],
dependency_proxy_blob_count: [:dependency_proxy_blobs],
dependency_proxy_blobs: [:dependency_proxy_blobs],
diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
index a72b9a09118..697cc6f5b03 100644
--- a/app/graphql/resolvers/concerns/resolves_merge_requests.rb
+++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
@@ -52,6 +52,7 @@ module ResolvesMergeRequests
security_auto_fix: [:author],
head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }],
timelogs: [:timelogs],
+ pipelines: [:merge_request_diffs], # used by `recent_diff_head_shas` to load pipelines
committers: [merge_request_diff: [:merge_request_diff_commits]]
}
end
diff --git a/app/graphql/resolvers/crm/contacts_resolver.rb b/app/graphql/resolvers/crm/contacts_resolver.rb
new file mode 100644
index 00000000000..58d0e2ce13d
--- /dev/null
+++ b/app/graphql/resolvers/crm/contacts_resolver.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Crm
+ class ContactsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+ include ResolvesIds
+
+ authorize :read_crm_contact
+
+ type Types::CustomerRelations::ContactType, null: true
+
+ argument :search, GraphQL::Types::String,
+ required: false,
+ description: 'Search term to find contacts with.'
+
+ argument :state, Types::CustomerRelations::ContactStateEnum,
+ required: false,
+ description: 'State of the contacts to search for.'
+
+ argument :ids, [::Types::GlobalIDType[CustomerRelations::Contact]],
+ required: false,
+ description: 'Filter contacts by IDs.'
+
+ def resolve(**args)
+ args[:ids] = resolve_ids(args.delete(:ids))
+
+ ::Crm::ContactsFinder.new(current_user, { group: group }.merge(args)).execute
+ end
+
+ def group
+ object.respond_to?(:sync) ? object.sync : object
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/crm/organizations_resolver.rb b/app/graphql/resolvers/crm/organizations_resolver.rb
new file mode 100644
index 00000000000..ca0a908ee22
--- /dev/null
+++ b/app/graphql/resolvers/crm/organizations_resolver.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Crm
+ class OrganizationsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+ include ResolvesIds
+
+ authorize :read_crm_organization
+
+ type Types::CustomerRelations::OrganizationType, null: true
+
+ argument :search, GraphQL::Types::String,
+ required: false,
+ description: 'Search term used to find organizations with.'
+
+ argument :state, Types::CustomerRelations::OrganizationStateEnum,
+ required: false,
+ description: 'State of the organization to search for.'
+
+ argument :ids, [Types::GlobalIDType[CustomerRelations::Organization]],
+ required: false,
+ description: 'Filter organizations by IDs.'
+
+ def resolve(**args)
+ args[:ids] = resolve_ids(args.delete(:ids))
+
+ ::Crm::OrganizationsFinder.new(current_user, { group: group }.merge(args)).execute
+ end
+
+ def group
+ object.respond_to?(:sync) ? object.sync : object
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/design_management/versions_resolver.rb b/app/graphql/resolvers/design_management/versions_resolver.rb
index de636655087..bd9b82283c3 100644
--- a/app/graphql/resolvers/design_management/versions_resolver.rb
+++ b/app/graphql/resolvers/design_management/versions_resolver.rb
@@ -42,8 +42,6 @@ module Resolvers
def cutoff(id, sha)
if sha.present? || id.present?
specific_version(id, sha)
- elsif at_version = context[:at_version_argument]
- by_id(at_version) # See: DesignsResolver
else
:unconstrained
end
diff --git a/app/graphql/resolvers/merge_request_pipelines_resolver.rb b/app/graphql/resolvers/merge_request_pipelines_resolver.rb
index f84eedb4c3b..deb698c63e1 100644
--- a/app/graphql/resolvers/merge_request_pipelines_resolver.rb
+++ b/app/graphql/resolvers/merge_request_pipelines_resolver.rb
@@ -21,8 +21,9 @@ module Resolvers
super
end
- def query_for(args)
- resolve_pipelines(project, args).merge(merge_request.all_pipelines)
+ def query_for(input)
+ mr, args = input
+ resolve_pipelines(mr.source_project, args).merge(mr.all_pipelines)
end
def model_class
@@ -30,7 +31,7 @@ module Resolvers
end
def query_input(**args)
- args
+ [merge_request, args]
end
def project
diff --git a/app/graphql/resolvers/milestones_resolver.rb b/app/graphql/resolvers/milestones_resolver.rb
index dc6d781f584..25ff783b408 100644
--- a/app/graphql/resolvers/milestones_resolver.rb
+++ b/app/graphql/resolvers/milestones_resolver.rb
@@ -4,6 +4,11 @@ module Resolvers
class MilestonesResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
include TimeFrameArguments
+ include LooksAhead
+
+ # authorize before resolution
+ authorize :read_milestone
+ authorizes_object!
argument :ids, [GraphQL::Types::ID],
required: false,
@@ -34,12 +39,10 @@ module Resolvers
NON_STABLE_CURSOR_SORTS = %i[expired_last_due_date_asc expired_last_due_date_desc].freeze
- def resolve(**args)
+ def resolve_with_lookahead(**args)
validate_timeframe_params!(args)
- authorize!
-
- milestones = MilestonesFinder.new(milestones_finder_params(args)).execute
+ milestones = apply_lookahead(MilestonesFinder.new(milestones_finder_params(args)).execute)
if non_stable_cursor_sort?(args[:sort])
offset_pagination(milestones)
@@ -50,6 +53,12 @@ module Resolvers
private
+ def preloads
+ {
+ releases: :releases
+ }
+ end
+
def milestones_finder_params(args)
{
ids: parse_gids(args[:ids]),
@@ -69,12 +78,6 @@ module Resolvers
raise NotImplementedError
end
- # MilestonesFinder does not check for current_user permissions,
- # so for now we need to keep it here.
- def authorize!
- Ability.allowed?(context[:current_user], :read_milestone, parent) || raise_resource_not_available_error!
- end
-
def parse_gids(gids)
gids&.map { |gid| GitlabSchema.parse_gid(gid, expected_type: Milestone).model_id }
end
diff --git a/app/graphql/resolvers/paginated_tree_resolver.rb b/app/graphql/resolvers/paginated_tree_resolver.rb
index d29d87ca204..1b4211366e0 100644
--- a/app/graphql/resolvers/paginated_tree_resolver.rb
+++ b/app/graphql/resolvers/paginated_tree_resolver.rb
@@ -17,7 +17,6 @@ module Resolvers
description: 'Used to get a recursive tree. Default is false.'
argument :ref, GraphQL::Types::String,
required: false,
- default_value: :head,
description: 'Commit ref to get the tree for. Default value is HEAD.'
alias_method :repository, :object
@@ -26,6 +25,7 @@ module Resolvers
return unless repository.exists?
cursor = args.delete(:after)
+ args[:ref] ||= :head
pagination_params = {
limit: @field.max_page_size || 100,
diff --git a/app/graphql/resolvers/tree_resolver.rb b/app/graphql/resolvers/tree_resolver.rb
index f02eb226810..553f9aa6cd9 100644
--- a/app/graphql/resolvers/tree_resolver.rb
+++ b/app/graphql/resolvers/tree_resolver.rb
@@ -16,7 +16,6 @@ module Resolvers
description: 'Used to get a recursive tree. Default is false.'
argument :ref, GraphQL::Types::String,
required: false,
- default_value: :head,
description: 'Commit ref to get the tree for. Default value is HEAD.'
alias_method :repository, :object
@@ -24,6 +23,7 @@ module Resolvers
def resolve(**args)
return unless repository.exists?
+ args[:ref] ||= :head
repository.tree(args[:ref], args[:path], recursive: args[:recursive])
end
end
diff --git a/app/graphql/resolvers/user_resolver.rb b/app/graphql/resolvers/user_resolver.rb
index 99fd0d4927d..f0fd60e9cbb 100644
--- a/app/graphql/resolvers/user_resolver.rb
+++ b/app/graphql/resolvers/user_resolver.rb
@@ -2,6 +2,8 @@
module Resolvers
class UserResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
description 'Retrieve a single user'
type Types::UserType, null: true
@@ -23,6 +25,8 @@ module Resolvers
end
def resolve(id: nil, username: nil)
+ authorize!
+
if id
GitlabSchema.object_from_id(id, expected_type: User)
else
@@ -39,5 +43,9 @@ module Resolvers
end
end
end
+
+ def authorize!
+ raise_resource_not_available_error! unless context[:current_user].present?
+ end
end
end
diff --git a/app/graphql/resolvers/users_resolver.rb b/app/graphql/resolvers/users_resolver.rb
index 1424c14083d..b0d704d09fc 100644
--- a/app/graphql/resolvers/users_resolver.rb
+++ b/app/graphql/resolvers/users_resolver.rb
@@ -47,10 +47,7 @@ module Resolvers
end
def authorize!(usernames)
- authorized = Ability.allowed?(context[:current_user], :read_users_list)
- authorized &&= usernames.present? if context[:current_user].blank?
-
- raise_resource_not_available_error! unless authorized
+ raise_resource_not_available_error! unless context[:current_user].present?
end
private
diff --git a/app/graphql/resolvers/work_items_resolver.rb b/app/graphql/resolvers/work_items_resolver.rb
new file mode 100644
index 00000000000..1bc74131b9e
--- /dev/null
+++ b/app/graphql/resolvers/work_items_resolver.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class WorkItemsResolver < BaseResolver
+ include SearchArguments
+ include LooksAhead
+
+ type Types::WorkItemType.connection_type, null: true
+
+ argument :iid, GraphQL::Types::String,
+ required: false,
+ description: 'IID of the issue. For example, "1".'
+ argument :iids, [GraphQL::Types::String],
+ required: false,
+ description: 'List of IIDs of work items. For example, `["1", "2"]`.'
+ argument :sort, Types::WorkItemSortEnum,
+ description: 'Sort work items by this criteria.',
+ required: false,
+ default_value: :created_desc
+ argument :state, Types::IssuableStateEnum,
+ required: false,
+ description: 'Current state of this work item.'
+ argument :types, [Types::IssueTypeEnum],
+ as: :issue_types,
+ description: 'Filter work items by the given work item types.',
+ required: false
+
+ def resolve_with_lookahead(**args)
+ # The project could have been loaded in batch by `BatchLoader`.
+ # At this point we need the `id` of the project to query for issues, so
+ # make sure it's loaded and not `nil` before continuing.
+ parent = object.respond_to?(:sync) ? object.sync : object
+ return WorkItem.none if parent.nil? || !parent.work_items_feature_flag_enabled?
+
+ args[:iids] ||= [args.delete(:iid)].compact if args[:iid]
+ args[:attempt_project_search_optimizations] = true if args[:search].present?
+
+ finder = ::WorkItems::WorkItemsFinder.new(current_user, args)
+
+ Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).batching_find_all { |q| apply_lookahead(q) }
+ end
+
+ def ready?(**args)
+ validate_anonymous_search_access! if args[:search].present?
+
+ super
+ end
+
+ private
+
+ def unconditional_includes
+ [
+ {
+ project: [:project_feature, :group]
+ },
+ :author
+ ]
+ end
+ end
+end
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index b4cd54b1332..6aee9a5c052 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -53,6 +53,30 @@ module Types
field_authorized?(object, ctx) && resolver_authorized?(object, ctx)
end
+ # This gets called from the gem's `calculate_complexity` method, allowing us
+ # to ensure our complexity calculation is used even for connections.
+ # This code is actually a copy of the default case in `calculate_complexity`
+ # in `lib/graphql/schema/field.rb`
+ # (https://github.com/rmosolgo/graphql-ruby/blob/master/lib/graphql/schema/field.rb)
+ def complexity_for(child_complexity:, query:, lookahead:)
+ defined_complexity = complexity
+
+ case defined_complexity
+ when Proc
+ arguments = query.arguments_for(lookahead.ast_nodes.first, self)
+
+ if arguments.respond_to?(:keyword_arguments)
+ defined_complexity.call(query.context, arguments.keyword_arguments, child_complexity)
+ else
+ child_complexity
+ end
+ when Numeric
+ defined_complexity + child_complexity
+ else
+ raise("Invalid complexity: #{defined_complexity.inspect} on #{path} (#{inspect})")
+ end
+ end
+
def base_complexity
complexity = DEFAULT_COMPLEXITY
complexity += 1 if calls_gitaly?
@@ -150,10 +174,9 @@ module Types
def connection_complexity_multiplier(ctx, args)
# Resolvers may add extra complexity depending on number of items being loaded.
- field_defn = to_graphql
- return 0 unless field_defn.connection?
+ return 0 unless connection?
- page_size = field_defn.connection_max_page_size || ctx.schema.default_max_page_size
+ page_size = max_page_size || ctx.schema.default_max_page_size
limit_value = [args[:first], args[:last], page_size].compact.min
multiplier = resolver&.try(:complexity_multiplier, args).to_f
limit_value * multiplier
diff --git a/app/graphql/types/ci/detailed_status_type.rb b/app/graphql/types/ci/detailed_status_type.rb
index e3413551a3f..3fab040cc0b 100644
--- a/app/graphql/types/ci/detailed_status_type.rb
+++ b/app/graphql/types/ci/detailed_status_type.rb
@@ -33,7 +33,7 @@ module Types
method: :status_tooltip
def id(parent:)
- "#{object.id}-#{parent.object.object.id}"
+ "#{object.id}-#{parent.id}"
end
def action
diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb
index f25fc56a588..b20a671179b 100644
--- a/app/graphql/types/ci/job_type.rb
+++ b/app/graphql/types/ci/job_type.rb
@@ -7,7 +7,7 @@ module Types
class JobType < BaseObject
graphql_name 'CiJob'
- connection_type_class(Types::CountableConnectionType)
+ connection_type_class(Types::LimitedCountableConnectionType)
expose_permissions Types::PermissionTypes::Ci::Job
diff --git a/app/graphql/types/ci/pipeline_merge_request_event_type_enum.rb b/app/graphql/types/ci/pipeline_merge_request_event_type_enum.rb
new file mode 100644
index 00000000000..a1236b8f2c1
--- /dev/null
+++ b/app/graphql/types/ci/pipeline_merge_request_event_type_enum.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class PipelineMergeRequestEventTypeEnum < BaseEnum
+ graphql_name 'PipelineMergeRequestEventType'
+ description 'Event type of the pipeline associated with a merge request'
+
+ value 'MERGED_RESULT',
+ 'Pipeline run on the changes from the source branch combined with the target branch.',
+ value: :merged_result
+ value 'DETACHED',
+ 'Pipeline run on the changes in the merge request source branch.',
+ value: :detached
+ end
+ end
+end
+
+Types::Ci::PipelineMergeRequestEventTypeEnum.prepend_mod
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index 81afc7f0f42..60418fec6c5 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -175,6 +175,9 @@ module Types
field :warning_messages, [Types::Ci::PipelineMessageType], null: true,
description: 'Pipeline warning messages.'
+ field :merge_request_event_type, Types::Ci::PipelineMergeRequestEventTypeEnum, null: true,
+ description: "Event type of the pipeline associated with a merge request."
+
def detailed_status
object.detailed_status(current_user)
end
diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb
index 6f957d2511f..949e216a982 100644
--- a/app/graphql/types/ci/runner_type.rb
+++ b/app/graphql/types/ci/runner_type.rb
@@ -85,6 +85,15 @@ module Types
method: :token_expires_at
field :version, GraphQL::Types::String, null: true,
description: 'Version of the runner.'
+ field :owner_project, ::Types::ProjectType, null: true,
+ description: 'Project that owns the runner. For project runners only.',
+ resolver: ::Resolvers::Ci::RunnerOwnerProjectResolver
+
+ markdown_field :maintenance_note_html, null: true
+
+ def maintenance_note_html_resolver
+ ::MarkupHelper.markdown(object.maintenance_note, context.to_h.dup)
+ end
def job_count
# We limit to 1 above the JOB_COUNT_LIMIT to indicate that more items exist after JOB_COUNT_LIMIT
@@ -136,16 +145,22 @@ module Types
# rubocop: disable CodeReuse/ActiveRecord
def batched_owners(runner_assoc_type, assoc_type, key, column_name)
- BatchLoader::GraphQL.for(runner.id).batch(key: key) do |runner_ids, loader, args|
- runner_and_owner_ids = runner_assoc_type.where(runner_id: runner_ids).pluck(:runner_id, column_name)
-
- owner_ids_by_runner_id = runner_and_owner_ids.group_by(&:first).transform_values { |v| v.pluck(1) }
- owner_ids = runner_and_owner_ids.pluck(1).uniq
-
+ BatchLoader::GraphQL.for(runner.id).batch(key: key) do |runner_ids, loader|
+ plucked_runner_and_owner_ids = runner_assoc_type
+ .select(:runner_id, column_name)
+ .where(runner_id: runner_ids)
+ .pluck(:runner_id, column_name)
+ # In plucked_runner_and_owner_ids, first() represents the runner ID, and second() the owner ID,
+ # so let's group the owner IDs by runner ID
+ runner_owner_ids_by_runner_id = plucked_runner_and_owner_ids
+ .group_by(&:first)
+ .transform_values { |runner_and_owner_id| runner_and_owner_id.map(&:second) }
+
+ owner_ids = runner_owner_ids_by_runner_id.values.flatten.uniq
owners = assoc_type.where(id: owner_ids).index_by(&:id)
runner_ids.each do |runner_id|
- loader.call(runner_id, owner_ids_by_runner_id[runner_id]&.map { |owner_id| owners[owner_id] } || [])
+ loader.call(runner_id, runner_owner_ids_by_runner_id[runner_id]&.map { |owner_id| owners[owner_id] } || [])
end
end
end
diff --git a/app/graphql/types/ci/runner_web_url_edge.rb b/app/graphql/types/ci/runner_web_url_edge.rb
index 035d75c22c6..7dfcd1f3510 100644
--- a/app/graphql/types/ci/runner_web_url_edge.rb
+++ b/app/graphql/types/ci/runner_web_url_edge.rb
@@ -4,8 +4,6 @@ module Types
module Ci
# rubocop: disable Graphql/AuthorizeTypes
class RunnerWebUrlEdge < ::Types::BaseEdge
- include FindClosest
-
field :edit_url, GraphQL::Types::String, null: true,
description: 'Web URL of the runner edit page. The value depends on where you put this field in the query. You can use it for projects or groups.',
extras: [:parent]
@@ -19,19 +17,18 @@ module Types
@runner = node.node
end
+ # here parent is a Keyset::Connection
def edit_url(parent:)
- runner_url(parent: parent, url_type: :edit_url)
+ runner_url(owner: parent.parent, url_type: :edit_url)
end
def web_url(parent:)
- runner_url(parent: parent, url_type: :default)
+ runner_url(owner: parent.parent, url_type: :default)
end
private
- def runner_url(parent:, url_type: :default)
- owner = closest_parent([::Types::ProjectType, ::Types::GroupType], parent)
-
+ def runner_url(owner:, url_type: :default)
# Only ::Group is supported at the moment, future iterations will include ::Project.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/16338
case owner
diff --git a/app/graphql/types/ci/status_action_type.rb b/app/graphql/types/ci/status_action_type.rb
index 26ca3c1438a..c0f61cf49f2 100644
--- a/app/graphql/types/ci/status_action_type.rb
+++ b/app/graphql/types/ci/status_action_type.rb
@@ -21,7 +21,8 @@ module Types
description: 'Title for the action, for example: Retry.'
def id(parent:)
- "#{parent.parent.object.object.class.name}-#{parent.object.object.id}"
+ # parent is a SimpleDelegator
+ "#{parent.subject.class.name}-#{parent.id}"
end
def action_method
diff --git a/app/graphql/types/concerns/find_closest.rb b/app/graphql/types/concerns/find_closest.rb
deleted file mode 100644
index 3064db19ea0..00000000000
--- a/app/graphql/types/concerns/find_closest.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-module FindClosest
- # Find the closest node which has any of the given types above this node, and return the domain object
- def closest_parent(types, parent)
- while parent
-
- if types.any? {|type| parent.object.instance_of? type}
- return parent.object.object
- else
- parent = parent.try(:parent)
- end
- end
- end
-end
diff --git a/app/graphql/types/customer_relations/contact_state_enum.rb b/app/graphql/types/customer_relations/contact_state_enum.rb
new file mode 100644
index 00000000000..445d2a41401
--- /dev/null
+++ b/app/graphql/types/customer_relations/contact_state_enum.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Types
+ module CustomerRelations
+ class ContactStateEnum < BaseEnum
+ graphql_name 'CustomerRelationsContactState'
+
+ value 'active',
+ description: "Active contact.",
+ value: :active
+
+ value 'inactive',
+ description: "Inactive contact.",
+ value: :inactive
+ end
+ end
+end
diff --git a/app/graphql/types/customer_relations/organization_state_enum.rb b/app/graphql/types/customer_relations/organization_state_enum.rb
new file mode 100644
index 00000000000..ecdd7d092ad
--- /dev/null
+++ b/app/graphql/types/customer_relations/organization_state_enum.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Types
+ module CustomerRelations
+ class OrganizationStateEnum < BaseEnum
+ graphql_name 'CustomerRelationsOrganizationState'
+
+ value 'active',
+ description: "Active organization.",
+ value: :active
+
+ value 'inactive',
+ description: "Inactive organization.",
+ value: :inactive
+ end
+ end
+end
diff --git a/app/graphql/types/global_id_type.rb b/app/graphql/types/global_id_type.rb
index 6a924c13a3c..145a5a22460 100644
--- a/app/graphql/types/global_id_type.rb
+++ b/app/graphql/types/global_id_type.rb
@@ -84,7 +84,8 @@ module Types
end
define_singleton_method(:suitable?) do |gid|
- next false if gid.nil?
+ # an argument can be nil, so allow it here
+ next true if gid.nil?
gid.model_name.safe_constantize.present? &&
gid.model_class.ancestors.include?(model_class)
diff --git a/app/graphql/types/group_member_type.rb b/app/graphql/types/group_member_type.rb
index 18242f7b8b1..c4582f31bec 100644
--- a/app/graphql/types/group_member_type.rb
+++ b/app/graphql/types/group_member_type.rb
@@ -15,7 +15,7 @@ module Types
field :notification_email,
resolver: Resolvers::GroupMembers::NotificationEmailResolver,
- description: "Group notification email for User. Only availble for admins."
+ description: "Group notification email for User. Only available for admins."
def group
Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, object.source_id).find
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
index a94cd6fad20..49971d52a30 100644
--- a/app/graphql/types/group_type.rb
+++ b/app/graphql/types/group_type.rb
@@ -201,11 +201,13 @@ module Types
field :organizations, Types::CustomerRelations::OrganizationType.connection_type,
null: true,
- description: "Find organizations of this group."
+ description: "Find organizations of this group.",
+ resolver: Resolvers::Crm::OrganizationsResolver
field :contacts, Types::CustomerRelations::ContactType.connection_type,
null: true,
- description: "Find contacts of this group."
+ description: "Find contacts of this group.",
+ resolver: Resolvers::Crm::ContactsResolver
field :work_item_types, Types::WorkItems::TypeType.connection_type,
resolver: Resolvers::WorkItems::TypesResolver,
diff --git a/app/graphql/types/issue_sort_enum.rb b/app/graphql/types/issue_sort_enum.rb
index db51e491d4e..7dced3c8e00 100644
--- a/app/graphql/types/issue_sort_enum.rb
+++ b/app/graphql/types/issue_sort_enum.rb
@@ -14,8 +14,10 @@ module Types
value 'TITLE_DESC', 'Title by descending order.', value: :title_desc
value 'POPULARITY_ASC', 'Number of upvotes (awarded "thumbs up" emoji) by ascending order.', value: :popularity_asc
value 'POPULARITY_DESC', 'Number of upvotes (awarded "thumbs up" emoji) by descending order.', value: :popularity_desc
- value 'ESCALATION_STATUS_ASC', 'Status from triggered to resolved. Defaults to `CREATED_DESC` if `incident_escalations` feature flag is disabled.', value: :escalation_status_asc
- value 'ESCALATION_STATUS_DESC', 'Status from resolved to triggered. Defaults to `CREATED_DESC` if `incident_escalations` feature flag is disabled.', value: :escalation_status_desc
+ value 'ESCALATION_STATUS_ASC', 'Status from triggered to resolved.', value: :escalation_status_asc
+ value 'ESCALATION_STATUS_DESC', 'Status from resolved to triggered.', value: :escalation_status_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/issue_type.rb b/app/graphql/types/issue_type.rb
index c83200bd614..58729b34fc7 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -127,6 +127,9 @@ module Types
field :moved_to, Types::IssueType, null: true,
description: 'Updated Issue after it got moved to another project.'
+ field :closed_as_duplicate_of, Types::IssueType, null: true,
+ description: 'Issue this issue was closed as a duplicate of.'
+
field :create_note_email, GraphQL::Types::String, null: true,
description: 'User specific email address for the issue.'
@@ -161,6 +164,10 @@ module Types
Gitlab::Graphql::Loaders::BatchModelLoader.new(Issue, object.moved_to_id).find
end
+ def closed_as_duplicate_of
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(Issue, object.duplicated_to_id).find
+ end
+
def discussion_locked
!!object.discussion_locked
end
diff --git a/app/graphql/types/limited_countable_connection_type.rb b/app/graphql/types/limited_countable_connection_type.rb
new file mode 100644
index 00000000000..f0698222ea3
--- /dev/null
+++ b/app/graphql/types/limited_countable_connection_type.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Types
+ # rubocop: disable Graphql/AuthorizeTypes
+ class LimitedCountableConnectionType < GraphQL::Types::Relay::BaseConnection
+ COUNT_LIMIT = 1000
+ COUNT_DESCRIPTION = "Limited count of collection. Returns limit + 1 for counts greater than the limit."
+
+ field :count, GraphQL::Types::Int, null: false, description: COUNT_DESCRIPTION do
+ argument :limit, GraphQL::Types::Int,
+ required: false, default_value: COUNT_LIMIT,
+ validates: { numericality: { greater_than: 0, less_than_or_equal_to: COUNT_LIMIT } },
+ description: "Limit value to be applied to the count query. Default is 1000."
+ end
+
+ def count(limit:)
+ relation = object.items
+
+ if relation.respond_to?(:page)
+ relation.page.total_count_with_limit(:all, limit: limit)
+ else
+ [relation.size, limit.next].min
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/merge_requests/interacts_with_merge_request.rb b/app/graphql/types/merge_requests/interacts_with_merge_request.rb
index 15621ef1472..bef2d39dc5c 100644
--- a/app/graphql/types/merge_requests/interacts_with_merge_request.rb
+++ b/app/graphql/types/merge_requests/interacts_with_merge_request.rb
@@ -5,8 +5,6 @@ module Types
module InteractsWithMergeRequest
extend ActiveSupport::Concern
- include FindClosest
-
included do
field :merge_request_interaction,
type: ::Types::UserMergeRequestInteractionType,
@@ -16,11 +14,9 @@ module Types
end
def merge_request_interaction(parent:, id: nil)
- merge_request = closest_parent([::Types::MergeRequestType], parent)
-
- return unless merge_request
-
- Users::MergeRequestInteraction.new(user: object, merge_request: merge_request)
+ # need the connection parent if called from a connection node:
+ parent = parent.parent if parent.try(:field)&.connection?
+ Users::MergeRequestInteraction.new(user: object, merge_request: parent)
end
end
end
diff --git a/app/graphql/types/milestone_type.rb b/app/graphql/types/milestone_type.rb
index 18e4a5d33e3..7741fd723f0 100644
--- a/app/graphql/types/milestone_type.rb
+++ b/app/graphql/types/milestone_type.rb
@@ -59,6 +59,10 @@ module Types
field :stats, Types::MilestoneStatsType, null: true,
description: 'Milestone statistics.'
+ field :releases, ::Types::ReleaseType.connection_type,
+ null: true,
+ description: 'Releases associated with this milestone.'
+
def stats
milestone
end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 7d8ada82d40..8642957af02 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -136,12 +136,16 @@ module Types
mount_mutation Mutations::UserPreferences::Update
mount_mutation Mutations::Packages::Destroy
mount_mutation Mutations::Packages::DestroyFile
+ mount_mutation Mutations::Packages::DestroyFiles
+ mount_mutation Mutations::Packages::Cleanup::Policy::Update
mount_mutation Mutations::Echo
- mount_mutation Mutations::WorkItems::Create
- mount_mutation Mutations::WorkItems::CreateFromTask
- mount_mutation Mutations::WorkItems::Delete
- mount_mutation Mutations::WorkItems::DeleteTask
- mount_mutation Mutations::WorkItems::Update
+ mount_mutation Mutations::WorkItems::Create, deprecated: { milestone: '15.1', reason: :alpha }
+ mount_mutation Mutations::WorkItems::CreateFromTask, deprecated: { milestone: '15.1', reason: :alpha }
+ mount_mutation Mutations::WorkItems::Delete, deprecated: { milestone: '15.1', reason: :alpha }
+ mount_mutation Mutations::WorkItems::DeleteTask, deprecated: { milestone: '15.1', reason: :alpha }
+ mount_mutation Mutations::WorkItems::Update, deprecated: { milestone: '15.1', reason: :alpha }
+ mount_mutation Mutations::WorkItems::UpdateWidgets, deprecated: { milestone: '15.1', reason: :alpha }
+ mount_mutation Mutations::WorkItems::UpdateTask, deprecated: { milestone: '15.1', reason: :alpha }
mount_mutation Mutations::SavedReplies::Create
mount_mutation Mutations::SavedReplies::Update
mount_mutation Mutations::SavedReplies::Destroy
diff --git a/app/graphql/types/packages/cleanup/keep_duplicated_package_files_enum.rb b/app/graphql/types/packages/cleanup/keep_duplicated_package_files_enum.rb
new file mode 100644
index 00000000000..bf8d625a334
--- /dev/null
+++ b/app/graphql/types/packages/cleanup/keep_duplicated_package_files_enum.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Cleanup
+ class KeepDuplicatedPackageFilesEnum < BaseEnum
+ graphql_name 'PackagesCleanupKeepDuplicatedPackageFilesEnum'
+
+ OPTIONS_MAPPING = {
+ 'all' => 'ALL_PACKAGE_FILES',
+ '1' => 'ONE_PACKAGE_FILE',
+ '10' => 'TEN_PACKAGE_FILES',
+ '20' => 'TWENTY_PACKAGE_FILES',
+ '30' => 'THIRTY_PACKAGE_FILES',
+ '40' => 'FORTY_PACKAGE_FILES',
+ '50' => 'FIFTY_PACKAGE_FILES'
+ }.freeze
+
+ ::Packages::Cleanup::Policy::KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES.each do |keep_value|
+ value OPTIONS_MAPPING[keep_value], value: keep_value, description: "Value to keep #{keep_value} package files"
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/cleanup/policy_type.rb b/app/graphql/types/packages/cleanup/policy_type.rb
new file mode 100644
index 00000000000..f08aace7df9
--- /dev/null
+++ b/app/graphql/types/packages/cleanup/policy_type.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Cleanup
+ class PolicyType < ::Types::BaseObject
+ graphql_name 'PackagesCleanupPolicy'
+ description 'A packages cleanup policy designed to keep only packages and packages assets that matter most'
+
+ authorize :admin_package
+
+ field :keep_n_duplicated_package_files,
+ Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum,
+ null: false,
+ description: 'Number of duplicated package files to retain.'
+ field :next_run_at,
+ Types::TimeType,
+ null: true,
+ description: 'Next time that this packages cleanup policy will be executed.'
+ end
+ 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 a2cefb872c9..07e6e7a55d6 100644
--- a/app/graphql/types/permission_types/base_permission_type.rb
+++ b/app/graphql/types/permission_types/base_permission_type.rb
@@ -12,11 +12,7 @@ module Types
end
def self.ability_field(ability, **kword_args)
- unless resolving_keywords?(kword_args)
- kword_args[:resolve] ||= -> (object, args, context) do
- can?(context[:current_user], ability, object, args.to_h)
- end
- end
+ define_field_resolver_method(ability) unless resolving_keywords?(kword_args)
permission_field(ability, **kword_args)
end
@@ -31,6 +27,14 @@ module Types
field(**kword_args) # rubocop:disable Graphql/Descriptions
end
+ def self.define_field_resolver_method(ability)
+ unless self.respond_to?(ability)
+ define_method ability.to_sym do |*args|
+ Ability.allowed?(context[:current_user], ability, object, args.to_h)
+ end
+ end
+ end
+
def self.resolving_keywords?(arguments)
RESOLVING_KEYWORDS.intersect?(arguments.keys.to_set)
end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index f1de8e985b3..603d5ead540 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -4,6 +4,8 @@ module Types
class ProjectType < BaseObject
graphql_name 'Project'
+ connection_type_class(Types::CountableConnectionType)
+
authorize :read_project
expose_permissions Types::PermissionTypes::Project
@@ -142,6 +144,14 @@ module Types
extras: [:lookahead],
resolver: Resolvers::IssuesResolver
+ field :work_items,
+ Types::WorkItemType.connection_type,
+ null: true,
+ deprecated: { milestone: '15.1', reason: :alpha },
+ description: 'Work items of the project.',
+ extras: [:lookahead],
+ resolver: Resolvers::WorkItemsResolver
+
field :issue_status_counts,
Types::IssueStatusCountsType,
null: true,
@@ -179,6 +189,11 @@ module Types
description: 'Packages of the project.',
resolver: Resolvers::ProjectPackagesResolver
+ field :packages_cleanup_policy,
+ Types::Packages::Cleanup::PolicyType,
+ null: true,
+ description: 'Packages cleanup policy for the project.'
+
field :jobs,
type: Types::Ci::JobType.connection_type,
null: true,
diff --git a/app/graphql/types/query_complexity_type.rb b/app/graphql/types/query_complexity_type.rb
index 13b618cf5ce..ddcf448c64a 100644
--- a/app/graphql/types/query_complexity_type.rb
+++ b/app/graphql/types/query_complexity_type.rb
@@ -5,7 +5,7 @@ module Types
class QueryComplexityType < ::Types::BaseObject
graphql_name 'QueryComplexity'
- ANALYZER = GraphQL::Analysis::QueryComplexity.new { |_query, complexity| complexity }
+ ANALYZER = GraphQL::Analysis::AST::QueryComplexity
alias_method :query, :object
@@ -23,7 +23,7 @@ module Types
description: 'GraphQL query complexity score.'
def score
- ::GraphQL::Analysis.analyze_query(query, [ANALYZER]).first
+ ::GraphQL::Analysis::AST.analyze_query(query, [ANALYZER]).first
end
end
# rubocop: enable Graphql/AuthorizeTypes
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 01b1a71896a..46d121f6552 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -52,6 +52,7 @@ module Types
field :milestone, ::Types::MilestoneType,
null: true,
+ extras: [:lookahead],
description: 'Find a milestone.' do
argument :id, ::Types::GlobalIDType[Milestone], required: true, description: 'Find a milestone by its ID.'
end
@@ -90,8 +91,8 @@ module Types
field :work_item, Types::WorkItemType,
null: true,
resolver: Resolvers::WorkItemResolver,
- description: 'Find a work item. Returns `null` if `work_items` feature flag is disabled.' \
- ' The feature is experimental and is subject to change without notice.'
+ deprecated: { milestone: '15.1', reason: :alpha },
+ description: 'Find a work item. Returns `null` if `work_items` feature flag is disabled.'
field :merge_request, Types::MergeRequestType,
null: true,
@@ -156,8 +157,9 @@ module Types
GitlabSchema.find_by_gid(id)
end
- def milestone(id:)
- GitlabSchema.find_by_gid(id)
+ def milestone(id:, lookahead:)
+ preloads = [:releases] if lookahead.selects?(:releases)
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(id.model_class, id.model_id, preloads).find
end
def container_repository(id:)
diff --git a/app/graphql/types/release_asset_link_type.rb b/app/graphql/types/release_asset_link_type.rb
index 33dcb5125e3..29738de27e5 100644
--- a/app/graphql/types/release_asset_link_type.rb
+++ b/app/graphql/types/release_asset_link_type.rb
@@ -7,6 +7,8 @@ module Types
authorize :read_release
+ present_using Releases::LinkPresenter
+
field :external, GraphQL::Types::Boolean, null: true, method: :external?,
description: 'Indicates the link points to an external resource.'
field :id, GraphQL::Types::ID, null: false,
@@ -22,12 +24,5 @@ module Types
description: 'Relative path for the direct asset link.'
field :direct_asset_url, GraphQL::Types::String, null: true,
description: 'Direct asset URL of the link.'
-
- def direct_asset_url
- return object.url unless object.filepath
-
- release = object.release.present
- release.download_url(object.filepath)
- end
end
end
diff --git a/app/graphql/types/release_type.rb b/app/graphql/types/release_type.rb
index 95b6b43bb46..43dc0c4ce85 100644
--- a/app/graphql/types/release_type.rb
+++ b/app/graphql/types/release_type.rb
@@ -13,6 +13,9 @@ module Types
present_using ReleasePresenter
+ field :id, ::Types::GlobalIDType[Release],
+ null: false,
+ description: 'Global ID of the release.'
field :assets, Types::ReleaseAssetsType, null: true, method: :itself,
description: 'Assets of the release.'
field :created_at, Types::TimeType, null: true,
diff --git a/app/graphql/types/terraform/state_type.rb b/app/graphql/types/terraform/state_type.rb
index bce34a85f85..be17fc41c2c 100644
--- a/app/graphql/types/terraform/state_type.rb
+++ b/app/graphql/types/terraform/state_type.rb
@@ -38,6 +38,10 @@ module Types
null: false,
description: 'Timestamp the Terraform state was updated.'
+ field :deleted_at, Types::TimeType,
+ null: true,
+ description: 'Timestamp the Terraform state was deleted.'
+
def locked_by_user
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.locked_by_user_id).find
end
diff --git a/app/graphql/types/time_type.rb b/app/graphql/types/time_type.rb
index 2db14953308..121515c04db 100644
--- a/app/graphql/types/time_type.rb
+++ b/app/graphql/types/time_type.rb
@@ -12,6 +12,9 @@ module Types
DESC
def self.coerce_input(value, ctx)
+ # arguments can be nil, so don't raise an error
+ return if value.nil?
+
Time.parse(value)
rescue ArgumentError, TypeError => e
raise GraphQL::CoercionError, e.message
diff --git a/app/graphql/types/todo_type.rb b/app/graphql/types/todo_type.rb
index f21b2b261a3..0de6b1d6f8a 100644
--- a/app/graphql/types/todo_type.rb
+++ b/app/graphql/types/todo_type.rb
@@ -53,6 +53,10 @@ module Types
description: 'Timestamp this to-do item was created.',
null: false
+ field :note, Types::Notes::NoteType,
+ description: 'Note which created this to-do item.',
+ null: true
+
def project
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
end
diff --git a/app/graphql/types/work_item_sort_enum.rb b/app/graphql/types/work_item_sort_enum.rb
new file mode 100644
index 00000000000..e644313d409
--- /dev/null
+++ b/app/graphql/types/work_item_sort_enum.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ class WorkItemSortEnum < SortEnum
+ graphql_name 'WorkItemSort'
+ description 'Values for sorting work items'
+
+ value 'TITLE_ASC', 'Title by ascending order.', value: :title_asc
+ value 'TITLE_DESC', 'Title by descending order.', value: :title_desc
+ end
+end
diff --git a/app/graphql/types/work_item_type.rb b/app/graphql/types/work_item_type.rb
index cd784d54959..18b9bfd1c9a 100644
--- a/app/graphql/types/work_item_type.rb
+++ b/app/graphql/types/work_item_type.rb
@@ -18,6 +18,8 @@ module Types
description: 'State of the work item.'
field :title, GraphQL::Types::String, null: false,
description: 'Title of the work item.'
+ field :widgets, [Types::WorkItems::WidgetInterface], null: true,
+ description: 'Collection of widgets that belong to the work item.'
field :work_item_type, Types::WorkItems::TypeType, null: false,
description: 'Type assigned to the work item.'
diff --git a/app/graphql/types/work_items/updated_task_input_type.rb b/app/graphql/types/work_items/updated_task_input_type.rb
new file mode 100644
index 00000000000..9f8afa2ff1b
--- /dev/null
+++ b/app/graphql/types/work_items/updated_task_input_type.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ module WorkItems
+ class UpdatedTaskInputType < BaseInputObject
+ graphql_name 'WorkItemUpdatedTaskInput'
+
+ include Mutations::WorkItems::UpdateArguments
+ end
+ end
+end
diff --git a/app/graphql/types/work_items/widget_interface.rb b/app/graphql/types/work_items/widget_interface.rb
new file mode 100644
index 00000000000..f3cf1d74829
--- /dev/null
+++ b/app/graphql/types/work_items/widget_interface.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Types
+ module WorkItems
+ module WidgetInterface
+ include Types::BaseInterface
+
+ graphql_name 'WorkItemWidget'
+
+ field :type, ::Types::WorkItems::WidgetTypeEnum, null: true,
+ description: 'Widget type.'
+
+ def self.resolve_type(object, context)
+ case object
+ when ::WorkItems::Widgets::Description
+ ::Types::WorkItems::Widgets::DescriptionType
+ when ::WorkItems::Widgets::Hierarchy
+ ::Types::WorkItems::Widgets::HierarchyType
+ else
+ raise "Unknown GraphQL type for widget #{object}"
+ end
+ end
+
+ orphan_types ::Types::WorkItems::Widgets::DescriptionType,
+ ::Types::WorkItems::Widgets::HierarchyType
+ end
+ end
+end
diff --git a/app/graphql/types/work_items/widget_type_enum.rb b/app/graphql/types/work_items/widget_type_enum.rb
new file mode 100644
index 00000000000..4e5933bff86
--- /dev/null
+++ b/app/graphql/types/work_items/widget_type_enum.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+ module WorkItems
+ class WidgetTypeEnum < BaseEnum
+ graphql_name 'WorkItemWidgetType'
+ description 'Type of a work item widget'
+
+ ::WorkItems::Type.available_widgets.each do |widget|
+ value widget.type.to_s.upcase, value: widget.type, description: "#{widget.type.to_s.titleize} widget."
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/work_items/widgets/description_input_type.rb b/app/graphql/types/work_items/widgets/description_input_type.rb
new file mode 100644
index 00000000000..382cfdf659f
--- /dev/null
+++ b/app/graphql/types/work_items/widgets/description_input_type.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module WorkItems
+ module Widgets
+ class DescriptionInputType < BaseInputObject
+ graphql_name 'WorkItemWidgetDescriptionInput'
+
+ argument :description, GraphQL::Types::String,
+ required: true,
+ description: copy_field_description(Types::WorkItemType, :description)
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/work_items/widgets/description_type.rb b/app/graphql/types/work_items/widgets/description_type.rb
new file mode 100644
index 00000000000..79192d7c3d4
--- /dev/null
+++ b/app/graphql/types/work_items/widgets/description_type.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Types
+ module WorkItems
+ module Widgets
+ # Disabling widget level authorization as it might be too granular
+ # and we already authorize the parent work item
+ # rubocop:disable Graphql/AuthorizeTypes
+ class DescriptionType < BaseObject
+ graphql_name 'WorkItemWidgetDescription'
+ description 'Represents a description widget'
+
+ implements Types::WorkItems::WidgetInterface
+
+ field :description, GraphQL::Types::String, null: true,
+ description: 'Description of the work item.'
+
+ markdown_field :description_html, null: true do |resolved_object|
+ resolved_object.work_item
+ end
+ end
+ # rubocop:enable Graphql/AuthorizeTypes
+ end
+ end
+end
diff --git a/app/graphql/types/work_items/widgets/hierarchy_type.rb b/app/graphql/types/work_items/widgets/hierarchy_type.rb
new file mode 100644
index 00000000000..057d5fbf056
--- /dev/null
+++ b/app/graphql/types/work_items/widgets/hierarchy_type.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Types
+ module WorkItems
+ module Widgets
+ # Disabling widget level authorization as it might be too granular
+ # and we already authorize the parent work item
+ # rubocop:disable Graphql/AuthorizeTypes
+ class HierarchyType < BaseObject
+ graphql_name 'WorkItemWidgetHierarchy'
+ description 'Represents a hierarchy widget'
+
+ implements Types::WorkItems::WidgetInterface
+
+ field :parent, ::Types::WorkItemType, null: true,
+ description: 'Parent work item.',
+ complexity: 5
+
+ field :children, ::Types::WorkItemType.connection_type, null: true,
+ description: 'Child work items.',
+ complexity: 5
+
+ def children
+ object.children.inc_relations_for_permission_check
+ end
+ end
+ # rubocop:enable Graphql/AuthorizeTypes
+ end
+ end
+end
diff --git a/app/helpers/access_tokens_helper.rb b/app/helpers/access_tokens_helper.rb
index d8d44601327..44200e84afb 100644
--- a/app/helpers/access_tokens_helper.rb
+++ b/app/helpers/access_tokens_helper.rb
@@ -29,7 +29,9 @@ module AccessTokensHelper
end
def expires_at_field_data
- {}
+ {
+ min_date: 1.day.from_now.iso8601
+ }
end
end
diff --git a/app/helpers/admin/application_settings/settings_helper.rb b/app/helpers/admin/application_settings/settings_helper.rb
new file mode 100644
index 00000000000..bd83ed19705
--- /dev/null
+++ b/app/helpers/admin/application_settings/settings_helper.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Admin
+ module ApplicationSettings
+ module SettingsHelper
+ def inactive_projects_deletion_data(settings)
+ {
+ delete_inactive_projects: settings.delete_inactive_projects.to_s,
+ inactive_projects_delete_after_months: settings.inactive_projects_delete_after_months,
+ inactive_projects_min_size_mb: settings.inactive_projects_min_size_mb,
+ inactive_projects_send_warning_email_after_months: settings.inactive_projects_send_warning_email_after_months
+ }
+ end
+ end
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 8cdfc267693..d2cc50be509 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
-require 'digest/md5'
require 'uri'
module ApplicationHelper
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 9023cca18dc..cd31d2c75ab 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -270,6 +270,7 @@ module ApplicationSettingsHelper
:inactive_projects_min_size_mb,
:inactive_projects_send_warning_email_after_months,
:invisible_captcha_enabled,
+ :jira_connect_application_key,
:max_artifacts_size,
:max_attachment_size,
:max_export_size,
@@ -407,6 +408,7 @@ module ApplicationSettingsHelper
:container_registry_import_max_retries,
:container_registry_import_start_max_retries,
:container_registry_import_max_step_duration,
+ :container_registry_pre_import_tags_rate,
:container_registry_pre_import_timeout,
:container_registry_import_timeout,
:container_registry_import_target_plan,
@@ -511,6 +513,32 @@ module ApplicationSettingsHelper
def registration_features_can_be_prompted?
!Gitlab::CurrentSettings.usage_ping_enabled?
end
+
+ def signup_form_data
+ {
+ host: new_user_session_url(host: Gitlab.config.gitlab.host),
+ settings_path: general_admin_application_settings_path(anchor: 'js-signup-settings'),
+ signup_enabled: @application_setting[:signup_enabled].to_s,
+ require_admin_approval_after_user_signup: @application_setting[:require_admin_approval_after_user_signup].to_s,
+ send_user_confirmation_email: @application_setting[:send_user_confirmation_email].to_s,
+ minimum_password_length: @application_setting[:minimum_password_length],
+ minimum_password_length_min: ApplicationSetting::DEFAULT_MINIMUM_PASSWORD_LENGTH,
+ minimum_password_length_max: Devise.password_length.max,
+ minimum_password_length_help_link:
+ 'https://about.gitlab.com/handbook/security/#gitlab-password-policy-guidelines',
+ domain_allowlist_raw: @application_setting.domain_allowlist_raw,
+ new_user_signups_cap: @application_setting[:new_user_signups_cap].to_s,
+ domain_denylist_enabled: @application_setting[:domain_denylist_enabled].to_s,
+ denylist_type_raw_selected:
+ (@application_setting.domain_denylist.present? || @application_setting.domain_denylist.blank?).to_s,
+ domain_denylist_raw: @application_setting.domain_denylist_raw,
+ 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.to_s,
+ after_sign_up_text: @application_setting[:after_sign_up_text].to_s,
+ pending_user_count: pending_user_count
+ }
+ end
end
ApplicationSettingsHelper.prepend_mod_with('ApplicationSettingsHelper')
diff --git a/app/helpers/breadcrumbs_helper.rb b/app/helpers/breadcrumbs_helper.rb
index 3a622a65685..38ed6e95a44 100644
--- a/app/helpers/breadcrumbs_helper.rb
+++ b/app/helpers/breadcrumbs_helper.rb
@@ -23,7 +23,7 @@ module BreadcrumbsHelper
def breadcrumb_list_item(link)
content_tag "li" do
- link + sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle")
+ link + sprite_icon("chevron-lg-right", size: 8, css_class: "breadcrumbs-list-angle")
end
end
diff --git a/app/helpers/ci/pipeline_editor_helper.rb b/app/helpers/ci/pipeline_editor_helper.rb
index da773e3e8a8..bb3f7b5aa79 100644
--- a/app/helpers/ci/pipeline_editor_helper.rb
+++ b/app/helpers/ci/pipeline_editor_helper.rb
@@ -33,6 +33,7 @@ module Ci
"project-namespace" => project.namespace.full_path,
"runner-help-page-path" => help_page_path('ci/runners/index'),
"total-branches" => total_branches,
+ "validate-tab-illustration-path" => image_path('illustrations/project-run-CICD-pipelines-sm.svg'),
"yml-help-page-path" => help_page_path('ci/yaml/index')
}
end
diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb
index 6366ca0dfb1..74318797069 100644
--- a/app/helpers/ci/runners_helper.rb
+++ b/app/helpers/ci/runners_helper.rb
@@ -65,7 +65,9 @@ module Ci
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
registration_token: Gitlab::CurrentSettings.runners_registration_token,
online_contact_timeout_secs: ::Ci::Runner::ONLINE_CONTACT_TIMEOUT.to_i,
- stale_timeout_secs: ::Ci::Runner::STALE_TIMEOUT.to_i
+ stale_timeout_secs: ::Ci::Runner::STALE_TIMEOUT.to_i,
+ empty_state_svg_path: image_path('illustrations/pipelines_empty.svg'),
+ empty_state_filtered_svg_path: image_path('illustrations/magnifying-glass.svg')
}
end
@@ -87,7 +89,9 @@ module Ci
group_full_path: group.full_path,
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
online_contact_timeout_secs: ::Ci::Runner::ONLINE_CONTACT_TIMEOUT.to_i,
- stale_timeout_secs: ::Ci::Runner::STALE_TIMEOUT.to_i
+ stale_timeout_secs: ::Ci::Runner::STALE_TIMEOUT.to_i,
+ empty_state_svg_path: image_path('illustrations/pipelines_empty.svg'),
+ empty_state_filtered_svg_path: image_path('illustrations/magnifying-glass.svg')
}
end
diff --git a/app/helpers/custom_metrics_helper.rb b/app/helpers/custom_metrics_helper.rb
index 5442120008a..8a9d94bd2a1 100644
--- a/app/helpers/custom_metrics_helper.rb
+++ b/app/helpers/custom_metrics_helper.rb
@@ -5,7 +5,7 @@ module CustomMetricsHelper
{
'custom-metrics-path' => url_for([project, metric]),
'metric-persisted' => metric.persisted?.to_s,
- 'edit-project-service-path' => edit_project_integration_path(project, ::Integrations::Prometheus),
+ 'edit-integration-path' => edit_project_settings_integration_path(project, ::Integrations::Prometheus),
'validate-query-path' => validate_query_project_prometheus_metrics_path(project),
'title' => metric.title.to_s,
'query' => metric.query.to_s,
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 522593dd487..71c8296ad2e 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -225,6 +225,23 @@ module DiffHelper
end
end
+ def conflicts(allow_tree_conflicts: false)
+ return unless options[:merge_ref_head_diff]
+
+ conflicts_service = MergeRequests::Conflicts::ListService.new(merge_request, allow_tree_conflicts: allow_tree_conflicts) # rubocop:disable CodeReuse/ServiceClass
+
+ return unless allow_tree_conflicts || conflicts_service.can_be_resolved_in_ui?
+
+ conflicts_service.conflicts.files.index_by(&:path)
+ rescue Gitlab::Git::Conflict::Resolver::ConflictSideMissing
+ # This exception is raised when changes on a fork isn't present on canonical repo yet.
+ # We can't list conflicts until the canonical repo gets the references from the fork
+ # which happens asynchronously when updating MR.
+ #
+ # Return empty hash to indicate that there are no conflicts.
+ {}
+ end
+
private
def diff_btn(title, name, selected)
@@ -271,16 +288,6 @@ module DiffHelper
Gitlab::CodeNavigationPath.new(merge_request.project, merge_request.diff_head_sha)
end
- def conflicts(allow_tree_conflicts: false)
- return unless options[:merge_ref_head_diff]
-
- conflicts_service = MergeRequests::Conflicts::ListService.new(merge_request, allow_tree_conflicts: allow_tree_conflicts) # rubocop:disable CodeReuse/ServiceClass
-
- return unless allow_tree_conflicts || conflicts_service.can_be_resolved_in_ui?
-
- conflicts_service.conflicts.files.index_by(&:path)
- end
-
def log_overflow_limits(diff_files:, collection_overflow:)
if diff_files.any?(&:too_large?)
Gitlab::Metrics.add_event(:diffs_overflow_single_file_limits)
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 59731dc2f6f..c23d905a008 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -62,7 +62,7 @@ module EmailsHelper
end
def header_logo
- if current_appearance&.header_logo?
+ if current_appearance&.header_logo? && !current_appearance.header_logo.filename.ends_with?('.svg')
image_tag(
current_appearance.header_logo_path,
style: 'height: 50px'
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 3b60bda8605..59d43c51db2 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -59,7 +59,7 @@ module EnvironmentsHelper
return {} unless project
{
- 'settings_path' => edit_project_integration_path(project, 'prometheus'),
+ 'settings_path' => edit_project_settings_integration_path(project, 'prometheus'),
'clusters_path' => project_clusters_path(project),
'dashboards_endpoint' => project_performance_monitoring_dashboards_path(project, format: :json),
'default_branch' => project.default_branch,
@@ -102,7 +102,6 @@ module EnvironmentsHelper
'metrics_endpoint' => additional_metrics_project_environment_path(project, environment, format: :json),
'dashboard_endpoint' => metrics_dashboard_project_environment_path(project, environment, format: :json),
'deployments_endpoint' => project_environment_deployments_path(project, environment, format: :json),
- 'alerts_endpoint' => project_prometheus_alerts_path(project, environment_id: environment.id, format: :json),
'operations_settings_path' => project_settings_operations_path(project),
'can_access_operations_settings' => can?(current_user, :admin_operations, project).to_s,
'panel_preview_endpoint' => project_metrics_dashboards_builder_path(project, format: :json)
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index 3a5dcb4e664..17812aed3ff 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module FormHelper
- def form_errors(model, type: 'form', truncate: [])
+ def form_errors(model, type: 'form', truncate: [], pajamas_alert: false)
errors = model.errors
return unless errors.any?
@@ -14,21 +14,37 @@ module FormHelper
truncate = Array.wrap(truncate)
- tag.div(class: 'alert alert-danger', id: 'error_explanation') do
- tag.h4(headline) <<
- tag.ul do
- messages = errors.map do |error|
- attribute = error.attribute
- message = error.message
-
- message = html_escape_once(errors.full_message(attribute, message)).html_safe
- message = tag.span(message, class: 'str-truncated-100') if truncate.include?(attribute)
-
- tag.li(message)
+ messages = errors.map do |error|
+ attribute = error.attribute
+ message = error.message
+
+ message = html_escape_once(errors.full_message(attribute, message)).html_safe
+ message = tag.span(message, class: 'str-truncated-100') if truncate.include?(attribute)
+ message = append_help_page_link(message, error.options) if error.options[:help_page_url].present?
+
+ tag.li(message)
+ end.join.html_safe
+
+ if pajamas_alert
+ render Pajamas::AlertComponent.new(
+ variant: :danger,
+ title: headline,
+ dismissible: false,
+ alert_options: { id: 'error_explanation', class: 'gl-mb-5' }
+ ) do |c|
+ c.body do
+ tag.ul(class: 'gl-pl-5 gl-mb-0') do
+ messages
end
-
- messages.join.html_safe
end
+ end
+ else
+ tag.div(class: 'alert alert-danger', id: 'error_explanation') do
+ tag.h4(headline) <<
+ tag.ul do
+ messages
+ end
+ end
end
end
@@ -120,6 +136,20 @@ module FormHelper
private
+ def append_help_page_link(message, options)
+ help_page_url = options[:help_page_url]
+ help_link_text = options[:help_link_text] || _('Learn more.')
+
+ help_link = link_to(
+ help_link_text,
+ help_page_url,
+ target: '_blank',
+ rel: 'noopener noreferrer'
+ )
+
+ message + " #{help_link}".html_safe
+ end
+
def multiple_assignees_dropdown_options(options)
new_options = options.dup
diff --git a/app/helpers/groups/crm_settings_helper.rb b/app/helpers/groups/crm_settings_helper.rb
deleted file mode 100644
index d7ca25a9d1b..00000000000
--- a/app/helpers/groups/crm_settings_helper.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module Groups
- module CrmSettingsHelper
- def crm_feature_available?(group)
- Feature.enabled?(:customer_relations, group)
- end
- end
-end
diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb
index ca61c4da41c..37b23345d2a 100644
--- a/app/helpers/groups/group_members_helper.rb
+++ b/app/helpers/groups/group_members_helper.rb
@@ -20,6 +20,13 @@ module Groups::GroupMembersHelper
}
end
+ def group_member_header_subtext(group)
+ html_escape(_('You can invite a new member to ' \
+ '%{strong_start}%{group_name}%{strong_end}.')) % { group_name: group.name,
+ strong_start: '<strong>'.html_safe,
+ strong_end: '</strong>'.html_safe }
+ end
+
private
def group_members_serialized(group, members)
@@ -53,12 +60,8 @@ module Groups::GroupMembersHelper
end
def group_group_links_list_data(group, include_relations, search)
- if ::Feature.enabled?(:group_member_inherited_group, group)
- group_links = group_group_links(group, include_relations)
- group_links = group_links.search(search) if search
- else
- group_links = group.shared_with_group_links
- end
+ group_links = group_group_links(group, include_relations)
+ group_links = group_links.search(search) if search
{
members: group_group_links_serialized(group, group_links),
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index c58a365b884..9ea9509bc28 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -144,6 +144,40 @@ module GroupsHelper
false
end
+ def group_name_and_path_app_data(group)
+ parent = group.parent
+
+ {
+ base_path: URI.join(root_url, parent&.full_path || "").to_s,
+ mattermost_enabled: Gitlab.config.mattermost.enabled.to_s
+ }
+ end
+
+ def subgroups_and_projects_list_app_data(group)
+ {
+ show_schema_markup: 'true',
+ new_subgroup_path: new_group_path(parent_id: group.id),
+ new_project_path: new_project_path(namespace_id: group.id),
+ new_subgroup_illustration: image_path('illustrations/subgroup-create-new-sm.svg'),
+ new_project_illustration: image_path('illustrations/project-create-new-sm.svg'),
+ empty_subgroup_illustration: image_path('illustrations/empty-state/empty-subgroup-md.svg'),
+ render_empty_state: 'true',
+ can_create_subgroups: can?(current_user, :create_subgroup, group).to_s,
+ can_create_projects: can?(current_user, :create_projects, group).to_s
+ }
+ end
+
+ def enabled_git_access_protocol_options_for_group
+ case ::Gitlab::CurrentSettings.enabled_git_access_protocol
+ when nil, ""
+ [[_("Both SSH and HTTP(S)"), "all"], [_("Only SSH"), "ssh"], [_("Only HTTP(S)"), "http"]]
+ when "ssh"
+ [[_("Only SSH"), "ssh"]]
+ when "http"
+ [[_("Only HTTP(S)"), "http"]]
+ end
+ end
+
private
def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false)
diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb
index 862938ac961..82d4ceee44e 100644
--- a/app/helpers/integrations_helper.rb
+++ b/app/helpers/integrations_helper.rb
@@ -58,7 +58,7 @@ module IntegrationsHelper
def scoped_integration_path(integration, project: nil, group: nil)
if project.present?
- project_integration_path(project, integration)
+ project_settings_integration_path(project, integration)
elsif group.present?
group_settings_integration_path(group, integration)
else
@@ -68,7 +68,7 @@ module IntegrationsHelper
def scoped_edit_integration_path(integration, project: nil, group: nil)
if project.present?
- edit_project_integration_path(project, integration)
+ edit_project_settings_integration_path(project, integration)
elsif group.present?
edit_group_settings_integration_path(group, integration)
else
@@ -82,7 +82,7 @@ module IntegrationsHelper
def scoped_test_integration_path(integration, project: nil, group: nil)
if project.present?
- test_project_integration_path(project, integration)
+ test_project_settings_integration_path(project, integration)
elsif group.present?
test_group_settings_integration_path(group, integration)
else
@@ -233,11 +233,11 @@ module IntegrationsHelper
end
def trigger_events_for_integration(integration)
- ServiceEventSerializer.new(service: integration).represent(integration.configurable_events).to_json
+ Integrations::EventSerializer.new(integration: integration).represent(integration.configurable_events).to_json
end
def fields_for_integration(integration)
- ServiceFieldSerializer.new(service: integration).represent(integration.global_fields).to_json
+ Integrations::FieldSerializer.new(integration: integration).represent(integration.global_fields).to_json
end
def integration_level(integration)
diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb
index e46270ab819..5d537767eaf 100644
--- a/app/helpers/invite_members_helper.rb
+++ b/app/helpers/invite_members_helper.rb
@@ -29,7 +29,7 @@ module InviteMembersHelper
invalid_groups: source.related_group_ids,
help_link: help_page_url('user/permissions'),
is_project: is_project,
- access_levels: member_class.access_level_roles.to_json
+ access_levels: member_class.permissible_access_level_roles(current_user, source).to_json
}.merge(group_select_data(source))
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 60dba73447c..a157b1b7b21 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -28,18 +28,16 @@ module IssuesHelper
end
def status_box_class(item)
- updated_mr_header_enabled = Feature.enabled?(:updated_mr_header, @project)
-
if item.try(:expired?)
- 'status-box-expired'
+ 'gl-bg-orange-500'
elsif item.try(:merged?)
- updated_mr_header_enabled ? 'badge-info' : 'status-box-mr-merged'
+ 'badge-info'
elsif item.closed?
- item.is_a?(MergeRequest) && updated_mr_header_enabled ? 'badge-danger' : 'status-box-mr-closed'
+ item.is_a?(MergeRequest) ? 'badge-danger' : 'gl-bg-red-500'
elsif item.try(:upcoming?)
- 'status-box-upcoming'
+ 'gl-bg-gray-500'
else
- item.is_a?(MergeRequest) && updated_mr_header_enabled ? 'badge-success' : 'status-box-open'
+ item.is_a?(MergeRequest) ? 'badge-success' : 'gl-bg-green-500'
end
end
@@ -218,6 +216,8 @@ module IssuesHelper
can_bulk_update: can?(current_user, :admin_issue, project).to_s,
can_edit: can?(current_user, :admin_project, project).to_s,
can_import_issues: can?(current_user, :import_issues, @project).to_s,
+ can_read_crm_contact: can?(current_user, :read_crm_contact, project.group).to_s,
+ can_read_crm_organization: can?(current_user, :read_crm_organization, project.group).to_s,
email: current_user&.notification_email_or_default,
emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
export_csv_path: export_csv_project_issues_path(project),
@@ -238,8 +238,12 @@ module IssuesHelper
def group_issues_list_data(group, current_user)
common_issues_list_data(group, current_user).merge(
+ can_create_projects: can?(current_user, :create_projects, group).to_s,
+ can_read_crm_contact: can?(current_user, :read_crm_contact, group).to_s,
+ can_read_crm_organization: can?(current_user, :read_crm_organization, group).to_s,
has_any_issues: @has_issues.to_s,
- has_any_projects: @has_projects.to_s
+ has_any_projects: @has_projects.to_s,
+ new_project_path: new_project_path(namespace_id: group.id)
)
end
diff --git a/app/helpers/jira_connect_helper.rb b/app/helpers/jira_connect_helper.rb
index 30f29e002b8..4ddfb0224d1 100644
--- a/app/helpers/jira_connect_helper.rb
+++ b/app/helpers/jira_connect_helper.rb
@@ -19,7 +19,7 @@ module JiraConnectHelper
def jira_connect_oauth_data
oauth_authorize_url = oauth_authorization_url(
- client_id: ENV['JIRA_CONNECT_OAUTH_CLIENT_ID'],
+ client_id: Gitlab::CurrentSettings.jira_connect_application_key,
response_type: 'code',
scope: 'api',
redirect_uri: jira_connect_oauth_callbacks_url,
@@ -32,7 +32,7 @@ module JiraConnectHelper
state: oauth_state,
oauth_token_payload: {
grant_type: :authorization_code,
- client_id: ENV['JIRA_CONNECT_OAUTH_CLIENT_ID'],
+ client_id: Gitlab::CurrentSettings.jira_connect_application_key,
redirect_uri: jira_connect_oauth_callbacks_url
}
}
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 7a4cc61af79..777d485797f 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -6,6 +6,12 @@ module MarkupHelper
include ActionView::Helpers::TextHelper
include ActionView::Context
+ # Let's increase the render timeout
+ # For a smaller one, a test that renders the blob content statically fails
+ # We can consider removing this custom timeout when refactor_blob_viewer FF is removed:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/324351
+ RENDER_TIMEOUT = 5.seconds
+
def plain?(filename)
Gitlab::MarkupHelper.plain?(filename)
end
@@ -88,7 +94,10 @@ module MarkupHelper
text,
tags: tags,
attributes: Rails::Html::WhiteListSanitizer.allowed_attributes +
- %w(style data-src data-name data-unicode-version data-iid data-project-path data-mr-title data-html)
+ %w(
+ style data-src data-name data-unicode-version data-html
+ data-reference-type data-project-path data-iid data-mr-title
+ )
)
# since <img> tags are stripped, this can leave empty <a> tags hanging around
@@ -136,14 +145,22 @@ module MarkupHelper
def markup_unsafe(file_name, text, context = {})
return '' unless text.present?
- if gitlab_markdown?(file_name)
- markdown_unsafe(text, context)
- elsif asciidoc?(file_name)
- asciidoc_unsafe(text, context)
- elsif plain?(file_name)
- plain_unsafe(text)
+ markup = proc do
+ if gitlab_markdown?(file_name)
+ markdown_unsafe(text, context)
+ elsif asciidoc?(file_name)
+ asciidoc_unsafe(text, context)
+ elsif plain?(file_name)
+ plain_unsafe(text)
+ else
+ other_markup_unsafe(file_name, text, context)
+ end
+ end
+
+ if Feature.enabled?(:markup_rendering_timeout, @project)
+ Gitlab::RenderTimeout.timeout(foreground: RENDER_TIMEOUT, &markup)
else
- other_markup_unsafe(file_name, text, context)
+ markup.call
end
rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, project_id: @project&.id, file_name: file_name)
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index aac49cfa234..4b1cbd3f1ae 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -84,3 +84,5 @@ module MembersHelper
}
end
end
+
+MembersHelper.prepend_mod_with('MembersHelper')
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index e1c9e7d3896..d840223a066 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -139,7 +139,7 @@ module MergeRequestsHelper
end
def toggle_draft_merge_request_path(issuable)
- wip_event = issuable.work_in_progress? ? 'unwip' : 'wip'
+ wip_event = issuable.draft? ? 'ready' : 'draft'
issuable_path(issuable, { merge_request: { wip_event: wip_event } })
end
@@ -246,13 +246,13 @@ module MergeRequestsHelper
''
end
- link_to branch, branch_path, title: branch, class: 'gl-link gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mb-n2'
+ link_to branch, branch_path, title: branch, class: 'gl-text-blue-500! gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mb-n2'
end
def merge_request_header(project, merge_request)
link_to_author = link_to_member(project, merge_request.author, size: 24, extra_class: 'gl-font-weight-bold', avatar: false)
copy_button = clipboard_button(text: merge_request.source_branch, title: _('Copy branch name'), class: 'btn btn-default btn-sm gl-button btn-default-tertiary btn-icon gl-display-none! gl-md-display-inline-block! js-source-branch-copy')
- target_branch = link_to merge_request.target_branch, project_tree_path(merge_request.target_project, merge_request.target_branch), title: merge_request.target_branch, class: 'gl-link gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mb-n2'
+ target_branch = link_to merge_request.target_branch, project_tree_path(merge_request.target_project, merge_request.target_branch), title: merge_request.target_branch, class: 'gl-text-blue-500! gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mb-n2'
_('%{author} requested to merge %{source_branch} %{copy_button} into %{target_branch} %{created_at}').html_safe % { author: link_to_author.html_safe, source_branch: merge_request_source_branch(merge_request).html_safe, copy_button: copy_button.html_safe, target_branch: target_branch.html_safe, created_at: time_ago_with_tooltip(merge_request.created_at, html_class: 'gl-display-inline-block').html_safe }
end
diff --git a/app/helpers/nav/new_dropdown_helper.rb b/app/helpers/nav/new_dropdown_helper.rb
index 715a5a02b50..469d6c1a7eb 100644
--- a/app/helpers/nav/new_dropdown_helper.rb
+++ b/app/helpers/nav/new_dropdown_helper.rb
@@ -16,7 +16,7 @@ module Nav
menu_sections.push(general_menu_section)
{
- title: _("New..."),
+ title: _("Create new"),
menu_sections: menu_sections.select { |x| x.fetch(:menu_items).any? }
}
end
diff --git a/app/helpers/nav/top_nav_helper.rb b/app/helpers/nav/top_nav_helper.rb
index 9420c95c9ce..3ceb60251c2 100644
--- a/app/helpers/nav/top_nav_helper.rb
+++ b/app/helpers/nav/top_nav_helper.rb
@@ -127,7 +127,7 @@ module Nav
href: dashboard_milestones_path,
active: active_nav_link?(controller: 'dashboard/milestones'),
icon: 'clock',
- data: { qa_selector: 'milestones_link' },
+ data: { qa_selector: 'milestones_link', **menu_data_tracking_attrs('milestones') },
shortcut_class: 'dashboard-shortcuts-milestones'
)
end
@@ -135,7 +135,7 @@ module Nav
if dashboard_nav_link?(:snippets)
builder.add_primary_menu_item_with_shortcut(
active: active_nav_link?(controller: 'dashboard/snippets'),
- data: { qa_selector: 'snippets_link' },
+ data: { qa_selector: 'snippets_link', **menu_data_tracking_attrs('snippets') },
href: dashboard_snippets_path,
**snippets_menu_item_attrs
)
@@ -148,7 +148,7 @@ module Nav
href: activity_dashboard_path,
active: active_nav_link?(path: 'dashboard#activity'),
icon: 'history',
- data: { qa_selector: 'activity_link' },
+ data: { qa_selector: 'activity_link', **menu_data_tracking_attrs('activity') },
shortcut_class: 'dashboard-shortcuts-activity'
)
end
@@ -158,13 +158,16 @@ module Nav
# we should be good.
# rubocop: disable Cop/UserAdmin
if current_user&.admin?
+ title = _('Admin')
+
builder.add_secondary_menu_item(
id: 'admin',
- title: _('Admin'),
+ title: title,
active: active_nav_link?(controller: 'admin/dashboard'),
icon: 'admin',
css_class: 'qa-admin-area-link',
- href: admin_root_path
+ href: admin_root_path,
+ data: { qa_selector: 'menu_item_link', qa_title: title, **menu_data_tracking_attrs(title) }
)
end
@@ -176,15 +179,18 @@ module Nav
active: active_nav_link?(controller: 'admin/sessions'),
icon: 'lock-open',
href: destroy_admin_session_path,
- data: { method: 'post' }
+ data: { method: 'post', **menu_data_tracking_attrs('leave_admin_mode') }
)
elsif current_user.admin?
+ title = _('Enter Admin Mode')
+
builder.add_secondary_menu_item(
id: 'enter_admin_mode',
- title: _('Enter Admin Mode'),
+ title: title,
active: active_nav_link?(controller: 'admin/sessions'),
icon: 'lock',
- href: new_admin_session_path
+ href: new_admin_session_path,
+ data: { qa_selector: 'menu_item_link', qa_title: title, **menu_data_tracking_attrs(title) }
)
end
end
@@ -218,6 +224,14 @@ module Nav
}
end
+ def menu_data_tracking_attrs(label)
+ tracking_attrs(
+ "menu_#{label.underscore.parameterize(separator: '_')}",
+ 'click_dropdown',
+ 'navigation'
+ )[:data] || {}
+ end
+
def container_view_props(namespace:, current_item:, submenu:)
{
namespace: namespace,
@@ -260,21 +274,51 @@ module Nav
def projects_submenu_items(builder:)
# These project links come from `app/views/layouts/nav/projects_dropdown/_show.html.haml`
- builder.add_primary_menu_item(id: 'your', title: _('Your projects'), href: dashboard_projects_path)
- builder.add_primary_menu_item(id: 'starred', title: _('Starred projects'), href: starred_dashboard_projects_path)
- builder.add_primary_menu_item(id: 'explore', title: _('Explore projects'), href: explore_root_path)
- builder.add_primary_menu_item(id: 'topics', title: _('Explore topics'), href: topics_explore_projects_path)
- builder.add_secondary_menu_item(id: 'create', title: _('Create new project'), href: new_project_path)
+ [
+ { id: 'your', title: _('Your projects'), href: dashboard_projects_path },
+ { id: 'starred', title: _('Starred projects'), href: starred_dashboard_projects_path },
+ { id: 'explore', title: _('Explore projects'), href: explore_root_path },
+ { id: 'topics', title: _('Explore topics'), href: topics_explore_projects_path }
+ ].each do |item|
+ builder.add_primary_menu_item(
+ **item,
+ data: { qa_selector: 'menu_item_link', qa_title: item[:title], **menu_data_tracking_attrs(item[:title]) }
+ )
+ end
+
+ title = _('Create new project')
+
+ builder.add_secondary_menu_item(
+ id: 'create',
+ title: title,
+ href: new_project_path,
+ data: { qa_selector: 'menu_item_link', qa_title: title, **menu_data_tracking_attrs(title) }
+ )
end
def groups_submenu
# These group links come from `app/views/layouts/nav/groups_dropdown/_show.html.haml`
builder = ::Gitlab::Nav::TopNavMenuBuilder.new
- builder.add_primary_menu_item(id: 'your', title: _('Your groups'), href: dashboard_groups_path)
- builder.add_primary_menu_item(id: 'explore', title: _('Explore groups'), href: explore_groups_path)
+
+ [
+ { id: 'your', title: _('Your groups'), href: dashboard_groups_path },
+ { id: 'explore', title: _('Explore groups'), href: explore_groups_path }
+ ].each do |item|
+ builder.add_primary_menu_item(
+ **item,
+ data: { qa_selector: 'menu_item_link', qa_title: item[:title], **menu_data_tracking_attrs(item[:title]) }
+ )
+ end
if current_user.can_create_group?
- builder.add_secondary_menu_item(id: 'create', title: _('Create group'), href: new_group_path)
+ title = _('Create group')
+
+ builder.add_secondary_menu_item(
+ id: 'create',
+ title: title,
+ href: new_group_path,
+ data: { qa_selector: 'menu_item_link', qa_title: title, **menu_data_tracking_attrs(title) }
+ )
end
builder.build
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 37cd491e19f..4d6ab7b8bf9 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -21,7 +21,7 @@ module NavHelper
def page_gutter_class
moved_sidebar_enabled = current_controller?('merge_requests') && moved_mr_sidebar_enabled?
- if page_has_markdown?
+ if page_has_markdown? && !current_controller?('conflicts')
if cookies[:collapsed_gutter] == 'true'
["page-gutter", "#{'right-sidebar-collapsed' unless moved_sidebar_enabled}"]
else
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 1afdb7a0ab9..b47f4633348 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -175,9 +175,7 @@ module NotesHelper
end
end
- def notes_data(issuable, start_at_zero = false)
- initial_last_fetched_at = start_at_zero ? 0 : Time.current.to_i * ::Gitlab::UpdatedNotesPaginator::MICROSECOND
-
+ def notes_data(issuable)
data = {
discussionsPath: discussions_path(issuable),
registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'),
@@ -188,7 +186,7 @@ module NotesHelper
reopenPath: reopen_issuable_path(issuable),
notesPath: notes_url,
prerenderedNotesCount: issuable.capped_notes_count(MAX_PRERENDERED_NOTES),
- lastFetchedAt: initial_last_fetched_at,
+ lastFetchedAt: Time.current.to_i * NotesActions::MICROSECOND,
notesFilter: current_user&.notes_filter_for(issuable)
}
diff --git a/app/helpers/projects/pipeline_helper.rb b/app/helpers/projects/pipeline_helper.rb
index 286026bc290..eeee8290914 100644
--- a/app/helpers/projects/pipeline_helper.rb
+++ b/app/helpers/projects/pipeline_helper.rb
@@ -2,13 +2,19 @@
module Projects
module PipelineHelper
+ extend ::Ci::BuildsHelper
+
def js_pipeline_tabs_data(project, pipeline)
{
can_generate_codequality_reports: pipeline.can_generate_codequality_reports?.to_json,
+ failed_jobs_count: pipeline.failed_builds.count,
+ failed_jobs_summary: prepare_failed_jobs_summary_data(pipeline.failed_builds),
+ full_path: project.full_path,
graphql_resource_etag: graphql_etag_pipeline_path(pipeline),
metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: project.namespace, project_id: project, format: :json),
pipeline_iid: pipeline.iid,
- pipeline_project_path: project.full_path
+ pipeline_project_path: project.full_path,
+ total_job_count: pipeline.total_size
}
end
end
diff --git a/app/helpers/projects/project_members_helper.rb b/app/helpers/projects/project_members_helper.rb
index 980c8ca6b80..d5cc2b72ae9 100644
--- a/app/helpers/projects/project_members_helper.rb
+++ b/app/helpers/projects/project_members_helper.rb
@@ -12,8 +12,35 @@ module Projects::ProjectMembersHelper
}.to_json
end
+ def project_member_header_subtext(project)
+ if can?(current_user, :admin_project_member, project)
+ share_project_description(project)
+ else
+ 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
+ }
+ end
+ end
+
private
+ def share_project_description(project)
+ share_with_group = project.allowed_to_share_with_group?
+ share_with_members = !membership_locked?
+
+ description =
+ if share_with_group && share_with_members
+ _("You can invite a new member to %{project_name} or invite another group.")
+ elsif share_with_group
+ _("You can invite another group to %{project_name}.")
+ elsif share_with_members
+ _("You can invite a new member to %{project_name}.")
+ end
+
+ html_escape(description) % { project_name: tag.strong(project.name) }
+ end
+
def project_members_serialized(project, members)
MemberSerializer.new.represent(members, { current_user: current_user, group: project.group, source: project })
end
@@ -38,3 +65,5 @@ module Projects::ProjectMembersHelper
}
end
end
+
+Projects::ProjectMembersHelper.prepend_mod_with('Projects::ProjectMembersHelper')
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index c3f22dc7693..6112d05f37d 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module ProjectsHelper
+ include Gitlab::Utils::StrongMemoize
+
def project_incident_management_setting
@project_incident_management_setting ||= @project.incident_management_setting ||
@project.build_incident_management_setting
@@ -331,22 +333,6 @@ module ProjectsHelper
false
end
- def share_project_description(project)
- share_with_group = project.allowed_to_share_with_group?
- share_with_members = !membership_locked?
-
- description =
- if share_with_group && share_with_members
- _("You can invite a new member to %{project_name} or invite another group.")
- elsif share_with_group
- _("You can invite another group to %{project_name}.")
- elsif share_with_members
- _("You can invite a new member to %{project_name}.")
- end
-
- html_escape(description) % { project_name: tag.strong(project.name) }
- end
-
def metrics_external_dashboard_url
@project.metrics_setting_external_dashboard_url
end
@@ -446,6 +432,30 @@ module ProjectsHelper
configure_oauth_import_message('GitLab.com', help_page_path("integration/gitlab"))
end
+ def show_inactive_project_deletion_banner?(project)
+ return false unless project.present? && project.saved?
+ return false unless delete_inactive_projects?
+ return false unless Feature.enabled?(:inactive_projects_deletion, project.root_namespace)
+
+ project.inactive?
+ end
+
+ def inactive_project_deletion_date(project)
+ Gitlab::InactiveProjectsDeletionWarningTracker.new(project.id).scheduled_deletion_date
+ end
+
+ def show_clusters_alert?(project)
+ Gitlab.com? && can_admin_associated_clusters?(project)
+ end
+
+ def clusters_deprecation_alert_message
+ if has_active_license?
+ s_('ClusterIntegration|The certificate-based Kubernetes integration has been deprecated and will be turned off at the end of November 2022. Please %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd} or reach out to GitLab support.')
+ else
+ s_('ClusterIntegration|The certificate-based Kubernetes integration has been deprecated and will be turned off at the end of November 2022. Please %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd}.')
+ end
+ end
+
private
def configure_oauth_import_message(provider, help_url)
@@ -596,6 +606,7 @@ module ProjectsHelper
feature = project.project_feature
{
packagesEnabled: !!project.packages_enabled,
+ packageRegistryAccessLevel: feature.package_registry_access_level,
visibilityLevel: project.visibility_level,
requestAccessEnabled: !!project.request_access_enabled,
issuesAccessLevel: feature.issues_access_level,
@@ -736,6 +747,24 @@ module ProjectsHelper
link_to(name, url)
end
end
+
+ def delete_inactive_projects?
+ strong_memoize(:delete_inactive_projects_setting) do
+ ::Gitlab::CurrentSettings.delete_inactive_projects?
+ end
+ end
+end
+
+def can_admin_associated_clusters?(project)
+ can_admin_project_clusters?(project) || can_admin_group_clusters?(project)
+end
+
+def can_admin_project_clusters?(project)
+ project.clusters.any? && can?(current_user, :admin_cluster, project)
+end
+
+def can_admin_group_clusters?(project)
+ project.group && project.group.clusters.any? && can?(current_user, :admin_cluster, project.group)
end
ProjectsHelper.prepend_mod_with('ProjectsHelper')
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 69bea0abd88..c8750cd9b52 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -117,6 +117,8 @@ module SearchHelper
end
def repository_ref(project)
+ return project.default_branch unless params[:project_id]
+
# Always #to_s the repository_ref param in case the value is also a number
params[:repository_ref].to_s.presence || project.default_branch
end
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 78b204fefe9..8558c664977 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -62,6 +62,18 @@ module SnippetsHelper
rel: 'noopener noreferrer')
end
+ def embedded_copy_snippet_button(blob)
+ return unless blob.rendered_as_text?(ignore_errors: false)
+
+ content_tag(:button,
+ class: 'gl-button btn btn-default copy-to-clipboard-btn',
+ title: 'Copy snippet contents',
+ onclick: "copyToClipboard('.blob-content[data-blob-id=\"#{blob.id}\"] > pre')"
+ ) do
+ external_snippet_icon('copy-to-clipboard')
+ end
+ end
+
def snippet_file_count(snippet)
file_count = snippet.statistics&.file_count
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 43ec02b6537..6f15cc7f4ec 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -267,6 +267,10 @@ module SortingHelper
options.concat([title_option])
end
+ def can_sort_by_issue_weight?(_viewing_issues)
+ false
+ end
+
def due_date_option
{ value: sort_value_due_date, text: sort_title_due_date, href: page_filter_path(sort: sort_value_due_date) }
end
diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb
index cb1a5f5ce0c..38ae9b5b634 100644
--- a/app/helpers/storage_helper.rb
+++ b/app/helpers/storage_helper.rb
@@ -25,20 +25,22 @@ module StorageHelper
end
def storage_enforcement_banner_info(namespace)
- return unless can?(current_user, :admin_namespace, namespace)
- return if namespace.paid?
- return unless namespace.storage_enforcement_date && namespace.storage_enforcement_date >= Date.today
- return if user_dismissed_storage_enforcement_banner?(namespace)
+ root_ancestor = namespace.root_ancestor
+
+ return unless can?(current_user, :admin_namespace, root_ancestor)
+ return if root_ancestor.paid?
+ return unless future_enforcement_date?(root_ancestor)
+ return if user_dismissed_storage_enforcement_banner?(root_ancestor)
{
text: html_escape_once(s_("UsageQuota|From %{storage_enforcement_date} storage limits will apply to this namespace. " \
"You are currently using %{used_storage} of namespace storage. " \
"View and manage your usage from %{strong_start}%{namespace_type} settings &gt; Usage quotas%{strong_end}.")).html_safe %
- { storage_enforcement_date: namespace.storage_enforcement_date, used_storage: storage_counter(namespace.root_storage_statistics&.storage_size || 0), strong_start: "<strong>".html_safe, strong_end: "</strong>".html_safe, namespace_type: namespace.type },
+ { storage_enforcement_date: root_ancestor.storage_enforcement_date, used_storage: storage_counter(root_ancestor.root_storage_statistics&.storage_size || 0), strong_start: "<strong>".html_safe, strong_end: "</strong>".html_safe, namespace_type: root_ancestor.type },
variant: 'warning',
- callouts_path: namespace.user_namespace? ? callouts_path : group_callouts_path,
- callouts_feature_name: storage_enforcement_banner_user_callouts_feature_name(namespace),
- learn_more_link: link_to(_('Learn more.'), help_page_path('/'), rel: 'noopener noreferrer', target: '_blank') # TBD: https://gitlab.com/gitlab-org/gitlab/-/issues/350632
+ callouts_path: root_ancestor.user_namespace? ? callouts_path : group_callouts_path,
+ callouts_feature_name: storage_enforcement_banner_user_callouts_feature_name(root_ancestor),
+ learn_more_link: link_to(_('Learn more.'), help_page_path('/'), rel: 'noopener noreferrer', target: '_blank')
}
end
@@ -63,8 +65,16 @@ module StorageHelper
if namespace.user_namespace?
current_user.dismissed_callout?(feature_name: storage_enforcement_banner_user_callouts_feature_name(namespace))
else
- current_user.dismissed_callout_for_group?(feature_name: storage_enforcement_banner_user_callouts_feature_name(namespace),
- group: namespace)
+ current_user.dismissed_callout_for_group?(
+ feature_name: storage_enforcement_banner_user_callouts_feature_name(namespace),
+ group: namespace
+ )
end
end
+
+ def future_enforcement_date?(namespace)
+ return true if ::Feature.enabled?(:namespace_storage_limit_bypass_date_check, namespace)
+
+ namespace.storage_enforcement_date.present? && namespace.storage_enforcement_date >= Date.today
+ end
end
diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb
index 82847534d8e..5ab70115f34 100644
--- a/app/helpers/system_note_helper.rb
+++ b/app/helpers/system_note_helper.rb
@@ -6,7 +6,7 @@ module SystemNoteHelper
'unapproved' => 'unapproval',
'cherry_pick' => 'cherry-pick-commit',
'commit' => 'commit',
- 'description' => 'pencil-square',
+ 'description' => 'pencil',
'merge' => 'git-merge',
'merged' => 'git-merge',
'opened' => 'issues',
@@ -14,7 +14,7 @@ module SystemNoteHelper
'time_tracking' => 'timer',
'assignee' => 'user',
'reviewer' => 'user',
- 'title' => 'pencil-square',
+ 'title' => 'pencil',
'task' => 'task-done',
'label' => 'label',
'cross_reference' => 'comment-dots',
@@ -24,7 +24,7 @@ module SystemNoteHelper
'milestone' => 'clock',
'discussion' => 'comment',
'moved' => 'arrow-right',
- 'outdated' => 'pencil-square',
+ 'outdated' => 'pencil',
'pinned_embed' => 'thumbtack',
'duplicate' => 'duplicate',
'locked' => 'lock',
@@ -40,7 +40,7 @@ module SystemNoteHelper
'new_alert_added' => 'warning',
'severity' => 'information-o',
'cloned' => 'documents',
- 'issue_type' => 'pencil-square',
+ 'issue_type' => 'pencil',
'attention_requested' => 'user',
'attention_request_removed' => 'user',
'contact' => 'users',
diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb
index 8216144c04c..665f7e0ddce 100644
--- a/app/helpers/tags_helper.rb
+++ b/app/helpers/tags_helper.rb
@@ -22,13 +22,4 @@ module TagsHelper
text.html_safe
end
-
- def delete_tag_modal_attributes(tag_name)
- {
- title: s_('TagsPage|Delete tag'),
- message: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag_name },
- okVariant: 'danger',
- okTitle: s_('TagsPage|Delete tag')
- }.to_json
- end
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index d3cc922423d..8529959f73c 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -100,17 +100,22 @@ module TodosHelper
def todo_target_state_pill(todo)
return unless show_todo_state?(todo)
- type =
- case todo.target
- when MergeRequest
- 'mr'
- when Issue
- 'issue'
- when AlertManagement::Alert
- 'alert'
+ state = todo.target.state.to_s
+
+ case todo.target
+ when MergeRequest
+ if state == 'closed'
+ background_class = 'gl-bg-red-500'
+ elsif state == 'merged'
+ background_class = 'gl-bg-blue-500'
end
+ when Issue
+ background_class = 'gl-bg-blue-500' if state == 'closed'
+ when AlertManagement::Alert
+ background_class = 'gl-bg-blue-500' if state == 'resolved'
+ end
- tag.span class: "gl-my-0 gl-px-2 status-box status-box-#{type}-#{todo.target.state.to_s.dasherize}" do
+ tag.span class: "gl-my-0 gl-px-2 status-box #{background_class}" do
todo.target.state.to_s.capitalize
end
end
diff --git a/app/helpers/tooling/visual_review_helper.rb b/app/helpers/tooling/visual_review_helper.rb
new file mode 100644
index 00000000000..da6eb3ec434
--- /dev/null
+++ b/app/helpers/tooling/visual_review_helper.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Tooling
+ module VisualReviewHelper
+ # Since we only use the visual review toolbar for the gitlab project,
+ # we can hardcode the project ID and project path for now.
+ #
+ # If we need to extend the review apps to other applications in the future,
+ # we should create REVIEW_APPS_PROJECT_ID and REVIEW_APPS_PROJECT_PATH
+ # environment variables (mapped to CI_PROJECT_ID and CI_PROJECT_PATH respectively),
+ # as well as setting `data-require-auth` according to the project visibility.
+ GITLAB_INSTANCE_URL = 'https://gitlab.com'
+ GITLAB_ORG_GITLAB_PROJECT_ID = '278964'
+ GITLAB_ORG_GITLAB_PROJECT_PATH = 'gitlab-org/gitlab'
+
+ def visual_review_toolbar_options
+ { 'data-merge-request-id': "#{ENV['REVIEW_APPS_MERGE_REQUEST_IID']}",
+ 'data-mr-url': "#{GITLAB_INSTANCE_URL}",
+ 'data-project-id': "#{GITLAB_ORG_GITLAB_PROJECT_ID}",
+ 'data-project-path': "#{GITLAB_ORG_GITLAB_PROJECT_PATH}",
+ 'data-require-auth': false,
+ 'id': 'review-app-toolbar-script',
+ 'src': 'https://gitlab.com/assets/webpack/visual_review_toolbar.js' }
+ end
+ end
+end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index fd460d71867..e46aa6a446c 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -140,7 +140,7 @@ module UsersHelper
messageHtml: message,
actionPrimary: {
text: s_('AdminUsers|Confirm user'),
- attributes: [{ variant: 'info', 'data-qa-selector': 'confirm_user_confirm_button' }]
+ attributes: [{ variant: 'confirm', 'data-qa-selector': 'confirm_user_confirm_button' }]
},
actionSecondary: {
text: _('Cancel'),
diff --git a/app/helpers/work_items_helper.rb b/app/helpers/work_items_helper.rb
new file mode 100644
index 00000000000..2c61fc20ca8
--- /dev/null
+++ b/app/helpers/work_items_helper.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module WorkItemsHelper
+ def work_items_index_data(project)
+ {
+ full_path: project.full_path,
+ issues_list_path: project_issues_path(project)
+ }
+ end
+end
diff --git a/app/mailers/emails/admin_notification.rb b/app/mailers/emails/admin_notification.rb
index e11f06d8fc9..f44dd448a35 100644
--- a/app/mailers/emails/admin_notification.rb
+++ b/app/mailers/emails/admin_notification.rb
@@ -15,5 +15,18 @@ module Emails
email = user.notification_email_or_default
mail to: email, subject: "Unsubscribed from GitLab administrator notifications"
end
+
+ def user_auto_banned_email(admin_id, user_id, max_project_downloads:, within_seconds:)
+ admin = User.find(admin_id)
+ @user = User.find(user_id)
+ @max_project_downloads = max_project_downloads
+ @within_minutes = within_seconds / 60
+
+ Gitlab::I18n.with_locale(admin.preferred_language) do
+ email_with_layout(
+ to: admin.notification_email_or_default,
+ subject: subject(_("We've detected unusual activity")))
+ end
+ end
end
end
diff --git a/app/mailers/emails/auto_devops.rb b/app/mailers/emails/auto_devops.rb
index 9705a3052d4..d10ba40d225 100644
--- a/app/mailers/emails/auto_devops.rb
+++ b/app/mailers/emails/auto_devops.rb
@@ -8,11 +8,9 @@ module Emails
add_project_headers
- mail(to: recipient,
- subject: auto_devops_disabled_subject(@project.name)) do |format|
- format.html { render layout: 'mailer' }
- format.text { render layout: 'mailer' }
- end
+ email_with_layout(
+ to: recipient,
+ subject: auto_devops_disabled_subject(@project.name))
end
private
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index bbc4be3b324..6a5680c080b 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -94,10 +94,9 @@ module Emails
@project = Project.find(project_id)
@results = results
- mail(to: @user.notification_email_for(@project.group), subject: subject('Imported issues')) do |format|
- format.html { render layout: 'mailer' }
- format.text { render layout: 'mailer' }
- end
+ email_with_layout(
+ to: @user.notification_email_for(@project.group),
+ subject: subject('Imported issues'))
end
def issues_csv_email(user, project, csv_data, export_status)
@@ -110,10 +109,9 @@ module Emails
filename = "#{project.full_path.parameterize}_issues_#{Date.today.iso8601}.csv"
attachments[filename] = { content: csv_data, mime_type: 'text/csv' }
- mail(to: user.notification_email_for(@project.group), subject: subject("Exported issues")) do |format|
- format.html { render layout: 'mailer' }
- format.text { render layout: 'mailer' }
- end
+ email_with_layout(
+ to: user.notification_email_for(@project.group),
+ subject: subject("Exported issues"))
end
private
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index ef2220751bf..c885e41671c 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -21,7 +21,7 @@ module Emails
user = User.find(recipient_id)
- member_email_with_layout(
+ email_with_layout(
to: user.notification_email_for(notification_group),
subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}"))
end
@@ -32,7 +32,7 @@ module Emails
return unless member_exists?
- member_email_with_layout(
+ email_with_layout(
to: member.user.notification_email_for(notification_group),
subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was granted"))
end
@@ -47,7 +47,7 @@ module Emails
human_name = @source_hidden ? 'Hidden' : member_source.human_name
- member_email_with_layout(
+ email_with_layout(
to: user.notification_email_for(notification_group),
subject: subject("Access to the #{human_name} #{member_source.model_name.singular} was denied"))
end
@@ -83,7 +83,7 @@ module Emails
subject_line = subjects[reminder_index] % { inviter: member.created_by.name }
- member_email_with_layout(
+ email_with_layout(
layout: 'unknown_user_mailer',
to: member.invite_email,
subject: subject(subject_line)
@@ -97,7 +97,7 @@ module Emails
return unless member_exists?
return unless member.created_by
- member_email_with_layout(
+ email_with_layout(
to: member.created_by.notification_email_for(notification_group),
subject: subject('Invitation accepted'))
end
@@ -111,7 +111,7 @@ module Emails
user = User.find(created_by_id)
- member_email_with_layout(
+ email_with_layout(
to: user.notification_email_for(notification_group),
subject: subject('Invitation declined'))
end
@@ -128,7 +128,7 @@ module Emails
_('Group membership expiration date removed')
end
- member_email_with_layout(
+ email_with_layout(
to: member.user.notification_email_for(notification_group),
subject: subject(subject))
end
@@ -176,13 +176,6 @@ module Emails
def member_source_class
@member_source_type.classify.constantize
end
-
- def member_email_with_layout(to:, subject:, layout: 'mailer')
- mail(to: to, subject: subject) do |format|
- format.html { render layout: layout }
- format.text { render layout: layout }
- end
- end
end
end
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 5cbc3c9ef9c..83d37a365de 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -149,10 +149,9 @@ module Emails
filename = "#{project.full_path.parameterize}_merge_requests_#{Date.current.iso8601}.csv"
attachments[filename] = { content: csv_data, mime_type: 'text/csv' }
- mail(to: user.notification_email_for(@project.group), subject: subject("Exported merge requests")) do |format|
- format.html { render layout: 'mailer' }
- format.text { render layout: 'mailer' }
- end
+ email_with_layout(
+ to: user.notification_email_for(@project.group),
+ subject: subject("Exported merge requests"))
end
def approved_merge_request_email(recipient_id, merge_request_id, approved_by_user_id, reason = nil)
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
index 5363ad63771..463f5d3943a 100644
--- a/app/mailers/emails/pipelines.rb
+++ b/app/mailers/emails/pipelines.rb
@@ -30,11 +30,9 @@ module Emails
add_headers
- mail(to: recipient,
- subject: subject(pipeline_subject(status))) do |format|
- format.html { render layout: 'mailer' }
- format.text { render layout: 'mailer' }
- end
+ email_with_layout(
+ to: recipient,
+ subject: subject(pipeline_subject(status)))
end
def add_headers
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index 31fcc7c15cb..81f082b9680 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -13,7 +13,7 @@ module Emails
@user = user
@recipient = recipient
- profile_email_with_layout(
+ email_with_layout(
to: recipient.notification_email_or_default,
subject: subject(_("GitLab Account Request")))
end
@@ -21,7 +21,7 @@ module Emails
def user_admin_rejection_email(name, email)
@name = name
- profile_email_with_layout(
+ email_with_layout(
to: email,
subject: subject(_("GitLab account request rejected")))
end
@@ -29,7 +29,7 @@ module Emails
def user_deactivated_email(name, email)
@name = name
- profile_email_with_layout(
+ email_with_layout(
to: email,
subject: subject(_('Your account has been deactivated')))
end
@@ -125,7 +125,7 @@ module Emails
@target_url = edit_profile_password_url
Gitlab::I18n.with_locale(@user.preferred_language) do
- profile_email_with_layout(
+ email_with_layout(
to: @user.notification_email_or_default,
subject: subject(_("%{host} sign-in from new location") % { host: Gitlab.config.gitlab.host }))
end
@@ -151,15 +151,6 @@ module Emails
mail(to: @user.notification_email_or_default, subject: subject(_("New email address added")))
end
end
-
- private
-
- def profile_email_with_layout(to:, subject:, layout: 'mailer')
- mail(to: to, subject: subject) do |format|
- format.html { render layout: layout }
- format.text { render layout: layout }
- end
- end
end
end
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index efc6ce163c0..ed3fa28b15f 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -75,11 +75,9 @@ module Emails
subject_text = "Action required: Project #{project.name} is scheduled to be deleted on " \
"#{deletion_date} due to inactivity"
- mail(to: user.notification_email_for(project.group),
- subject: subject(subject_text)) do |format|
- format.html { render layout: 'mailer' }
- format.text { render layout: 'mailer' }
- end
+ email_with_layout(
+ to: user.notification_email_for(project.group),
+ subject: subject(subject_text))
end
private
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 03b70fffde1..b70ce1d3655 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -222,6 +222,13 @@ class Notify < ApplicationMailer
headers['List-Unsubscribe'] = list_unsubscribe_methods.map { |e| "<#{e}>" }.join(',')
@unsubscribe_url = unsubscribe_sent_notification_url(@sent_notification)
end
+
+ def email_with_layout(to:, subject:, layout: 'mailer')
+ mail(to: to, subject: subject) do |format|
+ format.html { render layout: layout }
+ format.text { render layout: layout }
+ end
+ end
end
Notify.prepend_mod_with('Notify')
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index 60d59465165..61456ef79c8 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -205,6 +205,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.inactive_project_deletion_warning_email(project, user, '2022-04-22').message
end
+ def user_auto_banned_email
+ ::Notify.user_auto_banned_email(user.id, user.id, max_project_downloads: 5, within_seconds: 600).message
+ end
+
private
def project
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 6afd8875ad3..6acdc02c799 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -382,6 +382,9 @@ class ApplicationSetting < ApplicationRecord
allow_nil: false,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :container_registry_pre_import_tags_rate,
+ allow_nil: false,
+ numericality: { greater_than_or_equal_to: 0 }
validates :container_registry_import_target_plan, presence: true
validates :container_registry_import_created_before, presence: true
@@ -502,6 +505,10 @@ class ApplicationSetting < ApplicationRecord
length: { maximum: 255, message: _('is too long (maximum is %{count} characters)') },
allow_blank: true
+ validates :jira_connect_application_key,
+ length: { maximum: 255, message: _('is too long (maximum is %{count} characters)') },
+ allow_blank: true
+
with_options(presence: true, numericality: { only_integer: true, greater_than: 0 }) do
validates :throttle_unauthenticated_api_requests_per_period
validates :throttle_unauthenticated_api_period_in_seconds
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index a54dc4f691d..a89ea05fb62 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -102,6 +102,7 @@ module ApplicationSettingImplementation
import_sources: Settings.gitlab['import_sources'],
invisible_captcha_enabled: false,
issues_create_limit: 300,
+ jira_connect_application_key: nil,
local_markdown_version: 0,
login_recaptcha_protection_enabled: false,
mailgun_signing_key: nil,
@@ -224,6 +225,7 @@ module ApplicationSettingImplementation
container_registry_import_max_retries: 3,
container_registry_import_start_max_retries: 50,
container_registry_import_max_step_duration: 5.minutes,
+ container_registry_pre_import_tags_rate: 0.5,
container_registry_pre_import_timeout: 30.minutes,
container_registry_import_timeout: 10.minutes,
container_registry_import_target_plan: 'free',
@@ -508,8 +510,35 @@ module ApplicationSettingImplementation
'https://sandbox-prod.gitlab-static.net'
end
+ def ensure_key_restrictions!
+ return if Gitlab::Database.read_only?
+ return unless Gitlab::FIPS.enabled?
+
+ Gitlab::SSHPublicKey.supported_types.each do |key_type|
+ set_max_key_restriction!(key_type)
+ end
+ end
+
private
+ def set_max_key_restriction!(key_type)
+ attr_name = "#{key_type}_key_restriction"
+ current = self.attributes[attr_name].to_i
+
+ return if current == KeyRestrictionValidator::FORBIDDEN
+
+ min_size = self.class.default_min_key_size(key_type)
+
+ new_value =
+ if min_size == KeyRestrictionValidator::FORBIDDEN
+ min_size
+ else
+ [min_size, current].max
+ end
+
+ self.assign_attributes({ attr_name => new_value })
+ end
+
def separate_allowlists(string_array)
string_array.reduce([[], []]) do |(ip_allowlist, domain_allowlist), string|
address, port = parse_addr_and_port(string)
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index 22e5436dc5c..5430575ace7 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -70,7 +70,7 @@ class AwardEmoji < ApplicationRecord
def expire_cache
awardable.try(:bump_updated_at)
- awardable.try(:expire_etag_cache)
+ awardable.expire_etag_cache if awardable.is_a?(Note)
awardable.try(:update_upvotes_count) if upvote?
end
end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index dee533944e9..cad2fafe640 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -99,18 +99,7 @@ class BulkImports::Entity < ApplicationRecord
end
def pipeline_exists?(name)
- pipelines.any? { |_, pipeline| pipeline.to_s == name.to_s }
- end
-
- def create_pipeline_trackers!
- self.class.transaction do
- pipelines.each do |stage, pipeline|
- trackers.create!(
- stage: stage,
- pipeline_name: pipeline
- )
- end
- end
+ pipelines.any? { _1[:pipeline].to_s == name.to_s }
end
def entity_type
diff --git a/app/models/bulk_imports/export_status.rb b/app/models/bulk_imports/export_status.rb
index a9750a76987..4fea62edb2a 100644
--- a/app/models/bulk_imports/export_status.rb
+++ b/app/models/bulk_imports/export_status.rb
@@ -13,11 +13,15 @@ module BulkImports
end
def started?
- export_status['status'] == Export::STARTED
+ !empty? && export_status['status'] == Export::STARTED
end
def failed?
- export_status['status'] == Export::FAILED
+ !empty? && export_status['status'] == Export::FAILED
+ end
+
+ def empty?
+ export_status.nil?
end
def error
@@ -30,14 +34,7 @@ module BulkImports
def export_status
strong_memoize(:export_status) do
- status = fetch_export_status
-
- relation_export_status = status&.find { |item| item['relation'] == relation }
-
- # Consider empty response as failed export
- raise StandardError, 'Empty relation export status' unless relation_export_status&.present?
-
- relation_export_status
+ fetch_export_status&.find { |item| item['relation'] == relation }
end
rescue StandardError => e
{ 'status' => Export::FAILED, 'error' => e.message }
diff --git a/app/models/bulk_imports/file_transfer/project_config.rb b/app/models/bulk_imports/file_transfer/project_config.rb
index 38884df9fcf..8d4c68f7b5a 100644
--- a/app/models/bulk_imports/file_transfer/project_config.rb
+++ b/app/models/bulk_imports/file_transfer/project_config.rb
@@ -9,6 +9,8 @@ module BulkImports
).freeze
LFS_OBJECTS_RELATION = 'lfs_objects'
+ REPOSITORY_BUNDLE_RELATION = 'repository'
+ DESIGN_BUNDLE_RELATION = 'design'
def import_export_yaml
::Gitlab::ImportExport.config_file
@@ -19,7 +21,12 @@ module BulkImports
end
def file_relations
- [UPLOADS_RELATION, LFS_OBJECTS_RELATION]
+ [
+ UPLOADS_RELATION,
+ LFS_OBJECTS_RELATION,
+ REPOSITORY_BUNDLE_RELATION,
+ DESIGN_BUNDLE_RELATION
+ ]
end
end
end
diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb
index a994cc3f8ce..fa38b7617d2 100644
--- a/app/models/bulk_imports/tracker.rb
+++ b/app/models/bulk_imports/tracker.rb
@@ -18,6 +18,8 @@ class BulkImports::Tracker < ApplicationRecord
validates :stage, presence: true
+ delegate :file_extraction_pipeline?, to: :pipeline_class
+
DEFAULT_PAGE_SIZE = 500
scope :next_pipeline_trackers_for, -> (entity_id) {
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index a06b920342c..13af5b1f8d1 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -215,14 +215,10 @@ module Ci
end
def downstream_variables
- if ::Feature.enabled?(:ci_trigger_forward_variables, project)
- calculate_downstream_variables
- .reverse # variables priority
- .uniq { |var| var[:key] } # only one variable key to pass
- .reverse
- else
- legacy_downstream_variables
- end
+ calculate_downstream_variables
+ .reverse # variables priority
+ .uniq { |var| var[:key] } # only one variable key to pass
+ .reverse
end
def target_revision_ref
@@ -268,16 +264,6 @@ module Ci
}
end
- def legacy_downstream_variables
- variables = scoped_variables.concat(pipeline.persisted_variables)
-
- variables.to_runner_variables.yield_self do |all_variables|
- yaml_variables.to_a.map do |hash|
- { key: hash[:key], value: ::ExpandVariables.expand(hash[:value], all_variables) }
- end
- end
- end
-
def calculate_downstream_variables
expand_variables = scoped_variables
.concat(pipeline.persisted_variables)
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index eea8086d71d..e35198ba31f 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -137,13 +137,14 @@ module Ci
where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
end
- scope :with_reports, ->(reports_scope) do
- with_existing_job_artifacts(reports_scope)
+ scope :with_artifacts, ->(artifact_scope) do
+ with_existing_job_artifacts(artifact_scope)
.eager_load_job_artifacts
end
scope :eager_load_job_artifacts, -> { includes(:job_artifacts) }
scope :eager_load_tags, -> { includes(:tags) }
+ scope :eager_load_for_archiving_trace, -> { includes(:project, :pending_state) }
scope :eager_load_everything, -> do
includes(
@@ -424,10 +425,18 @@ module Ci
pipeline.manual_actions.reject { |action| action.name == self.name }
end
+ def environment_manual_actions
+ pipeline.manual_actions.filter { |action| action.expanded_environment_name == self.expanded_environment_name }
+ end
+
def other_scheduled_actions
pipeline.scheduled_actions.reject { |action| action.name == self.name }
end
+ def environment_scheduled_actions
+ pipeline.scheduled_actions.filter { |action| action.expanded_environment_name == self.expanded_environment_name }
+ end
+
def pages_generator?
Gitlab.config.pages.enabled &&
self.name == 'pages'
@@ -559,6 +568,10 @@ module Ci
options&.dig(:environment, :on_stop)
end
+ def stop_action_successful?
+ success?
+ end
+
##
# All variables, including persisted environment variables.
#
@@ -673,7 +686,7 @@ module Ci
end
def has_live_trace?
- trace.live_trace_exist?
+ trace.live?
end
def has_archived_trace?
@@ -795,6 +808,7 @@ module Ci
def execute_hooks
return unless project
+ return if user&.blocked?
project.execute_hooks(build_data.dup, :job_hooks) if project.has_active_hooks?(:job_hooks)
project.execute_integrations(build_data.dup, :job_hooks) if project.has_active_integrations?(:job_hooks)
@@ -826,12 +840,26 @@ module Ci
end
def erase_erasable_artifacts!
+ if project.refreshing_build_artifacts_size?
+ Gitlab::ProjectStatsRefreshConflictsLogger.warn_artifact_deletion_during_stats_refresh(
+ method: 'Ci::Build#erase_erasable_artifacts!',
+ project_id: project_id
+ )
+ end
+
job_artifacts.erasable.destroy_all # rubocop: disable Cop/DestroyAll
end
def erase(opts = {})
return false unless erasable?
+ if project.refreshing_build_artifacts_size?
+ Gitlab::ProjectStatsRefreshConflictsLogger.warn_artifact_deletion_during_stats_refresh(
+ method: 'Ci::Build#erase',
+ project_id: project_id
+ )
+ end
+
job_artifacts.destroy_all # rubocop: disable Cop/DestroyAll
erase_trace!
update_erased!(opts[:erased_by])
@@ -983,7 +1011,7 @@ module Ci
end
def collect_test_reports!(test_reports)
- test_reports.get_suite(group_name).tap do |test_suite|
+ test_reports.get_suite(test_suite_name).tap do |test_suite|
each_report(Ci::JobArtifact::TEST_REPORT_FILE_TYPES) do |file_type, blob|
Gitlab::Ci::Parsers.fabricate!(file_type).parse!(
blob,
@@ -1002,19 +1030,6 @@ module Ci
accessibility_report
end
- def collect_coverage_reports!(coverage_report)
- each_report(Ci::JobArtifact::COVERAGE_REPORT_FILE_TYPES) do |file_type, blob|
- Gitlab::Ci::Parsers.fabricate!(file_type).parse!(
- blob,
- coverage_report,
- project_path: project.full_path,
- worktree_paths: pipeline.all_worktree_paths
- )
- end
-
- coverage_report
- end
-
def collect_codequality_reports!(codequality_report)
each_report(Ci::JobArtifact::CODEQUALITY_REPORT_FILE_TYPES) do |file_type, blob|
Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, codequality_report)
@@ -1032,7 +1047,7 @@ module Ci
end
def report_artifacts
- job_artifacts.with_reports
+ job_artifacts.all_reports
end
# Virtual deployment status depending on the environment status.
@@ -1056,6 +1071,8 @@ module Ci
all_runtime_metadata.delete_all
end
+ deployment&.sync_status_with(self)
+
Gitlab::AppLogger.info(
message: 'Build doomed',
class: self.class.name,
@@ -1145,6 +1162,14 @@ module Ci
Gitlab::Utils::UsageData.track_usage_event('ci_users_executing_deployment_job', user_id) if user_id.present? && count_user_deployment?
end
+ def each_report(report_types)
+ job_artifacts_for_types(report_types).each do |report_artifact|
+ report_artifact.each_blob do |blob|
+ yield report_artifact.file_type, blob, report_artifact
+ end
+ end
+ end
+
protected
def run_status_commit_hooks!
@@ -1155,6 +1180,18 @@ module Ci
private
+ def test_suite_name
+ if matrix_build?
+ name
+ else
+ group_name
+ end
+ end
+
+ def matrix_build?
+ options.dig(:parallel, :matrix).present?
+ end
+
def stick_build_if_status_changed
return unless saved_change_to_status?
return unless running?
@@ -1184,14 +1221,6 @@ module Ci
end
end
- def each_report(report_types)
- job_artifacts_for_types(report_types).each do |report_artifact|
- report_artifact.each_blob do |blob|
- yield report_artifact.file_type, blob, report_artifact
- end
- end
- end
-
def job_artifacts_for_types(report_types)
# Use select to leverage cached associations and avoid N+1 queries
job_artifacts.select { |artifact| artifact.file_type.in?(report_types) }
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index c831ef12501..81943cfa651 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -124,10 +124,10 @@ module Ci
# We will start using this column once we complete https://gitlab.com/gitlab-org/gitlab/-/issues/285597
ignore_column :original_filename, remove_with: '14.7', remove_after: '2022-11-22'
- mount_file_store_uploader JobArtifactUploader
+ mount_file_store_uploader JobArtifactUploader, skip_store_file: true
- skip_callback :save, :after, :store_file!, if: :store_after_commit?
- after_commit :store_file_after_commit!, on: [:create, :update], if: :store_after_commit?
+ after_save :store_file_in_transaction!, unless: :store_after_commit?
+ after_commit :store_file_after_transaction!, on: [:create, :update], if: :store_after_commit?
validates :file_format, presence: true, unless: :trace?, on: :create
validate :validate_file_format!, unless: :trace?, on: :create
@@ -139,6 +139,10 @@ module Ci
scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) }
scope :for_job_ids, ->(job_ids) { where(job_id: job_ids) }
scope :for_job_name, ->(name) { joins(:job).where(ci_builds: { name: name }) }
+ scope :created_at_before, ->(time) { where(arel_table[:created_at].lteq(time)) }
+ scope :id_before, ->(id) { where(arel_table[:id].lteq(id)) }
+ scope :id_after, ->(id) { where(arel_table[:id].gt(id)) }
+ scope :ordered_by_id, -> { order(:id) }
scope :with_job, -> { joins(:job).includes(:job) }
@@ -148,7 +152,7 @@ module Ci
where(file_type: types)
end
- scope :with_reports, -> do
+ scope :all_reports, -> do
with_file_types(REPORT_TYPES.keys.map(&:to_s))
end
@@ -187,7 +191,7 @@ module Ci
scope :downloadable, -> { where(file_type: DOWNLOADABLE_TYPES) }
scope :unlocked, -> { joins(job: :pipeline).merge(::Ci::Pipeline.unlocked) }
scope :order_expired_asc, -> { order(expire_at: :asc) }
- scope :with_destroy_preloads, -> { includes(project: [:route, :statistics]) }
+ scope :with_destroy_preloads, -> { includes(project: [:route, :statistics, :build_artifacts_size_refresh]) }
scope :for_project, ->(project) { where(project_id: project) }
scope :created_in_time_range, ->(from: nil, to: nil) { where(created_at: from..to) }
@@ -358,11 +362,24 @@ module Ci
private
- def store_file_after_commit!
- return unless previous_changes.key?(:file)
+ def store_file_in_transaction!
+ store_file_now! if saved_change_to_file?
- store_file!
- update_file_store
+ file_stored_in_transaction_hooks
+ end
+
+ def store_file_after_transaction!
+ store_file_now! if previous_changes.key?(:file)
+
+ file_stored_after_transaction_hooks
+ end
+
+ # method overriden in EE
+ def file_stored_after_transaction_hooks
+ end
+
+ # method overriden in EE
+ def file_stored_in_transaction_hooks
end
def set_size
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index c10069382f2..5d316906bd3 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -81,6 +81,7 @@ module Ci
has_many :downloadable_artifacts, -> do
not_expired.or(where_exists(::Ci::Pipeline.artifacts_locked.where('ci_pipelines.id = ci_builds.commit_id'))).downloadable.with_job
end, through: :latest_builds, source: :job_artifacts
+ has_many :latest_successful_builds, -> { latest.success.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build'
has_many :messages, class_name: 'Ci::PipelineMessage', inverse_of: :pipeline
@@ -239,7 +240,9 @@ module Ci
next if transition.loopback?
pipeline.run_after_commit do
- PipelineHooksWorker.perform_async(pipeline.id)
+ unless pipeline.user&.blocked?
+ PipelineHooksWorker.perform_async(pipeline.id)
+ end
if pipeline.project.jira_subscription_exists?
# Passing the seq-id ensures this is idempotent
@@ -296,7 +299,12 @@ module Ci
ref_status = pipeline.ci_ref&.update_status_by!(pipeline)
pipeline.run_after_commit do
- PipelineNotificationWorker.perform_async(pipeline.id, ref_status: ref_status)
+ # We don't send notifications for a pipeline dropped due to the
+ # user been blocked.
+ unless pipeline.user&.blocked?
+ PipelineNotificationWorker
+ .perform_async(pipeline.id, ref_status: ref_status)
+ end
end
end
@@ -327,14 +335,14 @@ module Ci
scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) }
scope :created_before_id, -> (id) { where('ci_pipelines.id < ?', id) }
scope :before_pipeline, -> (pipeline) { created_before_id(pipeline.id).outside_pipeline_family(pipeline) }
- scope :with_pipeline_source, -> (source) { where(source: source)}
+ scope :with_pipeline_source, -> (source) { where(source: source) }
scope :outside_pipeline_family, ->(pipeline) do
where.not(id: pipeline.same_family_pipeline_ids)
end
scope :with_reports, -> (reports_scope) do
- where('EXISTS (?)', ::Ci::Build.latest.with_reports(reports_scope).where('ci_pipelines.id=ci_builds.commit_id').select(1))
+ where('EXISTS (?)', ::Ci::Build.latest.with_artifacts(reports_scope).where('ci_pipelines.id=ci_builds.commit_id').select(1))
end
scope :with_only_interruptible_builds, -> do
@@ -688,7 +696,7 @@ module Ci
def latest_report_artifacts
::Gitlab::SafeRequestStore.fetch("pipeline:#{self.id}:latest_report_artifacts") do
::Ci::JobArtifact.where(
- id: job_artifacts.with_reports
+ id: job_artifacts.all_reports
.select('max(ci_job_artifacts.id) as id')
.group(:file_type)
)
@@ -1049,12 +1057,16 @@ module Ci
@latest_builds_with_artifacts ||= builds.latest.with_artifacts_not_expired.to_a
end
- def latest_report_builds(reports_scope = ::Ci::JobArtifact.with_reports)
- builds.latest.with_reports(reports_scope)
+ def latest_report_builds(reports_scope = ::Ci::JobArtifact.all_reports)
+ builds.latest.with_artifacts(reports_scope)
end
def latest_test_report_builds
- latest_report_builds(Ci::JobArtifact.test_reports).preload(:project)
+ latest_report_builds(Ci::JobArtifact.test_reports).preload(:project, :metadata)
+ end
+
+ def latest_report_builds_in_self_and_descendants(reports_scope = ::Ci::JobArtifact.all_reports)
+ builds_in_self_and_descendants.with_artifacts(reports_scope)
end
def builds_with_coverage
@@ -1073,10 +1085,6 @@ module Ci
pipeline_artifacts&.report_exists?(:code_coverage)
end
- def can_generate_coverage_reports?
- has_reports?(Ci::JobArtifact.coverage_reports)
- end
-
def has_codequality_mr_diff_report?
pipeline_artifacts&.report_exists?(:code_quality_mr_diff)
end
@@ -1107,14 +1115,6 @@ module Ci
end
end
- def coverage_reports
- Gitlab::Ci::Reports::CoverageReports.new.tap do |coverage_reports|
- latest_report_builds(Ci::JobArtifact.coverage_reports).includes(:project).find_each do |build|
- build.collect_coverage_reports!(coverage_reports)
- end
- end
- end
-
def codequality_reports
Gitlab::Ci::Reports::CodequalityReports.new.tap do |codequality_reports|
latest_report_builds(Ci::JobArtifact.codequality_reports).each do |build|
@@ -1308,8 +1308,8 @@ module Ci
end
def has_expired_test_reports?
- strong_memoize(:artifacts_expired) do
- !has_reports?(::Ci::JobArtifact.test_reports.not_expired)
+ strong_memoize(:has_expired_test_reports) do
+ has_reports?(::Ci::JobArtifact.test_reports.expired)
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 7a1d52f5aea..61194c9b7d1 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -77,6 +77,7 @@ module Ci
has_one :last_build, -> { order('id DESC') }, class_name: 'Ci::Build'
before_save :ensure_token
+ before_save :update_semver, if: -> { version_changed? }
scope :active, -> (value = true) { where(active: value) }
scope :paused, -> { active(false) }
@@ -429,6 +430,7 @@ module Ci
values = values&.slice(:version, :revision, :platform, :architecture, :ip_address, :config, :executor) || {}
values[:contacted_at] = Time.current
values[:executor_type] = EXECUTOR_NAME_TO_TYPES.fetch(values.delete(:executor), :unknown)
+ values[:semver] = semver_from_version(values[:version])
cache_attributes(values)
@@ -449,6 +451,16 @@ module Ci
read_attribute(:contacted_at)
end
+ def semver_from_version(version)
+ parsed_runner_version = ::Gitlab::VersionInfo.parse(version)
+
+ parsed_runner_version.valid? ? parsed_runner_version.to_s : nil
+ end
+
+ def update_semver
+ self.semver = semver_from_version(self.version)
+ end
+
def namespace_ids
strong_memoize(:namespace_ids) do
runner_namespaces.pluck(:namespace_id).compact
diff --git a/app/models/ci/secure_file.rb b/app/models/ci/secure_file.rb
index 9c82e106d6e..078b05ff779 100644
--- a/app/models/ci/secure_file.rb
+++ b/app/models/ci/secure_file.rb
@@ -23,6 +23,8 @@ module Ci
after_initialize :generate_key_data
before_validation :assign_checksum
+ scope :order_by_created_at, -> { order(created_at: :desc) }
+
default_value_for(:file_store) { Ci::SecureFileUploader.default_store }
mount_file_store_uploader Ci::SecureFileUploader
diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb
index f78caf710a6..2df504cd3de 100644
--- a/app/models/ci/sources/pipeline.rb
+++ b/app/models/ci/sources/pipeline.rb
@@ -7,10 +7,10 @@ module Ci
self.table_name = "ci_sources_pipelines"
- belongs_to :project, class_name: "Project"
+ belongs_to :project, class_name: "::Project"
belongs_to :pipeline, class_name: "Ci::Pipeline", inverse_of: :source_pipeline
- belongs_to :source_project, class_name: "Project", foreign_key: :source_project_id
+ belongs_to :source_project, class_name: "::Project", foreign_key: :source_project_id
belongs_to :source_job, class_name: "CommitStatus", foreign_key: :source_job_id
belongs_to :source_bridge, class_name: "Ci::Bridge", foreign_key: :source_job_id
belongs_to :source_pipeline, class_name: "Ci::Pipeline", foreign_key: :source_pipeline_id
diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb
index 79fc2b58237..fb12ce7d292 100644
--- a/app/models/clusters/agent.rb
+++ b/app/models/clusters/agent.rb
@@ -10,8 +10,7 @@ module Clusters
belongs_to :created_by_user, class_name: 'User', optional: true
belongs_to :project, class_name: '::Project' # Otherwise, it will load ::Clusters::Project
- has_many :agent_tokens, class_name: 'Clusters::AgentToken'
- has_many :last_used_agent_tokens, -> { order_last_used_at_desc }, class_name: 'Clusters::AgentToken', inverse_of: :agent
+ has_many :agent_tokens, -> { order_last_used_at_desc }, class_name: 'Clusters::AgentToken', inverse_of: :agent
has_many :group_authorizations, class_name: 'Clusters::Agents::GroupAuthorization'
has_many :authorized_groups, class_name: '::Group', through: :group_authorizations, source: :group
@@ -23,6 +22,7 @@ module Clusters
scope :ordered_by_name, -> { order(:name) }
scope :with_name, -> (name) { where(name: name) }
+ scope :has_vulnerabilities, -> (value = true) { where(has_vulnerabilities: value) }
validates :name,
presence: true,
@@ -47,5 +47,9 @@ module Clusters
.offset(ACTIVITY_EVENT_LIMIT - 1)
.pick(:recorded_at)
end
+
+ def to_ability_name
+ :cluster
+ end
end
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index e62b6fa5fc5..bed0eab5a58 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.39.0'
+ VERSION = '0.41.0'
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/cluster_enabled_grant.rb b/app/models/clusters/cluster_enabled_grant.rb
new file mode 100644
index 00000000000..4dca6a78759
--- /dev/null
+++ b/app/models/clusters/cluster_enabled_grant.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Clusters
+ class ClusterEnabledGrant < ApplicationRecord
+ self.table_name = 'cluster_enabled_grants'
+
+ belongs_to :namespace
+ end
+end
diff --git a/app/models/clusters/integrations/prometheus.rb b/app/models/clusters/integrations/prometheus.rb
index 8b21fa351a3..0d6177beae7 100644
--- a/app/models/clusters/integrations/prometheus.rb
+++ b/app/models/clusters/integrations/prometheus.rb
@@ -55,13 +55,23 @@ module Clusters
private
def activate_project_integrations
- ::Clusters::Applications::ActivateServiceWorker
- .perform_async(cluster_id, ::Integrations::Prometheus.to_param)
+ if Feature.enabled?(:rename_integrations_workers)
+ ::Clusters::Applications::ActivateIntegrationWorker
+ .perform_async(cluster_id, ::Integrations::Prometheus.to_param)
+ else
+ ::Clusters::Applications::ActivateServiceWorker
+ .perform_async(cluster_id, ::Integrations::Prometheus.to_param)
+ end
end
def deactivate_project_integrations
- ::Clusters::Applications::DeactivateServiceWorker
- .perform_async(cluster_id, ::Integrations::Prometheus.to_param)
+ if Feature.enabled?(:rename_integrations_workers)
+ ::Clusters::Applications::DeactivateIntegrationWorker
+ .perform_async(cluster_id, ::Integrations::Prometheus.to_param)
+ else
+ ::Clusters::Applications::DeactivateServiceWorker
+ .perform_async(cluster_id, ::Integrations::Prometheus.to_param)
+ end
end
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 5293bfcf1ab..ca18cb50e02 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -513,11 +513,16 @@ class Commit
# We don't want to do anything for `Commit` model, so this is empty.
end
+ # We are continuing to support `(fixup!|squash!)` here as it is the prefix
+ # added by `git commit --fixup` which is used by some community members.
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/342937#note_892065311
+ #
DRAFT_REGEX = /\A\s*#{Gitlab::Regex.merge_request_draft}|(fixup!|squash!)\s/.freeze
- def work_in_progress?
+ def draft?
!!(title =~ DRAFT_REGEX)
end
+ alias_method :work_in_progress?, :draft?
def merged_merge_request?(user)
!!merged_merge_request(user)
diff --git a/app/models/commit_signatures/ssh_signature.rb b/app/models/commit_signatures/ssh_signature.rb
new file mode 100644
index 00000000000..dbfbe0c3889
--- /dev/null
+++ b/app/models/commit_signatures/ssh_signature.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module CommitSignatures
+ class SshSignature < ApplicationRecord
+ include CommitSignature
+
+ belongs_to :key, optional: false
+ end
+end
diff --git a/app/models/compare.rb b/app/models/compare.rb
index f1b0bf19c11..7f42e1ee491 100644
--- a/app/models/compare.rb
+++ b/app/models/compare.rb
@@ -40,7 +40,10 @@ class Compare
end
def commits
- @commits ||= Commit.decorate(@compare.commits, project)
+ @commits ||= begin
+ decorated_commits = Commit.decorate(@compare.commits, project)
+ CommitCollection.new(project, decorated_commits)
+ end
end
def start_commit
diff --git a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
index 7cc4bc569d3..1bdb89349aa 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
@@ -33,9 +33,14 @@ module Analytics
)
duration_in_seconds = Arel::Nodes::Extract.new(duration, :epoch)
+ # start_event_timestamp and end_event_timestamp do not really influence the order,
+ # but are included so that they are part of the returned result, for example when
+ # using Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher
keyset_order(
:total_time => { order_expression: arel_order(duration_in_seconds, direction), distinct: false, sql_type: 'double precision' },
- issuable_id_column => { order_expression: arel_order(arel_table[issuable_id_column], direction), distinct: true }
+ issuable_id_column => { order_expression: arel_order(arel_table[issuable_id_column], direction), distinct: true },
+ :end_event_timestamp => { order_expression: arel_order(arel_table[:end_event_timestamp], direction), distinct: true },
+ :start_event_timestamp => { order_expression: arel_order(arel_table[:start_event_timestamp], direction), distinct: true }
)
end
end
diff --git a/app/models/concerns/as_cte.rb b/app/models/concerns/as_cte.rb
new file mode 100644
index 00000000000..aa38ae3a9c1
--- /dev/null
+++ b/app/models/concerns/as_cte.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+# Convert any ActiveRecord::Relation to a Gitlab::SQL::CTE
+module AsCte
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def as_cte(name, **opts)
+ Gitlab::SQL::CTE.new(name, all, **opts)
+ end
+ end
+end
diff --git a/app/models/concerns/async_devise_email.rb b/app/models/concerns/async_devise_email.rb
index 38c99dc7e71..7cdbed2eef6 100644
--- a/app/models/concerns/async_devise_email.rb
+++ b/app/models/concerns/async_devise_email.rb
@@ -2,6 +2,7 @@
module AsyncDeviseEmail
extend ActiveSupport::Concern
+ include AfterCommitQueue
private
@@ -9,6 +10,8 @@ module AsyncDeviseEmail
def send_devise_notification(notification, *args)
return true unless can?(:receive_notifications)
- devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend
+ run_after_commit_or_now do
+ devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend
+ end
end
end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 896f0916d8c..1d0ce594f63 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -18,7 +18,7 @@ module Awardable
inner_query = award_emoji_table
.project('true')
.where(award_emoji_table[:user_id].eq(user.id))
- .where(award_emoji_table[:awardable_type].eq(self.name))
+ .where(award_emoji_table[:awardable_type].eq(base_class.name))
.where(award_emoji_table[:awardable_id].eq(self.arel_table[:id]))
inner_query = inner_query.where(award_emoji_table[:name].eq(name)) if name.present?
@@ -31,7 +31,7 @@ module Awardable
inner_query = award_emoji_table
.project('true')
.where(award_emoji_table[:user_id].eq(user.id))
- .where(award_emoji_table[:awardable_type].eq(self.name))
+ .where(award_emoji_table[:awardable_type].eq(base_class.name))
.where(award_emoji_table[:awardable_id].eq(self.arel_table[:id]))
inner_query = inner_query.where(award_emoji_table[:name].eq(name)) if name.present?
@@ -56,13 +56,11 @@ module Awardable
awardable_table = self.arel_table
awards_table = AwardEmoji.arel_table
- join_clause = awardable_table.join(awards_table, Arel::Nodes::OuterJoin).on(
- awards_table[:awardable_id].eq(awardable_table[:id]).and(
- awards_table[:awardable_type].eq(self.name).and(
- awards_table[:name].eq(emoji_name)
- )
- )
- ).join_sources
+ join_clause = awardable_table
+ .join(awards_table, Arel::Nodes::OuterJoin)
+ .on(awards_table[:awardable_id].eq(awardable_table[:id])
+ .and(awards_table[:awardable_type].eq(base_class.name).and(awards_table[:name].eq(emoji_name))))
+ .join_sources
joins(join_clause).group(awardable_table[:id]).reorder(
Arel.sql("COUNT(award_emoji.id) #{direction}")
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 9414d16beef..99dbe464a7c 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -24,6 +24,9 @@ module CacheMarkdownField
true
end
+ attr_accessor :skip_markdown_cache_validation
+ alias_method :skip_markdown_cache_validation?, :skip_markdown_cache_validation
+
# Returns the default Banzai render context for the cached markdown field.
def banzai_render_context(field)
raise ArgumentError, "Unknown field: #{field.inspect}" unless
@@ -91,7 +94,7 @@ module CacheMarkdownField
end
def invalidated_markdown_cache?
- cached_markdown_fields.html_fields.any? {|html_field| attribute_invalidated?(html_field) }
+ cached_markdown_fields.html_fields.any? { |html_field| attribute_invalidated?(html_field) }
end
def attribute_invalidated?(attr)
@@ -218,6 +221,8 @@ module CacheMarkdownField
# The HTML becomes invalid if any dependent fields change. For now, assume
# author and project invalidate the cache in all circumstances.
define_method(invalidation_method) do
+ return false if skip_markdown_cache_validation?
+
changed_fields = changed_attributes.keys
invalidations = changed_fields & [markdown_field.to_s, *INVALIDATED_BY]
!invalidations.empty? || !cached_html_up_to_date?(markdown_field)
diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb
index 27040a677ff..78340cf967b 100644
--- a/app/models/concerns/ci/artifactable.rb
+++ b/app/models/concerns/ci/artifactable.rb
@@ -21,7 +21,7 @@ module Ci
}, _suffix: true
scope :expired_before, -> (timestamp) { where(arel_table[:expire_at].lt(timestamp)) }
- scope :expired, -> (limit) { expired_before(Time.current).limit(limit) }
+ scope :expired, -> { expired_before(Time.current) }
scope :project_id_in, ->(ids) { where(project_id: ids) }
end
diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb
index 94d11c871ca..8ed6c54441b 100644
--- a/app/models/concerns/enums/ci/pipeline.rb
+++ b/app/models/concerns/enums/ci/pipeline.rb
@@ -15,7 +15,7 @@ module Enums
size_limit_exceeded: 21,
job_activity_limit_exceeded: 22,
deployments_limit_exceeded: 23,
- user_blocked: 24,
+ # 24 was previously used by the deprecated `user_blocked`
project_deleted: 25
}
end
diff --git a/app/models/concerns/file_store_mounter.rb b/app/models/concerns/file_store_mounter.rb
index bfcf8a1e7b9..f1ac734635d 100644
--- a/app/models/concerns/file_store_mounter.rb
+++ b/app/models/concerns/file_store_mounter.rb
@@ -4,9 +4,16 @@ module FileStoreMounter
extend ActiveSupport::Concern
class_methods do
- def mount_file_store_uploader(uploader)
+ # When `skip_store_file: true` is used, the model MUST explicitly call `store_file_now!`
+ def mount_file_store_uploader(uploader, skip_store_file: false)
mount_uploader(:file, uploader)
+ if skip_store_file
+ skip_callback :save, :after, :store_file!
+
+ return
+ end
+
# This hook is a no-op when the file is uploaded after_commit
after_save :update_file_store, if: :saved_change_to_file?
end
@@ -16,4 +23,9 @@ module FileStoreMounter
# The file.object_store is set during `uploader.store!` and `uploader.migrate!`
update_column(:file_store, file.object_store)
end
+
+ def store_file_now!
+ store_file!
+ update_file_store
+ end
end
diff --git a/app/models/concerns/integrations/base_data_fields.rb b/app/models/concerns/integrations/base_data_fields.rb
index 3cedb90756f..11bdd3aae7b 100644
--- a/app/models/concerns/integrations/base_data_fields.rb
+++ b/app/models/concerns/integrations/base_data_fields.rb
@@ -4,12 +4,15 @@ module Integrations
module BaseDataFields
extend ActiveSupport::Concern
+ LEGACY_FOREIGN_KEY_NAME = %w(
+ Integrations::IssueTrackerData
+ Integrations::JiraTrackerData
+ ).freeze
+
included do
# TODO: Once we rename the tables we can't rely on `table_name` anymore.
# https://gitlab.com/gitlab-org/gitlab/-/issues/331953
- belongs_to :integration, inverse_of: self.table_name.to_sym, foreign_key: :service_id
-
- delegate :activated?, to: :integration, allow_nil: true
+ belongs_to :integration, inverse_of: self.table_name.to_sym, foreign_key: foreign_key_name
validates :integration, presence: true
end
@@ -23,6 +26,26 @@ module Integrations
algorithm: 'aes-256-gcm'
}
end
+
+ private
+
+ # Older data field models use the `service_id` foreign key for the
+ # integration association.
+ def foreign_key_name
+ return :service_id if self.name.in?(LEGACY_FOREIGN_KEY_NAME)
+
+ :integration_id
+ end
+ end
+
+ def activated?
+ !!integration&.activated?
+ end
+
+ def to_database_hash
+ as_json(
+ only: self.class.column_names
+ ).except('id', 'service_id', 'integration_id', 'created_at', 'updated_at')
end
end
end
diff --git a/app/models/concerns/integrations/has_data_fields.rb b/app/models/concerns/integrations/has_data_fields.rb
index 25a1d855119..635147a2f3c 100644
--- a/app/models/concerns/integrations/has_data_fields.rb
+++ b/app/models/concerns/integrations/has_data_fields.rb
@@ -12,7 +12,8 @@ module Integrations
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
unless method_defined?(arg)
def #{arg}
- data_fields.send('#{arg}') || (properties && properties['#{arg}'])
+ value = data_fields.send('#{arg}')
+ value.nil? ? properties&.dig('#{arg}') : value
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 713a4386fee..4dca07132ef 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -106,23 +106,23 @@ module Issuable
scope :closed, -> { with_state(:closed) }
# rubocop:disable GitlabSecurity/SqlInjection
- # The `to_ability_name` method is not an user input.
+ # The `assignee_association_name` method is not an user input.
scope :assigned, -> do
- where("EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)")
+ where("EXISTS (SELECT TRUE FROM #{assignee_association_name}_assignees WHERE #{assignee_association_name}_id = #{assignee_association_name}s.id)")
end
scope :unassigned, -> do
- where("NOT EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)")
+ where("NOT EXISTS (SELECT TRUE FROM #{assignee_association_name}_assignees WHERE #{assignee_association_name}_id = #{assignee_association_name}s.id)")
end
scope :assigned_to, ->(users) do
- assignees_class = self.reflect_on_association("#{to_ability_name}_assignees").klass
+ assignees_class = self.reflect_on_association("#{assignee_association_name}_assignees").klass
- condition = assignees_class.where(user_id: users).where(Arel.sql("#{to_ability_name}_id = #{to_ability_name}s.id"))
+ condition = assignees_class.where(user_id: users).where(Arel.sql("#{assignee_association_name}_id = #{assignee_association_name}s.id"))
where(condition.arel.exists)
end
scope :not_assigned_to, ->(users) do
- assignees_class = self.reflect_on_association("#{to_ability_name}_assignees").klass
+ assignees_class = self.reflect_on_association("#{assignee_association_name}_assignees").klass
- condition = assignees_class.where(user_id: users).where(Arel.sql("#{to_ability_name}_id = #{to_ability_name}s.id"))
+ condition = assignees_class.where(user_id: users).where(Arel.sql("#{assignee_association_name}_id = #{assignee_association_name}s.id"))
where(condition.arel.exists.not)
end
# rubocop:enable GitlabSecurity/SqlInjection
@@ -195,8 +195,6 @@ module Issuable
end
def supports_escalation?
- return false unless ::Feature.enabled?(:incident_escalations, project)
-
incident?
end
@@ -414,6 +412,10 @@ module Issuable
def parent_class
::Project
end
+
+ def assignee_association_name
+ to_ability_name
+ end
end
def state
diff --git a/app/models/concerns/limitable.rb b/app/models/concerns/limitable.rb
index 6ff540b7866..0cccb7b51a8 100644
--- a/app/models/concerns/limitable.rb
+++ b/app/models/concerns/limitable.rb
@@ -15,17 +15,29 @@ module Limitable
validate :validate_plan_limit_not_exceeded, on: :create
end
+ def exceeds_limits?
+ limits, relation = fetch_plan_limit_data
+
+ limits&.exceeded?(limit_name, relation)
+ end
+
private
def validate_plan_limit_not_exceeded
+ limits, relation = fetch_plan_limit_data
+
+ check_plan_limit_not_exceeded(limits, relation)
+ end
+
+ def fetch_plan_limit_data
if GLOBAL_SCOPE == limit_scope
- validate_global_plan_limit_not_exceeded
+ global_plan_limits
else
- validate_scoped_plan_limit_not_exceeded
+ scoped_plan_limits
end
end
- def validate_scoped_plan_limit_not_exceeded
+ def scoped_plan_limits
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)
@@ -34,18 +46,18 @@ module Limitable
relation = limit_relation ? self.public_send(limit_relation) : self.class.where(limit_scope => scope_relation) # rubocop:disable GitlabSecurity/PublicSend
limits = scope_relation.actual_limits
- check_plan_limit_not_exceeded(limits, relation)
+ [limits, relation]
end
- def validate_global_plan_limit_not_exceeded
+ def global_plan_limits
relation = self.class.all
limits = Plan.default.actual_limits
- check_plan_limit_not_exceeded(limits, relation)
+ [limits, relation]
end
def check_plan_limit_not_exceeded(limits, relation)
- return unless limits.exceeded?(limit_name, relation)
+ return unless limits&.exceeded?(limit_name, relation)
errors.add(:base, _("Maximum number of %{name} (%{count}) exceeded") %
{ name: limit_name.humanize(capitalize: false), count: limits.public_send(limit_name) }) # rubocop:disable GitlabSecurity/PublicSend
diff --git a/app/models/concerns/pg_full_text_searchable.rb b/app/models/concerns/pg_full_text_searchable.rb
index bfc539ee392..813827478da 100644
--- a/app/models/concerns/pg_full_text_searchable.rb
+++ b/app/models/concerns/pg_full_text_searchable.rb
@@ -24,6 +24,7 @@ module PgFullTextSearchable
LONG_WORDS_REGEX = %r([A-Za-z0-9+/@]{50,}).freeze
TSVECTOR_MAX_LENGTH = 1.megabyte.freeze
TEXT_SEARCH_DICTIONARY = 'english'
+ URL_SCHEME_REGEX = %r{(?<=\A|\W)\w+://(?=\w+)}.freeze
def update_search_data!
tsvector_sql_nodes = self.class.pg_full_text_searchable_columns.map do |column, weight|
@@ -104,6 +105,10 @@ module PgFullTextSearchable
def pg_full_text_search(search_term)
search_data_table = reflect_on_association(:search_data).klass.arel_table
+ # This fixes an inconsistency with how to_tsvector and websearch_to_tsquery process URLs
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/354784#note_905431920
+ search_term = remove_url_scheme(search_term)
+
joins(:search_data).where(
Arel::Nodes::InfixOperation.new(
'@@',
@@ -115,5 +120,11 @@ module PgFullTextSearchable
)
)
end
+
+ private
+
+ def remove_url_scheme(search_term)
+ search_term.gsub(URL_SCHEME_REGEX, '')
+ end
end
end
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 0cab874a240..900e8f7d39b 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -66,6 +66,10 @@ module ProjectFeaturesCompatibility
write_feature_attribute_string(:snippets_access_level, value)
end
+ def package_registry_access_level=(value)
+ write_feature_attribute_string(:package_registry_access_level, value)
+ end
+
def pages_access_level=(value)
write_feature_attribute_string(:pages_access_level, value)
end
diff --git a/app/models/concerns/sensitive_serializable_hash.rb b/app/models/concerns/sensitive_serializable_hash.rb
index 94451fcd2c2..4ad8d16fcb9 100644
--- a/app/models/concerns/sensitive_serializable_hash.rb
+++ b/app/models/concerns/sensitive_serializable_hash.rb
@@ -10,7 +10,7 @@ module SensitiveSerializableHash
class_methods do
def prevent_from_serialization(*keys)
self.attributes_exempt_from_serializable_hash ||= []
- self.attributes_exempt_from_serializable_hash.concat keys
+ self.attributes_exempt_from_serializable_hash += keys
end
end
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index 948190dfadf..e418842a30b 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -23,22 +23,8 @@ module Storage
former_parent_full_path = parent_was&.full_path
parent_full_path = parent&.full_path
Gitlab::UploadsTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path)
-
- if any_project_with_pages_deployed?
- run_after_commit do
- Gitlab::PagesTransfer.new.async.move_namespace(path, former_parent_full_path, parent_full_path)
- end
- end
else
Gitlab::UploadsTransfer.new.rename_namespace(full_path_before_last_save, full_path)
-
- if any_project_with_pages_deployed?
- full_path_was = full_path_before_last_save
-
- run_after_commit do
- Gitlab::PagesTransfer.new.async.rename_namespace(full_path_was, full_path)
- end
- end
end
# If repositories moved successfully we need to
diff --git a/app/models/container_registry/event.rb b/app/models/container_registry/event.rb
index 5409bdf5af4..47d21d21afd 100644
--- a/app/models/container_registry/event.rb
+++ b/app/models/container_registry/event.rb
@@ -76,8 +76,8 @@ module ContainerRegistry
return unless supported?
return unless target_tag?
return unless project
- return unless Feature.enabled?(:container_registry_project_statistics, project)
+ Rails.cache.delete(project.root_ancestor.container_repositories_size_cache_key)
ProjectCacheWorker.perform_async(project.id, [], [:container_registry_size])
end
end
diff --git a/app/models/customer_relations/contact.rb b/app/models/customer_relations/contact.rb
index cdb449e00bf..ded6ab8687a 100644
--- a/app/models/customer_relations/contact.rb
+++ b/app/models/customer_relations/contact.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class CustomerRelations::Contact < ApplicationRecord
+ include Gitlab::SQL::Pattern
+ include Sortable
include StripAttribute
self.table_name = "customer_relations_contacts"
@@ -39,6 +41,25 @@ class CustomerRelations::Contact < ApplicationRecord
']'
end
+ # Searches for contacts with a matching first name, last name, email or description.
+ #
+ # This method uses ILIKE on PostgreSQL
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
+ def self.search(query)
+ fuzzy_search(query, [:first_name, :last_name, :email, :description], use_minimum_char_limit: false)
+ end
+
+ def self.search_by_state(state)
+ where(state: state)
+ end
+
+ def self.sort_by_name
+ order("last_name ASC, first_name ASC")
+ end
+
def self.find_ids_by_emails(group, emails)
raise ArgumentError, "Cannot lookup more than #{MAX_PLUCK} emails" if emails.length > MAX_PLUCK
diff --git a/app/models/customer_relations/organization.rb b/app/models/customer_relations/organization.rb
index 32adcc7492b..705e84250c9 100644
--- a/app/models/customer_relations/organization.rb
+++ b/app/models/customer_relations/organization.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class CustomerRelations::Organization < ApplicationRecord
+ include Gitlab::SQL::Pattern
+ include Sortable
include StripAttribute
self.table_name = "customer_relations_organizations"
@@ -21,6 +23,25 @@ class CustomerRelations::Organization < ApplicationRecord
validates :description, length: { maximum: 1024 }
validate :validate_root_group
+ # Searches for organizations with a matching name or description.
+ #
+ # This method uses ILIKE on PostgreSQL
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
+ def self.search(query)
+ fuzzy_search(query, [:name, :description], use_minimum_char_limit: false)
+ end
+
+ def self.search_by_state(state)
+ where(state: state)
+ end
+
+ def self.sort_by_name
+ order(name: :asc)
+ end
+
def self.find_by_name(group_id, name)
where(group: group_id)
.where('LOWER(name) = LOWER(?)', name)
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 4204ad707b2..fc0dd7e00c7 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -52,6 +52,7 @@ class Deployment < ApplicationRecord
scope :upcoming, -> { where(status: %i[blocked running]) }
scope :older_than, -> (deployment) { where('deployments.id < ?', deployment.id) }
scope :with_api_entity_associations, -> { preload({ deployable: { runner: [], tags: [], user: [], job_artifacts_archive: [] } }) }
+ scope :with_environment_page_associations, -> { preload(project: [], environment: [], deployable: [:user, :metadata, :project, pipeline: [:manual_actions]]) }
scope :finished_after, ->(date) { where('finished_at >= ?', date) }
scope :finished_before, ->(date) { where('finished_at < ?', date) }
@@ -109,7 +110,11 @@ class Deployment < ApplicationRecord
after_transition any => :running do |deployment|
deployment.run_after_commit do
- Deployments::HooksWorker.perform_async(deployment_id: id, status_changed_at: Time.current)
+ if Feature.enabled?(:deployment_hooks_skip_worker, deployment.project)
+ deployment.execute_hooks(Time.current)
+ else
+ Deployments::HooksWorker.perform_async(deployment_id: id, status_changed_at: Time.current)
+ end
end
end
@@ -123,7 +128,11 @@ class Deployment < ApplicationRecord
after_transition any => FINISHED_STATUSES do |deployment|
deployment.run_after_commit do
- Deployments::HooksWorker.perform_async(deployment_id: id, status_changed_at: Time.current)
+ if Feature.enabled?(:deployment_hooks_skip_worker, deployment.project)
+ deployment.execute_hooks(Time.current)
+ else
+ Deployments::HooksWorker.perform_async(deployment_id: id, status_changed_at: Time.current)
+ end
end
end
@@ -173,6 +182,38 @@ class Deployment < ApplicationRecord
find(ids)
end
+ # This method returns the deployment records of the last deployment pipeline, that successfully executed for the given environment.
+ # e.g.
+ # A pipeline contains
+ # - deploy job A => production environment
+ # - deploy job B => production environment
+ # In this case, `last_deployment_group` returns both deployments.
+ #
+ # NOTE: Preload environment.last_deployment and pipeline.latest_successful_builds prior to avoid N+1.
+ def self.last_deployment_group_for_environment(env)
+ return self.none unless env.last_deployment_pipeline&.latest_successful_builds&.present?
+
+ BatchLoader.for(env).batch do |environments, loader|
+ latest_successful_build_ids = []
+ environments_hash = {}
+
+ environments.each do |environment|
+ environments_hash[environment.id] = environment
+
+ # Refer comment note above, if not preloaded this can lead to N+1.
+ latest_successful_build_ids << environment.last_deployment_pipeline.latest_successful_builds.map(&:id)
+ end
+
+ Deployment
+ .where(deployable_type: 'CommitStatus', deployable_id: latest_successful_build_ids.flatten)
+ .preload(last_deployment_group_associations)
+ .group_by { |deployment| deployment.environment_id }
+ .each do |env_id, deployment_group|
+ loader.call(environments_hash[env_id], deployment_group)
+ end
+ end
+ end
+
def self.distinct_on_environment
order('environment_id, deployments.id DESC')
.select('DISTINCT ON (environment_id) deployments.*')
@@ -247,11 +288,27 @@ class Deployment < ApplicationRecord
end
def manual_actions
- @manual_actions ||= deployable.try(:other_manual_actions)
+ environment_manual_actions
+ end
+
+ def other_manual_actions
+ @other_manual_actions ||= deployable.try(:other_manual_actions)
+ end
+
+ def environment_manual_actions
+ @environment_manual_actions ||= deployable.try(:environment_manual_actions)
end
def scheduled_actions
- @scheduled_actions ||= deployable.try(:other_scheduled_actions)
+ environment_scheduled_actions
+ end
+
+ def environment_scheduled_actions
+ @environment_scheduled_actions ||= deployable.try(:environment_scheduled_actions)
+ end
+
+ def other_scheduled_actions
+ @other_scheduled_actions ||= deployable.try(:other_scheduled_actions)
end
def playable_build
@@ -414,6 +471,18 @@ class Deployment < ApplicationRecord
raise ArgumentError, "The status #{status.inspect} is invalid"
end
end
+
+ def self.last_deployment_group_associations
+ {
+ deployable: {
+ pipeline: {
+ manual_actions: []
+ }
+ }
+ }
+ end
+
+ private_class_method :last_deployment_group_associations
end
Deployment.prepend_mod_with('Deployment')
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 865f5c68af1..da6ab5ed077 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -59,7 +59,7 @@ class Environment < ApplicationRecord
allow_nil: true,
addressable_url: true
- delegate :manual_actions, to: :last_deployment, allow_nil: true
+ delegate :manual_actions, :other_manual_actions, to: :last_deployment, allow_nil: true
delegate :auto_rollback_enabled?, to: :project
scope :available, -> { with_state(:available) }
@@ -132,10 +132,16 @@ class Environment < ApplicationRecord
end
event :stop do
- transition available: :stopped
+ transition available: :stopping, if: :wait_for_stop?
+ transition available: :stopped, unless: :wait_for_stop?
+ end
+
+ event :stop_complete do
+ transition %i(available stopping) => :stopped
end
state :available
+ state :stopping
state :stopped
before_transition any => :stopped do |environment|
@@ -202,7 +208,7 @@ class Environment < ApplicationRecord
# - deploy job A => production environment
# - deploy job B => production environment
# In this case, `last_deployment_group` returns both deployments, whereas `last_deployable` returns only B.
- def last_deployment_group
+ def legacy_last_deployment_group
return Deployment.none unless last_deployment_pipeline
successful_deployments.where(
@@ -293,6 +299,10 @@ class Environment < ApplicationRecord
end
end
+ def wait_for_stop?
+ stop_actions.present?
+ end
+
def stop_with_actions!(current_user)
return unless available?
@@ -314,20 +324,26 @@ class Environment < ApplicationRecord
def stop_actions
strong_memoize(:stop_actions) do
- # Fix N+1 queries it brings to the serializer.
- # Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780
last_deployment_group.map(&:stop_action).compact
end
end
+ def last_deployment_group
+ if ::Feature.enabled?(:batch_load_environment_last_deployment_group, project)
+ Deployment.last_deployment_group_for_environment(self)
+ else
+ legacy_last_deployment_group
+ end
+ end
+
def reset_auto_stop
update_column(:auto_stop_at, nil)
end
def actions_for(environment)
- return [] unless manual_actions
+ return [] unless other_manual_actions
- manual_actions.select do |action|
+ other_manual_actions.select do |action|
action.expanded_environment_name == environment
end
end
diff --git a/app/models/error_tracking/client_key.rb b/app/models/error_tracking/client_key.rb
index 8e59f6f9ecb..bbc57573aa9 100644
--- a/app/models/error_tracking/client_key.rb
+++ b/app/models/error_tracking/client_key.rb
@@ -7,6 +7,7 @@ class ErrorTracking::ClientKey < ApplicationRecord
validates :public_key, presence: true, length: { maximum: 255 }
scope :active, -> { where(active: true) }
+ scope :enabled_key_for, -> (project_id, public_key) { active.where(project_id: project_id, public_key: public_key) }
after_initialize :generate_key
diff --git a/app/models/error_tracking/error_event.rb b/app/models/error_tracking/error_event.rb
index 18c1467e6f6..3ee82b219dc 100644
--- a/app/models/error_tracking/error_event.rb
+++ b/app/models/error_tracking/error_event.rb
@@ -15,7 +15,7 @@ class ErrorTracking::ErrorEvent < ApplicationRecord
validates :occurred_at, presence: true
def stacktrace
- @stacktrace ||= build_stacktrace
+ @stacktrace ||= ErrorTracking::StacktraceBuilder.new(payload).stacktrace
end
# For compatibility with sentry integration
@@ -30,56 +30,4 @@ class ErrorTracking::ErrorEvent < ApplicationRecord
def release
payload.dig('release')
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) if pre_context
- context += lines_with_position([error_line], error_line_no)
- context += lines_with_position(post_context, error_line_no + 1) if post_context
-
- 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/group.rb b/app/models/group.rb
index 86f4b14cb6c..f5aad6e74ff 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -362,7 +362,7 @@ class Group < Namespace
end
def add_users(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
- Members::Groups::BulkCreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
+ Members::Groups::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
self,
users,
access_level,
@@ -374,7 +374,7 @@ class Group < Namespace
end
def add_user(user, access_level, current_user: nil, expires_at: nil, ldap: false, blocking_refresh: true)
- Members::Groups::CreatorService.new( # rubocop:disable CodeReuse/ServiceClass
+ Members::Groups::CreatorService.add_user( # rubocop:disable CodeReuse/ServiceClass
self,
user,
access_level,
@@ -382,7 +382,7 @@ class Group < Namespace
expires_at: expires_at,
ldap: ldap,
blocking_refresh: blocking_refresh
- ).execute
+ )
end
def add_guest(user, current_user = nil)
@@ -432,8 +432,9 @@ class Group < Namespace
end
# Check if user is a last owner of the group.
+ # Excludes project_bots
def last_owner?(user)
- has_owner?(user) && single_owner?
+ has_owner?(user) && all_owners_excluding_project_bots.size == 1
end
def member_last_owner?(member)
@@ -442,8 +443,8 @@ class Group < Namespace
last_owner?(member.user)
end
- def single_owner?
- members_with_parents.owners.size == 1
+ def all_owners_excluding_project_bots
+ members_with_parents.owners.merge(User.without_project_bot)
end
def single_blocked_owner?
@@ -863,6 +864,12 @@ class Group < Namespace
end
end
+ def gitlab_deploy_token
+ strong_memoize(:gitlab_deploy_token) do
+ deploy_tokens.gitlab_deploy_token
+ end
+ end
+
private
def feature_flag_enabled_for_self_or_ancestor?(feature_flag)
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index 9f45160d3a8..b7ace34141e 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -31,11 +31,6 @@ class ProjectHook < WebHook
_('Webhooks')
end
- override :rate_limit
- def rate_limit
- project.actual_limits.limit_for(:web_hook_calls)
- end
-
override :application_context
def application_context
super.merge(project: project)
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 88941df691c..37fd612e652 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -19,6 +19,15 @@ class WebHook < ApplicationRecord
algorithm: 'aes-256-gcm',
key: Settings.attr_encrypted_db_key_base_32
+ attr_encrypted :url_variables,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_32,
+ algorithm: 'aes-256-gcm',
+ marshal: true,
+ marshaler: ::Gitlab::Json,
+ encode: false,
+ encode_iv: false
+
has_many :web_hook_logs
validates :url, presence: true
@@ -26,6 +35,9 @@ class WebHook < ApplicationRecord
validates :token, format: { without: /\n/ }
validates :push_events_branch_filter, branch_filter: true
+ validates :url_variables, json_schema: { filename: 'web_hooks_url_variables' }
+
+ after_initialize :initialize_url_variables
scope :executable, -> do
next all unless Feature.enabled?(:web_hooks_disable_failed)
@@ -115,19 +127,12 @@ class WebHook < ApplicationRecord
# @return [Boolean] Whether or not the WebHook is currently throttled.
def rate_limited?
- return false unless rate_limit
-
- Gitlab::ApplicationRateLimiter.peek(
- :web_hook_calls,
- scope: [self],
- threshold: rate_limit
- )
+ rate_limiter.rate_limited?
end
- # Threshold for the rate-limit.
- # Overridden in ProjectHook and GroupHook, other WebHooks are not rate-limited.
+ # @return [Integer] The rate limit for the WebHook. `0` for no limit.
def rate_limit
- nil
+ rate_limiter.limit
end
# Returns the associated Project or Group for the WebHook if one exists.
@@ -140,9 +145,36 @@ class WebHook < ApplicationRecord
{ related_class: type }
end
+ def alert_status
+ if temporarily_disabled?
+ :temporarily_disabled
+ elsif permanently_disabled?
+ :disabled
+ else
+ :executable
+ end
+ end
+
+ # Exclude binary columns by default - they have no sensible JSON encoding
+ def serializable_hash(options = nil)
+ options = options.try(:dup) || {}
+ options[:except] = Array(options[:except]).dup
+ options[:except].concat [:encrypted_url_variables, :encrypted_url_variables_iv]
+
+ super(options)
+ end
+
private
def web_hooks_disable_failed?
Feature.enabled?(:web_hooks_disable_failed)
end
+
+ def initialize_url_variables
+ self.url_variables = {} if encrypted_url_variables.nil?
+ end
+
+ def rate_limiter
+ @rate_limiter ||= Gitlab::WebHooks::RateLimiter.new(self)
+ end
end
diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb
index 8c0565e4a38..2f03b3591cf 100644
--- a/app/models/hooks/web_hook_log.rb
+++ b/app/models/hooks/web_hook_log.rb
@@ -7,6 +7,8 @@ class WebHookLog < ApplicationRecord
include CreatedAtFilterable
include PartitionedTable
+ OVERSIZE_REQUEST_DATA = { 'oversize' => true }.freeze
+
self.primary_key = :id
partitioned_by :created_at, strategy: :monthly, retain_for: 3.months
@@ -26,6 +28,13 @@ class WebHookLog < ApplicationRecord
.order(created_at: :desc)
end
+ # Delete a batch of log records. Returns true if there may be more remaining.
+ def self.delete_batch_for(web_hook, batch_size:)
+ raise ArgumentError, 'batch_size is too small' if batch_size < 1
+
+ where(web_hook: web_hook).limit(batch_size).delete_all == batch_size
+ end
+
def success?
response_status =~ /^2/
end
@@ -34,6 +43,10 @@ class WebHookLog < ApplicationRecord
response_status == WebHookService::InternalErrorResponse::ERROR_MESSAGE
end
+ def oversize?
+ request_data == OVERSIZE_REQUEST_DATA
+ end
+
private
def obfuscate_basic_auth
diff --git a/app/models/integration.rb b/app/models/integration.rb
index b5064cfae2d..726e95b7cbf 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -13,7 +13,6 @@ class Integration < ApplicationRecord
include IgnorableColumns
extend ::Gitlab::Utils::Override
- ignore_column :type, remove_with: '15.0', remove_after: '2022-04-22'
ignore_column :properties, remove_with: '15.1', remove_after: '2022-05-22'
UnknownType = Class.new(StandardError)
@@ -47,7 +46,9 @@ class Integration < ApplicationRecord
Integrations::BaseSlashCommands
].freeze
+ SECTION_TYPE_CONFIGURATION = 'configuration'
SECTION_TYPE_CONNECTION = 'connection'
+ SECTION_TYPE_TRIGGER = 'trigger'
attr_encrypted :properties,
mode: :per_attribute_iv,
@@ -143,7 +144,7 @@ class Integration < ApplicationRecord
# :nocov: Tested on subclasses.
def self.field(name, storage: field_storage, **attrs)
- fields << ::Integrations::Field.new(name: name, **attrs)
+ fields << ::Integrations::Field.new(name: name, integration_class: self, **attrs)
case storage
when :properties
@@ -465,13 +466,14 @@ class Integration < ApplicationRecord
super.except('properties')
end
- # return a hash of columns => values suitable for passing to insert_all
- def to_integration_hash
+ # Returns a hash of attributes (columns => values) used for inserting into the database.
+ def to_database_hash
column = self.class.attribute_aliases.fetch('type', 'type')
- as_json(except: %w[id instance project_id group_id])
- .merge(column => type)
- .merge(reencrypt_properties)
+ as_json(
+ except: %w[id instance project_id group_id created_at updated_at]
+ ).merge(column => type)
+ .merge(reencrypt_properties)
end
def reencrypt_properties
@@ -484,10 +486,6 @@ class Integration < ApplicationRecord
{ 'encrypted_properties' => ep, 'encrypted_properties_iv' => iv }
end
- def to_data_fields_hash
- data_fields.as_json(only: data_fields.class.column_names).except('id', 'service_id', 'integration_id')
- end
-
def event_channel_names
[]
end
@@ -501,10 +499,7 @@ class Integration < ApplicationRecord
end
def api_field_names
- fields
- .reject { _1[:type] == 'password' }
- .pluck(:name)
- .grep_v(/password|token|key/)
+ fields.reject { _1[:type] == 'password' }.pluck(:name)
end
def global_fields
@@ -579,7 +574,11 @@ class Integration < ApplicationRecord
def async_execute(data)
return unless supported_events.include?(data[:object_kind])
- ProjectServiceWorker.perform_async(id, data)
+ if Feature.enabled?(:rename_integrations_workers)
+ Integrations::ExecuteWorker.perform_async(id, data)
+ else
+ ProjectServiceWorker.perform_async(id, data)
+ end
end
# override if needed
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index 4e144a688f6..4e30c1ccc69 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -6,25 +6,25 @@ module Integrations
prepend EnableSslVerification
field :bamboo_url,
- title: s_('BambooService|Bamboo URL'),
- placeholder: s_('https://bamboo.example.com'),
- help: s_('BambooService|Bamboo service root URL.'),
+ title: -> { s_('BambooService|Bamboo URL') },
+ placeholder: -> { s_('https://bamboo.example.com') },
+ help: -> { s_('BambooService|Bamboo service root URL.') },
required: true
field :build_key,
- help: s_('BambooService|Bamboo build plan key.'),
- non_empty_password_title: s_('BambooService|Enter new build key'),
- non_empty_password_help: s_('BambooService|Leave blank to use your current build key.'),
- placeholder: s_('KEY'),
+ help: -> { s_('BambooService|Bamboo build plan key.') },
+ non_empty_password_title: -> { s_('BambooService|Enter new build key') },
+ non_empty_password_help: -> { s_('BambooService|Leave blank to use your current build key.') },
+ placeholder: -> { s_('KEY') },
required: true
field :username,
- help: s_('BambooService|The user with API access to the Bamboo server.')
+ help: -> { s_('BambooService|The user with API access to the Bamboo server.') }
field :password,
type: 'password',
- non_empty_password_title: s_('ProjectService|Enter new password'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
+ non_empty_password_title: -> { s_('ProjectService|Enter new password') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current password') }
validates :bamboo_url, presence: true, public_url: true, if: :activated?
validates :build_key, presence: true, if: :activated?
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index 9bf208abcf7..33d4eecbf49 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -249,7 +249,7 @@ module Integrations
ref = data[:ref] || data.dig(:object_attributes, :ref)
return true if ref.blank? # No need to check protected branches when there is no ref
- return true if Gitlab::Git.tag_ref?(ref) # Skip protected branch check because it doesn't support tags
+ return true if Gitlab::Git.tag_ref?(project.repository.expand_ref(ref) || ref) # Skip protected branch check because it doesn't support tags
notify_for_branch?(data)
end
diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb
index d1e54ce86ee..def646c6d49 100644
--- a/app/models/integrations/buildkite.rb
+++ b/app/models/integrations/buildkite.rb
@@ -11,16 +11,18 @@ module Integrations
ENDPOINT = "https://buildkite.com"
field :project_url,
- title: _('Pipeline URL'),
+ title: -> { _('Pipeline URL') },
placeholder: "#{ENDPOINT}/example-org/test-pipeline",
required: true
field :token,
type: 'password',
- title: _('Token'),
- help: s_('ProjectService|The token you get after you create a Buildkite pipeline with a GitLab repository.'),
- non_empty_password_title: s_('ProjectService|Enter new token'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
+ title: -> { _('Token') },
+ help: -> do
+ s_('ProjectService|The token you get after you create a Buildkite pipeline with a GitLab repository.')
+ end,
+ non_empty_password_title: -> { s_('ProjectService|Enter new token') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
required: true
validates :project_url, presence: true, public_url: true, if: :activated?
diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb
index 0c65ed8cd5f..35524503dea 100644
--- a/app/models/integrations/drone_ci.rb
+++ b/app/models/integrations/drone_ci.rb
@@ -11,15 +11,15 @@ module Integrations
DRONE_SAAS_HOSTNAME = 'cloud.drone.io'
field :drone_url,
- title: s_('ProjectService|Drone server URL'),
+ title: -> { s_('ProjectService|Drone server URL') },
placeholder: 'http://drone.example.com',
required: true
field :token,
type: 'password',
- help: s_('ProjectService|Token for the Drone project.'),
- non_empty_password_title: s_('ProjectService|Enter new token'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
+ help: -> { s_('ProjectService|Token for the Drone project.') },
+ non_empty_password_title: -> { s_('ProjectService|Enter new token') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
required: true
validates :drone_url, presence: true, public_url: true, if: :activated?
diff --git a/app/models/integrations/field.rb b/app/models/integrations/field.rb
index ca7833c1a56..cbda418755b 100644
--- a/app/models/integrations/field.rb
+++ b/app/models/integrations/field.rb
@@ -13,10 +13,11 @@ module Integrations
exposes_secrets
].freeze
- attr_reader :name
+ attr_reader :name, :integration_class
- def initialize(name:, type: 'text', api_only: false, **attributes)
+ def initialize(name:, integration_class:, type: 'text', api_only: false, **attributes)
@name = name.to_s.freeze
+ @integration_class = integration_class
attributes[:type] = SECRET_NAME.match?(@name) ? 'password' : type
attributes[:api_only] = api_only
@@ -27,7 +28,7 @@ module Integrations
return name if key == :name
value = @attributes[key]
- return value.call if value.respond_to?(:call)
+ return integration_class.class_exec(&value) if value.respond_to?(:call)
value
end
diff --git a/app/models/integrations/harbor.rb b/app/models/integrations/harbor.rb
index 6b561575f30..44813795fc0 100644
--- a/app/models/integrations/harbor.rb
+++ b/app/models/integrations/harbor.rb
@@ -81,7 +81,7 @@ module Integrations
[
{ key: 'HARBOR_URL', value: url },
{ key: 'HARBOR_PROJECT', value: project_name },
- { key: 'HARBOR_USERNAME', value: username },
+ { key: 'HARBOR_USERNAME', value: username.gsub(/^robot\$/, 'robot$$') },
{ key: 'HARBOR_PASSWORD', value: password, public: false, masked: true }
]
end
diff --git a/app/models/integrations/irker.rb b/app/models/integrations/irker.rb
index 116d1fb233d..780f4bef0c9 100644
--- a/app/models/integrations/irker.rb
+++ b/app/models/integrations/irker.rb
@@ -24,14 +24,23 @@ module Integrations
end
def self.supported_events
- %w(push)
+ %w[push]
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
- IrkerWorker.perform_async(project_id, channels,
- colorize_messages, data, settings)
+ if Feature.enabled?(:rename_integrations_workers)
+ Integrations::IrkerWorker.perform_async(
+ project_id, channels,
+ colorize_messages, data, settings
+ )
+ else
+ ::IrkerWorker.perform_async(
+ project_id, channels,
+ colorize_messages, data, settings
+ )
+ end
end
def settings
@@ -42,7 +51,15 @@ module Integrations
end
def fields
- recipients_docs_link = ActionController::Base.helpers.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'
+ recipients_docs_link = ActionController::Base.helpers.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', title: s_('IrkerService|Server host (optional)'),
help: s_('IrkerService|irker daemon hostname (defaults to localhost).') },
@@ -53,14 +70,29 @@ module Integrations
placeholder: 'irc://irc.network.net:6697/' },
{ 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 } },
+ help: format(
+ 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
- docs_link = ActionController::Base.helpers.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 }
+ docs_link = ActionController::Base.helpers.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'
+ )
+
+ format(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
@@ -104,12 +136,11 @@ module Integrations
end
def consider_uri(uri)
- return if uri.scheme.nil?
-
+ return unless uri.is_a?(URI) && uri.scheme.present?
# Authorize both irc://domain.com/#chan and irc://domain.com/chan
- if uri.is_a?(URI) && uri.scheme[/^ircs?\z/] && !uri.path.nil?
- uri.to_s
- end
+ return unless uri.scheme =~ /\Aircs?\z/ && !uri.path.nil?
+
+ uri.to_s
end
end
end
diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb
index a1abbce72bc..ab39d1f7b77 100644
--- a/app/models/integrations/jenkins.rb
+++ b/app/models/integrations/jenkins.rb
@@ -8,24 +8,24 @@ module Integrations
extend Gitlab::Utils::Override
field :jenkins_url,
- title: s_('ProjectService|Jenkins server URL'),
+ title: -> { s_('ProjectService|Jenkins server URL') },
required: true,
placeholder: 'http://jenkins.example.com',
- help: s_('The URL of the Jenkins server.')
+ help: -> { s_('The URL of the Jenkins server.') }
field :project_name,
required: true,
placeholder: 'my_project_name',
- help: s_('The name of the Jenkins project. Copy the name from the end of the URL to the project.')
+ help: -> { s_('The name of the Jenkins project. Copy the name from the end of the URL to the project.') }
field :username,
- help: s_('The username for the Jenkins server.')
+ help: -> { s_('The username for the Jenkins server.') }
field :password,
type: 'password',
- help: s_('The password for the Jenkins server.'),
- non_empty_password_title: s_('ProjectService|Enter new password.'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current password.')
+ help: -> { s_('The password for the Jenkins server.') },
+ non_empty_password_title: -> { s_('ProjectService|Enter new password.') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current password.') }
before_validation :reset_password
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index 992bd01bf5f..125f52104d4 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -24,7 +24,10 @@ module Integrations
validates :password, presence: true, if: :activated?
validates :jira_issue_transition_id,
- format: { with: Gitlab::Regex.jira_transition_id_regex, message: s_("JiraService|IDs must be a list of numbers that can be split with , or ;") },
+ format: {
+ with: Gitlab::Regex.jira_transition_id_regex,
+ message: ->(*_) { s_("JiraService|IDs must be a list of numbers that can be split with , or ;") }
+ },
allow_blank: true
# Jira Cloud version is deprecating authentication via username and password.
diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb
index 71cd4ddaf82..625ee0bc522 100644
--- a/app/models/integrations/microsoft_teams.rb
+++ b/app/models/integrations/microsoft_teams.rb
@@ -35,10 +35,16 @@ module Integrations
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" },
- { type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'If selected, successful pipelines do not trigger a notification event.' },
+ { type: 'text', section: SECTION_TYPE_CONNECTION, name: 'webhook', required: true, placeholder: "#{webhook_placeholder}" },
+ {
+ type: 'checkbox',
+ section: SECTION_TYPE_CONFIGURATION,
+ name: 'notify_only_broken_pipelines',
+ help: 'If selected, successful pipelines do not trigger a notification event.'
+ },
{
type: 'select',
+ section: SECTION_TYPE_CONFIGURATION,
name: 'branches_to_be_notified',
title: s_('Integrations|Branches for which notifications are to be sent'),
choices: branch_choices
@@ -46,6 +52,26 @@ module Integrations
]
end
+ def sections
+ [
+ {
+ type: SECTION_TYPE_CONNECTION,
+ title: s_('Integrations|Connection details'),
+ description: help
+ },
+ {
+ type: SECTION_TYPE_TRIGGER,
+ title: s_('Integrations|Trigger'),
+ description: s_('Integrations|An event will be triggered when one of the following items happen.')
+ },
+ {
+ type: SECTION_TYPE_CONFIGURATION,
+ title: s_('Integrations|Notification settings'),
+ description: s_('Integrations|Configure the scope of notifications.')
+ }
+ ]
+ end
+
private
def notify(message, opts)
diff --git a/app/models/integrations/mock_ci.rb b/app/models/integrations/mock_ci.rb
index cd2928136ef..0b3a9bc5405 100644
--- a/app/models/integrations/mock_ci.rb
+++ b/app/models/integrations/mock_ci.rb
@@ -8,7 +8,7 @@ module Integrations
ALLOWED_STATES = %w[failed canceled running pending success success-with-warnings skipped not_found].freeze
field :mock_service_url,
- title: s_('ProjectService|Mock service URL'),
+ title: -> { s_('ProjectService|Mock service URL') },
placeholder: 'http://localhost:4004',
required: true
diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb
index 427034edb79..36060565317 100644
--- a/app/models/integrations/prometheus.rb
+++ b/app/models/integrations/prometheus.rb
@@ -84,6 +84,8 @@ module Integrations
# Check we can connect to the Prometheus API
def test(*args)
+ return { success: false, result: 'Prometheus configuration error' } unless prometheus_client
+
prometheus_client.ping
{ success: true, result: 'Checked API endpoint' }
rescue Gitlab::PrometheusClient::Error => err
diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb
index 1205173e40b..a23aa5f783d 100644
--- a/app/models/integrations/teamcity.rb
+++ b/app/models/integrations/teamcity.rb
@@ -9,21 +9,21 @@ module Integrations
TEAMCITY_SAAS_HOSTNAME = /\A[^\.]+\.teamcity\.com\z/i.freeze
field :teamcity_url,
- title: s_('ProjectService|TeamCity server URL'),
+ title: -> { s_('ProjectService|TeamCity server URL') },
placeholder: 'https://teamcity.example.com',
required: true
field :build_type,
- help: s_('ProjectService|The build configuration ID of the TeamCity project.'),
+ help: -> { s_('ProjectService|The build configuration ID of the TeamCity project.') },
required: true
field :username,
- help: s_('ProjectService|Must have permission to trigger a manual build in TeamCity.')
+ help: -> { s_('ProjectService|Must have permission to trigger a manual build in TeamCity.') }
field :password,
type: 'password',
- non_empty_password_title: s_('ProjectService|Enter new password'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
+ non_empty_password_title: -> { s_('ProjectService|Enter new password') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current password') }
validates :teamcity_url, presence: true, public_url: true, if: :activated?
validates :build_type, presence: true, if: :activated?
diff --git a/app/models/integrations/zentao_tracker_data.rb b/app/models/integrations/zentao_tracker_data.rb
index 468e4e5d7d7..e9d63abd66b 100644
--- a/app/models/integrations/zentao_tracker_data.rb
+++ b/app/models/integrations/zentao_tracker_data.rb
@@ -2,18 +2,7 @@
module Integrations
class ZentaoTrackerData < ApplicationRecord
- belongs_to :integration, inverse_of: :zentao_tracker_data, foreign_key: :integration_id
- delegate :activated?, to: :integration
- validates :integration, presence: true
-
- scope :encryption_options, -> do
- {
- key: Settings.attr_encrypted_db_key_base_32,
- encode: true,
- mode: :per_attribute_iv,
- algorithm: 'aes-256-gcm'
- }
- end
+ include BaseDataFields
attr_encrypted :url, encryption_options
attr_encrypted :api_url, encryption_options
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d4eb77ef6de..47aa2b24feb 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -122,12 +122,13 @@ class Issue < ApplicationRecord
scope :order_due_date_asc, -> { reorder(arel_table[:due_date].asc.nulls_last) }
scope :order_due_date_desc, -> { reorder(arel_table[:due_date].desc.nulls_last) }
scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) }
- scope :order_closed_date_desc, -> { reorder(closed_at: :desc) }
scope :order_created_at_desc, -> { reorder(created_at: :desc) }
scope :order_severity_asc, -> { includes(:issuable_severity).order('issuable_severities.severity ASC NULLS FIRST') }
scope :order_severity_desc, -> { includes(:issuable_severity).order('issuable_severities.severity DESC NULLS LAST') }
scope :order_escalation_status_asc, -> { includes(:incident_management_issuable_escalation_status).order(IncidentManagement::IssuableEscalationStatus.arel_table[:status].asc.nulls_last).references(:incident_management_issuable_escalation_status) }
scope :order_escalation_status_desc, -> { includes(:incident_management_issuable_escalation_status).order(IncidentManagement::IssuableEscalationStatus.arel_table[:status].desc.nulls_last).references(:incident_management_issuable_escalation_status) }
+ scope :order_closed_at_asc, -> { reorder(arel_table[:closed_at].asc.nulls_last) }
+ scope :order_closed_at_desc, -> { reorder(arel_table[:closed_at].desc.nulls_last) }
scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) }
scope :with_web_entity_associations, -> { preload(:author, project: [:project_feature, :route, namespace: :route]) }
@@ -138,7 +139,8 @@ class Issue < ApplicationRecord
scope :with_api_entity_associations, -> {
preload(:timelogs, :closed_by, :assignees, :author, :labels, :issuable_severity,
milestone: { project: [:route, { namespace: :route }] },
- project: [:route, { namespace: :route }])
+ project: [:route, { namespace: :route }],
+ duplicated_to: { project: [:project_feature] })
}
scope :with_issue_type, ->(types) { where(issue_type: types) }
scope :without_issue_type, ->(types) { where.not(issue_type: types) }
@@ -149,7 +151,7 @@ class Issue < ApplicationRecord
scope :without_hidden, -> {
if Feature.enabled?(:ban_user_feature_flag)
- where('NOT EXISTS (?)', Users::BannedUser.select(1).where('issues.author_id = banned_users.user_id'))
+ where.not(author_id: Users::BannedUser.all.select(:user_id))
else
all
end
@@ -295,7 +297,7 @@ class Issue < ApplicationRecord
end
def self.link_reference_pattern
- @link_reference_pattern ||= super("issues", Gitlab::Regex.issue)
+ @link_reference_pattern ||= super(%r{issues(?:\/incident)?}, Gitlab::Regex.issue)
end
def self.reference_valid?(reference)
@@ -330,6 +332,8 @@ class Issue < ApplicationRecord
when 'severity_desc' then order_severity_desc.with_order_id_desc
when 'escalation_status_asc' then order_escalation_status_asc.with_order_id_desc
when 'escalation_status_desc' then order_escalation_status_desc.with_order_id_desc
+ when 'closed_at_asc' then order_closed_at_asc
+ when 'closed_at_desc' then order_closed_at_desc
else
super
end
@@ -613,6 +617,11 @@ class Issue < ApplicationRecord
super || WorkItems::Type.default_by_type(issue_type)
end
+ def expire_etag_cache
+ key = Gitlab::Routing.url_helpers.realtime_changes_project_issue_path(project, self)
+ Gitlab::EtagCaching::Store.new.touch(key)
+ end
+
private
override :persist_pg_full_text_search_vector
@@ -643,11 +652,6 @@ class Issue < ApplicationRecord
!confidential? && !hidden? && !::Gitlab::ExternalAuthorization.enabled?
end
- def expire_etag_cache
- key = Gitlab::Routing.url_helpers.realtime_changes_project_issue_path(project, self)
- Gitlab::EtagCaching::Store.new.touch(key)
- end
-
def could_not_move(exception)
# Symptom of running out of space - schedule rebalancing
Issues::RebalancingWorker.perform_async(nil, *project.self_or_root_group_ids)
diff --git a/app/models/key.rb b/app/models/key.rb
index e093f9faad3..5268ce2e040 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'digest/md5'
-
class Key < ApplicationRecord
include AfterCommitQueue
include Sortable
@@ -30,6 +28,7 @@ class Key < ApplicationRecord
validate :key_meets_restrictions
validate :expiration, on: :create
+ validate :banned_key, if: :should_check_for_banned_key?
delegate :name, :email, to: :user, prefix: true
@@ -144,6 +143,27 @@ class Key < ApplicationRecord
end
end
+ def should_check_for_banned_key?
+ return false unless user
+
+ key_changed? && Feature.enabled?(:ssh_banned_key, user)
+ end
+
+ def banned_key
+ return unless public_key.banned?
+
+ help_page_url = Rails.application.routes.url_helpers.help_page_url(
+ 'security/ssh_keys_restrictions',
+ anchor: 'block-banned-or-compromised-keys'
+ )
+
+ errors.add(
+ :key,
+ _('cannot be used because it belongs to a compromised private key. Stop using this key and generate a new one.'),
+ help_page_url: help_page_url
+ )
+ end
+
def forbidden_key_type_message
allowed_types = Gitlab::CurrentSettings.allowed_key_types.map(&:upcase)
diff --git a/app/models/label.rb b/app/models/label.rb
index 7f4556c11c9..6608a0573cb 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -118,7 +118,7 @@ class Label < ApplicationRecord
| # Integer-based label ID, or
(?<label_name>
# String-based single-word label title, or
- [A-Za-z0-9_\-\?\.&]+
+ #{Gitlab::Regex.sep_by_1(/:{1,2}/, /[A-Za-z0-9_\-\?\.&]+/)}
(?<!\.|\?)
|
# String-based multi-word label surrounded in quotes
diff --git a/app/models/member.rb b/app/models/member.rb
index 45ad47f56a4..bb5d2b10f8e 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -199,7 +199,6 @@ class Member < ApplicationRecord
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?
after_create :create_notification_setting, unless: [:pending?, :importing?]
after_create :post_create_hook, unless: [:pending?, :importing?], if: :hook_prerequisites_met?
after_update :post_update_hook, unless: [:pending?, :importing?], if: :hook_prerequisites_met?
@@ -207,6 +206,7 @@ class Member < ApplicationRecord
after_destroy :post_destroy_hook, unless: :pending?, if: :hook_prerequisites_met?
after_save :log_invitation_token_cleanup
+ after_commit :send_request, if: :request?, unless: :importing?, on: [:create]
after_commit on: [:create, :update], unless: :importing? do
refresh_member_authorized_projects(blocking: blocking_refresh)
end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index a8a4fbedc41..87af6a9a7f7 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -7,6 +7,7 @@ class GroupMember < Member
SOURCE_TYPE = 'Namespace'
SOURCE_TYPE_FORMAT = /\ANamespace\z/.freeze
+ THRESHOLD_FOR_REFRESHING_AUTHORIZATIONS_VIA_PROJECTS = 1000
belongs_to :group, foreign_key: 'source_id'
alias_attribute :namespace_id, :source_id
@@ -28,6 +29,12 @@ class GroupMember < Member
attr_accessor :last_owner, :last_blocked_owner
+ # For those who get to see a modal with a role dropdown, here are the options presented
+ def self.permissible_access_level_roles(_, _)
+ # This method is a stopgap in preparation for https://gitlab.com/gitlab-org/gitlab/-/issues/364087
+ access_level_roles
+ end
+
def self.access_level_roles
Gitlab::Access.options_with_owner
end
@@ -60,8 +67,28 @@ class GroupMember < Member
# 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?
+ return unless user_id
+ return super if Feature.disabled?(:refresh_authorizations_via_affected_projects_on_group_membership, group)
- super
+ # rubocop:disable CodeReuse/ServiceClass
+ projects_to_refresh = Groups::ProjectsRequiringAuthorizationsRefresh::OnDirectMembershipFinder.new(group).execute
+ threshold_exceeded = (projects_to_refresh.size > THRESHOLD_FOR_REFRESHING_AUTHORIZATIONS_VIA_PROJECTS)
+
+ # We want to try the new approach only if the number of affected projects are greater than the set threshold.
+ return super unless threshold_exceeded
+
+ AuthorizedProjectUpdate::ProjectAccessChangedService
+ .new(projects_to_refresh)
+ .execute(blocking: false)
+
+ # Until we compare the inconsistency rates of the new approach
+ # 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
def send_invite
@@ -91,7 +118,10 @@ class GroupMember < Member
end
def after_accept_invite
- notification_service.accept_group_invite(self)
+ run_after_commit_or_now do
+ notification_service.accept_group_invite(self)
+ end
+
update_two_factor_requirement
super
diff --git a/app/models/members/last_group_owner_assigner.rb b/app/models/members/last_group_owner_assigner.rb
index dcf0a2d0ad3..c85116858c7 100644
--- a/app/models/members/last_group_owner_assigner.rb
+++ b/app/models/members/last_group_owner_assigner.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+# Optimization class to fix group member n+1 queries
class LastGroupOwnerAssigner
def initialize(group, members)
@group = group
@@ -39,6 +40,6 @@ class LastGroupOwnerAssigner
end
def owners
- @owners ||= group.members_with_parents.owners.load
+ @owners ||= group.all_owners_excluding_project_bots.load
end
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 995c26d7221..791cb6f0dff 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -44,7 +44,7 @@ class ProjectMember < Member
project_ids.each do |project_id|
project = Project.find(project_id)
- Members::Projects::BulkCreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
+ Members::Projects::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
project,
users,
access_level,
@@ -73,6 +73,16 @@ class ProjectMember < Member
truncate_teams [project.id]
end
+ # For those who get to see a modal with a role dropdown, here are the options presented
+ def permissible_access_level_roles(current_user, project)
+ # This method is a stopgap in preparation for https://gitlab.com/gitlab-org/gitlab/-/issues/364087
+ if Ability.allowed?(current_user, :manage_owners, project)
+ Gitlab::Access.options_with_owner
+ else
+ ProjectMember.access_level_roles
+ end
+ end
+
def access_level_roles
Gitlab::Access.options
end
@@ -158,7 +168,9 @@ class ProjectMember < Member
end
def after_accept_invite
- notification_service.accept_project_invite(self)
+ run_after_commit_or_now do
+ notification_service.accept_project_invite(self)
+ end
super
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 39b5949ea7a..1a3464d05a2 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -231,7 +231,10 @@ class MergeRequest < ApplicationRecord
# rubocop: disable CodeReuse/ServiceClass
after_transition [:unchecked, :checking] => :cannot_be_merged do |merge_request, transition|
if merge_request.notify_conflict?
- NotificationService.new.merge_request_unmergeable(merge_request)
+ merge_request.run_after_commit do
+ NotificationService.new.merge_request_unmergeable(merge_request)
+ end
+
TodoService.new.merge_request_became_unmergeable(merge_request)
end
end
@@ -1150,6 +1153,19 @@ class MergeRequest < ApplicationRecord
can_be_merged? && !should_be_rebased?
end
+ def mergeability_checks
+ # We want to have the cheapest checks first in the list, that way we can
+ # fail fast before running the more expensive ones.
+ #
+ [
+ ::MergeRequests::Mergeability::CheckOpenStatusService,
+ ::MergeRequests::Mergeability::CheckDraftStatusService,
+ ::MergeRequests::Mergeability::CheckBrokenStatusService,
+ ::MergeRequests::Mergeability::CheckDiscussionsStatusService,
+ ::MergeRequests::Mergeability::CheckCiStatusService
+ ]
+ end
+
# rubocop: disable CodeReuse/ServiceClass
def mergeable_state?(skip_ci_check: false, skip_discussions_check: false)
if Feature.enabled?(:improved_mergeability_checks, self.project)
@@ -1654,9 +1670,9 @@ class MergeRequest < ApplicationRecord
# TODO: consider renaming this as with exposed artifacts we generate reports,
# not always compare
# issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
- def compare_reports(service_class, current_user = nil, report_type = nil )
+ def compare_reports(service_class, current_user = nil, report_type = nil, additional_params = {} )
with_reactive_cache(service_class.name, current_user&.id, report_type) do |data|
- unless service_class.new(project, current_user, id: id, report_type: report_type)
+ unless service_class.new(project, current_user, id: id, report_type: report_type, additional_params: additional_params)
.latest?(comparison_base_pipeline(service_class.name), actual_head_pipeline, data)
raise InvalidateReactiveCache
end
@@ -1696,7 +1712,12 @@ class MergeRequest < ApplicationRecord
service_class.new(project, current_user, id: id, report_type: report_type).execute(comparison_base_pipeline(identifier), actual_head_pipeline)
end
- def recent_diff_head_shas(limit = 100)
+ MAX_RECENT_DIFF_HEAD_SHAS = 100
+
+ def recent_diff_head_shas(limit = MAX_RECENT_DIFF_HEAD_SHAS)
+ # see MergeRequestDiff.recent
+ return merge_request_diffs.to_a.sort_by(&:id).reverse.first(limit).pluck(:head_commit_sha) if merge_request_diffs.loaded?
+
merge_request_diffs.recent(limit).pluck(:head_commit_sha)
end
@@ -1955,6 +1976,10 @@ class MergeRequest < ApplicationRecord
end
end
+ def target_default_branch?
+ target_branch == project.default_branch
+ end
+
private
attr_accessor :skip_fetch_ref
diff --git a/app/models/merge_request/cleanup_schedule.rb b/app/models/merge_request/cleanup_schedule.rb
index 35194b2b318..7f52a110da1 100644
--- a/app/models/merge_request/cleanup_schedule.rb
+++ b/app/models/merge_request/cleanup_schedule.rb
@@ -8,6 +8,9 @@ class MergeRequest::CleanupSchedule < ApplicationRecord
failed: 3
}.freeze
+ # NOTE: Limit the number of stuck schedule jobs to retry just in case it becomes too big.
+ STUCK_RETRY_LIMIT = 5
+
belongs_to :merge_request, inverse_of: :cleanup_schedule
validates :scheduled_at, presence: true
@@ -48,6 +51,11 @@ class MergeRequest::CleanupSchedule < ApplicationRecord
.order('scheduled_at DESC')
}
+ # NOTE: It is considered stuck as it is unusual to take more than 6 hours to finish the cleanup task.
+ scope :stuck, -> {
+ where('updated_at <= NOW() - interval \'6 hours\' AND status = ?', STATUSES[:running])
+ }
+
def self.start_next
MergeRequest::CleanupSchedule.transaction do
cleanup_schedule = scheduled_and_unstarted.lock('FOR UPDATE SKIP LOCKED').first
@@ -58,4 +66,8 @@ class MergeRequest::CleanupSchedule < ApplicationRecord
cleanup_schedule
end
end
+
+ def self.stuck_retry!
+ self.stuck.limit(STUCK_RETRY_LIMIT).map(&:retry!)
+ end
end
diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb
index f3f64971426..f7648937c1d 100644
--- a/app/models/merge_request_diff_file.rb
+++ b/app/models/merge_request_diff_file.rb
@@ -15,9 +15,11 @@ class MergeRequestDiffFile < ApplicationRecord
end
def utf8_diff
- return '' if diff.blank?
+ fetched_diff = diff
- encode_utf8(diff) if diff.respond_to?(:encoding)
+ return '' if fetched_diff.blank?
+
+ encode_utf8(fetched_diff) if fetched_diff.respond_to?(:encoding)
end
def diff
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index fcd641671f5..5bb06cdbb4a 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -73,6 +73,8 @@ class Namespace < ApplicationRecord
has_one :ci_namespace_mirror, class_name: 'Ci::NamespaceMirror'
has_many :sync_events, class_name: 'Namespaces::SyncEvent'
+ has_one :cluster_enabled_grant, inverse_of: :namespace, class_name: 'Clusters::ClusterEnabledGrant'
+
validates :owner, presence: true, if: ->(n) { n.owner_required? }
validates :name,
presence: true,
@@ -208,7 +210,7 @@ class Namespace < ApplicationRecord
end
end
- def clean_path(path)
+ def clean_path(path, limited_to: Namespace.all)
path = path.dup
# Get the email username by removing everything after an `@` sign.
path.gsub!(/@.*\z/, "")
@@ -229,7 +231,7 @@ class Namespace < ApplicationRecord
path = "blank" if path.blank?
uniquify = Uniquify.new
- uniquify.string(path) { |s| Namespace.find_by_path_or_name(s) }
+ uniquify.string(path) { |s| limited_to.find_by_path_or_name(s) }
end
def clean_name(value)
@@ -411,12 +413,10 @@ class Namespace < ApplicationRecord
return { scope: :group, status: auto_devops_enabled } unless auto_devops_enabled.nil?
strong_memoize(:first_auto_devops_config) do
- if has_parent? && cache_first_auto_devops_config?
+ if has_parent?
Rails.cache.fetch(first_auto_devops_config_cache_key_for(id), expires_in: 1.day) do
parent.first_auto_devops_config
end
- elsif has_parent?
- parent.first_auto_devops_config
else
{ scope: :instance, status: Gitlab::CurrentSettings.auto_devops_enabled? }
end
@@ -427,6 +427,28 @@ class Namespace < ApplicationRecord
aggregation_schedule.present?
end
+ def container_repositories_size_cache_key
+ "namespaces:#{id}:container_repositories_size"
+ end
+
+ def container_repositories_size
+ strong_memoize(:container_repositories_size) do
+ next unless Gitlab.com?
+ next unless root?
+ next unless ContainerRegistry::GitlabApiClient.supports_gitlab_api?
+ next 0 if all_container_repositories.empty?
+ next unless all_container_repositories.all_migrated?
+
+ Rails.cache.fetch(container_repositories_size_cache_key, expires_in: 7.days) do
+ ContainerRegistry::GitlabApiClient.deduplicated_size(full_path)
+ end
+ end
+ end
+
+ def all_container_repositories
+ ContainerRepository.for_project_id(all_projects)
+ end
+
def pages_virtual_domain
Pages::VirtualDomain.new(
all_projects_with_pages.includes(:route, :project_feature, pages_metadatum: :pages_deployment),
@@ -524,19 +546,35 @@ class Namespace < ApplicationRecord
end
def storage_enforcement_date
+ return Date.current if Feature.enabled?(:namespace_storage_limit_bypass_date_check, self)
+
# should return something like Date.new(2022, 02, 03)
# TBD: https://gitlab.com/gitlab-org/gitlab/-/issues/350632
nil
end
def certificate_based_clusters_enabled?
- ::Gitlab::SafeRequestStore.fetch("certificate_based_clusters:ns:#{self.id}") do
- Feature.enabled?(:certificate_based_clusters, self, type: :ops)
- end
+ cluster_enabled_granted? || certificate_based_clusters_enabled_ff?
+ end
+
+ def enabled_git_access_protocol
+ # If the instance-level setting is enabled, we defer to that
+ return ::Gitlab::CurrentSettings.enabled_git_access_protocol unless ::Gitlab::CurrentSettings.enabled_git_access_protocol.blank?
+
+ # Otherwise we use the stored setting on the group
+ namespace_settings&.enabled_git_access_protocol
end
private
+ def cluster_enabled_granted?
+ (Gitlab.com? || Gitlab.dev_or_test_env?) && root_ancestor.cluster_enabled_grant.present?
+ end
+
+ def certificate_based_clusters_enabled_ff?
+ Feature.enabled?(:certificate_based_clusters, type: :ops)
+ end
+
def expire_child_caches
Namespace.where(id: descendants).each_batch do |namespaces|
namespaces.touch_all
@@ -611,7 +649,7 @@ class Namespace < ApplicationRecord
return
end
- if parent.project_namespace?
+ if parent&.project_namespace?
errors.add(:parent_id, _('project namespace cannot be the parent of another namespace'))
end
@@ -638,8 +676,6 @@ class Namespace < ApplicationRecord
end
def expire_first_auto_devops_config_cache
- return unless cache_first_auto_devops_config?
-
descendants_to_expire = self_and_descendants.as_ids
return if descendants_to_expire.load.empty?
@@ -647,10 +683,6 @@ class Namespace < ApplicationRecord
Rails.cache.delete_multi(keys)
end
- def cache_first_auto_devops_config?
- ::Feature.enabled?(:namespaces_cache_first_auto_devops_config)
- end
-
def write_projects_repository_config
all_projects.find_each do |project|
project.set_full_path
@@ -670,8 +702,6 @@ class Namespace < ApplicationRecord
end
def first_auto_devops_config_cache_key_for(group_id)
- return "namespaces:{first_auto_devops_config}:#{group_id}" unless sync_traversal_ids?
-
# Use SHA2 of `traversal_ids` to account for moving a namespace within the same root ancestor hierarchy.
"namespaces:{#{traversal_ids.first}}:first_auto_devops_config:#{group_id}:#{Digest::SHA2.hexdigest(traversal_ids.join(' '))}"
end
diff --git a/app/models/namespace/root_storage_statistics.rb b/app/models/namespace/root_storage_statistics.rb
index 96715863892..77974a0f36b 100644
--- a/app/models/namespace/root_storage_statistics.rb
+++ b/app/models/namespace/root_storage_statistics.rb
@@ -44,15 +44,26 @@ class Namespace::RootStorageStatistics < ApplicationRecord
def merged_attributes
attributes_from_project_statistics.merge!(
attributes_from_personal_snippets,
- attributes_from_namespace_statistics
+ attributes_from_namespace_statistics,
+ attributes_for_container_registry_size
) { |key, v1, v2| v1 + v2 }
end
+ def attributes_for_container_registry_size
+ container_registry_size = namespace.container_repositories_size || 0
+
+ {
+ storage_size: container_registry_size,
+ container_registry_size: container_registry_size
+ }.with_indifferent_access
+ end
+
def attributes_from_project_statistics
from_project_statistics
- .take
- .attributes
- .slice(*STATISTICS_ATTRIBUTES)
+ .take
+ .attributes
+ .slice(*STATISTICS_ATTRIBUTES)
+ .with_indifferent_access
end
def from_project_statistics
@@ -74,7 +85,10 @@ class Namespace::RootStorageStatistics < ApplicationRecord
def attributes_from_personal_snippets
return {} unless namespace.user_namespace?
- from_personal_snippets.take.slice(SNIPPETS_SIZE_STAT_NAME)
+ from_personal_snippets
+ .take
+ .slice(SNIPPETS_SIZE_STAT_NAME)
+ .with_indifferent_access
end
def from_personal_snippets
@@ -102,7 +116,12 @@ class Namespace::RootStorageStatistics < ApplicationRecord
# guard clause.
return {} unless namespace.group_namespace?
- from_namespace_statistics.take.slice(*self.class.namespace_statistics_attributes)
+ from_namespace_statistics
+ .take
+ .slice(
+ *self.class.namespace_statistics_attributes
+ )
+ .with_indifferent_access
end
end
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index ef917c8a22e..504daf2662e 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -9,14 +9,17 @@ class NamespaceSetting < ApplicationRecord
belongs_to :namespace, inverse_of: :namespace_settings
+ enum jobs_to_be_done: { basics: 0, move_repository: 1, code_storage: 2, exploring: 3, ci: 4, other: 5 }, _suffix: true
+ enum enabled_git_access_protocol: { all: 0, ssh: 1, http: 2 }, _suffix: true
+
+ validates :enabled_git_access_protocol, inclusion: { in: enabled_git_access_protocols.keys }
+
validate :default_branch_name_content
validate :allow_mfa_for_group
validate :allow_resource_access_token_creation_for_group
before_validation :normalize_default_branch_name
- enum jobs_to_be_done: { basics: 0, move_repository: 1, code_storage: 2, exploring: 3, ci: 4, other: 5 }, _suffix: true
-
chronic_duration_attr :runner_token_expiration_interval_human_readable, :runner_token_expiration_interval
chronic_duration_attr :subgroup_runner_token_expiration_interval_human_readable, :subgroup_runner_token_expiration_interval
chronic_duration_attr :project_runner_token_expiration_interval_human_readable, :project_runner_token_expiration_interval
@@ -24,7 +27,7 @@ class NamespaceSetting < ApplicationRecord
NAMESPACE_SETTINGS_PARAMS = [:default_branch_name, :delayed_project_removal,
:lock_delayed_project_removal, :resource_access_token_creation_allowed,
:prevent_sharing_groups_outside_hierarchy, :new_user_signups_cap,
- :setup_for_company, :jobs_to_be_done, :runner_token_expiration_interval,
+ :setup_for_company, :jobs_to_be_done, :runner_token_expiration_interval, :enabled_git_access_protocol,
:subgroup_runner_token_expiration_interval, :project_runner_token_expiration_interval].freeze
self.primary_key = :namespace_id
diff --git a/app/models/namespaces/project_namespace.rb b/app/models/namespaces/project_namespace.rb
index fbd87e3232d..2a2ea11ddc5 100644
--- a/app/models/namespaces/project_namespace.rb
+++ b/app/models/namespaces/project_namespace.rb
@@ -2,6 +2,13 @@
module Namespaces
class ProjectNamespace < Namespace
+ # These aliases are added to make it easier to sync parent/parent_id attribute with
+ # project.namespace/project.namespace_id attribute.
+ #
+ # TODO: we can remove these attribute aliases when we no longer need to sync these with project model,
+ # see project#sync_attributes
+ alias_attribute :namespace, :parent
+ alias_attribute :namespace_id, :parent_id
has_one :project, foreign_key: :project_namespace_id, inverse_of: :project_namespace
def self.sti_name
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index b0350b0288f..687fa6a5334 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -42,11 +42,11 @@ module Namespaces
UnboundedSearch = Class.new(StandardError)
included do
- before_update :lock_both_roots, if: -> { sync_traversal_ids? && parent_id_changed? }
- after_update :sync_traversal_ids, if: -> { sync_traversal_ids? && saved_change_to_parent_id? }
+ before_update :lock_both_roots, if: -> { parent_id_changed? }
+ after_update :sync_traversal_ids, if: -> { saved_change_to_parent_id? }
# This uses rails internal before_commit API to sync traversal_ids on namespace create, right before transaction is committed.
# This helps reduce the time during which the root namespace record is locked to ensure updated traversal_ids are valid
- before_commit :sync_traversal_ids, on: [:create], if: -> { sync_traversal_ids? }
+ before_commit :sync_traversal_ids, on: [:create]
end
class_methods do
@@ -76,10 +76,6 @@ module Namespaces
end
end
- def sync_traversal_ids?
- Feature.enabled?(:sync_traversal_ids, root_ancestor)
- end
-
def use_traversal_ids?
return false unless Feature.enabled?(:use_traversal_ids)
diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb
index f0e9a8feeb2..6f404ec12d0 100644
--- a/app/models/namespaces/traversal/linear_scopes.rb
+++ b/app/models/namespaces/traversal/linear_scopes.rb
@@ -5,6 +5,8 @@ module Namespaces
module LinearScopes
extend ActiveSupport::Concern
+ include AsCte
+
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
@@ -25,25 +27,15 @@ module Namespaces
def self_and_ancestors(include_self: true, upto: nil, hierarchy_order: nil)
return super unless use_traversal_ids_for_ancestor_scopes?
- ancestors_cte, base_cte = ancestor_ctes
- namespaces = Arel::Table.new(:namespaces)
-
- records = unscoped
- .with(base_cte.to_arel, ancestors_cte.to_arel)
- .distinct
- .from([ancestors_cte.table, namespaces])
- .where(namespaces[:id].eq(ancestors_cte.table[:ancestor_id]))
- .order_by_depth(hierarchy_order)
-
- unless include_self
- records = records.where(ancestors_cte.table[:base_id].not_eq(ancestors_cte.table[:ancestor_id]))
- end
-
- if upto
- records = records.where.not(id: unscoped.where(id: upto).select('unnest(traversal_ids)'))
+ if Feature.enabled?(:use_traversal_ids_for_ancestor_scopes_with_inner_join)
+ self_and_ancestors_from_inner_join(include_self: include_self,
+ upto: upto, hierarchy_order:
+ hierarchy_order)
+ else
+ self_and_ancestors_from_ancestors_cte(include_self: include_self,
+ upto: upto,
+ hierarchy_order: hierarchy_order)
end
-
- records
end
def self_and_ancestor_ids(include_self: true)
@@ -87,7 +79,7 @@ module Namespaces
depth_order = hierarchy_order == :asc ? :desc : :asc
all
- .select(Arel.star, 'array_length(traversal_ids, 1) as depth')
+ .select(Namespace.default_select_columns, 'array_length(traversal_ids, 1) as depth')
.order(depth: depth_order, id: :asc)
end
@@ -125,26 +117,106 @@ module Namespaces
use_traversal_ids?
end
+ def self_and_ancestors_from_ancestors_cte(include_self: true, upto: nil, hierarchy_order: nil)
+ base_cte = all.select('namespaces.id', 'namespaces.traversal_ids').as_cte(:base_ancestors_cte)
+
+ # We have to alias id with 'AS' to avoid ambiguous column references by calling methods.
+ ancestors_cte = unscoped
+ .unscope(where: [:type])
+ .select('id as base_id',
+ "#{unnest_func(base_cte.table['traversal_ids']).to_sql} as ancestor_id")
+ .from(base_cte.table)
+ .as_cte(:ancestors_cte)
+
+ namespaces = Arel::Table.new(:namespaces)
+
+ records = unscoped
+ .with(base_cte.to_arel, ancestors_cte.to_arel)
+ .distinct
+ .from([ancestors_cte.table, namespaces])
+ .where(namespaces[:id].eq(ancestors_cte.table[:ancestor_id]))
+ .order_by_depth(hierarchy_order)
+
+ unless include_self
+ records = records.where(ancestors_cte.table[:base_id].not_eq(ancestors_cte.table[:ancestor_id]))
+ end
+
+ if upto
+ records = records.where.not(id: unscoped.where(id: upto).select('unnest(traversal_ids)'))
+ end
+
+ records
+ end
+
+ def self_and_ancestors_from_inner_join(include_self: true, upto: nil, hierarchy_order: nil)
+ base_cte = all.reselect('namespaces.traversal_ids').as_cte(:base_ancestors_cte)
+
+ unnest = if include_self
+ base_cte.table[:traversal_ids]
+ else
+ base_cte_traversal_ids = 'base_ancestors_cte.traversal_ids'
+ traversal_ids_range = "1:array_length(#{base_cte_traversal_ids},1)-1"
+ Arel.sql("#{base_cte_traversal_ids}[#{traversal_ids_range}]")
+ end
+
+ ancestor_subselect = "SELECT DISTINCT #{unnest_func(unnest).to_sql} FROM base_ancestors_cte"
+ ancestors_join = <<~SQL
+ INNER JOIN (#{ancestor_subselect}) AS ancestors(ancestor_id) ON namespaces.id = ancestors.ancestor_id
+ SQL
+
+ namespaces = Arel::Table.new(:namespaces)
+
+ records = unscoped
+ .with(base_cte.to_arel)
+ .from(namespaces)
+ .joins(ancestors_join)
+ .order_by_depth(hierarchy_order)
+
+ if upto
+ upto_ancestor_ids = unscoped.where(id: upto).select(unnest_func(Arel.sql('traversal_ids')))
+ records = records.where.not(id: upto_ancestor_ids)
+ end
+
+ records
+ end
+
def self_and_descendants_with_comparison_operators(include_self: true)
base = all.select(:traversal_ids)
- base_cte = Gitlab::SQL::CTE.new(:descendants_base_cte, base)
+ base = base.select(:id) if Feature.enabled?(:linear_scopes_superset)
+ base_cte = base.as_cte(:descendants_base_cte)
namespaces = Arel::Table.new(:namespaces)
+ withs = [base_cte.to_arel]
+ froms = []
+
+ if Feature.enabled?(:linear_scopes_superset)
+ superset_cte = self.superset_cte(base_cte.table.name)
+ withs += [superset_cte.to_arel]
+ froms = [superset_cte.table]
+ else
+ froms = [base_cte.table]
+ end
+
+ # Order is important. namespace should be last to handle future joins.
+ froms += [namespaces]
+
+ base_ref = froms.first
+
# Bound the search space to ourselves (optional) and descendants.
#
# WHERE next_traversal_ids_sibling(base_cte.traversal_ids) > namespaces.traversal_ids
records = unscoped
.distinct
- .with(base_cte.to_arel)
- .from([base_cte.table, namespaces])
- .where(next_sibling_func(base_cte.table[:traversal_ids]).gt(namespaces[:traversal_ids]))
+ .with(*withs)
+ .from(froms)
+ .where(next_sibling_func(base_ref[:traversal_ids]).gt(namespaces[:traversal_ids]))
# AND base_cte.traversal_ids <= namespaces.traversal_ids
if include_self
- records.where(base_cte.table[:traversal_ids].lteq(namespaces[:traversal_ids]))
+ records.where(base_ref[:traversal_ids].lteq(namespaces[:traversal_ids]))
else
- records.where(base_cte.table[:traversal_ids].lt(namespaces[:traversal_ids]))
+ records.where(base_ref[:traversal_ids].lt(namespaces[:traversal_ids]))
end
end
@@ -152,6 +224,10 @@ module Namespaces
Arel::Nodes::NamedFunction.new('next_traversal_ids_sibling', args)
end
+ def unnest_func(*args)
+ Arel::Nodes::NamedFunction.new('unnest', args)
+ end
+
def self_and_descendants_with_duplicates_with_array_operator(include_self: true)
base_ids = select(:id)
@@ -166,18 +242,19 @@ module Namespaces
end
end
- def ancestor_ctes
- base_scope = all.select('namespaces.id', 'namespaces.traversal_ids')
- base_cte = Gitlab::SQL::CTE.new(:base_ancestors_cte, base_scope)
-
- # We have to alias id with 'AS' to avoid ambiguous column references by calling methods.
- ancestors_scope = unscoped
- .unscope(where: [:type])
- .select('id as base_id', 'unnest(traversal_ids) as ancestor_id')
- .from(base_cte.table)
- ancestors_cte = Gitlab::SQL::CTE.new(:ancestors_cte, ancestors_scope)
-
- [ancestors_cte, base_cte]
+ def superset_cte(base_name)
+ superset_sql = <<~SQL
+ SELECT d1.traversal_ids
+ FROM #{base_name} d1
+ WHERE NOT EXISTS (
+ SELECT 1
+ FROM #{base_name} d2
+ WHERE d2.id = ANY(d1.traversal_ids)
+ AND d2.id <> d1.id
+ )
+ SQL
+
+ Gitlab::SQL::CTE.new(:superset, superset_sql, materialized: false)
end
end
end
diff --git a/app/models/namespaces/traversal/recursive.rb b/app/models/namespaces/traversal/recursive.rb
index 53eac27aa54..1c5d395cb3c 100644
--- a/app/models/namespaces/traversal/recursive.rb
+++ b/app/models/namespaces/traversal/recursive.rb
@@ -63,19 +63,17 @@ module Namespaces
# Returns all the descendants of the current namespace.
def descendants
- object_hierarchy(self.class.where(parent_id: id))
- .base_and_descendants
+ object_hierarchy(self.class.where(parent_id: id)).base_and_descendants
end
alias_method :recursive_descendants, :descendants
def self_and_descendants
- object_hierarchy(self.class.where(id: id))
- .base_and_descendants
+ object_hierarchy(self.class.where(id: id)).base_and_descendants
end
alias_method :recursive_self_and_descendants, :self_and_descendants
def self_and_descendant_ids
- recursive_self_and_descendants.select(:id)
+ object_hierarchy(self.class.where(id: id)).base_and_descendant_ids
end
alias_method :recursive_self_and_descendant_ids, :self_and_descendant_ids
diff --git a/app/models/note.rb b/app/models/note.rb
index 3d2ac69a2ab..41e45a8759f 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -124,7 +124,6 @@ class Note < ApplicationRecord
scope :common, -> { where(noteable_type: ["", nil]) }
scope :fresh, -> { order_created_asc.with_order_id_asc }
scope :updated_after, ->(time) { where('updated_at > ?', time) }
- scope :with_updated_at, ->(time) { where(updated_at: time) }
scope :with_discussion_ids, ->(discussion_ids) { where(discussion_id: discussion_ids) }
scope :with_suggestions, -> { joins(:suggestions) }
scope :inc_author, -> { includes(:author) }
diff --git a/app/models/packages/cleanup/policy.rb b/app/models/packages/cleanup/policy.rb
index 87c101cfb8c..d7df90a4ce0 100644
--- a/app/models/packages/cleanup/policy.rb
+++ b/app/models/packages/cleanup/policy.rb
@@ -15,7 +15,7 @@ module Packages
validates :keep_n_duplicated_package_files,
inclusion: {
in: KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES,
- message: 'keep_n_duplicated_package_files is invalid'
+ message: 'is invalid'
}
# used by Schedulable
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index 7744e578df5..90a1bb4bc69 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -103,7 +103,15 @@ class Packages::Package < ApplicationRecord
scope :for_projects, ->(project_ids) { where(project_id: project_ids) }
scope :with_name, ->(name) { where(name: name) }
scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) }
- scope :with_normalized_pypi_name, ->(name) { where("LOWER(regexp_replace(name, '[-_.]+', '-', 'g')) = ?", name.downcase) }
+
+ scope :with_normalized_pypi_name, ->(name) do
+ where(
+ "LOWER(regexp_replace(name, ?, '-', 'g')) = ?",
+ Gitlab::Regex::Packages::PYPI_NORMALIZED_NAME_REGEX_STRING,
+ name.downcase
+ )
+ end
+
scope :search_by_name, ->(query) { fuzzy_search(query, [:name], use_minimum_char_limit: false) }
scope :with_version, ->(version) { where(version: version) }
scope :without_version_like, -> (version) { where.not(arel_table[:version].matches(version)) }
@@ -315,6 +323,13 @@ class Packages::Package < ApplicationRecord
::Packages::MarkPackageFilesForDestructionWorker.perform_async(id)
end
+ # As defined in PEP 503 https://peps.python.org/pep-0503/#normalized-names
+ def normalized_pypi_name
+ return name unless pypi?
+
+ name.gsub(/#{Gitlab::Regex::Packages::PYPI_NORMALIZED_NAME_REGEX_STRING}/, '-').downcase
+ end
+
private
def composer_tag_version?
diff --git a/app/models/project.rb b/app/models/project.rb
index b66ec28b659..dca47911d20 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -121,6 +121,8 @@ class Project < ApplicationRecord
before_save :ensure_runners_token
before_validation :ensure_project_namespace_in_sync
+ before_validation :set_package_registry_access_level, if: :packages_enabled_changed?
+
after_save :update_project_statistics, if: :saved_change_to_namespace_id?
after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_namespace_id? }
@@ -418,6 +420,8 @@ class Project < ApplicationRecord
has_one :ci_project_mirror, class_name: 'Ci::ProjectMirror'
has_many :sync_events, class_name: 'Projects::SyncEvent'
+ has_one :build_artifacts_size_refresh, class_name: 'Projects::BuildArtifactsSizeRefresh'
+
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :project_setting, update_only: true
@@ -443,7 +447,7 @@ class Project < ApplicationRecord
:pages_enabled?, :analytics_enabled?, :snippets_enabled?, :public_pages?, :private_pages?,
:merge_requests_access_level, :forking_access_level, :issues_access_level,
:wiki_access_level, :snippets_access_level, :builds_access_level,
- :repository_access_level, :pages_access_level, :metrics_dashboard_access_level, :analytics_access_level,
+ :repository_access_level, :package_registry_access_level, :pages_access_level, :metrics_dashboard_access_level, :analytics_access_level,
:operations_enabled?, :operations_access_level, :security_and_compliance_access_level,
:container_registry_access_level, :container_registry_enabled?,
to: :project_feature, allow_nil: true
@@ -598,6 +602,7 @@ class Project < ApplicationRecord
scope :inc_routes, -> { includes(:route, namespace: :route) }
scope :with_statistics, -> { includes(:statistics) }
scope :with_namespace, -> { includes(:namespace) }
+ scope :with_group, -> { includes(:group) }
scope :with_import_state, -> { includes(:import_state) }
scope :include_project_feature, -> { includes(:project_feature) }
scope :include_integration, -> (integration_association_name) { includes(integration_association_name) }
@@ -1167,7 +1172,7 @@ class Project < ApplicationRecord
job_type = type.to_s.capitalize
if job_id
- Gitlab::AppLogger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id}.")
+ Gitlab::AppLogger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id} (primary: #{::Gitlab::Database::LoadBalancing::Session.current.use_primary?}).")
else
Gitlab::AppLogger.error("#{job_type} job failed to create for #{full_path}.")
end
@@ -2161,6 +2166,7 @@ class Project < ApplicationRecord
.append(key: 'CI_PROJECT_ID', value: id.to_s)
.append(key: 'CI_PROJECT_NAME', value: path)
.append(key: 'CI_PROJECT_TITLE', value: title)
+ .append(key: 'CI_PROJECT_DESCRIPTION', value: description)
.append(key: 'CI_PROJECT_PATH', value: full_path)
.append(key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug)
.append(key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path)
@@ -2504,7 +2510,13 @@ class Project < ApplicationRecord
end
def gitlab_deploy_token
- @gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
+ strong_memoize(:gitlab_deploy_token) do
+ if Feature.enabled?(:ci_variable_for_group_gitlab_deploy_token, self)
+ deploy_tokens.gitlab_deploy_token || group&.gitlab_deploy_token
+ else
+ deploy_tokens.gitlab_deploy_token
+ end
+ end
end
def any_lfs_file_locks?
@@ -2573,16 +2585,7 @@ class Project < ApplicationRecord
end
def access_request_approvers_to_be_notified
- # For a personal project:
- # The creator is added as a member with `Owner` access level, starting from GitLab 14.8
- # The creator was added as a member with `Maintainer` access level, before GitLab 14.8
- # So, to make sure access requests for all personal projects work as expected,
- # we need to filter members with the scope `owners_and_maintainers`.
- access_request_approvers = if personal?
- members.owners_and_maintainers
- else
- members.maintainers
- end
+ access_request_approvers = members.owners_and_maintainers
access_request_approvers.connected_to_user.order_recent_sign_in.limit(Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
end
@@ -2900,6 +2903,14 @@ class Project < ApplicationRecord
last_activity_at < ::Gitlab::CurrentSettings.inactive_projects_send_warning_email_after_months.months.ago
end
+ def refreshing_build_artifacts_size?
+ build_artifacts_size_refresh&.started?
+ end
+
+ def security_training_available?
+ licensed_feature_available?(:security_training)
+ end
+
private
# overridden in EE
@@ -3098,7 +3109,6 @@ class Project < ApplicationRecord
# create project_namespace when project is created
build_project_namespace if project_namespace_creation_enabled?
- # we need to keep project and project namespace in sync if there is one
sync_attributes(project_namespace) if sync_project_namespace?
end
@@ -3111,11 +3121,24 @@ class Project < ApplicationRecord
end
def sync_attributes(project_namespace)
- project_namespace.name = name
- project_namespace.path = path
- project_namespace.parent = namespace
- project_namespace.shared_runners_enabled = shared_runners_enabled
- project_namespace.visibility_level = visibility_level
+ attributes_to_sync = changes.slice(*%w(name path namespace_id namespace visibility_level shared_runners_enabled))
+ .transform_values { |val| val[1] }
+
+ # if visibility_level is not set explicitly for project, it defaults to 0,
+ # but for namespace visibility_level defaults to 20,
+ # so it gets out of sync right away if we do not set it explicitly when creating the project namespace
+ attributes_to_sync['visibility_level'] ||= visibility_level if new_record?
+
+ # when a project is associated with a group while the group is created we need to ensure we associate the new
+ # group with the project namespace as well.
+ # E.g.
+ # project = create(:project) <- project is saved
+ # create(:group, projects: [project]) <- associate project with a group that is not yet created.
+ if attributes_to_sync.has_key?('namespace_id') && attributes_to_sync['namespace_id'].blank? && namespace.present?
+ attributes_to_sync['parent'] = namespace
+ end
+
+ project_namespace.assign_attributes(attributes_to_sync)
end
# SyncEvents are created by PG triggers (with the function `insert_projects_sync_event`)
@@ -3132,6 +3155,23 @@ class Project < ApplicationRecord
raise ExportLimitExceeded, _('The project size exceeds the export limit.')
end
end
+
+ def set_package_registry_access_level
+ return if !project_feature || project_feature.package_registry_access_level_changed?
+
+ self.project_feature.package_registry_access_level = packages_enabled ? enabled_package_registry_access_level_by_project_visibility : ProjectFeature::DISABLED
+ end
+
+ def enabled_package_registry_access_level_by_project_visibility
+ case visibility_level
+ when PUBLIC
+ ProjectFeature::PUBLIC
+ when INTERNAL
+ ProjectFeature::ENABLED
+ else
+ ProjectFeature::PRIVATE
+ end
+ end
end
Project.prepend_mod_with('Project')
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 27692fe76f0..f478af32788 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -20,6 +20,7 @@ class ProjectFeature < ApplicationRecord
operations
security_and_compliance
container_registry
+ package_registry
].freeze
EXPORTABLE_FEATURES = (FEATURES - [:security_and_compliance, :pages]).freeze
@@ -29,7 +30,8 @@ class ProjectFeature < ApplicationRecord
PRIVATE_FEATURES_MIN_ACCESS_LEVEL = {
merge_requests: Gitlab::Access::REPORTER,
metrics_dashboard: Gitlab::Access::REPORTER,
- container_registry: Gitlab::Access::REPORTER
+ container_registry: Gitlab::Access::REPORTER,
+ package_registry: Gitlab::Access::REPORTER
}.freeze
PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT = { repository: Gitlab::Access::REPORTER }.freeze
@@ -76,6 +78,14 @@ class ProjectFeature < ApplicationRecord
end
end
+ default_value_for(:package_registry_access_level) do |feature|
+ if ::Gitlab.config.packages.enabled
+ ENABLED
+ else
+ DISABLED
+ end
+ end
+
default_value_for(:container_registry_access_level) do |feature|
if gitlab_config_features.container_registry
ENABLED
@@ -142,6 +152,12 @@ class ProjectFeature < ApplicationRecord
!public_pages?
end
+ def package_registry_access_level=(value)
+ super(value).tap do
+ project.packages_enabled = self.package_registry_access_level != DISABLED if project
+ end
+ end
+
private
# Validates builds and merge requests access level
@@ -157,7 +173,7 @@ class ProjectFeature < ApplicationRecord
end
def feature_validation_exclusion
- %i(pages)
+ %i(pages package_registry)
end
override :resource_member?
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 95fc135f38f..a0af1b47d01 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -26,7 +26,7 @@ class ProjectStatistics < ApplicationRecord
pipeline_artifacts_size: %i[storage_size],
snippets_size: %i[storage_size]
}.freeze
- NAMESPACE_RELATABLE_COLUMNS = [:repository_size, :wiki_size, :lfs_objects_size, :uploads_size].freeze
+ NAMESPACE_RELATABLE_COLUMNS = [:repository_size, :wiki_size, :lfs_objects_size, :uploads_size, :container_registry_size].freeze
scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) }
@@ -77,8 +77,6 @@ class ProjectStatistics < ApplicationRecord
end
def update_container_registry_size
- return unless Feature.enabled?(:container_registry_project_statistics, project)
-
self.container_registry_size = project.container_repositories_size || 0
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index bb5363598df..97ab5aa2619 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -44,7 +44,7 @@ class ProjectTeam
end
def add_users(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
- Members::Projects::BulkCreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
+ Members::Projects::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
project,
users,
access_level,
@@ -56,12 +56,12 @@ class ProjectTeam
end
def add_user(user, access_level, current_user: nil, expires_at: nil)
- Members::Projects::CreatorService.new(project, # rubocop:disable CodeReuse/ServiceClass
- user,
- access_level,
- current_user: current_user,
- expires_at: expires_at)
- .execute
+ Members::Projects::CreatorService.add_user( # rubocop:disable CodeReuse/ServiceClass
+ project,
+ user,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at)
end
# Remove all users from project team
diff --git a/app/models/projects/build_artifacts_size_refresh.rb b/app/models/projects/build_artifacts_size_refresh.rb
index 959f486a50a..dee4afdefa6 100644
--- a/app/models/projects/build_artifacts_size_refresh.rb
+++ b/app/models/projects/build_artifacts_size_refresh.rb
@@ -36,6 +36,7 @@ module Projects
before_transition created: :running do |refresh|
refresh.reset_project_statistics!
refresh.refresh_started_at = Time.zone.now
+ refresh.last_job_artifact_id_on_refresh_start = refresh.project.job_artifacts.last&.id
end
before_transition running: any do |refresh, transition|
@@ -49,6 +50,7 @@ module Projects
scope :stale, -> { with_state(:running).where('updated_at < ?', STALE_WINDOW.ago) }
scope :remaining, -> { with_state(:created, :pending).or(stale) }
+ scope :processing_queue, -> { remaining.order(state: :desc) }
def self.enqueue_refresh(projects)
now = Time.zone.now
@@ -64,8 +66,7 @@ module Projects
next_refresh = nil
transaction do
- next_refresh = remaining
- .order(:state, :updated_at)
+ next_refresh = processing_queue
.lock('FOR UPDATE SKIP LOCKED')
.take
@@ -83,9 +84,14 @@ module Projects
def next_batch(limit:)
project.job_artifacts.select(:id, :size)
- .where('created_at <= ? AND id > ?', refresh_started_at, last_job_artifact_id.to_i)
- .order(:created_at)
+ .id_before(last_job_artifact_id_on_refresh_start)
+ .id_after(last_job_artifact_id.to_i)
+ .ordered_by_id
.limit(limit)
end
+
+ def started?
+ !created?
+ end
end
end
diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb
index 6b507429e57..5b2467daddc 100644
--- a/app/models/protected_tag.rb
+++ b/app/models/protected_tag.rb
@@ -8,7 +8,11 @@ class ProtectedTag < ApplicationRecord
protected_ref_access_levels :create
def self.protected?(project, ref_name)
- refs = project.protected_tags.select(:name)
+ return false if ref_name.blank?
+
+ refs = Gitlab::SafeRequestStore.fetch("protected-tag:#{project.cache_key}:refs") do
+ project.protected_tags.select(:name)
+ end
self.matching(ref_name, protected_refs: refs).present?
end
diff --git a/app/models/release.rb b/app/models/release.rb
index c6c0920c4d0..ee5d7bab190 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -31,6 +31,7 @@ class Release < ApplicationRecord
validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, if: :description_changed?
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
validates :links, nested_attributes_duplicates: { scope: :release, child_attributes: %i[name url filepath] }
+ validates :author_id, presence: true, on: :create, if: :validate_release_with_author?
scope :sorted, -> { order(released_at: :desc) }
scope :preloaded, -> {
@@ -54,7 +55,7 @@ class Release < ApplicationRecord
MAX_NUMBER_TO_DISPLAY = 3
def to_param
- CGI.escape(tag)
+ tag
end
def commit
@@ -117,6 +118,10 @@ class Release < ApplicationRecord
end
end
+ def validate_release_with_author?
+ Feature.enabled?(:validate_release_with_author, self.project)
+ end
+
def set_released_at
self.released_at ||= created_at
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index dc0b5b54fb0..0135020e586 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -13,6 +13,7 @@ class Repository
REF_KEEP_AROUND = 'keep-around'
REF_ENVIRONMENTS = 'environments'
REF_PIPELINES = 'pipelines'
+ REF_TMP = 'tmp'
ARCHIVE_CACHE_TIME = 60 # Cache archives referred to by a (mutable) ref for 1 minute
ARCHIVE_CACHE_TIME_IMMUTABLE = 3600 # Cache archives referred to by an immutable reference for 1 hour
@@ -175,8 +176,8 @@ class Repository
end
# Returns a list of commits that are not present in any reference
- def new_commits(newrev, allow_quarantine: false)
- commits = raw.new_commits(newrev, allow_quarantine: allow_quarantine)
+ def new_commits(newrev)
+ commits = raw.new_commits(newrev)
::Commit.decorate(commits, container)
end
diff --git a/app/models/resource_event.rb b/app/models/resource_event.rb
index 54fa4137f73..8b82e0f343c 100644
--- a/app/models/resource_event.rb
+++ b/app/models/resource_event.rb
@@ -11,7 +11,6 @@ class ResourceEvent < ApplicationRecord
belongs_to :user
scope :created_after, ->(time) { where('created_at > ?', time) }
- scope :created_on_or_before, ->(time) { where('created_at <= ?', time) }
def discussion_id
strong_memoize(:discussion_id) do
diff --git a/app/models/route.rb b/app/models/route.rb
index 12b2d5c5bb2..2f6b0a8e8f1 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -13,7 +13,6 @@ class Route < ApplicationRecord
presence: true,
uniqueness: { case_sensitive: false }
- before_validation :delete_conflicting_orphaned_routes
after_create :delete_conflicting_redirects
after_update :delete_conflicting_redirects, if: :saved_change_to_path?
after_update :create_redirect_for_old_path
@@ -71,13 +70,4 @@ class Route < ApplicationRecord
def create_redirect_for_old_path
create_redirect(path_before_last_save) if saved_change_to_path?
end
-
- def delete_conflicting_orphaned_routes
- conflicting = self.class.iwhere(path: path)
- conflicting_orphaned_routes = conflicting.select do |route|
- route.source.nil?
- end
-
- conflicting_orphaned_routes.each(&:destroy)
- end
end
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index 8c3b85ac4c3..4d17a4d332c 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -23,13 +23,10 @@ module Terraform
scope :ordered_by_name, -> { order(:name) }
scope :with_name, -> (name) { where(name: name) }
- validates :name, presence: true, uniqueness: { scope: :project_id }
- validates :project_id, presence: true
+ validates :project_id, :name, presence: true
validates :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH },
format: { with: HEX_REGEXP, message: 'only allows hex characters' }
- before_destroy :ensure_state_is_unlocked
-
default_value_for(:uuid, allows_nil: false) { SecureRandom.hex(UUID_LENGTH / 2) }
def latest_file
@@ -90,13 +87,6 @@ module Terraform
new_version.save!
end
- def ensure_state_is_unlocked
- return unless locked?
-
- errors.add(:base, s_("Terraform|You cannot remove the State file because it's locked. Unlock the State file first before removing it."))
- throw :abort # rubocop:disable Cop/BanCatchThrow
- end
-
def parse_serial(file)
Gitlab::Json.parse(file)["serial"]
rescue JSON::ParserError
diff --git a/app/models/terraform/state_version.rb b/app/models/terraform/state_version.rb
index 31ff7e4c27d..c50eaa66860 100644
--- a/app/models/terraform/state_version.rb
+++ b/app/models/terraform/state_version.rb
@@ -2,6 +2,7 @@
module Terraform
class StateVersion < ApplicationRecord
+ include EachBatch
include FileStoreMounter
belongs_to :terraform_state, class_name: 'Terraform::State', optional: false
diff --git a/app/models/time_tracking/timelog_category.rb b/app/models/time_tracking/timelog_category.rb
new file mode 100644
index 00000000000..26614f6fc44
--- /dev/null
+++ b/app/models/time_tracking/timelog_category.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module TimeTracking
+ class TimelogCategory < ApplicationRecord
+ include StripAttribute
+ include CaseSensitivity
+
+ self.table_name = "timelog_categories"
+
+ belongs_to :namespace, foreign_key: 'namespace_id'
+
+ strip_attributes! :name
+
+ validates :namespace, presence: true
+ validates :name, presence: true
+ validates :name, uniqueness: { case_sensitive: false, scope: [:namespace_id] }
+ validates :name, length: { maximum: 255 }
+ validates :description, length: { maximum: 1024 }
+ validates :color, color: true, allow_blank: false, length: { maximum: 7 }
+ validates :billing_rate,
+ if: :billable?,
+ presence: true,
+ numericality: { greater_than: 0 }
+
+ DEFAULT_COLOR = ::Gitlab::Color.of('#6699cc')
+
+ attribute :color, ::Gitlab::Database::Type::Color.new
+ default_value_for :color, DEFAULT_COLOR
+
+ def self.find_by_name(namespace_id, name)
+ where(namespace: namespace_id)
+ .iwhere(name: name)
+ end
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index b9a8e5855bf..c86fb56795c 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -90,6 +90,7 @@ class User < ApplicationRecord
include ForcedEmailConfirmation
MINIMUM_INACTIVE_DAYS = 90
+ MINIMUM_DAYS_CREATED = 7
# Override Devise::Models::Trackable#update_tracked_fields!
# to limit database writes to at most once every hour
@@ -338,7 +339,6 @@ class User < ApplicationRecord
delegate :path, to: :namespace, allow_nil: true, prefix: true
delegate :job_title, :job_title=, to: :user_detail, allow_nil: true
- delegate :other_role, :other_role=, to: :user_detail, allow_nil: true
delegate :bio, :bio=, 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
@@ -414,7 +414,9 @@ class User < ApplicationRecord
after_transition any => :deactivated do |user|
next unless Gitlab::CurrentSettings.user_deactivation_emails_enabled
- NotificationService.new.user_deactivated(user.name, user.notification_email_or_default)
+ user.run_after_commit do
+ NotificationService.new.user_deactivated(user.name, user.notification_email_or_default)
+ end
end
# rubocop: enable CodeReuse/ServiceClass
@@ -478,7 +480,7 @@ class User < ApplicationRecord
scope :order_oldest_last_activity, -> { reorder(arel_table[:last_activity_on].asc.nulls_first) }
scope :by_id_and_login, ->(id, login) { where(id: id).where('username = LOWER(:login) OR email = LOWER(:login)', login: login) }
scope :dormant, -> { with_state(:active).human_or_service_user.where('last_activity_on <= ?', MINIMUM_INACTIVE_DAYS.day.ago.to_date) }
- scope :with_no_activity, -> { with_state(:active).human_or_service_user.where(last_activity_on: nil) }
+ scope :with_no_activity, -> { with_state(:active).human_or_service_user.where(last_activity_on: nil).where('created_at <= ?', MINIMUM_DAYS_CREATED.day.ago.to_date) }
scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) }
scope :by_ids_or_usernames, -> (ids, usernames) { where(username: usernames).or(where(id: ids)) }
scope :without_forbidden_states, -> { where.not(state: FORBIDDEN_SEARCH_STATES) }
@@ -1657,33 +1659,15 @@ class User < ApplicationRecord
def ci_owned_runners
@ci_owned_runners ||= begin
- if ci_owned_runners_cross_joins_fix_enabled?
- Ci::Runner
- .from_union([ci_owned_project_runners_from_project_members,
- ci_owned_project_runners_from_group_members,
- ci_owned_group_runners])
- else
- Ci::Runner
- .from_union([ci_legacy_owned_project_runners, ci_legacy_owned_group_runners])
- .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336436')
- end
+ Ci::Runner
+ .from_union([ci_owned_project_runners_from_project_members,
+ ci_owned_project_runners_from_group_members,
+ ci_owned_group_runners])
end
end
def owns_runner?(runner)
- if ci_owned_runners_cross_joins_fix_enabled?
- ci_owned_runners.exists?(runner.id)
- else
- ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336436') do
- ci_owned_runners.exists?(runner.id)
- end
- end
- end
-
- def ci_owned_runners_cross_joins_fix_enabled?
- strong_memoize(:ci_owned_runners_cross_joins_fix_enabled) do
- Feature.enabled?(:ci_owned_runners_cross_joins_fix, self)
- end
+ ci_owned_runners.exists?(runner.id)
end
def notification_email_for(notification_group)
@@ -2265,20 +2249,6 @@ class User < ApplicationRecord
::Gitlab::Auth::Ldap::Access.allowed?(self)
end
- def ci_legacy_owned_project_runners
- Ci::RunnerProject
- .select('ci_runners.*')
- .joins(:runner)
- .where(project: authorized_projects(Gitlab::Access::MAINTAINER))
- end
-
- def ci_legacy_owned_group_runners
- Ci::RunnerNamespace
- .select('ci_runners.*')
- .joins(:runner)
- .where(namespace_id: owned_groups.self_and_descendant_ids)
- end
-
def ci_owned_project_runners_from_project_members
project_ids = project_members.where('access_level >= ?', Gitlab::Access::MAINTAINER).pluck(:source_id)
@@ -2334,12 +2304,7 @@ class User < ApplicationRecord
.merge(search_members)
.shortest_traversal_ids_prefixes
- # Use efficient btree index to perform search
- if Feature.enabled?(:ci_owned_runners_unnest_index, self)
- Ci::NamespaceMirror.contains_traversal_ids(traversal_ids)
- else
- Ci::NamespaceMirror.contains_any_of_namespaces(traversal_ids.map(&:last))
- end
+ Ci::NamespaceMirror.contains_traversal_ids(traversal_ids)
end
end
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index 3787ad1c380..b9b69d12729 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -2,6 +2,9 @@
class UserDetail < ApplicationRecord
extend ::Gitlab::Utils::Override
+ include IgnorableColumns
+
+ ignore_columns :other_role, remove_after: '2022-07-22', remove_with: '15.3'
REGISTRATION_OBJECTIVE_PAIRS = { basics: 0, move_repository: 1, code_storage: 2, exploring: 3, ci: 4, other: 5, joining_team: 6 }.freeze
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index b3729c84dd6..0ecae4d148a 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -51,12 +51,16 @@ module Users
attention_requests_side_nav: 48,
minute_limit_banner: 49,
preview_user_over_limit_free_plan_alert: 50, # EE-only
- user_reached_limit_free_plan_alert: 51 # EE-only
+ user_reached_limit_free_plan_alert: 51, # EE-only
+ submit_license_usage_data_banner: 52, # EE-only
+ personal_project_limitations_banner: 53 # EE-only
}
validates :feature_name,
presence: true,
uniqueness: { scope: :user_id },
inclusion: { in: Users::Callout.feature_names.keys }
+
+ scope :with_feature_name, -> (feature_name) { where(feature_name: feature_name) }
end
end
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index 32d70fcd3b7..c9cb3b0b796 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -227,32 +227,22 @@ class Wiki
end
def create_page(title, content, format = :markdown, message = nil)
- if Feature.enabled?(:gitaly_replace_wiki_create_page, container, type: :undefined)
- with_valid_format(format) do |default_extension|
- if file_exists_by_regex?(title)
- raise_duplicate_page_error!
- end
-
- capture_git_error(:created) do
- create_wiki_repository unless repository_exists?
- sanitized_path = sluggified_full_path(title, default_extension)
- repository.create_file(user, sanitized_path, content, **multi_commit_options(:created, message, title))
- repository.expire_status_cache if repository.empty?
- after_wiki_activity
-
- true
- rescue Gitlab::Git::Index::IndexError
- raise_duplicate_page_error!
- end
+ with_valid_format(format) do |default_extension|
+ if file_exists_by_regex?(title)
+ raise_duplicate_page_error!
end
- else
- commit = commit_details(:created, message, title)
- wiki.write_page(title, format.to_sym, content, commit)
- repository.expire_status_cache if repository.empty?
- after_wiki_activity
+ capture_git_error(:created) do
+ create_wiki_repository unless repository_exists?
+ sanitized_path = sluggified_full_path(title, default_extension)
+ repository.create_file(user, sanitized_path, content, **multi_commit_options(:created, message, title))
+ repository.expire_status_cache if repository.empty?
+ after_wiki_activity
- true
+ true
+ rescue Gitlab::Git::Index::IndexError
+ raise_duplicate_page_error!
+ end
end
rescue Gitlab::Git::Wiki::DuplicatePageError => e
@error_message = _("Duplicate page: %{error_message}" % { error_message: e.message })
@@ -395,17 +385,6 @@ class Wiki
}
end
- def commit_details(action, message = nil, title = nil)
- commit_message = build_commit_message(action, message, title)
- git_user = Gitlab::Git::User.from_gitlab(user)
-
- Gitlab::Git::Wiki::CommitDetails.new(user.id,
- git_user.username,
- git_user.name,
- git_user.email,
- commit_message)
- end
-
def build_commit_message(action, message, title)
message.presence || default_message(action, title)
end
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 557694da35a..bdd9aae90a4 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -4,10 +4,29 @@ class WorkItem < Issue
self.table_name = 'issues'
self.inheritance_column = :_type_disabled
+ has_one :parent_link, class_name: '::WorkItems::ParentLink', foreign_key: :work_item_id
+ has_one :work_item_parent, through: :parent_link, class_name: 'WorkItem'
+
+ has_many :child_links, class_name: '::WorkItems::ParentLink', foreign_key: :work_item_parent_id
+ has_many :work_item_children, through: :child_links, class_name: 'WorkItem',
+ foreign_key: :work_item_id, source: :work_item
+
+ scope :inc_relations_for_permission_check, -> { includes(:author, project: :project_feature) }
+
+ def self.assignee_association_name
+ 'issue'
+ end
+
def noteable_target_type_name
'issue'
end
+ def widgets
+ work_item_type.widgets.map do |widget_class|
+ widget_class.new(self)
+ end
+ end
+
private
def record_create_action
diff --git a/app/models/work_items/parent_link.rb b/app/models/work_items/parent_link.rb
new file mode 100644
index 00000000000..3c405dbce3b
--- /dev/null
+++ b/app/models/work_items/parent_link.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module WorkItems
+ class ParentLink < ApplicationRecord
+ self.table_name = 'work_item_parent_links'
+
+ MAX_CHILDREN = 100
+
+ belongs_to :work_item
+ belongs_to :work_item_parent, class_name: 'WorkItem'
+
+ validates :work_item, :work_item_parent, presence: true
+ validate :validate_child_type
+ validate :validate_parent_type
+ validate :validate_same_project
+ validate :validate_max_children
+
+ private
+
+ def validate_child_type
+ return unless work_item
+
+ unless work_item.task?
+ errors.add :work_item, _('Only Task can be assigned as a child in hierarchy.')
+ end
+ end
+
+ def validate_parent_type
+ return unless work_item_parent
+
+ unless work_item_parent.issue?
+ errors.add :work_item_parent, _('Only Issue can be parent of Task.')
+ end
+ end
+
+ def validate_same_project
+ return if work_item.nil? || work_item_parent.nil?
+
+ if work_item.resource_parent != work_item_parent.resource_parent
+ errors.add :work_item_parent, _('Parent must be in the same project as child.')
+ end
+ end
+
+ def validate_max_children
+ return unless work_item_parent
+
+ max = persisted? ? MAX_CHILDREN : MAX_CHILDREN - 1
+ if work_item_parent.child_links.count > max
+ errors.add :work_item_parent, _('Parent already has maximum number of children.')
+ end
+ end
+ end
+end
diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb
index 0d390fa131d..bf251a3ade5 100644
--- a/app/models/work_items/type.rb
+++ b/app/models/work_items/type.rb
@@ -20,6 +20,14 @@ module WorkItems
task: { name: 'Task', icon_name: 'issue-type-task', enum_value: 4 }
}.freeze
+ WIDGETS_FOR_TYPE = {
+ issue: [Widgets::Description, Widgets::Hierarchy],
+ incident: [Widgets::Description],
+ test_case: [Widgets::Description],
+ requirement: [Widgets::Description],
+ task: [Widgets::Description, Widgets::Hierarchy]
+ }.freeze
+
cache_markdown_field :description, pipeline: :single_line
enum base_type: BASE_TYPES.transform_values { |value| value[:enum_value] }
@@ -40,6 +48,10 @@ module WorkItems
scope :order_by_name_asc, -> { order(arel_table[:name].lower.asc) }
scope :by_type, ->(base_type) { where(base_type: base_type) }
+ def self.available_widgets
+ WIDGETS_FOR_TYPE.values.flatten.uniq
+ end
+
def self.default_by_type(type)
found_type = find_by(namespace_id: nil, base_type: type)
return found_type if found_type
@@ -60,6 +72,10 @@ module WorkItems
namespace.blank?
end
+ def widgets
+ WIDGETS_FOR_TYPE[base_type.to_sym]
+ end
+
private
def strip_whitespace
diff --git a/app/models/work_items/widgets/base.rb b/app/models/work_items/widgets/base.rb
new file mode 100644
index 00000000000..e7075a7a0e8
--- /dev/null
+++ b/app/models/work_items/widgets/base.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module WorkItems
+ module Widgets
+ class Base
+ def self.type
+ name.demodulize.underscore.to_sym
+ end
+
+ def self.api_symbol
+ "#{type}_widget".to_sym
+ end
+
+ def type
+ self.class.type
+ end
+
+ def initialize(work_item)
+ @work_item = work_item
+ end
+
+ attr_reader :work_item
+ end
+ end
+end
diff --git a/app/models/work_items/widgets/description.rb b/app/models/work_items/widgets/description.rb
new file mode 100644
index 00000000000..35b6d295321
--- /dev/null
+++ b/app/models/work_items/widgets/description.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module WorkItems
+ module Widgets
+ class Description < Base
+ delegate :description, to: :work_item
+
+ def update(params:)
+ work_item.description = params[:description] if params&.key?(:description)
+ end
+ end
+ end
+end
diff --git a/app/models/work_items/widgets/hierarchy.rb b/app/models/work_items/widgets/hierarchy.rb
new file mode 100644
index 00000000000..dadd341de83
--- /dev/null
+++ b/app/models/work_items/widgets/hierarchy.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module WorkItems
+ module Widgets
+ class Hierarchy < Base
+ def parent
+ return unless Feature.enabled?(:work_items_hierarchy, work_item.project)
+
+ work_item.work_item_parent
+ end
+
+ def children
+ return WorkItem.none unless Feature.enabled?(:work_items_hierarchy, work_item.project)
+
+ work_item.work_item_children
+ end
+ end
+ end
+end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 9aae295aea7..6ca30ba5dab 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -76,7 +76,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
with_scope :subject
condition(:has_project_with_service_desk_enabled) { @subject.has_project_with_service_desk_enabled? }
- condition(:crm_enabled, score: 0, scope: :subject) { Feature.enabled?(:customer_relations, @subject) && @subject.crm_enabled? }
+ condition(:crm_enabled, score: 0, scope: :subject) { @subject.crm_enabled? }
condition(:group_runner_registration_allowed) do
Feature.disabled?(:runner_registration_control) || Gitlab::CurrentSettings.valid_runner_registrars.include?('group')
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index 4e6df79773e..f1efcb25331 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -13,7 +13,9 @@ class IssuablePolicy < BasePolicy
condition(:is_author) { @subject&.author == @user }
- rule { can?(:guest_access) & assignee_or_author }.policy do
+ condition(:is_incident) { @subject.incident? }
+
+ rule { can?(:guest_access) & assignee_or_author & ~is_incident }.policy do
enable :read_issue
enable :update_issue
enable :reopen_issue
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index a341d1ef661..2b6dcc56fa0 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -15,7 +15,7 @@ class IssuePolicy < IssuablePolicy
desc "Project belongs to a group, crm is enabled and user can read contacts in the root group"
condition(:can_read_crm_contacts, scope: :subject) do
subject.project.group&.crm_enabled? &&
- @user.can?(:read_crm_contact, @subject.project.root_ancestor)
+ (@user&.can?(:read_crm_contact, @subject.project.root_ancestor) || @user&.support_bot?)
end
desc "Issue is confidential"
diff --git a/app/policies/packages/cleanup/policy_policy.rb b/app/policies/packages/cleanup/policy_policy.rb
new file mode 100644
index 00000000000..6c2aacef174
--- /dev/null
+++ b/app/policies/packages/cleanup/policy_policy.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Packages
+ module Cleanup
+ class PolicyPolicy < BasePolicy
+ delegate { @subject.project }
+ end
+ end
+end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 7c439fe8b29..3bce26be756 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -4,12 +4,6 @@ class ProjectPolicy < BasePolicy
include CrudPolicyHelpers
include ReadonlyAbilities
- desc "User is a project owner"
- condition :owner do
- (project.owner.present? && project.owner == @user) ||
- project.group&.has_owner?(@user)
- end
-
desc "Project has public builds enabled"
condition(:public_builds, scope: :subject, score: 0) { project.public_builds? }
@@ -30,6 +24,17 @@ class ProjectPolicy < BasePolicy
desc "User has maintainer access"
condition(:maintainer) { team_access_level >= Gitlab::Access::MAINTAINER }
+ desc "User has owner access"
+ condition :owner do
+ owner_of_personal_namespace = project.owner.present? && project.owner == @user
+
+ unless owner_of_personal_namespace
+ group_or_project_owner = team_access_level >= Gitlab::Access::OWNER
+ end
+
+ owner_of_personal_namespace || group_or_project_owner
+ end
+
desc "User is a project bot"
condition(:project_bot) { user.project_bot? && team_member? }
@@ -198,6 +203,10 @@ class ProjectPolicy < BasePolicy
Feature.disabled?(:runner_registration_control) || Gitlab::CurrentSettings.valid_runner_registrars.include?('project')
end
+ condition :registry_enabled do
+ Gitlab.config.registry.enabled
+ end
+
# `:read_project` may be prevented in EE, but `:read_project_for_iids` should
# not.
rule { guest | admin }.enable :read_project_for_iids
@@ -236,6 +245,7 @@ class ProjectPolicy < BasePolicy
enable :set_warn_about_potentially_unwanted_characters
enable :register_project_runners
+ enable :manage_owners
end
rule { can?(:guest_access) }.policy do
@@ -423,6 +433,7 @@ class ProjectPolicy < BasePolicy
rule { can?(:maintainer_access) }.policy do
enable :destroy_package
+ enable :admin_package
enable :admin_issue_board
enable :push_to_delete_protected_branch
enable :update_snippet
@@ -658,6 +669,7 @@ class ProjectPolicy < BasePolicy
enable :read_design
enable :read_design_activity
enable :read_issue_link
+ enable :read_work_item
end
rule { can?(:developer_access) }.policy do
@@ -752,6 +764,10 @@ class ProjectPolicy < BasePolicy
enable :import_project_members_from_another_project
end
+ rule { registry_enabled & can?(:admin_container_image) }.policy do
+ enable :view_package_registry_project_settings
+ end
+
private
def user_is_user?
diff --git a/app/policies/work_item_policy.rb b/app/policies/work_item_policy.rb
index e191e8d26ca..ea7559592e1 100644
--- a/app/policies/work_item_policy.rb
+++ b/app/policies/work_item_policy.rb
@@ -8,4 +8,9 @@ class WorkItemPolicy < IssuePolicy
rule { can?(:update_issue) }.enable :update_work_item
rule { can?(:read_issue) }.enable :read_work_item
+ # because IssuePolicy delegates to ProjectPolicy and
+ # :read_work_item is enabled in ProjectPolicy too, we
+ # need to make sure we also prevent this rule if read_issue
+ # is prevented
+ rule { ~can?(:read_issue) }.prevent :read_work_item
end
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index aeab914dc9e..2dcc6cd5df3 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -27,11 +27,11 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
end
def blob_data(to)
- @_blob_data ||= Gitlab::Diff::CustomDiff.transformed_blob_data(blob) || limited_blob_data(to: to)
+ @_blob_data ||= limited_blob_data(to: to)
end
def blob_language
- @_blob_language ||= Gitlab::Diff::CustomDiff.transformed_blob_language(blob) || gitattr_language || detect_language
+ @_blob_language ||= gitattr_language || detect_language
end
def raw_plain_data
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index 410b633df50..887980430f4 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -17,8 +17,7 @@ module Ci
size_limit_exceeded: 'The pipeline size limit was exceeded.',
job_activity_limit_exceeded: 'The pipeline job activity limit was exceeded.',
deployments_limit_exceeded: 'The pipeline deployments limit was exceeded.',
- project_deleted: 'The project associated with this pipeline was deleted.',
- user_blocked: 'The user who created this pipeline is blocked.' }
+ project_deleted: 'The project associated with this pipeline was deleted.' }
end
presents ::Ci::Pipeline, as: :pipeline
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 6dd3908b21d..efab1e84923 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -191,13 +191,17 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def mergeable_discussions_state
- # This avoids calling MergeRequest#mergeable_discussions_state without
- # considering the state of the MR first. If a MR isn't mergeable, we can
- # safely short-circuit it.
- if merge_request.mergeable_state?(skip_ci_check: true, skip_discussions_check: true)
+ if Feature.enabled?(:change_response_code_merge_status, project)
merge_request.mergeable_discussions_state?
else
- false
+ # This avoids calling MergeRequest#mergeable_discussions_state without
+ # considering the state of the MR first. If a MR isn't mergeable, we can
+ # safely short-circuit it.
+ if merge_request.mergeable_state?(skip_ci_check: true, skip_discussions_check: true)
+ merge_request.mergeable_discussions_state?
+ else
+ false
+ end
end
end
diff --git a/app/presenters/packages/pypi/package_presenter.rb b/app/presenters/packages/pypi/package_presenter.rb
deleted file mode 100644
index a779ce41cf9..00000000000
--- a/app/presenters/packages/pypi/package_presenter.rb
+++ /dev/null
@@ -1,96 +0,0 @@
-# frozen_string_literal: true
-
-# Display package version data acording to PyPI
-# Simple API: https://warehouse.pypa.io/api-reference/legacy/#simple-project-api
-module Packages
- module Pypi
- class PackagePresenter
- include API::Helpers::RelatedResourcesHelpers
-
- def initialize(packages, project_or_group)
- @packages = packages
- @project_or_group = project_or_group
- end
-
- # Returns the HTML body for PyPI simple API.
- # Basically a list of package download links for a specific
- # package
- def body
- <<-HTML
- <!DOCTYPE html>
- <html>
- <head>
- <title>Links for #{escape(name)}</title>
- </head>
- <body>
- <h1>Links for #{escape(name)}</h1>
- #{links}
- </body>
- </html>
- HTML
- end
-
- private
-
- def links
- refs = []
-
- @packages.map do |package|
- package_files = package.installable_package_files
-
- package_files.each do |file|
- url = build_pypi_package_path(file)
-
- refs << package_link(url, package.pypi_metadatum.required_python, file.file_name)
- end
- end
-
- refs.join
- end
-
- def package_link(url, required_python, filename)
- "<a href=\"#{url}\" data-requires-python=\"#{escape(required_python)}\">#{filename}</a><br>"
- end
-
- def build_pypi_package_path(file)
- params = {
- id: @project_or_group.id,
- sha256: file.file_sha256,
- file_identifier: file.file_name
- }
-
- if project?
- expose_url(
- api_v4_projects_packages_pypi_files_file_identifier_path(
- params, true
- )
- ) + "#sha256=#{file.file_sha256}"
- elsif group?
- expose_url(
- api_v4_groups___packages_pypi_files_file_identifier_path(
- params, true
- )
- ) + "#sha256=#{file.file_sha256}"
- else
- ''
- end
- end
-
- def name
- @packages.first.name
- end
-
- def escape(str)
- ERB::Util.html_escape(str)
- end
-
- def project?
- @project_or_group.is_a?(::Project)
- end
-
- def group?
- @project_or_group.is_a?(::Group)
- end
- end
- end
-end
diff --git a/app/presenters/packages/pypi/simple_index_presenter.rb b/app/presenters/packages/pypi/simple_index_presenter.rb
new file mode 100644
index 00000000000..ffe4eeb9585
--- /dev/null
+++ b/app/presenters/packages/pypi/simple_index_presenter.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+# Display package repository index acording to PyPI
+# Simple API: https://peps.python.org/pep-0503/
+module Packages
+ module Pypi
+ class SimpleIndexPresenter < SimplePresenterBase
+ private
+
+ def links
+ refs = []
+
+ available_packages.each_batch do |batch|
+ batch.each do |package|
+ url = build_pypi_package_path(package)
+
+ refs << package_link(url, package.pypi_metadatum.required_python, package.name)
+ end
+ end
+
+ refs.join
+ end
+
+ def build_pypi_package_path(package)
+ params = {
+ id: @project_or_group.id,
+ package_name: package.normalized_pypi_name
+ }
+
+ if project?
+ expose_url(
+ api_v4_projects_packages_pypi_simple_package_name_path(
+ params, true
+ )
+ )
+ elsif group?
+ expose_url(
+ api_v4_groups___packages_pypi_simple_package_name_path(
+ params, true
+ )
+ )
+ end
+ end
+
+ def body_name
+ @project_or_group.name
+ end
+ end
+ end
+end
diff --git a/app/presenters/packages/pypi/simple_package_versions_presenter.rb b/app/presenters/packages/pypi/simple_package_versions_presenter.rb
new file mode 100644
index 00000000000..0baa0714463
--- /dev/null
+++ b/app/presenters/packages/pypi/simple_package_versions_presenter.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+# Display package version data acording to PyPI
+# Simple API: https://warehouse.pypa.io/api-reference/legacy/#simple-project-api
+# Generates the HTML body for PyPI simple API.
+# Basically a list of package download links for a specific
+# package
+module Packages
+ module Pypi
+ class SimplePackageVersionsPresenter < SimplePresenterBase
+ private
+
+ def links
+ refs = []
+
+ available_packages.each_batch do |batch|
+ batch.each do |package|
+ package_files = package.installable_package_files
+
+ package_files.each do |file|
+ url = build_pypi_package_file_path(file)
+
+ refs << package_link(url, package.pypi_metadatum.required_python, file.file_name)
+ end
+ end
+ end
+
+ refs.join
+ end
+
+ def build_pypi_package_file_path(file)
+ params = {
+ id: @project_or_group.id,
+ sha256: file.file_sha256,
+ file_identifier: file.file_name
+ }
+
+ if project?
+ expose_url(
+ api_v4_projects_packages_pypi_files_file_identifier_path(
+ params, true
+ )
+ ) + "#sha256=#{file.file_sha256}"
+ elsif group?
+ expose_url(
+ api_v4_groups___packages_pypi_files_file_identifier_path(
+ params, true
+ )
+ ) + "#sha256=#{file.file_sha256}"
+ end
+ end
+
+ def body_name
+ @packages.first.name
+ end
+ end
+ end
+end
diff --git a/app/presenters/packages/pypi/simple_presenter_base.rb b/app/presenters/packages/pypi/simple_presenter_base.rb
new file mode 100644
index 00000000000..a459319539c
--- /dev/null
+++ b/app/presenters/packages/pypi/simple_presenter_base.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+# Display package version data acording to PyPI
+# Simple API: https://warehouse.pypa.io/api-reference/legacy/#simple-project-api
+module Packages
+ module Pypi
+ class SimplePresenterBase
+ include API::Helpers::RelatedResourcesHelpers
+
+ def initialize(packages, project_or_group)
+ @packages = packages
+ @project_or_group = project_or_group
+ end
+
+ def body
+ <<-HTML
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Links for #{escape(body_name)}</title>
+ </head>
+ <body>
+ <h1>Links for #{escape(body_name)}</h1>
+ #{links}
+ </body>
+ </html>
+ HTML
+ end
+
+ private
+
+ def package_link(url, required_python, name)
+ "<a href=\"#{url}\" data-requires-python=\"#{escape(required_python)}\">#{name}</a>"
+ end
+
+ def escape(str)
+ ERB::Util.html_escape(str)
+ end
+
+ def project?
+ @project_or_group.is_a?(::Project)
+ end
+
+ def group?
+ @project_or_group.is_a?(::Group)
+ end
+
+ def available_packages
+ @packages.not_pending_destruction
+ end
+ end
+ end
+end
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index af1b254c46f..84aec19cba0 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -28,7 +28,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
commits_anchor_data,
branches_anchor_data,
tags_anchor_data,
- files_anchor_data,
storage_anchor_data,
releases_anchor_data
].compact.select(&:is_link)
@@ -161,26 +160,16 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
can_current_user_push_to_branch?(default_branch)
end
- def files_anchor_data
- AnchorData.new(true,
- statistic_icon('doc-code') +
- _('%{strong_start}%{human_size}%{strong_end} Files').html_safe % {
- human_size: storage_counter(statistics.total_repository_size),
- strong_start: '<strong class="project-stat-value">'.html_safe,
- strong_end: '</strong>'.html_safe
- },
- empty_repo? ? nil : project_tree_path(project))
- end
-
def storage_anchor_data
+ can_show_quota = can?(current_user, :admin_project, project) && !empty_repo?
AnchorData.new(true,
statistic_icon('disk') +
- _('%{strong_start}%{human_size}%{strong_end} Storage').html_safe % {
+ _('%{strong_start}%{human_size}%{strong_end} Project Storage').html_safe % {
human_size: storage_counter(statistics.storage_size),
strong_start: '<strong class="project-stat-value">'.html_safe,
strong_end: '</strong>'.html_safe
},
- empty_repo? ? nil : project_tree_path(project))
+ can_show_quota ? project_usage_quotas_path(project) : nil)
end
def releases_anchor_data
diff --git a/app/presenters/projects/security/configuration_presenter.rb b/app/presenters/projects/security/configuration_presenter.rb
index 772be0125a0..8a6569e7bf3 100644
--- a/app/presenters/projects/security/configuration_presenter.rb
+++ b/app/presenters/projects/security/configuration_presenter.rb
@@ -24,7 +24,8 @@ module Projects
gitlab_ci_history_path: gitlab_ci_history_path,
auto_fix_enabled: autofix_enabled,
can_toggle_auto_fix_settings: can_toggle_autofix,
- auto_fix_user_path: auto_fix_user_path
+ auto_fix_user_path: auto_fix_user_path,
+ security_training_enabled: project.security_training_available?
}
end
diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb
index e3323b75188..b760786aa4c 100644
--- a/app/presenters/projects/settings/deploy_keys_presenter.rb
+++ b/app/presenters/projects/settings/deploy_keys_presenter.rb
@@ -58,7 +58,7 @@ module Projects
end
def as_json
- serializer = DeployKeySerializer.new # rubocop: disable CodeReuse/Serializer
+ serializer = DeployKeys::DeployKeySerializer.new # rubocop: disable CodeReuse/Serializer
opts = { user: current_user, project: project, readable_project_ids: readable_project_ids }
{
diff --git a/app/presenters/releases/link_presenter.rb b/app/presenters/releases/link_presenter.rb
new file mode 100644
index 00000000000..cc89762a922
--- /dev/null
+++ b/app/presenters/releases/link_presenter.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Releases
+ class LinkPresenter < Gitlab::View::Presenter::Delegated
+ def direct_asset_url
+ return @subject.url unless @subject.filepath
+
+ release = @subject.release.present
+ release.download_url(@subject.filepath)
+ end
+ end
+end
diff --git a/app/presenters/service_hook_presenter.rb b/app/presenters/service_hook_presenter.rb
index f2a06358918..b34679c85cf 100644
--- a/app/presenters/service_hook_presenter.rb
+++ b/app/presenters/service_hook_presenter.rb
@@ -4,10 +4,10 @@ class ServiceHookPresenter < Gitlab::View::Presenter::Delegated
presents ::ServiceHook, as: :service_hook
def logs_details_path(log)
- project_integration_hook_log_path(integration.project, integration, log)
+ project_settings_integration_hook_log_path(integration.project, integration, log)
end
def logs_retry_path(log)
- retry_project_integration_hook_log_path(integration.project, integration, log)
+ retry_project_settings_integration_hook_log_path(integration.project, integration, log)
end
end
diff --git a/app/presenters/snippet_blob_presenter.rb b/app/presenters/snippet_blob_presenter.rb
index 51ce6ccea58..2e5d3ae21d9 100644
--- a/app/presenters/snippet_blob_presenter.rb
+++ b/app/presenters/snippet_blob_presenter.rb
@@ -3,6 +3,8 @@
class SnippetBlobPresenter < BlobPresenter
include GitlabRoutingHelper
+ delegator_override_with Gitlab::Utils::StrongMemoize # This module inclusion is expected. See https://gitlab.com/gitlab-org/gitlab/-/issues/352884.
+
presents ::SnippetBlob
def rich_data
diff --git a/app/serializers/analytics_issue_entity.rb b/app/serializers/analytics_issue_entity.rb
index a0d6d120a48..2e3e32faef6 100644
--- a/app/serializers/analytics_issue_entity.rb
+++ b/app/serializers/analytics_issue_entity.rb
@@ -30,6 +30,10 @@ class AnalyticsIssueEntity < Grape::Entity
url_to(:namespace_project_issue, object)
end
+ expose :end_event_timestamp do |object|
+ object[:end_event_timestamp] && interval_in_words(object[:end_event_timestamp])
+ end
+
private
def url_to(route, object)
diff --git a/app/serializers/deploy_key_entity.rb b/app/serializers/deploy_key_entity.rb
deleted file mode 100644
index 486189b84ca..00000000000
--- a/app/serializers/deploy_key_entity.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-class DeployKeyEntity < Grape::Entity
- expose :id
- expose :user_id
- expose :title
- expose :fingerprint
- expose :fingerprint_sha256
- expose :destroyed_when_orphaned?, as: :destroyed_when_orphaned
- expose :almost_orphaned?, as: :almost_orphaned
- expose :created_at
- expose :updated_at
- expose :deploy_keys_projects, using: DeployKeysProjectEntity do |deploy_key|
- deploy_key.deploy_keys_projects.select do |deploy_key_project|
- !deploy_key_project.project&.pending_delete? && (allowed_to_read_project?(deploy_key_project.project) || options[:user].admin?)
- end
- end
- expose :can_edit
- expose :user, as: :owner, using: ::API::Entities::UserBasic, if: -> (_, opts) { can_read_owner?(opts) }
-
- private
-
- def can_edit
- Ability.allowed?(options[:user], :update_deploy_key, object) ||
- Ability.allowed?(options[:user], :update_deploy_keys_project, object.deploy_keys_project_for(options[:project]))
- end
-
- def can_read_owner?(opts)
- opts[:with_owner] && Ability.allowed?(options[:user], :read_user, object.user)
- end
-
- def allowed_to_read_project?(project)
- if options[:readable_project_ids]
- options[:readable_project_ids].include?(project.id)
- else
- Ability.allowed?(options[:user], :read_project, project)
- end
- end
-end
diff --git a/app/serializers/deploy_key_serializer.rb b/app/serializers/deploy_key_serializer.rb
deleted file mode 100644
index a1cd98b631b..00000000000
--- a/app/serializers/deploy_key_serializer.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-class DeployKeySerializer < BaseSerializer
- entity DeployKeyEntity
-end
diff --git a/app/serializers/deploy_keys/basic_deploy_key_entity.rb b/app/serializers/deploy_keys/basic_deploy_key_entity.rb
new file mode 100644
index 00000000000..9184bc5f0ce
--- /dev/null
+++ b/app/serializers/deploy_keys/basic_deploy_key_entity.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module DeployKeys
+ class BasicDeployKeyEntity < Grape::Entity
+ expose :id
+ expose :user_id
+ expose :title
+ expose :fingerprint
+ expose :fingerprint_sha256
+ expose :destroyed_when_orphaned?, as: :destroyed_when_orphaned
+ expose :almost_orphaned?, as: :almost_orphaned
+ expose :created_at
+ expose :updated_at
+ expose :can_edit
+ expose :user, as: :owner, using: ::API::Entities::UserBasic, if: -> (_, opts) { can_read_owner?(opts) }
+
+ private
+
+ def can_edit
+ Ability.allowed?(options[:user], :update_deploy_key, object) ||
+ Ability.allowed?(options[:user], :update_deploy_keys_project, object.deploy_keys_project_for(options[:project]))
+ end
+
+ def can_read_owner?(opts)
+ opts[:with_owner] && Ability.allowed?(options[:user], :read_user, object.user)
+ end
+ end
+end
diff --git a/app/serializers/deploy_keys/basic_deploy_key_serializer.rb b/app/serializers/deploy_keys/basic_deploy_key_serializer.rb
new file mode 100644
index 00000000000..699f3baac78
--- /dev/null
+++ b/app/serializers/deploy_keys/basic_deploy_key_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module DeployKeys
+ class BasicDeployKeySerializer < BaseSerializer
+ entity BasicDeployKeyEntity
+ end
+end
diff --git a/app/serializers/deploy_keys/deploy_key_entity.rb b/app/serializers/deploy_keys/deploy_key_entity.rb
new file mode 100644
index 00000000000..79f386d1529
--- /dev/null
+++ b/app/serializers/deploy_keys/deploy_key_entity.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module DeployKeys
+ class DeployKeyEntity < BasicDeployKeyEntity
+ expose :deploy_keys_projects, using: DeployKeysProjectEntity do |deploy_key|
+ deploy_key.deploy_keys_projects.select do |deploy_key_project|
+ !deploy_key_project.project&.pending_delete? &&
+ (allowed_to_read_project?(deploy_key_project.project) || options[:user].can_admin_all_resources?)
+ end
+ end
+
+ private
+
+ def allowed_to_read_project?(project)
+ if options[:readable_project_ids]
+ options[:readable_project_ids].include?(project.id)
+ else
+ Ability.allowed?(options[:user], :read_project, project)
+ end
+ end
+ end
+end
diff --git a/app/serializers/deploy_keys/deploy_key_serializer.rb b/app/serializers/deploy_keys/deploy_key_serializer.rb
new file mode 100644
index 00000000000..b00ef65696f
--- /dev/null
+++ b/app/serializers/deploy_keys/deploy_key_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module DeployKeys
+ class DeployKeySerializer < BaseSerializer
+ entity DeployKeyEntity
+ end
+end
diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb
index ef856ee0116..9f8628fe849 100644
--- a/app/serializers/diff_file_entity.rb
+++ b/app/serializers/diff_file_entity.rb
@@ -56,8 +56,7 @@ class DiffFileEntity < DiffFileBaseEntity
# Used for inline diffs
expose :highlighted_diff_lines, using: DiffLineEntity, if: -> (diff_file, options) { inline_diff_view?(options) && diff_file.text? } do |diff_file|
- file = conflict_file(options, diff_file) || diff_file
- file.diff_lines_for_serializer
+ highlighted_diff_lines_for(diff_file, options)
end
expose :is_fully_expanded do |diff_file|
@@ -89,6 +88,15 @@ class DiffFileEntity < DiffFileBaseEntity
# If nothing is present, inline will be the default.
options.fetch(:diff_view, :inline).to_sym
end
+
+ def highlighted_diff_lines_for(diff_file, options)
+ file = conflict_file(options, diff_file) || diff_file
+
+ file.diff_lines_for_serializer
+ rescue Gitlab::Git::Conflict::Parser::UnmergeableFile
+ # Fallback to diff_file as it means that conflict lines can't be parsed due to limit
+ diff_file.diff_lines_for_serializer
+ end
end
DiffFileEntity.prepend_mod
diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb
index b13140efea7..3f236fa55df 100644
--- a/app/serializers/environment_serializer.rb
+++ b/app/serializers/environment_serializer.rb
@@ -52,19 +52,30 @@ class EnvironmentSerializer < BaseSerializer
end
def batch_load(resource)
+ temp_deployment_associations = deployment_associations
+
resource = resource.preload(environment_associations.except(:last_deployment, :upcoming_deployment))
+ if ::Feature.enabled?(:batch_load_environment_last_deployment_group, resource.first&.project)
+ temp_deployment_associations[:deployable][:pipeline][:latest_successful_builds] = []
+ end
+
Preloaders::Environments::DeploymentPreloader.new(resource)
- .execute_with_union(:last_deployment, deployment_associations)
+ .execute_with_union(:last_deployment, temp_deployment_associations)
Preloaders::Environments::DeploymentPreloader.new(resource)
- .execute_with_union(:upcoming_deployment, deployment_associations)
+ .execute_with_union(:upcoming_deployment, temp_deployment_associations)
resource.to_a.tap do |environments|
environments.each do |environment|
# Batch loading the commits of the deployments
environment.last_deployment&.commit&.try(:lazy_author)
environment.upcoming_deployment&.commit&.try(:lazy_author)
+
+ if ::Feature.enabled?(:batch_load_environment_last_deployment_group, environment.project)
+ # Batch loading last_deployment_group which is called later by environment.stop_actions
+ environment.last_deployment_group
+ end
end
end
end
@@ -89,10 +100,11 @@ class EnvironmentSerializer < BaseSerializer
user: [],
metadata: [],
pipeline: {
- manual_actions: [],
- scheduled_actions: []
+ manual_actions: [:metadata, :deployment],
+ scheduled_actions: [:metadata]
},
- project: project_associations
+ project: project_associations,
+ deployment: []
}
}
end
diff --git a/app/serializers/integrations/event_entity.rb b/app/serializers/integrations/event_entity.rb
new file mode 100644
index 00000000000..170f660f334
--- /dev/null
+++ b/app/serializers/integrations/event_entity.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Integrations
+ class EventEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :title do |event|
+ IntegrationsHelper.integration_event_title(event)
+ end
+
+ expose :event_field_name, as: :name
+
+ expose :value do |event|
+ integration[event_field_name]
+ end
+
+ expose :description do |event|
+ IntegrationsHelper.integration_event_description(integration, event)
+ end
+
+ expose :field, if: ->(_, _) { event_field } do
+ expose :name do |event|
+ event_field[:name]
+ end
+ expose :value do |event|
+ integration.public_send(event_field[:name]) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ private
+
+ alias_method :event, :object
+
+ def event_field_name
+ IntegrationsHelper.integration_event_field_name(event)
+ end
+
+ def event_field
+ @event_field ||= integration.event_field(event)
+ end
+
+ def integration
+ request.integration
+ end
+ end
+end
diff --git a/app/serializers/integrations/event_serializer.rb b/app/serializers/integrations/event_serializer.rb
new file mode 100644
index 00000000000..fab7f9d459f
--- /dev/null
+++ b/app/serializers/integrations/event_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Integrations
+ class EventSerializer < BaseSerializer
+ entity Integrations::EventEntity
+ end
+end
diff --git a/app/serializers/integrations/field_entity.rb b/app/serializers/integrations/field_entity.rb
new file mode 100644
index 00000000000..697b53a737e
--- /dev/null
+++ b/app/serializers/integrations/field_entity.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Integrations
+ class FieldEntity < Grape::Entity
+ include RequestAwareEntity
+ include Gitlab::Utils::StrongMemoize
+
+ expose :section, :type, :name, :placeholder, :required, :choices, :checkbox_label
+
+ expose :title do |field|
+ non_empty_password?(field) ? field[:non_empty_password_title] : field[:title]
+ end
+
+ expose :help do |field|
+ non_empty_password?(field) ? field[:non_empty_password_help] : field[:help]
+ end
+
+ expose :value do |field|
+ value = value_for(field)
+
+ if non_empty_password?(field)
+ 'true'
+ elsif field[:type] == 'checkbox'
+ ActiveRecord::Type::Boolean.new.deserialize(value).to_s
+ else
+ value
+ end
+ end
+
+ private
+
+ def integration
+ request.integration
+ end
+
+ def value_for(field)
+ strong_memoize(:value_for) do
+ # field[:name] is not user input and so can assume is safe
+ integration.public_send(field[:name]) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ def non_empty_password?(field)
+ strong_memoize(:non_empty_password) do
+ field[:type] == 'password' && value_for(field).present?
+ end
+ end
+ end
+end
diff --git a/app/serializers/integrations/field_serializer.rb b/app/serializers/integrations/field_serializer.rb
new file mode 100644
index 00000000000..c8f9823e997
--- /dev/null
+++ b/app/serializers/integrations/field_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Integrations
+ class FieldSerializer < BaseSerializer
+ entity Integrations::FieldEntity
+ end
+end
diff --git a/app/serializers/issue_board_entity.rb b/app/serializers/issue_board_entity.rb
index bcad28d6aad..ebd0f037160 100644
--- a/app/serializers/issue_board_entity.rb
+++ b/app/serializers/issue_board_entity.rb
@@ -3,6 +3,10 @@
class IssueBoardEntity < Grape::Entity
include RequestAwareEntity
+ format_with(:upcase) do |item|
+ item.try(:upcase)
+ end
+
expose :id
expose :iid
expose :title
@@ -51,6 +55,11 @@ class IssueBoardEntity < Grape::Entity
expose :assignable_labels_endpoint, if: -> (issue) { issue.project } do |issue|
project_labels_path(issue.project, format: :json, include_ancestor_groups: true)
end
+
+ expose :issue_type,
+ as: :type,
+ format_with: :upcase,
+ documentation: { type: "String", desc: "One of #{::WorkItems::Type.base_types.keys.map(&:upcase)}" }
end
IssueBoardEntity.prepend_mod_with('IssueBoardEntity')
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 852a2e62b7d..eba2c49bc2e 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -3,6 +3,10 @@
class IssueEntity < IssuableEntity
include TimeTrackableEntity
+ format_with(:upcase) do |item|
+ item.try(:upcase)
+ end
+
expose :state
expose :milestone_id
expose :updated_by_id
@@ -75,6 +79,11 @@ class IssueEntity < IssuableEntity
expose :issue_email_participants do |issue|
issue.issue_email_participants.map { |x| { email: x.email } }
end
+
+ expose :issue_type,
+ as: :type,
+ format_with: :upcase,
+ documentation: { type: "String", desc: "One of #{::WorkItems::Type.base_types.keys.map(&:upcase)}" }
end
IssueEntity.prepend_mod_with('IssueEntity')
diff --git a/app/serializers/linked_issue_entity.rb b/app/serializers/linked_issue_entity.rb
index 769e3ed7310..4a28213fbac 100644
--- a/app/serializers/linked_issue_entity.rb
+++ b/app/serializers/linked_issue_entity.rb
@@ -3,6 +3,10 @@
class LinkedIssueEntity < Grape::Entity
include RequestAwareEntity
+ format_with(:upcase) do |item|
+ item.try(:upcase)
+ end
+
expose :id, :confidential, :title
expose :assignees, using: UserEntity
@@ -21,6 +25,11 @@ class LinkedIssueEntity < Grape::Entity
Gitlab::UrlBuilder.build(link, only_path: true)
end
+ expose :issue_type,
+ as: :type,
+ format_with: :upcase,
+ documentation: { type: "String", desc: "One of #{::WorkItems::Type.base_types.keys.map(&:upcase)}" }
+
expose :relation_path
expose :due_date, :created_at, :closed_at
diff --git a/app/serializers/merge_request_poll_cached_widget_entity.rb b/app/serializers/merge_request_poll_cached_widget_entity.rb
index 9d001d18aa6..0c5af67bcda 100644
--- a/app/serializers/merge_request_poll_cached_widget_entity.rb
+++ b/app/serializers/merge_request_poll_cached_widget_entity.rb
@@ -18,7 +18,8 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
expose :rebase_in_progress?, as: :rebase_in_progress
expose :commits_count
expose :merge_ongoing?, as: :merge_ongoing
- expose :work_in_progress?, as: :work_in_progress
+ expose :draft?, as: :draft
+ expose :draft?, as: :work_in_progress
expose :cannot_be_merged?, as: :has_conflicts
expose :can_be_merged?, as: :can_be_merged
expose :remove_source_branch?, as: :remove_source_branch
diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb
index 12998d70a22..fc1534a88aa 100644
--- a/app/serializers/merge_request_poll_widget_entity.rb
+++ b/app/serializers/merge_request_poll_widget_entity.rb
@@ -33,13 +33,17 @@ class MergeRequestPollWidgetEntity < Grape::Entity
# Booleans
expose :mergeable_discussions_state?, as: :mergeable_discussions_state do |merge_request|
- # This avoids calling MergeRequest#mergeable_discussions_state without
- # considering the state of the MR first. If a MR isn't mergeable, we can
- # safely short-circuit it.
- if merge_request.mergeable_state?(skip_ci_check: true, skip_discussions_check: true)
+ if Feature.enabled?(:change_response_code_merge_status, merge_request.project)
merge_request.mergeable_discussions_state?
else
- false
+ # This avoids calling MergeRequest#mergeable_discussions_state without
+ # considering the state of the MR first. If a MR isn't mergeable, we can
+ # safely short-circuit it.
+ if merge_request.mergeable_state?(skip_ci_check: true, skip_discussions_check: true)
+ merge_request.mergeable_discussions_state?
+ else
+ false
+ end
end
end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 5bf91ed0a51..cf984207ad1 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -128,20 +128,6 @@ class MergeRequestWidgetEntity < Grape::Entity
end
end
- expose :codeclimate, if: -> (mr, _) { head_pipeline_downloadable_path_for_report_type(:codequality) } do
- expose :head_path do |merge_request|
- head_pipeline_downloadable_path_for_report_type(:codequality)
- end
-
- expose :base_path do |merge_request|
- if use_merge_base_with_merged_results?
- merge_base_pipeline_downloadable_path_for_report_type(:codequality)
- else
- base_pipeline_downloadable_path_for_report_type(:codequality)
- end
- end
- end
-
expose :security_reports_docs_path do |merge_request|
help_page_path('user/application_security/index.md', anchor: 'view-security-scan-information-in-merge-requests')
end
diff --git a/app/serializers/prometheus_alert_entity.rb b/app/serializers/prometheus_alert_entity.rb
index 92905d2b389..fb25889e4db 100644
--- a/app/serializers/prometheus_alert_entity.rb
+++ b/app/serializers/prometheus_alert_entity.rb
@@ -13,10 +13,6 @@ class PrometheusAlertEntity < Grape::Entity
prometheus_alert.computed_operator
end
- expose :alert_path do |prometheus_alert|
- project_prometheus_alert_path(prometheus_alert.project, prometheus_alert.prometheus_metric_id, environment_id: prometheus_alert.environment.id, format: :json)
- end
-
private
alias_method :prometheus_alert, :object
diff --git a/app/serializers/service_event_entity.rb b/app/serializers/service_event_entity.rb
deleted file mode 100644
index 49a4944b2b0..00000000000
--- a/app/serializers/service_event_entity.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-class ServiceEventEntity < Grape::Entity
- include RequestAwareEntity
-
- expose :title do |event|
- IntegrationsHelper.integration_event_title(event)
- end
-
- expose :event_field_name, as: :name
-
- expose :value do |event|
- integration[event_field_name]
- end
-
- expose :description do |event|
- IntegrationsHelper.integration_event_description(integration, event)
- end
-
- expose :field, if: -> (_, _) { event_field } do
- expose :name do |event|
- event_field[:name]
- end
- expose :value do |event|
- integration.public_send(event_field[:name]) # rubocop:disable GitlabSecurity/PublicSend
- end
- end
-
- private
-
- alias_method :event, :object
-
- def event_field_name
- IntegrationsHelper.integration_event_field_name(event)
- end
-
- def event_field
- @event_field ||= integration.event_field(event)
- end
-
- def integration
- request.service
- end
-end
diff --git a/app/serializers/service_event_serializer.rb b/app/serializers/service_event_serializer.rb
deleted file mode 100644
index 7f5fe36e571..00000000000
--- a/app/serializers/service_event_serializer.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-class ServiceEventSerializer < BaseSerializer
- entity ServiceEventEntity
-end
diff --git a/app/serializers/service_field_entity.rb b/app/serializers/service_field_entity.rb
deleted file mode 100644
index b13f2c0e217..00000000000
--- a/app/serializers/service_field_entity.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-class ServiceFieldEntity < Grape::Entity
- include RequestAwareEntity
- include Gitlab::Utils::StrongMemoize
-
- expose :section, :type, :name, :placeholder, :required, :choices, :checkbox_label
-
- expose :title do |field|
- non_empty_password?(field) ? field[:non_empty_password_title] : field[:title]
- end
-
- expose :help do |field|
- non_empty_password?(field) ? field[:non_empty_password_help] : field[:help]
- end
-
- expose :value do |field|
- value = value_for(field)
-
- if non_empty_password?(field)
- 'true'
- elsif field[:type] == 'checkbox'
- ActiveRecord::Type::Boolean.new.deserialize(value).to_s
- else
- value
- end
- end
-
- private
-
- def service
- request.service
- end
-
- def value_for(field)
- strong_memoize(:value_for) do
- # field[:name] is not user input and so can assume is safe
- service.public_send(field[:name]) # rubocop:disable GitlabSecurity/PublicSend
- end
- end
-
- def non_empty_password?(field)
- strong_memoize(:non_empty_password) do
- field[:type] == 'password' && value_for(field).present?
- end
- end
-end
diff --git a/app/serializers/service_field_serializer.rb b/app/serializers/service_field_serializer.rb
deleted file mode 100644
index 120d0f5820e..00000000000
--- a/app/serializers/service_field_serializer.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-class ServiceFieldSerializer < BaseSerializer
- entity ServiceFieldEntity
-end
diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb
index 4ed4368d3b7..2a32f0c74ac 100644
--- a/app/services/auto_merge/base_service.rb
+++ b/app/services/auto_merge/base_service.rb
@@ -55,7 +55,10 @@ module AutoMerge
def available_for?(merge_request)
strong_memoize("available_for_#{merge_request.id}") do
merge_request.can_be_merged_by?(current_user) &&
- merge_request.mergeable_state?(skip_ci_check: true) &&
+ merge_request.open? &&
+ !merge_request.broken? &&
+ !merge_request.draft? &&
+ merge_request.mergeable_discussions_state? &&
yield
end
end
diff --git a/app/services/boards/base_items_list_service.rb b/app/services/boards/base_items_list_service.rb
index 01fad14d036..2a9cbb83cc4 100644
--- a/app/services/boards/base_items_list_service.rb
+++ b/app/services/boards/base_items_list_service.rb
@@ -78,12 +78,15 @@ module Boards
end
def list
- return unless params.key?(:id)
+ return unless params.key?(:id) || params.key?(:list)
strong_memoize(:list) do
id = params[:id]
+ list = params[:list]
- if board.lists.loaded?
+ if list.present?
+ list
+ elsif board.lists.loaded?
board.lists.find { |l| l.id == id }
else
board.lists.find(id)
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 6021d634f86..465025ef2e9 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -20,7 +20,7 @@ module Boards
private
def order(items)
- return items.order_closed_date_desc if list&.closed?
+ return items.order_closed_at_desc if list&.closed?
items.order_by_relative_position
end
diff --git a/app/services/bulk_create_integration_service.rb b/app/services/bulk_create_integration_service.rb
index 3a214122ed3..8fbb7f4f347 100644
--- a/app/services/bulk_create_integration_service.rb
+++ b/app/services/bulk_create_integration_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class BulkCreateIntegrationService
+ include Integrations::BulkOperationHashes
+
def initialize(integration, batch, association)
@integration = integration
@batch = batch
@@ -8,13 +10,13 @@ class BulkCreateIntegrationService
end
def execute
- service_list = ServiceList.new(batch, integration_hash, association).to_array
+ service_list = ServiceList.new(batch, integration_hash(:create), association).to_array
Integration.transaction do
results = bulk_insert(*service_list)
if integration.data_fields_present?
- data_list = DataList.new(results, data_fields_hash, integration.data_fields.class).to_array
+ data_list = DataList.new(results, data_fields_hash(:create), integration.data_fields.class).to_array
bulk_insert(*data_list)
end
@@ -30,12 +32,4 @@ class BulkCreateIntegrationService
klass.insert_all(items_to_insert, returning: [:id])
end
-
- def integration_hash
- integration.to_integration_hash.tap { |json| json['inherit_from_id'] = integration.inherit_from_id || integration.id }
- end
-
- def data_fields_hash
- integration.to_data_fields_hash
- end
end
diff --git a/app/services/bulk_imports/create_pipeline_trackers_service.rb b/app/services/bulk_imports/create_pipeline_trackers_service.rb
new file mode 100644
index 00000000000..5c9c68e62b5
--- /dev/null
+++ b/app/services/bulk_imports/create_pipeline_trackers_service.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class CreatePipelineTrackersService
+ def initialize(entity)
+ @entity = entity
+ end
+
+ def execute!
+ entity.class.transaction do
+ entity.pipelines.each do |pipeline|
+ status = skip_pipeline?(pipeline) ? -2 : 0
+
+ entity.trackers.create!(
+ stage: pipeline[:stage],
+ pipeline_name: pipeline[:pipeline],
+ status: status
+ )
+ end
+ end
+ end
+
+ private
+
+ attr_reader :entity
+
+ def skip_pipeline?(pipeline)
+ return false unless source_version.valid?
+
+ minimum_version, maximum_version = pipeline.values_at(:minimum_source_version, :maximum_source_version)
+
+ if minimum_version && non_patch_source_version < Gitlab::VersionInfo.parse(minimum_version)
+ log_skipped_pipeline(pipeline, minimum_version, maximum_version)
+ return true
+ end
+
+ if maximum_version && non_patch_source_version > Gitlab::VersionInfo.parse(maximum_version)
+ log_skipped_pipeline(pipeline, minimum_version, maximum_version)
+ return true
+ end
+
+ false
+ end
+
+ def source_version
+ @source_version ||= entity.bulk_import.source_version_info
+ end
+
+ def non_patch_source_version
+ Gitlab::VersionInfo.new(source_version.major, source_version.minor, 0)
+ end
+
+ def log_skipped_pipeline(pipeline, minimum_version, maximum_version)
+ logger.info(
+ message: 'Pipeline skipped as source instance version not compatible with pipeline',
+ entity_id: entity.id,
+ pipeline_name: pipeline[:pipeline],
+ minimum_source_version: minimum_version,
+ maximum_source_version: maximum_version,
+ source_version: source_version.to_s
+ )
+ end
+
+ def logger
+ @logger ||= Gitlab::Import::Logger.build
+ end
+ end
+end
diff --git a/app/services/bulk_imports/file_export_service.rb b/app/services/bulk_imports/file_export_service.rb
index a9d06d84277..b2d114368a1 100644
--- a/app/services/bulk_imports/file_export_service.rb
+++ b/app/services/bulk_imports/file_export_service.rb
@@ -30,6 +30,10 @@ module BulkImports
UploadsExportService.new(portable, export_path)
when FileTransfer::ProjectConfig::LFS_OBJECTS_RELATION
LfsObjectsExportService.new(portable, export_path)
+ when FileTransfer::ProjectConfig::REPOSITORY_BUNDLE_RELATION
+ RepositoryBundleExportService.new(portable.repository, export_path, relation)
+ when FileTransfer::ProjectConfig::DESIGN_BUNDLE_RELATION
+ RepositoryBundleExportService.new(portable.design_repository, export_path, relation)
else
raise BulkImports::Error, 'Unsupported relation export type'
end
diff --git a/app/services/bulk_imports/lfs_objects_export_service.rb b/app/services/bulk_imports/lfs_objects_export_service.rb
index fa606e4e5a3..1f745201c8a 100644
--- a/app/services/bulk_imports/lfs_objects_export_service.rb
+++ b/app/services/bulk_imports/lfs_objects_export_service.rb
@@ -32,6 +32,8 @@ module BulkImports
destination_filepath = File.join(export_path, lfs_object.oid)
if lfs_object.local_store?
+ return unless File.exist?(lfs_object.file.path)
+
copy_files(lfs_object.file.path, destination_filepath)
else
download(lfs_object.file.url, destination_filepath)
diff --git a/app/services/bulk_imports/repository_bundle_export_service.rb b/app/services/bulk_imports/repository_bundle_export_service.rb
new file mode 100644
index 00000000000..31a2ed6d1af
--- /dev/null
+++ b/app/services/bulk_imports/repository_bundle_export_service.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class RepositoryBundleExportService
+ def initialize(repository, export_path, export_filename)
+ @repository = repository
+ @export_path = export_path
+ @export_filename = export_filename
+ end
+
+ def execute
+ repository.bundle_to_disk(bundle_filepath) if repository.exists?
+ end
+
+ private
+
+ attr_reader :repository, :export_path, :export_filename
+
+ def bundle_filepath
+ File.join(export_path, "#{export_filename}.bundle")
+ end
+ end
+end
diff --git a/app/services/bulk_update_integration_service.rb b/app/services/bulk_update_integration_service.rb
index 29c4d0cc220..57ceec57962 100644
--- a/app/services/bulk_update_integration_service.rb
+++ b/app/services/bulk_update_integration_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class BulkUpdateIntegrationService
+ include Integrations::BulkOperationHashes
+
def initialize(integration, batch)
@integration = integration
@batch = batch
@@ -9,10 +11,13 @@ class BulkUpdateIntegrationService
# rubocop: disable CodeReuse/ActiveRecord
def execute
Integration.transaction do
- Integration.where(id: batch_ids).update_all(integration_hash)
+ Integration.where(id: batch_ids).update_all(integration_hash(:update))
if integration.data_fields_present?
- integration.data_fields.class.where(data_fields_foreign_key => batch_ids).update_all(data_fields_hash)
+ integration.data_fields.class.where(data_fields_foreign_key => batch_ids)
+ .update_all(
+ data_fields_hash(:update)
+ )
end
end
end
@@ -27,14 +32,6 @@ class BulkUpdateIntegrationService
integration.data_fields.class.reflections['integration'].foreign_key
end
- def integration_hash
- integration.to_integration_hash.tap { |json| json['inherit_from_id'] = integration.inherit_from_id || integration.id }
- end
-
- def data_fields_hash
- integration.to_data_fields_hash
- end
-
def batch_ids
@batch_ids ||=
if batch.is_a?(ActiveRecord::Relation)
diff --git a/app/services/ci/job_artifacts/destroy_all_expired_service.rb b/app/services/ci/job_artifacts/destroy_all_expired_service.rb
index 4070875ffe1..b5dd5b843c6 100644
--- a/app/services/ci/job_artifacts/destroy_all_expired_service.rb
+++ b/app/services/ci/job_artifacts/destroy_all_expired_service.rb
@@ -60,7 +60,7 @@ module Ci
end
def destroy_batch(artifacts)
- Ci::JobArtifacts::DestroyBatchService.new(artifacts).execute
+ Ci::JobArtifacts::DestroyBatchService.new(artifacts, skip_projects_on_refresh: true).execute
end
def loop_timeout?
diff --git a/app/services/ci/job_artifacts/destroy_batch_service.rb b/app/services/ci/job_artifacts/destroy_batch_service.rb
index 5121a8b0a8b..49b65f13804 100644
--- a/app/services/ci/job_artifacts/destroy_batch_service.rb
+++ b/app/services/ci/job_artifacts/destroy_batch_service.rb
@@ -17,14 +17,21 @@ module Ci
# +pick_up_at+:: When to pick up for deletion of files
# Returns:
# +Hash+:: A hash with status and destroyed_artifacts_count keys
- def initialize(job_artifacts, pick_up_at: nil, fix_expire_at: fix_expire_at?)
+ def initialize(job_artifacts, pick_up_at: nil, fix_expire_at: fix_expire_at?, skip_projects_on_refresh: false)
@job_artifacts = job_artifacts.with_destroy_preloads.to_a
@pick_up_at = pick_up_at
@fix_expire_at = fix_expire_at
+ @skip_projects_on_refresh = skip_projects_on_refresh
end
# rubocop: disable CodeReuse/ActiveRecord
def execute(update_stats: true)
+ if @skip_projects_on_refresh
+ exclude_artifacts_undergoing_stats_refresh
+ else
+ track_artifacts_undergoing_stats_refresh
+ end
+
# Detect and fix artifacts that had `expire_at` wrongly backfilled by migration
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47723
detect_and_fix_wrongly_expired_artifacts
@@ -154,6 +161,34 @@ module Ci
Gitlab::AppLogger.info(message: "Fixed expire_at from artifacts.", fixed_artifacts_expire_at_count: artifacts.count)
end
+
+ def track_artifacts_undergoing_stats_refresh
+ project_ids = @job_artifacts.find_all do |artifact|
+ artifact.project.refreshing_build_artifacts_size?
+ end.map(&:project_id).uniq
+
+ project_ids.each do |project_id|
+ Gitlab::ProjectStatsRefreshConflictsLogger.warn_artifact_deletion_during_stats_refresh(
+ method: 'Ci::JobArtifacts::DestroyBatchService#execute',
+ project_id: project_id
+ )
+ end
+ end
+
+ def exclude_artifacts_undergoing_stats_refresh
+ project_ids = Set.new
+
+ @job_artifacts.reject! do |artifact|
+ next unless artifact.project.refreshing_build_artifacts_size?
+
+ project_ids << artifact.project_id
+ end
+
+ Gitlab::ProjectStatsRefreshConflictsLogger.warn_skipped_artifact_deletion_during_stats_refresh(
+ method: 'Ci::JobArtifacts::DestroyBatchService#execute',
+ project_ids: project_ids
+ )
+ end
end
end
end
diff --git a/app/services/ci/pipeline_artifacts/coverage_report_service.rb b/app/services/ci/pipeline_artifacts/coverage_report_service.rb
index 8209639fa22..b0acb1d5a0b 100644
--- a/app/services/ci/pipeline_artifacts/coverage_report_service.rb
+++ b/app/services/ci/pipeline_artifacts/coverage_report_service.rb
@@ -2,30 +2,44 @@
module Ci
module PipelineArtifacts
class CoverageReportService
- def execute(pipeline)
- return unless pipeline.can_generate_coverage_reports?
- return if pipeline.has_coverage_reports?
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(pipeline)
+ @pipeline = pipeline
+ end
- file = build_carrierwave_file(pipeline)
+ def execute
+ return if pipeline.has_coverage_reports?
+ return if report.empty?
pipeline.pipeline_artifacts.create!(
project_id: pipeline.project_id,
file_type: :code_coverage,
file_format: Ci::PipelineArtifact::REPORT_TYPES.fetch(:code_coverage),
- size: file["tempfile"].size,
- file: file,
+ size: carrierwave_file["tempfile"].size,
+ file: carrierwave_file,
expire_at: Ci::PipelineArtifact::EXPIRATION_DATE.from_now
)
end
private
- def build_carrierwave_file(pipeline)
- CarrierWaveStringFile.new_file(
- file_content: pipeline.coverage_reports.to_json,
- filename: Ci::PipelineArtifact::DEFAULT_FILE_NAMES.fetch(:code_coverage),
- content_type: 'application/json'
- )
+ attr_reader :pipeline
+
+ def report
+ strong_memoize(:report) do
+ Gitlab::Ci::Reports::CoverageReportGenerator.new(pipeline).report
+ end
+ end
+
+ def carrierwave_file
+ strong_memoize(:carrier_wave_file) do
+ CarrierWaveStringFile.new_file(
+ file_content: report.to_json,
+ filename: Ci::PipelineArtifact::DEFAULT_FILE_NAMES.fetch(:code_coverage),
+ content_type: 'application/json'
+ )
+ end
end
end
end
diff --git a/app/services/ci/pipeline_artifacts/destroy_all_expired_service.rb b/app/services/ci/pipeline_artifacts/destroy_all_expired_service.rb
index 7b6590a117c..17c039885e5 100644
--- a/app/services/ci/pipeline_artifacts/destroy_all_expired_service.rb
+++ b/app/services/ci/pipeline_artifacts/destroy_all_expired_service.rb
@@ -25,7 +25,7 @@ module Ci
private
def destroy_artifacts_batch
- artifacts = ::Ci::PipelineArtifact.unlocked.expired(BATCH_SIZE).to_a
+ artifacts = ::Ci::PipelineArtifact.unlocked.expired.limit(BATCH_SIZE).to_a
return false if artifacts.empty?
artifacts.each(&:destroy!)
diff --git a/app/services/ci/runners/reset_registration_token_service.rb b/app/services/ci/runners/reset_registration_token_service.rb
index 2a3fb08c5e1..81a70a771cf 100644
--- a/app/services/ci/runners/reset_registration_token_service.rb
+++ b/app/services/ci/runners/reset_registration_token_service.rb
@@ -13,11 +13,10 @@ module Ci
def execute
return unless @user.present? && @user.can?(:update_runners_registration_token, scope)
- case scope
- when ::ApplicationSetting
+ if scope.respond_to?(:runners_registration_token)
scope.reset_runners_registration_token!
- ApplicationSetting.current_without_cache.runners_registration_token
- when ::Group, ::Project
+ scope.runners_registration_token
+ else
scope.reset_runners_token!
scope.runners_token
end
diff --git a/app/services/clusters/applications/schedule_update_service.rb b/app/services/clusters/applications/schedule_update_service.rb
deleted file mode 100644
index 4fabf1d809e..00000000000
--- a/app/services/clusters/applications/schedule_update_service.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-# DEPRECATED: To be removed as part of https://gitlab.com/groups/gitlab-org/-/epics/5877
-module Clusters
- module Applications
- class ScheduleUpdateService
- BACKOFF_DELAY = 2.minutes
-
- attr_accessor :application, :project
-
- def initialize(cluster_prometheus_adapter, project)
- @application = cluster_prometheus_adapter&.cluster&.application_prometheus
- @project = project
- end
-
- def execute
- return unless application
- return if application.externally_installed?
-
- if recently_scheduled?
- worker_class.perform_in(BACKOFF_DELAY, application.name, application.id, project.id, Time.current)
- else
- worker_class.perform_async(application.name, application.id, project.id, Time.current)
- end
- end
-
- private
-
- def worker_class
- ::ClusterUpdateAppWorker
- end
-
- def recently_scheduled?
- return false unless application.last_update_started_at
-
- application.last_update_started_at.utc >= Time.current.utc - BACKOFF_DELAY
- end
- end
- end
-end
diff --git a/app/services/concerns/integrations/bulk_operation_hashes.rb b/app/services/concerns/integrations/bulk_operation_hashes.rb
new file mode 100644
index 00000000000..3f13c764ebe
--- /dev/null
+++ b/app/services/concerns/integrations/bulk_operation_hashes.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+# Returns hashes of attributes suitable for passing to `.insert_all` or `update_all`
+module Integrations
+ module BulkOperationHashes
+ private
+
+ def integration_hash(operation)
+ integration
+ .to_database_hash
+ .merge('inherit_from_id' => integration.inherit_from_id || integration.id)
+ .merge(update_timestamps(operation))
+ end
+
+ def data_fields_hash(operation)
+ integration
+ .data_fields
+ .to_database_hash
+ .merge(update_timestamps(operation))
+ end
+
+ def update_timestamps(operation)
+ time_now = Time.current
+
+ {
+ 'created_at' => (time_now if operation == :create),
+ 'updated_at' => time_now
+ }.compact
+ end
+ end
+end
diff --git a/app/services/concerns/members/bulk_create_users.rb b/app/services/concerns/members/bulk_create_users.rb
deleted file mode 100644
index e60c84af89e..00000000000
--- a/app/services/concerns/members/bulk_create_users.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-module Members
- module BulkCreateUsers
- extend ActiveSupport::Concern
-
- included do
- class << self
- def add_users(source, users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
- return [] unless users.present?
-
- emails, users, existing_members = parse_users_list(source, users)
-
- Member.transaction do
- (emails + users).map! do |user|
- new(source,
- user,
- access_level,
- existing_members: existing_members,
- current_user: current_user,
- expires_at: expires_at,
- tasks_to_be_done: tasks_to_be_done,
- tasks_project_id: tasks_project_id)
- .execute
- end
- end
- end
-
- private
-
- def parse_users_list(source, list)
- emails = []
- user_ids = []
- users = []
- existing_members = {}
-
- list.each do |item|
- case item
- when User
- users << item
- when Integer
- user_ids << item
- when /\A\d+\Z/
- user_ids << item.to_i
- when Devise.email_regexp
- emails << item
- end
- end
-
- # the below will automatically discard invalid user_ids
- users.concat(User.id_in(user_ids)) if user_ids.present?
- users.uniq! # de-duplicate just in case as there is no controlling if user records and ids are sent multiple times
-
- users_by_emails = source.users_by_emails(emails) # preloads our request store for all emails
- # in case emails belong to a user that is being invited by user or user_id, remove them from
- # emails and let users/user_ids handle it.
- parsed_emails = emails.select do |email|
- user = users_by_emails[email]
- !user || (users.exclude?(user) && user_ids.exclude?(user.id))
- end
-
- if users.present?
- # helps not have to perform another query per user id to see if the member exists later on when fetching
- existing_members = source.members_and_requesters.with_user(users).index_by(&:user_id)
- end
-
- [parsed_emails, users, existing_members]
- end
- end
- end
-
- def initialize(source, user, access_level, **args)
- super
-
- @existing_members = args[:existing_members] || (raise ArgumentError, "existing_members must be included in the args hash")
- end
-
- private
-
- attr_reader :existing_members
-
- def find_or_initialize_member_by_user
- existing_members[user.id] || source.members.build(user_id: user.id)
- end
- end
-end
diff --git a/app/services/environments/stop_service.rb b/app/services/environments/stop_service.rb
index 54ad94947ff..75c878c9350 100644
--- a/app/services/environments/stop_service.rb
+++ b/app/services/environments/stop_service.rb
@@ -7,7 +7,11 @@ module Environments
def execute(environment)
return unless can?(current_user, :stop_environment, environment)
- environment.stop_with_actions!(current_user)
+ if params[:force]
+ environment.stop_complete!
+ else
+ environment.stop_with_actions!(current_user)
+ end
end
def execute_for_branch(branch_name)
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 5a2c29f8e7a..2ab4bb47462 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -25,12 +25,14 @@ class EventCreateService
def open_mr(merge_request, current_user)
create_record_event(merge_request, current_user, :created).tap do
track_event(event_action: :created, event_target: MergeRequest, author_id: current_user.id)
+ track_mr_snowplow_event(merge_request, current_user, :create)
end
end
def close_mr(merge_request, current_user)
create_record_event(merge_request, current_user, :closed).tap do
track_event(event_action: :closed, event_target: MergeRequest, author_id: current_user.id)
+ track_mr_snowplow_event(merge_request, current_user, :close)
end
end
@@ -41,6 +43,7 @@ class EventCreateService
def merge_mr(merge_request, current_user)
create_record_event(merge_request, current_user, :merged).tap do
track_event(event_action: :merged, event_target: MergeRequest, author_id: current_user.id)
+ track_mr_snowplow_event(merge_request, current_user, :merge)
end
end
@@ -64,6 +67,7 @@ class EventCreateService
create_record_event(note, current_user, :commented).tap do
if note.is_a?(DiffNote) && note.for_merge_request?
track_event(event_action: :commented, event_target: MergeRequest, author_id: current_user.id)
+ track_mr_snowplow_event(note, current_user, :comment)
end
end
end
@@ -225,6 +229,20 @@ class EventCreateService
def track_event(**params)
Gitlab::UsageDataCounters::TrackUniqueEvents.track_event(**params)
end
+
+ def track_mr_snowplow_event(record, current_user, action)
+ return unless Feature.enabled?(:route_hll_to_snowplow_phase2)
+
+ project = record.project
+ Gitlab::Tracking.event(
+ Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION.to_s,
+ action.to_s,
+ label: 'merge_requests_users',
+ project: project,
+ namespace: project.namespace,
+ user: current_user
+ )
+ end
end
EventCreateService.prepend_mod_with('EventCreateService')
diff --git a/app/services/git/branch_push_service.rb b/app/services/git/branch_push_service.rb
index 3c27ad56ebb..91f14251608 100644
--- a/app/services/git/branch_push_service.rb
+++ b/app/services/git/branch_push_service.rb
@@ -39,6 +39,7 @@ module Git
def enqueue_update_mrs
return if params[:merge_request_branches]&.exclude?(branch_name)
+ # TODO: pass params[:push_options] to worker
UpdateMergeRequestsWorker.perform_async(
project.id,
current_user.id,
diff --git a/app/services/import/base_service.rb b/app/services/import/base_service.rb
index 4a43b2f7425..ab3e9c7abba 100644
--- a/app/services/import/base_service.rb
+++ b/app/services/import/base_service.rb
@@ -8,6 +8,10 @@ module Import
@params = params
end
+ def authorized?
+ can?(current_user, :create_projects, target_namespace)
+ end
+
private
def find_or_create_namespace(namespace, owner)
diff --git a/app/services/import/bitbucket_server_service.rb b/app/services/import/bitbucket_server_service.rb
index d1c22f06464..20f6c987c92 100644
--- a/app/services/import/bitbucket_server_service.rb
+++ b/app/services/import/bitbucket_server_service.rb
@@ -72,10 +72,6 @@ module Import
@url ||= params[:bitbucket_server_url]
end
- def authorized?
- can?(current_user, :create_projects, target_namespace)
- end
-
def allow_local_requests?
Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
end
diff --git a/app/services/import/fogbugz_service.rb b/app/services/import/fogbugz_service.rb
new file mode 100644
index 00000000000..d1003823456
--- /dev/null
+++ b/app/services/import/fogbugz_service.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+module Import
+ class FogbugzService < Import::BaseService
+ attr_reader :client, :params, :current_user
+
+ def execute(credentials)
+ url = credentials[:uri]
+
+ if blocked_url?(url)
+ return log_and_return_error("Invalid URL: #{url}", _("Invalid URL: %{url}") % { url: url }, :bad_request)
+ end
+
+ unless authorized?
+ return log_and_return_error(
+ "You don't have permissions to create this project",
+ _("You don't have permissions to create this project"),
+ :unauthorized
+ )
+ end
+
+ unless repo
+ return log_and_return_error(
+ "Project #{repo_id} could not be found",
+ s_("Fogbugz|Project %{repo} could not be found") % { repo: repo_id },
+ :unprocessable_entity)
+ end
+
+ project = create_project(credentials)
+
+ if project.persisted?
+ success(project)
+ elsif project.errors[:import_source_disabled].present?
+ error(project.errors[:import_source_disabled], :forbidden)
+ else
+ error(project_save_error(project), :unprocessable_entity)
+ end
+ rescue StandardError => e
+ log_and_return_error(
+ "Fogbugz import failed due to an error: #{e}",
+ s_("Fogbugz|Fogbugz import failed due to an error: %{error}" % { error: e }),
+ :bad_request)
+ end
+
+ private
+
+ def create_project(credentials)
+ Gitlab::FogbugzImport::ProjectCreator.new(
+ repo,
+ project_name,
+ target_namespace,
+ current_user,
+ credentials,
+ umap
+ ).execute
+ end
+
+ def repo_id
+ @repo_id ||= params[:repo_id]
+ end
+
+ def repo
+ @repo ||= client.repo(repo_id)
+ end
+
+ def project_name
+ @project_name ||= params[:new_name].presence || repo.name
+ end
+
+ def namespace_path
+ @namespace_path ||= params[:target_namespace].presence || current_user.namespace_path
+ end
+
+ def target_namespace
+ @target_namespace ||= find_or_create_namespace(namespace_path, current_user.namespace_path)
+ end
+
+ def umap
+ @umap ||= params[:umap]
+ end
+
+ def allow_local_requests?
+ Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
+ end
+
+ def blocked_url?(url)
+ Gitlab::UrlBlocker.blocked_url?(
+ url,
+ allow_localhost: allow_local_requests?,
+ allow_local_network: allow_local_requests?,
+ schemes: %w(http https)
+ )
+ end
+
+ def log_and_return_error(message, translated_message, error_type)
+ log_error(message)
+ error(translated_message, error_type)
+ end
+
+ def log_error(message)
+ Gitlab::Import::Logger.error(
+ message: 'Import failed due to a Fogbugz error',
+ error: message
+ )
+ end
+ end
+end
diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb
index 033f6bcb043..ff5d5d2c4c1 100644
--- a/app/services/import/github_service.rb
+++ b/app/services/import/github_service.rb
@@ -89,10 +89,6 @@ module Import
end
end
- def authorized?
- can?(current_user, :create_projects, target_namespace)
- end
-
def url
@url ||= params[:github_hostname]
end
diff --git a/app/services/incident_management/timeline_events/base_service.rb b/app/services/incident_management/timeline_events/base_service.rb
index cae58465e4a..7168e2fdd38 100644
--- a/app/services/incident_management/timeline_events/base_service.rb
+++ b/app/services/incident_management/timeline_events/base_service.rb
@@ -3,6 +3,8 @@
module IncidentManagement
module TimelineEvents
class BaseService
+ include Gitlab::Utils::UsageData
+
def allowed?
user&.can?(:admin_incident_management_timeline_event, incident)
end
diff --git a/app/services/incident_management/timeline_events/create_service.rb b/app/services/incident_management/timeline_events/create_service.rb
index 7d287e1bd82..5e5feed65c2 100644
--- a/app/services/incident_management/timeline_events/create_service.rb
+++ b/app/services/incident_management/timeline_events/create_service.rb
@@ -3,6 +3,7 @@
module IncidentManagement
module TimelineEvents
DEFAULT_ACTION = 'comment'
+ DEFAULT_EDITABLE = false
class CreateService < TimelineEvents::BaseService
def initialize(incident, user, params)
@@ -23,7 +24,8 @@ module IncidentManagement
action: params.fetch(:action, DEFAULT_ACTION),
note_html: params[:note_html].presence || params[:note],
occurred_at: params[:occurred_at],
- promoted_from_note: params[:promoted_from_note]
+ promoted_from_note: params[:promoted_from_note],
+ editable: params.fetch(:editable, DEFAULT_EDITABLE)
}
timeline_event = IncidentManagement::TimelineEvent.new(timeline_event_params)
@@ -31,6 +33,7 @@ module IncidentManagement
if timeline_event.save
add_system_note(timeline_event)
+ track_usage_event(:incident_management_timeline_event_created, user.id)
success(timeline_event)
else
error_in_save(timeline_event)
diff --git a/app/services/incident_management/timeline_events/destroy_service.rb b/app/services/incident_management/timeline_events/destroy_service.rb
index 8bb186c289a..90e95ae8869 100644
--- a/app/services/incident_management/timeline_events/destroy_service.rb
+++ b/app/services/incident_management/timeline_events/destroy_service.rb
@@ -18,6 +18,7 @@ module IncidentManagement
if timeline_event.destroy
add_system_note(incident, user)
+ track_usage_event(:incident_management_timeline_event_deleted, user.id)
success(timeline_event)
else
error_in_save(timeline_event)
diff --git a/app/services/incident_management/timeline_events/update_service.rb b/app/services/incident_management/timeline_events/update_service.rb
index fe8b4879561..83497b123dd 100644
--- a/app/services/incident_management/timeline_events/update_service.rb
+++ b/app/services/incident_management/timeline_events/update_service.rb
@@ -17,11 +17,13 @@ module IncidentManagement
end
def execute
+ return error_non_editable unless timeline_event.editable?
return error_no_permissions unless allowed?
if timeline_event.update(update_params)
add_system_note(timeline_event)
+ track_usage_event(:incident_management_timeline_event_edited, user.id)
success(timeline_event)
else
error_in_save(timeline_event)
@@ -56,6 +58,10 @@ module IncidentManagement
:none
end
+
+ def error_non_editable
+ error(_('You cannot edit this timeline event.'))
+ end
end
end
end
diff --git a/app/services/issuable/clone/base_service.rb b/app/services/issuable/clone/base_service.rb
index 1d2c5c06d1b..ce9918a4b56 100644
--- a/app/services/issuable/clone/base_service.rb
+++ b/app/services/issuable/clone/base_service.rb
@@ -41,14 +41,15 @@ module Issuable
end
def update_new_entity_description
- rewritten_description = MarkdownContentRewriterService.new(
+ update_description_params = MarkdownContentRewriterService.new(
current_user,
- original_entity.description,
+ original_entity,
+ :description,
original_entity.project,
new_parent
).execute
- new_entity.update!(description: rewritten_description)
+ new_entity.update!(update_description_params)
end
def update_new_entity_attributes
diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb
index 9ee54c7ba0f..5cf32ee3e40 100644
--- a/app/services/issuable/common_system_notes_service.rb
+++ b/app/services/issuable/common_system_notes_service.rb
@@ -54,7 +54,7 @@ module Issuable
def create_draft_note(old_title)
return unless issuable.is_a?(MergeRequest)
- if MergeRequest.work_in_progress?(old_title) != issuable.work_in_progress?
+ if MergeRequest.draft?(old_title) != issuable.draft?
SystemNoteService.handle_merge_request_draft(issuable, issuable.project, current_user)
end
end
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 7ab663718db..edf6d75b632 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -56,6 +56,7 @@ module Issues
handle_add_related_issue(issue)
resolve_discussions_with_issue(issue)
create_escalation_status(issue)
+ try_to_associate_contact(issue)
super
end
@@ -99,6 +100,13 @@ module Issues
IssueLinks::CreateService.new(issue, issue.author, { target_issuable: @add_related_issue }).execute
end
+
+ def try_to_associate_contact(issue)
+ return unless issue.external_author
+ return unless current_user.can?(:set_issue_crm_contacts, issue)
+
+ set_crm_contacts(issue, [issue.external_author])
+ end
end
end
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index e210e6a2362..d210ba2a76c 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -110,7 +110,6 @@ module Issues
end
def copy_contacts
- return unless Feature.enabled?(:customer_relations, original_entity.project.root_ancestor)
return unless original_entity.project.root_ancestor == new_entity.project.root_ancestor
new_entity.customer_relations_contacts = original_entity.customer_relations_contacts
diff --git a/app/services/jira_connect_subscriptions/create_service.rb b/app/services/jira_connect_subscriptions/create_service.rb
index 2f31a3c8d4e..d5ab3800dcf 100644
--- a/app/services/jira_connect_subscriptions/create_service.rb
+++ b/app/services/jira_connect_subscriptions/create_service.rb
@@ -5,13 +5,18 @@ module JiraConnectSubscriptions
include Gitlab::Utils::StrongMemoize
MERGE_REQUEST_SYNC_BATCH_SIZE = 20
MERGE_REQUEST_SYNC_BATCH_DELAY = 1.minute.freeze
- NOT_SITE_ADMIN = 'The Jira user is not a site administrator.'
def execute
- return error(NOT_SITE_ADMIN, 403) unless can_administer_jira?
+ if !params[:jira_user]
+ return error(s_('JiraConnect|Could not fetch user information from Jira. ' \
+ 'Check the permissions in Jira and try again.'), 403)
+ elsif !can_administer_jira?
+ return error(s_('JiraConnect|The Jira user is not a site administrator. ' \
+ 'Check the permissions in Jira and try again.'), 403)
+ end
unless namespace && can?(current_user, :create_jira_connect_subscription, namespace)
- return error('Invalid namespace. Please make sure you have sufficient permissions', 401)
+ return error(s_('JiraConnect|Cannot find namespace. Make sure you have sufficient permissions.'), 401)
end
create_subscription
@@ -20,7 +25,7 @@ module JiraConnectSubscriptions
private
def can_administer_jira?
- @params[:jira_user]&.site_admin?
+ params[:jira_user]&.site_admin?
end
def create_subscription
diff --git a/app/services/markdown_content_rewriter_service.rb b/app/services/markdown_content_rewriter_service.rb
index bc6fd592eaa..4d8f523fa77 100644
--- a/app/services/markdown_content_rewriter_service.rb
+++ b/app/services/markdown_content_rewriter_service.rb
@@ -4,29 +4,69 @@
# which rewrite references to GitLab objects and uploads within the content
# based on their visibility by the `target_parent`.
class MarkdownContentRewriterService
- REWRITERS = [Gitlab::Gfm::ReferenceRewriter, Gitlab::Gfm::UploadsRewriter].freeze
+ include Gitlab::Utils::StrongMemoize
- def initialize(current_user, content, source_parent, target_parent)
- # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39654#note_399095117
- raise ArgumentError, 'The rewriter classes require that `source_parent` is a `Project`' \
- unless source_parent.is_a?(Project)
+ REWRITERS = [Gitlab::Gfm::ReferenceRewriter, Gitlab::Gfm::UploadsRewriter].freeze
+ def initialize(current_user, object, field, source_parent, target_parent)
@current_user = current_user
- @content = content.presence
@source_parent = source_parent
@target_parent = target_parent
+ @object = object
+ @field = field
+
+ validate_parameters!
+
+ @content = object[field].dup.presence
+ @html_field = object.cached_markdown_fields.html_field(field)
+ @content_html = object.cached_html_for(field)
+
+ @rewriters =
+ REWRITERS.map do |rewriter_class|
+ rewriter_class.new(@content, content_html, source_parent, current_user)
+ end
+
+ @result = {
+ field => nil,
+ html_field => nil
+ }.with_indifferent_access
end
def execute
- return unless content
+ return result unless content
- REWRITERS.inject(content) do |text, klass|
- rewriter = klass.new(text, source_parent, current_user)
- rewriter.rewrite(target_parent)
+ unless safe_to_copy_markdown?
+ rewriters.each do |rewriter|
+ rewriter.rewrite(target_parent)
+ end
+ end
+
+ result[field] = content
+ result[html_field] = content_html if safe_to_copy_markdown?
+ result[:skip_markdown_cache_validation] = safe_to_copy_markdown?
+
+ result
+ end
+
+ def safe_to_copy_markdown?
+ strong_memoize(:safe_to_copy_markdown) do
+ rewriters.none?(&:needs_rewrite?)
end
end
private
- attr_reader :current_user, :content, :source_parent, :target_parent
+ def validate_parameters!
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39654#note_399095117
+ raise ArgumentError, 'The rewriter classes require that `source_parent` is a `Project`' \
+ unless source_parent.is_a?(Project)
+
+ if object.cached_markdown_fields[field].nil?
+ raise ArgumentError, 'The `field` attribute does not contain cached markdown'
+ end
+ end
+
+ attr_reader :current_user, :content, :source_parent,
+ :target_parent, :rewriters, :content_html,
+ :field, :html_field, :object, :result
end
diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb
index 919c22894c1..5337279f702 100644
--- a/app/services/members/approve_access_request_service.rb
+++ b/app/services/members/approve_access_request_service.rb
@@ -3,7 +3,7 @@
module Members
class ApproveAccessRequestService < Members::BaseService
def execute(access_requester, skip_authorization: false, skip_log_audit_event: false)
- raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_update_access_requester?(access_requester)
+ validate_access!(access_requester) unless skip_authorization
access_requester.access_level = params[:access_level] if params[:access_level]
access_requester.accept_request
@@ -15,9 +15,24 @@ module Members
private
+ def validate_access!(access_requester)
+ raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester)
+
+ if approving_member_with_owner_access_level?(access_requester) &&
+ cannot_assign_owner_responsibilities_to_member_in_project?(access_requester)
+ raise Gitlab::Access::AccessDeniedError
+ end
+ end
+
def can_update_access_requester?(access_requester)
can?(current_user, update_member_permission(access_requester), access_requester)
end
+
+ def approving_member_with_owner_access_level?(access_requester)
+ access_level_value = params[:access_level] || access_requester.access_level
+
+ access_level_value == Gitlab::Access::OWNER
+ end
end
end
diff --git a/app/services/members/base_service.rb b/app/services/members/base_service.rb
index 3f55f661d9b..62b8fc5d6f7 100644
--- a/app/services/members/base_service.rb
+++ b/app/services/members/base_service.rb
@@ -60,5 +60,18 @@ module Members
TodosDestroyer::EntityLeaveWorker.perform_in(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, type)
end
end
+
+ def cannot_assign_owner_responsibilities_to_member_in_project?(member)
+ # The purpose of this check is -
+ # We can have direct members who are "Owners" in a project going forward and
+ # we do not want Maintainers of the project updating/adding/removing other "Owners"
+ # within the project.
+ # Only OWNERs in a project should be able to manage any action around OWNERship in that project.
+ member.is_a?(ProjectMember) &&
+ !can?(current_user, :manage_owners, member.source)
+ end
+
+ alias_method :cannot_revoke_owner_responsibilities_from_member_in_project?,
+ :cannot_assign_owner_responsibilities_to_member_in_project?
end
end
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index 8485e7cbafa..57d9da4cefd 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -22,6 +22,11 @@ module Members
def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, create_member_permission(source), source)
+ # rubocop:disable Layout/EmptyLineAfterGuardClause
+ raise Gitlab::Access::AccessDeniedError if adding_at_least_one_owner &&
+ cannot_assign_owner_responsibilities_to_member_in_project?
+ # rubocop:enable Layout/EmptyLineAfterGuardClause
+
validate_invite_source!
validate_invitable!
@@ -45,6 +50,14 @@ module Members
attr_reader :source, :errors, :invites, :member_created_namespace_id, :members,
:tasks_to_be_done_members, :member_created_member_task_id
+ def adding_at_least_one_owner
+ params[:access_level] == Gitlab::Access::OWNER
+ end
+
+ def cannot_assign_owner_responsibilities_to_member_in_project?
+ source.is_a?(Project) && !current_user.can?(:manage_owners, source)
+ end
+
def invites_from_params
# String, Nil, Array, Integer
return params[:user_id] if params[:user_id].is_a?(Array)
diff --git a/app/services/members/creator_service.rb b/app/services/members/creator_service.rb
index 81986a2883f..276093a00a9 100644
--- a/app/services/members/creator_service.rb
+++ b/app/services/members/creator_service.rb
@@ -12,6 +12,105 @@ module Members
def access_levels
Gitlab::Access.sym_options_with_owner
end
+
+ def add_users( # rubocop:disable Metrics/ParameterLists
+ source,
+ users,
+ access_level,
+ current_user: nil,
+ expires_at: nil,
+ tasks_to_be_done: [],
+ tasks_project_id: nil,
+ ldap: nil,
+ blocking_refresh: nil
+ )
+ return [] unless users.present?
+
+ # If this user is attempting to manage Owner members and doesn't have permission, do not allow
+ return [] if managing_owners?(current_user, access_level) && cannot_manage_owners?(source, current_user)
+
+ emails, users, existing_members = parse_users_list(source, users)
+
+ Member.transaction do
+ (emails + users).map! do |user|
+ new(source,
+ user,
+ access_level,
+ existing_members: existing_members,
+ current_user: current_user,
+ expires_at: expires_at,
+ tasks_to_be_done: tasks_to_be_done,
+ tasks_project_id: tasks_project_id,
+ ldap: ldap,
+ blocking_refresh: blocking_refresh)
+ .execute
+ end
+ end
+ end
+
+ def add_user( # rubocop:disable Metrics/ParameterLists
+ source,
+ user,
+ access_level,
+ current_user: nil,
+ expires_at: nil,
+ ldap: nil,
+ blocking_refresh: nil
+ )
+ add_users(source,
+ [user],
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at,
+ ldap: ldap,
+ blocking_refresh: blocking_refresh).first
+ end
+
+ private
+
+ def managing_owners?(current_user, access_level)
+ current_user && Gitlab::Access.sym_options_with_owner[access_level] == Gitlab::Access::OWNER
+ end
+
+ def parse_users_list(source, list)
+ emails = []
+ user_ids = []
+ users = []
+ existing_members = {}
+
+ list.each do |item|
+ case item
+ when User
+ users << item
+ when Integer
+ user_ids << item
+ when /\A\d+\Z/
+ user_ids << item.to_i
+ when Devise.email_regexp
+ emails << item
+ end
+ end
+
+ # the below will automatically discard invalid user_ids
+ users.concat(User.id_in(user_ids)) if user_ids.present?
+ # de-duplicate just in case as there is no controlling if user records and ids are sent multiple times
+ users.uniq!
+
+ users_by_emails = source.users_by_emails(emails) # preloads our request store for all emails
+ # in case emails belong to a user that is being invited by user or user_id, remove them from
+ # emails and let users/user_ids handle it.
+ parsed_emails = emails.select do |email|
+ user = users_by_emails[email]
+ !user || (users.exclude?(user) && user_ids.exclude?(user.id))
+ end
+
+ if users.present? || users_by_emails.present?
+ # helps not have to perform another query per user id to see if the member exists later on when fetching
+ existing_members = source.members_and_requesters.with_user(users + users_by_emails.values).index_by(&:user_id)
+ end
+
+ [parsed_emails, users, existing_members]
+ end
end
def initialize(source, user, access_level, **args)
@@ -21,10 +120,12 @@ module Members
@args = args
end
+ private_class_method :new
+
def execute
find_or_build_member
commit_member
- create_member_task
+ after_commit_tasks
member
end
@@ -92,6 +193,10 @@ module Members
end
end
+ def after_commit_tasks
+ create_member_task
+ end
+
def create_member_task
return unless member.persisted?
return if member_task_attributes.value?(nil)
@@ -163,15 +268,19 @@ module Members
end
def find_or_initialize_member_by_user
- # have to use members and requesters here since project/group limits on requested_at being nil for members and
- # wouldn't be found in `source.members` if it already existed
- # this of course will not treat active invites the same since we aren't searching on email
- source.members_and_requesters.find_or_initialize_by(user_id: user.id) # rubocop:disable CodeReuse/ActiveRecord
+ # We have to use `members_and_requesters` here since the given `members` is modified in the models
+ # to act more like a scope(removing the requested_at members) and therefore ActiveRecord has issues with that
+ # on build and refreshing that relation.
+ existing_members[user.id] || source.members_and_requesters.build(user_id: user.id) # rubocop:disable CodeReuse/ActiveRecord
end
def ldap
args[:ldap] || false
end
+
+ def existing_members
+ args[:existing_members] || {}
+ end
end
end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index bb2d419c046..0a8344c58db 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -3,7 +3,12 @@
module Members
class DestroyService < Members::BaseService
def execute(member, skip_authorization: false, skip_subresources: false, unassign_issuables: false, destroy_bot: false)
- raise Gitlab::Access::AccessDeniedError unless skip_authorization || authorized?(member, destroy_bot)
+ unless skip_authorization
+ raise Gitlab::Access::AccessDeniedError unless authorized?(member, destroy_bot)
+
+ raise Gitlab::Access::AccessDeniedError if destroying_member_with_owner_access_level?(member) &&
+ cannot_revoke_owner_responsibilities_from_member_in_project?(member)
+ end
@skip_auth = skip_authorization
@@ -90,6 +95,10 @@ module Members
can?(current_user, destroy_bot_member_permission(member), member)
end
+ def destroying_member_with_owner_access_level?(member)
+ member.owner?
+ end
+
def destroy_member_permission(member)
case member
when GroupMember
diff --git a/app/services/members/groups/bulk_creator_service.rb b/app/services/members/groups/bulk_creator_service.rb
deleted file mode 100644
index 57cec241584..00000000000
--- a/app/services/members/groups/bulk_creator_service.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module Members
- module Groups
- class BulkCreatorService < Members::Groups::CreatorService
- include Members::BulkCreateUsers
- end
- end
-end
diff --git a/app/services/members/groups/creator_service.rb b/app/services/members/groups/creator_service.rb
index a6f0daa99aa..dd3d44e4d96 100644
--- a/app/services/members/groups/creator_service.rb
+++ b/app/services/members/groups/creator_service.rb
@@ -3,6 +3,12 @@
module Members
module Groups
class CreatorService < Members::CreatorService
+ class << self
+ def cannot_manage_owners?(source, current_user)
+ source.max_member_access_for_user(current_user) < Gitlab::Access::OWNER
+ end
+ end
+
private
def can_create_new_member?
diff --git a/app/services/members/mailgun/process_webhook_service.rb b/app/services/members/mailgun/process_webhook_service.rb
deleted file mode 100644
index e359a83ad42..00000000000
--- a/app/services/members/mailgun/process_webhook_service.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-module Members
- module Mailgun
- class ProcessWebhookService
- ProcessWebhookServiceError = Class.new(StandardError)
-
- def initialize(payload)
- @payload = payload
- end
-
- def execute
- @member = Member.find_by_invite_token(invite_token)
- update_member_and_log if member
- rescue ProcessWebhookServiceError => e
- Gitlab::ErrorTracking.track_exception(e)
- end
-
- private
-
- attr_reader :payload, :member
-
- def update_member_and_log
- log_update_event if member.update(invite_email_success: false)
- end
-
- def log_update_event
- Gitlab::AppLogger.info "UPDATED MEMBER INVITE_EMAIL_SUCCESS: member_id: #{member.id}"
- end
-
- def invite_token
- # may want to validate schema in some way using ::JSONSchemer.schema(SCHEMA_PATH).valid?(message) if this
- # gets more complex
- payload.dig('user-variables', ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY) ||
- raise(ProcessWebhookServiceError, "Failed to receive #{::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY} in user-variables: #{payload}")
- end
- end
- end
-end
diff --git a/app/services/members/projects/bulk_creator_service.rb b/app/services/members/projects/bulk_creator_service.rb
deleted file mode 100644
index 68e71e35d12..00000000000
--- a/app/services/members/projects/bulk_creator_service.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module Members
- module Projects
- class BulkCreatorService < Members::Projects::CreatorService
- include Members::BulkCreateUsers
- end
- end
-end
diff --git a/app/services/members/projects/creator_service.rb b/app/services/members/projects/creator_service.rb
index 9e9389d3c18..cde1d0462e8 100644
--- a/app/services/members/projects/creator_service.rb
+++ b/app/services/members/projects/creator_service.rb
@@ -3,9 +3,18 @@
module Members
module Projects
class CreatorService < Members::CreatorService
+ class << self
+ def cannot_manage_owners?(source, current_user)
+ !Ability.allowed?(current_user, :manage_owners, source)
+ end
+ end
+
private
def can_create_new_member?
+ return false if assigning_project_member_with_owner_access_level? &&
+ cannot_assign_owner_responsibilities_to_member_in_project?
+
# This access check(`admin_project_member`) will write to safe request store cache for the user being added.
# This means any operations inside the same request will need to purge that safe request
# store cache if operations are needed to be done inside the same request that checks max member access again on
@@ -14,6 +23,11 @@ module Members
end
def can_update_existing_member?
+ # rubocop:disable Layout/EmptyLineAfterGuardClause
+ raise ::Gitlab::Access::AccessDeniedError if assigning_project_member_with_owner_access_level? &&
+ cannot_assign_owner_responsibilities_to_member_in_project?
+ # rubocop:enable Layout/EmptyLineAfterGuardClause
+
current_user.can?(:update_project_member, member)
end
@@ -21,6 +35,16 @@ module Members
# this condition is reached during testing setup a lot due to use of `.add_user`
member.project.personal_namespace_holder?(member.user)
end
+
+ def assigning_project_member_with_owner_access_level?
+ return true if member && member.owner?
+
+ access_level == Gitlab::Access::OWNER
+ end
+
+ def cannot_assign_owner_responsibilities_to_member_in_project?
+ member.is_a?(ProjectMember) && !current_user.can?(:manage_owners, member.source)
+ end
end
end
end
diff --git a/app/services/members/update_service.rb b/app/services/members/update_service.rb
index 257698f65ae..b4d1b80e5a3 100644
--- a/app/services/members/update_service.rb
+++ b/app/services/members/update_service.rb
@@ -5,6 +5,7 @@ module Members
# returns the updated member
def execute(member, permission: :update)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, action_member_permission(permission, member), member)
+ raise Gitlab::Access::AccessDeniedError if prevent_upgrade_to_owner?(member) || prevent_downgrade_from_owner?(member)
old_access_level = member.human_access
old_expiry = member.expires_at
@@ -28,6 +29,22 @@ module Members
def downgrading_to_guest?
params[:access_level] == Gitlab::Access::GUEST
end
+
+ def upgrading_to_owner?
+ params[:access_level] == Gitlab::Access::OWNER
+ end
+
+ def downgrading_from_owner?(member)
+ member.owner?
+ end
+
+ def prevent_upgrade_to_owner?(member)
+ upgrading_to_owner? && cannot_assign_owner_responsibilities_to_member_in_project?(member)
+ end
+
+ def prevent_downgrade_from_owner?(member)
+ downgrading_from_owner?(member) && cannot_revoke_owner_responsibilities_from_member_in_project?(member)
+ end
end
end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 44be254441d..2b6a66b9dee 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -121,16 +121,21 @@ module MergeRequests
override :handle_quick_actions
def handle_quick_actions(merge_request)
super
- handle_wip_event(merge_request)
+ handle_draft_event(merge_request)
end
- def handle_wip_event(merge_request)
- if wip_event = params.delete(:wip_event)
+ def handle_draft_event(merge_request)
+ if draft_event = params.delete(:wip_event)
# We update the title that is provided in the params or we use the mr title
title = params[:title] || merge_request.title
- params[:title] = case wip_event
- when 'wip' then MergeRequest.wip_title(title)
- when 'unwip' then MergeRequest.wipless_title(title)
+ # Supports both `wip` and `draft` permutations of draft_event
+ # This support can be removed >= %15.2
+ #
+ params[:title] = case draft_event
+ when 'wip' then MergeRequest.draft_title(title)
+ when 'draft' then MergeRequest.draft_title(title)
+ when 'unwip' then MergeRequest.draftless_title(title)
+ when 'ready' then MergeRequest.draftless_title(title)
end
end
end
@@ -185,9 +190,12 @@ module MergeRequests
def create_pipeline_for(merge_request, user, async: false)
if async
+ # TODO: pass push_options to worker
MergeRequests::CreatePipelineWorker.perform_async(project.id, user.id, merge_request.id)
else
- MergeRequests::CreatePipelineService.new(project: project, current_user: user).execute(merge_request)
+ MergeRequests::CreatePipelineService
+ .new(project: project, current_user: user, params: params.slice(:push_options))
+ .execute(merge_request)
end
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index ee6f204be45..cc786ac02bd 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -46,7 +46,7 @@ module MergeRequests
:source_branch_ref,
:source_project,
:compare_commits,
- :wip_title,
+ :draft_title,
:description,
:first_multiline_commit,
:errors,
@@ -206,7 +206,7 @@ module MergeRequests
def set_draft_title_if_needed
return unless compare_commits.empty? || Gitlab::Utils.to_boolean(params[:draft])
- merge_request.title = wip_title
+ merge_request.title = draft_title
end
# When your branch name starts with an iid followed by a dash this pattern will be
@@ -223,6 +223,7 @@ module MergeRequests
# more than one commit in the MR
#
def assign_title_and_description
+ assign_description_from_repository_template
assign_title_and_description_from_commits
merge_request.title ||= title_from_issue if target_project.issues_enabled? || target_project.external_issue_tracker
merge_request.title ||= source_branch.titleize.humanize
@@ -286,6 +287,37 @@ module MergeRequests
title_parts.join(' ')
end
+ def assign_description_from_repository_template
+ return unless merge_request.description.blank?
+
+ # Use TemplateFinder to load the default template. We need this mainly for
+ # the project_id, in case it differs from the target project. Conveniently,
+ # since the underlying merge_request_template_names_hash is cached, this
+ # should also be relatively cheap and allows us to bail early if the project
+ # does not have a default template.
+ templates = TemplateFinder.all_template_names(target_project, :merge_requests)
+ template = templates.values.flatten.find { |tmpl| tmpl[:name].casecmp?('default') }
+
+ return unless template
+
+ begin
+ repository_template = TemplateFinder.build(
+ :merge_requests,
+ target_project,
+ {
+ name: template[:name],
+ source_template_project_id: template[:project_id]
+ }
+ ).execute
+ rescue ::Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
+ return
+ end
+
+ return unless repository_template.present?
+
+ merge_request.description = repository_template.content
+ end
+
def issue_iid
strong_memoize(:issue_iid) do
@params_issue_iid || begin
diff --git a/app/services/merge_requests/create_pipeline_service.rb b/app/services/merge_requests/create_pipeline_service.rb
index 9d7f8393ba5..37c734613e7 100644
--- a/app/services/merge_requests/create_pipeline_service.rb
+++ b/app/services/merge_requests/create_pipeline_service.rb
@@ -9,9 +9,11 @@ module MergeRequests
end
def create_detached_merge_request_pipeline(merge_request)
- Ci::CreatePipelineService.new(pipeline_project(merge_request),
- current_user,
- ref: pipeline_ref_for_detached_merge_request_pipeline(merge_request))
+ Ci::CreatePipelineService
+ .new(pipeline_project(merge_request),
+ current_user,
+ ref: pipeline_ref_for_detached_merge_request_pipeline(merge_request),
+ push_options: params[:push_options])
.execute(:merge_request_event, merge_request: merge_request)
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 5e7eee4f1c3..f51923b7035 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -117,6 +117,8 @@ module MergeRequests
if delete_source_branch?
MergeRequests::DeleteSourceBranchWorker.perform_async(@merge_request.id, @merge_request.source_branch_sha, branch_deletion_user.id)
end
+
+ merge_request_merge_param
end
def clean_merge_jid
@@ -135,6 +137,12 @@ module MergeRequests
@merge_request.can_remove_source_branch?(branch_deletion_user)
end
+ def merge_request_merge_param
+ if @merge_request.can_remove_source_branch?(branch_deletion_user) && !params.fetch('should_remove_source_branch', nil).nil?
+ @merge_request.update(merge_params: @merge_request.merge_params.merge('should_remove_source_branch' => params['should_remove_source_branch']))
+ end
+ end
+
def handle_merge_error(log_message:, save_message_on_model: false)
Gitlab::AppLogger.error("MergeService ERROR: #{merge_request_info} - #{log_message}")
@merge_request.update(merge_error: log_message) if save_message_on_model
diff --git a/app/services/merge_requests/mergeability/run_checks_service.rb b/app/services/merge_requests/mergeability/run_checks_service.rb
index fd6907c976b..1d4b96b3090 100644
--- a/app/services/merge_requests/mergeability/run_checks_service.rb
+++ b/app/services/merge_requests/mergeability/run_checks_service.rb
@@ -4,23 +4,13 @@ module MergeRequests
class RunChecksService
include Gitlab::Utils::StrongMemoize
- # We want to have the cheapest checks first in the list,
- # that way we can fail fast before running the more expensive ones
- CHECKS = [
- CheckOpenStatusService,
- CheckDraftStatusService,
- CheckBrokenStatusService,
- CheckDiscussionsStatusService,
- CheckCiStatusService
- ].freeze
-
def initialize(merge_request:, params:)
@merge_request = merge_request
@params = params
end
def execute
- CHECKS.each_with_object([]) do |check_class, results|
+ merge_request.mergeability_checks.each_with_object([]) do |check_class, results|
check = check_class.new(merge_request: merge_request, params: params)
next if check.skip?
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index 980c757bcbc..286c082ac8a 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -48,7 +48,7 @@ module MergeRequests
# We are intentionally only closing Issues asynchronously (excluding ExternalIssues)
# as the worker only supports finding an Issue. We are also only experiencing
# SQL timeouts when closing an Issue.
- if Feature.enabled?(:async_mr_close_issue, project) && issue.is_a?(Issue)
+ if issue.is_a?(Issue)
MergeRequests::CloseIssueWorker.perform_async(
project.id,
current_user.id,
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index f7a0f90b95f..5205d34baae 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -255,17 +255,17 @@ module MergeRequests
commit_shas = merge_request.commit_shas
- wip_commit = @commits.detect do |commit|
- commit.work_in_progress? && commit_shas.include?(commit.sha)
+ draft_commit = @commits.detect do |commit|
+ commit.draft? && commit_shas.include?(commit.sha)
end
- if wip_commit && !merge_request.work_in_progress?
- merge_request.update(title: merge_request.wip_title)
+ if draft_commit && !merge_request.draft?
+ merge_request.update(title: merge_request.draft_title)
SystemNoteService.add_merge_request_draft_from_commit(
merge_request,
merge_request.project,
@current_user,
- wip_commit
+ draft_commit
)
end
end
diff --git a/app/services/merge_requests/reload_merge_head_diff_service.rb b/app/services/merge_requests/reload_merge_head_diff_service.rb
index 4724dd1c068..f6506779aba 100644
--- a/app/services/merge_requests/reload_merge_head_diff_service.rb
+++ b/app/services/merge_requests/reload_merge_head_diff_service.rb
@@ -43,3 +43,5 @@ module MergeRequests
end
end
end
+
+MergeRequests::ReloadMergeHeadDiffService.prepend_mod
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 6e8afaecbba..603da4ef535 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -180,16 +180,16 @@ module MergeRequests
return unless changed_fields.include?("title")
old_title, new_title = merge_request.previous_changes["title"]
- old_title_wip = MergeRequest.work_in_progress?(old_title)
- new_title_wip = MergeRequest.work_in_progress?(new_title)
+ old_title_draft = MergeRequest.draft?(old_title)
+ new_title_draft = MergeRequest.draft?(new_title)
- if !old_title_wip && new_title_wip
- # Marked as Draft/WIP
+ if !old_title_draft && new_title_draft
+ # Marked as Draft
#
merge_request_activity_counter
.track_marked_as_draft_action(user: current_user)
- elsif old_title_wip && !new_title_wip
- # Unmarked as Draft/WIP
+ elsif old_title_draft && !new_title_draft
+ # Unmarked as Draft
#
notify_draft_status_changed(merge_request)
diff --git a/app/services/metrics/dashboard/base_service.rb b/app/services/metrics/dashboard/base_service.rb
index 5be8ae62548..5975fa28b0b 100644
--- a/app/services/metrics/dashboard/base_service.rb
+++ b/app/services/metrics/dashboard/base_service.rb
@@ -14,7 +14,6 @@ module Metrics
STAGES::VariableEndpointInserter,
STAGES::PanelIdsInserter,
STAGES::TrackPanelType,
- STAGES::AlertsInserter,
STAGES::UrlValidator
].freeze
diff --git a/app/services/metrics/dashboard/panel_preview_service.rb b/app/services/metrics/dashboard/panel_preview_service.rb
index 02dd908e229..260b49a5b19 100644
--- a/app/services/metrics/dashboard/panel_preview_service.rb
+++ b/app/services/metrics/dashboard/panel_preview_service.rb
@@ -10,7 +10,6 @@ module Metrics
::Gitlab::Metrics::Dashboard::Stages::CommonMetricsInserter,
::Gitlab::Metrics::Dashboard::Stages::MetricEndpointInserter,
::Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter,
- ::Gitlab::Metrics::Dashboard::Stages::AlertsInserter,
::Gitlab::Metrics::Dashboard::Stages::UrlValidator
].freeze
diff --git a/app/services/metrics/dashboard/system_dashboard_service.rb b/app/services/metrics/dashboard/system_dashboard_service.rb
index 29b8f23f40d..1bd31b2ba21 100644
--- a/app/services/metrics/dashboard/system_dashboard_service.rb
+++ b/app/services/metrics/dashboard/system_dashboard_service.rb
@@ -17,8 +17,7 @@ module Metrics
STAGES::CustomMetricsDetailsInserter,
STAGES::MetricEndpointInserter,
STAGES::VariableEndpointInserter,
- STAGES::PanelIdsInserter,
- STAGES::AlertsInserter
+ STAGES::PanelIdsInserter
].freeze
class << self
diff --git a/app/services/note_summary.rb b/app/services/note_summary.rb
index 6fe14939aaa..0f2555b9ff0 100644
--- a/app/services/note_summary.rb
+++ b/app/services/note_summary.rb
@@ -4,9 +4,9 @@ class NoteSummary
attr_reader :note
attr_reader :metadata
- def initialize(noteable, project, author, body, action: nil, commit_count: nil)
+ def initialize(noteable, project, author, body, action: nil, commit_count: nil, created_at: nil)
@note = { noteable: noteable,
- created_at: noteable.system_note_timestamp,
+ created_at: created_at || noteable.system_note_timestamp,
project: project, author: author, note: body }
@metadata = { action: action, commit_count: commit_count }.compact
diff --git a/app/services/notes/copy_service.rb b/app/services/notes/copy_service.rb
index 6e5b4596602..e7182350837 100644
--- a/app/services/notes/copy_service.rb
+++ b/app/services/notes/copy_service.rb
@@ -38,17 +38,16 @@ module Notes
def params_from_note(note, new_note)
new_discussion_ids[note.discussion_id] ||= Discussion.discussion_id(new_note)
- rewritten_note = MarkdownContentRewriterService.new(current_user, note.note, from_project, to_noteable.resource_parent).execute
- new_params = {
+ new_params = sanitized_note_params(note)
+
+ new_params.merge!(
project: to_noteable.project,
noteable: to_noteable,
discussion_id: new_discussion_ids[note.discussion_id],
- note: rewritten_note,
- note_html: nil,
created_at: note.created_at,
updated_at: note.updated_at
- }
+ )
if note.system_note_metadata
new_params[:system_note_metadata] = note.system_note_metadata.dup
@@ -61,6 +60,14 @@ module Notes
new_params
end
+ # Skip copying cached markdown HTML if text
+ # does not contain references or uploads.
+ def sanitized_note_params(note)
+ MarkdownContentRewriterService
+ .new(current_user, note, :note, from_project, to_noteable.resource_parent)
+ .execute
+ end
+
def copy_award_emoji(from_note, to_note)
AwardEmojis::CopyService.new(from_note, to_note).execute
end
diff --git a/app/services/notification_recipients/build_service.rb b/app/services/notification_recipients/build_service.rb
index aeb859af4d9..e63e19e365c 100644
--- a/app/services/notification_recipients/build_service.rb
+++ b/app/services/notification_recipients/build_service.rb
@@ -29,10 +29,6 @@ module NotificationRecipients
::NotificationRecipients::Builder::ProjectMaintainers.new(target, **args).notification_recipients
end
- def self.build_new_release_recipients(*args)
- ::NotificationRecipients::Builder::NewRelease.new(*args).notification_recipients
- end
-
def self.build_new_review_recipients(*args)
::NotificationRecipients::Builder::NewReview.new(*args).notification_recipients
end
diff --git a/app/services/notification_recipients/builder/new_release.rb b/app/services/notification_recipients/builder/new_release.rb
deleted file mode 100644
index 67676b6eec8..00000000000
--- a/app/services/notification_recipients/builder/new_release.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-module NotificationRecipients
- module Builder
- class NewRelease < Base
- attr_reader :target
-
- def initialize(target)
- @target = target
- end
-
- def build!
- add_recipients(target.project.authorized_users, :custom, nil)
- end
-
- def custom_action
- :new_release
- end
-
- def acting_user
- target.author
- end
- end
- end
-end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 32b23d4978f..2477fcd02e5 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -447,7 +447,9 @@ class NotificationService
return false
end
- recipients = NotificationRecipients::BuildService.build_new_release_recipients(release)
+ recipients = NotificationRecipients::BuildService.build_recipients(release,
+ release.author,
+ action: "new")
recipients.each do |recipient|
mailer.new_release_email(recipient.user.id, release, recipient.reason).deliver_later
diff --git a/app/services/packages/cleanup/update_policy_service.rb b/app/services/packages/cleanup/update_policy_service.rb
new file mode 100644
index 00000000000..6744accc007
--- /dev/null
+++ b/app/services/packages/cleanup/update_policy_service.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Packages
+ module Cleanup
+ class UpdatePolicyService < BaseProjectService
+ ALLOWED_ATTRIBUTES = %i[keep_n_duplicated_package_files].freeze
+
+ def execute
+ return ServiceResponse.error(message: 'Access denied') unless allowed?
+
+ if policy.update(policy_params)
+ ServiceResponse.success(payload: { packages_cleanup_policy: policy })
+ else
+ ServiceResponse.error(message: policy.errors.full_messages.to_sentence || 'Bad request')
+ end
+ end
+
+ private
+
+ def policy
+ strong_memoize(:policy) do
+ project.packages_cleanup_policy
+ end
+ end
+
+ def allowed?
+ can?(current_user, :admin_package, project)
+ end
+
+ def policy_params
+ params.slice(*ALLOWED_ATTRIBUTES)
+ 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 2a6eeff402e..0e5771a2e73 100644
--- a/app/services/packages/go/create_package_service.rb
+++ b/app/services/packages/go/create_package_service.rb
@@ -38,11 +38,12 @@ module Packages
raise GoZipSizeError, "#{version.mod.name}@#{version.name}.#{type} exceeds size limit" if file.size > project.actual_limits.golang_max_file_size
digests = {
- md5: Digest::MD5.hexdigest(content),
sha1: Digest::SHA1.hexdigest(content),
sha256: Digest::SHA256.hexdigest(content)
}
+ digests[:md5] = Digest::MD5.hexdigest(content) unless Gitlab::FIPS.enabled?
+
[file, digests]
end
diff --git a/app/services/packages/maven/metadata/append_package_file_service.rb b/app/services/packages/maven/metadata/append_package_file_service.rb
index e991576ebc6..c778620ea8e 100644
--- a/app/services/packages/maven/metadata/append_package_file_service.rb
+++ b/app/services/packages/maven/metadata/append_package_file_service.rb
@@ -36,7 +36,7 @@ module Packages
sha256: file_sha256
)
- append_metadata_file(content: file_md5, file_name: MD5_FILE_NAME)
+ append_metadata_file(content: file_md5, file_name: MD5_FILE_NAME) unless Gitlab::FIPS.enabled?
append_metadata_file(content: file_sha1, file_name: SHA1_FILE_NAME)
append_metadata_file(content: file_sha256, file_name: SHA256_FILE_NAME)
append_metadata_file(content: file_sha512, file_name: SHA512_FILE_NAME)
@@ -70,6 +70,8 @@ module Packages
end
def digest_from(content, type)
+ return if type == :md5 && Gitlab::FIPS.enabled?
+
digest_class = case type
when :md5
Digest::MD5
diff --git a/app/services/packages/rubygems/create_gemspec_service.rb b/app/services/packages/rubygems/create_gemspec_service.rb
index 22533264480..52642aeb0a0 100644
--- a/app/services/packages/rubygems/create_gemspec_service.rb
+++ b/app/services/packages/rubygems/create_gemspec_service.rb
@@ -24,12 +24,14 @@ module Packages
file.write(content)
file.flush
+ md5 = Gitlab::FIPS.enabled? ? nil : Digest::MD5.hexdigest(content)
+
package.package_files.create!(
file: file,
size: file.size,
file_name: "#{gemspec.name}.gemspec",
file_sha1: Digest::SHA1.hexdigest(content),
- file_md5: Digest::MD5.hexdigest(content),
+ file_md5: md5,
file_sha256: Digest::SHA256.hexdigest(content)
)
ensure
diff --git a/app/services/pages/delete_service.rb b/app/services/pages/delete_service.rb
index 8d33e6c1000..95e99daeb6c 100644
--- a/app/services/pages/delete_service.rb
+++ b/app/services/pages/delete_service.rb
@@ -11,7 +11,20 @@ module Pages
# > The default strategy is :nullify which sets the foreign keys to NULL.
PagesDomain.for_project(project).delete_all
+ publish_deleted_event
+
DestroyPagesDeploymentsWorker.perform_async(project.id)
end
+
+ private
+
+ def publish_deleted_event
+ event = Pages::PageDeletedEvent.new(data: {
+ project_id: project.id,
+ namespace_id: project.namespace_id
+ })
+
+ Gitlab::EventStore.publish(event)
+ end
end
end
diff --git a/app/services/pages_domains/create_acme_order_service.rb b/app/services/pages_domains/create_acme_order_service.rb
index c600f497fa5..e289a78091b 100644
--- a/app/services/pages_domains/create_acme_order_service.rb
+++ b/app/services/pages_domains/create_acme_order_service.rb
@@ -2,6 +2,9 @@
module PagesDomains
class CreateAcmeOrderService
+ # elliptic curve algorithm to generate the private key
+ ECDSA_CURVE = "prime256v1"
+
attr_reader :pages_domain
def initialize(pages_domain)
@@ -14,7 +17,12 @@ module PagesDomains
challenge = order.new_challenge
- private_key = OpenSSL::PKey::RSA.new(4096)
+ private_key = if Feature.enabled?(:pages_lets_encrypt_ecdsa, pages_domain.project)
+ OpenSSL::PKey::EC.generate(ECDSA_CURVE)
+ else
+ OpenSSL::PKey::RSA.new(4096)
+ end
+
saved_order = pages_domain.acme_orders.create!(
url: order.url,
expires_at: order.expires,
diff --git a/app/services/projects/after_rename_service.rb b/app/services/projects/after_rename_service.rb
index a3d54bc6b58..2ed4346e5ca 100644
--- a/app/services/projects/after_rename_service.rb
+++ b/app/services/projects/after_rename_service.rb
@@ -95,20 +95,6 @@ module Projects
.new
.rename_project(path_before, project_path, namespace_full_path)
end
-
- if project.pages_deployed?
- # Block will be evaluated in the context of project so we need
- # to bind to a local variable to capture it, as the instance
- # variable and method aren't available on Project
- path_before_local = @path_before
-
- project.run_after_commit_or_now do
- Gitlab::PagesTransfer
- .new
- .async
- .rename_project(path_before_local, path, namespace.full_path)
- end
- end
end
def log_completion
diff --git a/app/services/projects/destroy_rollback_service.rb b/app/services/projects/destroy_rollback_service.rb
deleted file mode 100644
index 7f0ca63a406..00000000000
--- a/app/services/projects/destroy_rollback_service.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- class DestroyRollbackService < BaseService
- include Gitlab::ShellAdapter
-
- def execute
- return unless project
-
- Projects::ForksCountService.new(project).delete_cache
-
- unless rollback_repository(project.repository)
- raise_error(s_('DeleteProject|Failed to restore project repository. Please contact the administrator.'))
- end
-
- unless rollback_repository(project.wiki.repository)
- raise_error(s_('DeleteProject|Failed to restore wiki repository. Please contact the administrator.'))
- end
- end
-
- private
-
- def rollback_repository(repository)
- return true unless repository
-
- result = Repositories::DestroyRollbackService.new(repository).execute
-
- result[:status] == :success
- end
- end
-end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index a73244c6971..bc5be5bdff3 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -10,11 +10,6 @@ module Projects
def async_execute
project.update_attribute(:pending_delete, true)
- # Ensure no repository +deleted paths are kept,
- # regardless of any issue with the ProjectDestroyWorker
- # job process.
- schedule_stale_repos_removal
-
job_id = ProjectDestroyWorker.perform_async(project.id, current_user.id, params)
log_info("User #{current_user.id} scheduled destruction of project #{project.full_path} with job ID #{job_id}")
end
@@ -109,16 +104,6 @@ module Projects
result[:status] == :success
end
- def schedule_stale_repos_removal
- repos = [project.repository, project.wiki.repository]
-
- repos.each do |repository|
- next unless repository
-
- Repositories::ShellDestroyService.new(repository).execute(Repositories::ShellDestroyService::STALE_REMOVAL_DELAY)
- end
- end
-
def attempt_rollback(project, message)
return unless project
@@ -191,6 +176,10 @@ module Projects
# rubocop: enable CodeReuse/ActiveRecord
def destroy_ci_records!
+ # Make sure to destroy this first just in case the project is undergoing stats refresh.
+ # This is to avoid logging the artifact deletion in Ci::JobArtifacts::DestroyBatchService.
+ project.build_artifacts_size_refresh&.destroy
+
project.all_pipelines.find_each(batch_size: BATCH_SIZE) do |pipeline| # rubocop: disable CodeReuse/ActiveRecord
# Destroy artifacts, then builds, then pipelines
# All builds have already been dropped by Ci::AbortPipelinesService,
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index 72492b6f5a5..d8d35422590 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -143,7 +143,12 @@ module Projects
project_id: project.id
)
- notification_service.project_not_exported(project, current_user, shared.errors)
+ user = current_user
+ errors = shared.errors
+
+ project.run_after_commit_or_now do |project|
+ NotificationService.new.project_not_exported(project, user, errors)
+ end
end
end
end
diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb
index ee4d559e612..925512f31d7 100644
--- a/app/services/projects/open_issues_count_service.rb
+++ b/app/services/projects/open_issues_count_service.rb
@@ -63,12 +63,12 @@ module Projects
# rubocop: disable CodeReuse/ActiveRecord
def self.query(projects, public_only: true)
- issues_filtered_by_type = Issue.opened.with_issue_type(Issue::TYPES_FOR_LIST)
+ open_issues = Issue.opened
if public_only
- issues_filtered_by_type.public_only.where(project: projects)
+ open_issues.public_only.where(project: projects)
else
- issues_filtered_by_type.where(project: projects)
+ open_issues.where(project: projects)
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/projects/operations/update_service.rb b/app/services/projects/operations/update_service.rb
index b66435d013b..d01e96a1a2d 100644
--- a/app/services/projects/operations/update_service.rb
+++ b/app/services/projects/operations/update_service.rb
@@ -112,7 +112,7 @@ module Projects
integration = project.find_or_initialize_integration(::Integrations::Prometheus.to_param)
integration.assign_attributes(attrs)
- attrs = integration.to_integration_hash.except('created_at', 'updated_at')
+ attrs = integration.to_database_hash
{ prometheus_integration_attributes: attrs }
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 2ad5c303be2..666227951c6 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -120,7 +120,6 @@ module Projects
# Overridden in EE
def post_update_hooks(project)
- move_pages(project)
ensure_personal_project_owner_membership(project)
end
@@ -232,13 +231,6 @@ module Projects
)
end
- def move_pages(project)
- return unless project.pages_deployed?
-
- transfer = Gitlab::PagesTransfer.new.async
- transfer.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
- end
-
def old_wiki_repo_path
"#{old_path}#{::Gitlab::GlRepository::WIKI.path_suffix}"
end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index c6ea364320f..8ded2516b97 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -3,6 +3,7 @@
module Projects
class UpdatePagesService < BaseService
InvalidStateError = Class.new(StandardError)
+ WrongUploadedDeploymentSizeError = Class.new(StandardError)
BLOCK_SIZE = 32.kilobytes
PUBLIC_DIR = 'public'
@@ -39,6 +40,9 @@ module Projects
end
rescue InvalidStateError => e
error(e.message)
+ rescue WrongUploadedDeploymentSizeError => e
+ error("Uploading artifacts to pages storage failed")
+ raise e
rescue StandardError => e
error(e.message)
raise e
@@ -80,6 +84,10 @@ module Projects
ci_build_id: build.id
)
+ if deployment.size != file.size || deployment.file.size != file.size
+ raise(WrongUploadedDeploymentSizeError)
+ end
+
validate_outdated_sha!
project.update_pages_deployment!(deployment)
diff --git a/app/services/releases/base_service.rb b/app/services/releases/base_service.rb
index 249333e6d13..7fb59dad508 100644
--- a/app/services/releases/base_service.rb
+++ b/app/services/releases/base_service.rb
@@ -19,6 +19,10 @@ module Releases
params[:tag]
end
+ def tag_message
+ params[:tag_message]
+ end
+
def ref
params[:ref]
end
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
index caa6a003205..e3134070231 100644
--- a/app/services/releases/create_service.rb
+++ b/app/services/releases/create_service.rb
@@ -34,7 +34,7 @@ module Releases
result = Tags::CreateService
.new(project, current_user)
- .execute(tag_name, ref, nil)
+ .execute(tag_name, ref, tag_message)
return result unless result[:status] == :success
diff --git a/app/services/repositories/base_service.rb b/app/services/repositories/base_service.rb
index 13ad126f8f0..4d7e4ffe267 100644
--- a/app/services/repositories/base_service.rb
+++ b/app/services/repositories/base_service.rb
@@ -3,8 +3,6 @@
class Repositories::BaseService < BaseService
include Gitlab::ShellAdapter
- DELETED_FLAG = '+deleted'
-
attr_reader :repository
delegate :container, :disk_path, :full_path, to: :repository
@@ -21,16 +19,6 @@ class Repositories::BaseService < BaseService
gitlab_shell.mv_repository(repository.shard, from_path, to_path)
end
- # Build a path for removing repositories
- # We use `+` because its not allowed by GitLab so user can not create
- # project with name cookies+119+deleted and capture someone stalled repository
- #
- # gitlab/cookies.git -> gitlab/cookies+119+deleted.git
- #
- def removal_path
- "#{disk_path}+#{container.id}#{DELETED_FLAG}"
- end
-
# If we get a Gitaly error, the repository may be corrupted. We can
# ignore these errors since we're going to trash the repositories
# anyway.
diff --git a/app/services/repositories/changelog_service.rb b/app/services/repositories/changelog_service.rb
index eafd9d7a55e..7a78b323453 100644
--- a/app/services/repositories/changelog_service.rb
+++ b/app/services/repositories/changelog_service.rb
@@ -6,6 +6,19 @@ module Repositories
DEFAULT_TRAILER = 'Changelog'
DEFAULT_FILE = 'CHANGELOG.md'
+ # The maximum number of commits allowed to fetch in `from` and `to` range.
+ #
+ # This value is arbitrarily chosen. Increasing it means more Gitaly calls
+ # and more presure on Gitaly services.
+ #
+ # This number is 3x of the average number of commits per GitLab releases.
+ # Some examples for GitLab's own releases:
+ #
+ # * 13.6.0: 4636 commits
+ # * 13.5.0: 5912 commits
+ # * 13.4.0: 5541 commits
+ COMMITS_LIMIT = 15_000
+
# The `project` specifies the `Project` to generate the changelog section
# for.
#
@@ -75,6 +88,8 @@ module Repositories
commits =
ChangelogCommitsFinder.new(project: @project, from: from, to: @to)
+ verify_commit_range!(from, @to)
+
commits.each_page(@trailer) do |page|
mrs = mrs_finder.execute(page)
@@ -82,6 +97,9 @@ module Repositories
# batch of commits, instead of needing a query for every commit.
page.each(&:lazy_author)
+ # Preload author permissions
+ @project.team.max_member_access_for_user_ids(page.map(&:author).compact.map(&:id))
+
page.each do |commit|
release.add_entry(
title: commit.title,
@@ -117,5 +135,19 @@ module Repositories
'could be found to use instead'
)
end
+
+ def verify_commit_range!(from, to)
+ return unless Feature.enabled?(:changelog_commits_limitation, @project)
+
+ commits = @project.repository.commits_by(oids: [from, to])
+
+ raise Gitlab::Changelog::Error, "Invalid or not found commit value in the given range" unless commits.count == 2
+
+ _, commits_count = @project.repository.diverging_commit_count(from, to)
+
+ if commits_count > COMMITS_LIMIT
+ raise Gitlab::Changelog::Error, "The commits range exceeds #{COMMITS_LIMIT} elements."
+ end
+ end
end
end
diff --git a/app/services/repositories/destroy_rollback_service.rb b/app/services/repositories/destroy_rollback_service.rb
deleted file mode 100644
index a19e305607f..00000000000
--- a/app/services/repositories/destroy_rollback_service.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-class Repositories::DestroyRollbackService < Repositories::BaseService
- def execute
- # There is a possibility project does not have repository or wiki
- return success unless repo_exists?(removal_path)
-
- # Flush the cache for both repositories.
- ignore_git_errors { repository.before_delete }
-
- if mv_repository(removal_path, disk_path)
- log_info(%Q{Repository "#{removal_path}" moved to "#{disk_path}" for repository "#{full_path}"})
-
- success
- elsif repo_exists?(removal_path)
- # If the repo does not exist, there is no need to return an
- # error because there was nothing to do.
- move_error(removal_path)
- else
- success
- end
- rescue Gitlab::Git::Repository::NoRepository
- success
- end
-end
diff --git a/app/services/repositories/destroy_service.rb b/app/services/repositories/destroy_service.rb
index c5a0af56066..a87b8d09244 100644
--- a/app/services/repositories/destroy_service.rb
+++ b/app/services/repositories/destroy_service.rb
@@ -10,31 +10,25 @@ class Repositories::DestroyService < Repositories::BaseService
# Git data (e.g. a list of branch names).
ignore_git_errors { repository.before_delete }
- if mv_repository(disk_path, removal_path)
- log_info(%Q{Repository "#{disk_path}" moved to "#{removal_path}" for repository "#{full_path}"})
+ # Use variables that aren't methods on Project, because they are used in a callback
+ current_storage = repository.shard
+ current_path = "#{disk_path}.git"
- current_repository = repository
-
- # Because GitlabShellWorker is inside a run_after_commit callback it will
- # never be triggered on a read-only instance.
- #
- # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/223272
- if Gitlab::Database.read_only?
- Repositories::ShellDestroyService.new(current_repository).execute
- else
- container.run_after_commit do
- Repositories::ShellDestroyService.new(current_repository).execute
- end
+ # Because #remove happens inside a run_after_commit callback it will
+ # never be triggered on a read-only instance.
+ #
+ # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/223272
+ if Gitlab::Database.read_only?
+ Gitlab::Git::Repository.new(current_storage, current_path, nil, nil).remove
+ else
+ container.run_after_commit do
+ Gitlab::Git::Repository.new(current_storage, current_path, nil, nil).remove
end
+ end
- log_info("Repository \"#{full_path}\" was removed")
+ log_info("Repository \"#{full_path}\" was removed")
- success
- elsif repo_exists?(disk_path)
- move_error(disk_path)
- else
- success
- end
+ success
rescue Gitlab::Git::Repository::NoRepository
success
end
diff --git a/app/services/repositories/shell_destroy_service.rb b/app/services/repositories/shell_destroy_service.rb
deleted file mode 100644
index d25cb28c6d7..00000000000
--- a/app/services/repositories/shell_destroy_service.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-class Repositories::ShellDestroyService < Repositories::BaseService
- REPO_REMOVAL_DELAY = 5.minutes.to_i
- STALE_REMOVAL_DELAY = REPO_REMOVAL_DELAY * 2
-
- def execute(delay = REPO_REMOVAL_DELAY)
- return success unless repository
-
- GitlabShellWorker.perform_in(delay,
- :remove_repository,
- repository.shard,
- removal_path)
- end
-end
diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb
index f7ffe288d57..316e6367aa7 100644
--- a/app/services/resource_access_tokens/create_service.rb
+++ b/app/services/resource_access_tokens/create_service.rb
@@ -12,13 +12,15 @@ module ResourceAccessTokens
def execute
return error("User does not have permission to create #{resource_type} access token") unless has_permission_to_create?
+ access_level = params[:access_level] || Gitlab::Access::MAINTAINER
+ return error("Could not provision owner access to project access token") if do_not_allow_owner_access_level_for_project_bot?(access_level)
+
user = create_user
return error(user.errors.full_messages.to_sentence) unless user.persisted?
user.update!(external: true) if current_user.external?
- access_level = params[:access_level] || Gitlab::Access::MAINTAINER
member = create_membership(resource, user, access_level)
unless member.persisted?
@@ -120,6 +122,12 @@ module ResourceAccessTokens
def success(access_token)
ServiceResponse.success(payload: { access_token: access_token })
end
+
+ def do_not_allow_owner_access_level_for_project_bot?(access_level)
+ resource.is_a?(Project) &&
+ access_level == Gitlab::Access::OWNER &&
+ !current_user.can?(:manage_owners, resource)
+ end
end
end
diff --git a/app/services/resource_events/base_change_timebox_service.rb b/app/services/resource_events/base_change_timebox_service.rb
index d802bbee107..372f1c9d816 100644
--- a/app/services/resource_events/base_change_timebox_service.rb
+++ b/app/services/resource_events/base_change_timebox_service.rb
@@ -22,7 +22,7 @@ module ResourceEvents
end
def build_resource_args
- key = resource.class.name.foreign_key
+ key = resource.class.base_class.name.foreign_key
{
user_id: user.id,
diff --git a/app/services/resource_events/base_synthetic_notes_builder_service.rb b/app/services/resource_events/base_synthetic_notes_builder_service.rb
index 192d40129a3..36de70dc291 100644
--- a/app/services/resource_events/base_synthetic_notes_builder_service.rb
+++ b/app/services/resource_events/base_synthetic_notes_builder_service.rb
@@ -25,8 +25,7 @@ module ResourceEvents
def apply_common_filters(events)
events = apply_pagination(events)
- events = apply_last_fetched_at(events)
- apply_fetch_until(events)
+ apply_last_fetched_at(events)
end
def apply_pagination(events)
@@ -44,12 +43,6 @@ module ResourceEvents
events.created_after(last_fetched_at)
end
- def apply_fetch_until(events)
- return events unless params[:fetch_until].present?
-
- events.created_on_or_before(params[:fetch_until])
- end
-
def resource_parent
strong_memoize(:resource_parent) do
resource.project || resource.group
diff --git a/app/services/service_ping/submit_service.rb b/app/services/service_ping/submit_service.rb
index 343fc00a2f0..b850592f7ba 100644
--- a/app/services/service_ping/submit_service.rb
+++ b/app/services/service_ping/submit_service.rb
@@ -10,8 +10,9 @@ module ServicePing
SubmissionError = Class.new(StandardError)
- def initialize(skip_db_write: false)
+ def initialize(skip_db_write: false, payload: nil)
@skip_db_write = skip_db_write
+ @payload = payload
end
def execute
@@ -19,7 +20,7 @@ module ServicePing
start = Time.current
begin
- usage_data = ServicePing::BuildPayload.new.execute
+ usage_data = payload || ServicePing::BuildPayload.new.execute
response = submit_usage_data_payload(usage_data)
rescue StandardError => e
return unless Gitlab::CurrentSettings.usage_ping_enabled?
@@ -34,7 +35,7 @@ module ServicePing
}
submit_payload({ error: error_payload }, path: ERROR_PATH)
- usage_data = Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values)
+ usage_data = payload || Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values)
response = submit_usage_data_payload(usage_data)
end
@@ -45,7 +46,7 @@ module ServicePing
raise SubmissionError, "Invalid usage_data_id in response: #{version_usage_data_id}"
end
- unless @skip_db_write
+ unless skip_db_write
raw_usage_data = save_raw_usage_data(usage_data)
raw_usage_data.update_version_metadata!(usage_data_id: version_usage_data_id)
ServicePing::DevopsReport.new(response).execute
@@ -58,6 +59,8 @@ module ServicePing
private
+ attr_reader :payload, :skip_db_write
+
def metrics_collection_time(payload, parents = [])
return [] unless payload.is_a?(Hash)
diff --git a/app/services/service_response.rb b/app/services/service_response.rb
index 6bc394d2ae2..c7ab75a4426 100644
--- a/app/services/service_response.rb
+++ b/app/services/service_response.rb
@@ -18,6 +18,24 @@ class ServiceResponse
self.http_status = http_status
end
+ def track_exception(as: StandardError, **extra_data)
+ if error?
+ e = as.new(message)
+ Gitlab::ErrorTracking.track_exception(e, extra_data)
+ end
+
+ self
+ end
+
+ def track_and_raise_exception(as: StandardError, **extra_data)
+ if error?
+ e = as.new(message)
+ Gitlab::ErrorTracking.track_and_raise_exception(e, extra_data)
+ end
+
+ self
+ end
+
def [](key)
to_h[key]
end
diff --git a/app/services/snippets/bulk_destroy_service.rb b/app/services/snippets/bulk_destroy_service.rb
index 430e8330b59..6eab9fb320e 100644
--- a/app/services/snippets/bulk_destroy_service.rb
+++ b/app/services/snippets/bulk_destroy_service.rb
@@ -25,11 +25,9 @@ module Snippets
rescue SnippetAccessError
service_response_error("You don't have access to delete these snippets.", 403)
rescue DeleteRepositoryError
- attempt_rollback_repositories
service_response_error('Failed to delete snippet repositories.', 400)
rescue StandardError
# In case the delete operation fails
- attempt_rollback_repositories
service_response_error('Failed to remove snippets.', 400)
end
@@ -55,18 +53,6 @@ module Snippets
end
end
- def attempt_rollback_repositories
- snippets.each do |snippet|
- result = Repositories::DestroyRollbackService.new(snippet.repository).execute
-
- log_rollback_error(snippet) if result[:status] == :error
- end
- end
-
- def log_rollback_error(snippet)
- Gitlab::AppLogger.error("Repository #{snippet.full_path} in path #{snippet.disk_path} could not be rolled back")
- end
-
def service_response_error(message, http_status)
ServiceResponse.error(message: message, http_status: http_status)
end
diff --git a/app/services/snippets/destroy_service.rb b/app/services/snippets/destroy_service.rb
index 96157434462..6c62d7d5e45 100644
--- a/app/services/snippets/destroy_service.rb
+++ b/app/services/snippets/destroy_service.rb
@@ -31,7 +31,6 @@ module Snippets
rescue DestroyError
service_response_error('Failed to remove snippet repository.', 400)
rescue StandardError
- attempt_rollback_repository
service_response_error('Failed to remove snippet.', 400)
end
@@ -45,10 +44,6 @@ module Snippets
snippet.destroy!
end
- def attempt_rollback_repository
- Repositories::DestroyRollbackService.new(snippet.repository).execute
- end
-
def user_can_delete_snippet?
can?(current_user, :admin_snippet, snippet)
end
diff --git a/app/services/static_site_editor/config_service.rb b/app/services/static_site_editor/config_service.rb
deleted file mode 100644
index c8e7165e076..00000000000
--- a/app/services/static_site_editor/config_service.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-# frozen_string_literal: true
-
-module StaticSiteEditor
- class ConfigService < ::BaseContainerService
- ValidationError = Class.new(StandardError)
-
- def initialize(container:, current_user: nil, params: {})
- super
-
- @project = container
- @repository = project.repository
- @ref = params.fetch(:ref)
- end
-
- def execute
- check_access!
-
- file_config = load_file_config!
- file_data = file_config.to_hash_with_defaults
- generated_data = load_generated_config.data
-
- check_for_duplicate_keys!(generated_data, file_data)
- data = merged_data(generated_data, file_data)
-
- ServiceResponse.success(payload: data)
- rescue ValidationError => e
- ServiceResponse.error(message: e.message)
- rescue StandardError => e
- Gitlab::ErrorTracking.track_and_raise_exception(e)
- end
-
- private
-
- attr_reader :project, :repository, :ref
-
- def static_site_editor_config_file
- '.gitlab/static-site-editor.yml'
- end
-
- def check_access!
- unless can?(current_user, :download_code, project)
- raise ValidationError, 'Insufficient permissions to read configuration'
- end
- end
-
- def load_file_config!
- yaml = yaml_from_repo.presence || '{}'
- file_config = Gitlab::StaticSiteEditor::Config::FileConfig.new(yaml)
-
- unless file_config.valid?
- raise ValidationError, file_config.errors.first
- end
-
- file_config
- rescue Gitlab::StaticSiteEditor::Config::FileConfig::ConfigError => e
- raise ValidationError, e.message
- end
-
- def load_generated_config
- Gitlab::StaticSiteEditor::Config::GeneratedConfig.new(
- repository,
- ref,
- params.fetch(:path),
- params[:return_url]
- )
- end
-
- def check_for_duplicate_keys!(generated_data, file_data)
- duplicate_keys = generated_data.keys & file_data.keys
- raise ValidationError, "Duplicate key(s) '#{duplicate_keys}' found." if duplicate_keys.present?
- end
-
- def merged_data(generated_data, file_data)
- generated_data.merge(file_data)
- end
-
- def yaml_from_repo
- repository.blob_data_at(ref, static_site_editor_config_file)
- rescue GRPC::NotFound
- # Return nil in the case of a GRPC::NotFound exception, so the default config will be used.
- # Allow any other unexpected exception will be tracked and re-raised.
- nil
- end
- end
-end
diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb
index 89212288a6b..f9e5c3725d8 100644
--- a/app/services/system_notes/issuables_service.rb
+++ b/app/services/system_notes/issuables_service.rb
@@ -2,6 +2,18 @@
module SystemNotes
class IssuablesService < ::SystemNotes::BaseService
+ # We create cross-referenced system notes when a commit relates to an issue.
+ # There are two options what time to use for the system note:
+ # 1. The push date (default)
+ # 2. The commit date
+ #
+ # The commit date is useful when an existing Git repository is imported to GitLab.
+ # It helps to preserve an original order of all notes (comments, commits, status changes, e.t.c)
+ # in the imported issues. Otherwise, all commits will be linked before or after all other imported notes.
+ #
+ # See also the discussion in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60700#note_612724683
+ USE_COMMIT_DATE_FOR_CROSS_REFERENCE_NOTE = false
+
#
# noteable_ref - Referenced noteable object
#
@@ -216,7 +228,8 @@ module SystemNotes
)
else
track_cross_reference_action
- create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference'))
+ created_at = mentioner.created_at if USE_COMMIT_DATE_FOR_CROSS_REFERENCE_NOTE && mentioner.is_a?(Commit)
+ create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference', created_at: created_at))
end
end
diff --git a/app/services/system_notes/merge_requests_service.rb b/app/services/system_notes/merge_requests_service.rb
index 546a23c95c2..7758c1e8597 100644
--- a/app/services/system_notes/merge_requests_service.rb
+++ b/app/services/system_notes/merge_requests_service.rb
@@ -27,7 +27,7 @@ module SystemNotes
end
def handle_merge_request_draft
- action = noteable.work_in_progress? ? "draft" : "ready"
+ action = noteable.draft? ? "draft" : "ready"
body = "marked this merge request as **#{action}**"
diff --git a/app/services/terraform/remote_state_handler.rb b/app/services/terraform/remote_state_handler.rb
index f13477b8b34..849afaddec6 100644
--- a/app/services/terraform/remote_state_handler.rb
+++ b/app/services/terraform/remote_state_handler.rb
@@ -3,6 +3,7 @@
module Terraform
class RemoteStateHandler < BaseService
StateLockedError = Class.new(StandardError)
+ StateDeletedError = Class.new(StandardError)
UnauthorizedError = Class.new(StandardError)
def find_with_lock
@@ -66,14 +67,15 @@ module Terraform
find_params = { project: project, name: params[:name] }
- return find_state!(find_params) if find_only
+ state = if find_only
+ find_state!(find_params)
+ else
+ Terraform::State.create_or_find_by(find_params)
+ end
- state = Terraform::State.create_or_find_by(find_params)
+ raise StateDeletedError if state.deleted_at?
- # https://github.com/rails/rails/issues/36027
- return state unless state.errors.of_kind? :name, :taken
-
- find_state(find_params)
+ state
end
def lock_matches?(state)
diff --git a/app/services/terraform/states/destroy_service.rb b/app/services/terraform/states/destroy_service.rb
new file mode 100644
index 00000000000..728533a60ed
--- /dev/null
+++ b/app/services/terraform/states/destroy_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Terraform
+ module States
+ class DestroyService
+ def initialize(state)
+ @state = state
+ end
+
+ def execute
+ return unless state.deleted_at?
+
+ state.versions.each_batch(column: :version) do |batch|
+ process_batch(batch)
+ end
+
+ state.destroy!
+ end
+
+ private
+
+ attr_reader :state
+
+ # Overridden in EE
+ def process_batch(batch)
+ batch.each do |version|
+ version.file.remove!
+ end
+ end
+ end
+ end
+end
+
+Terraform::States::DestroyService.prepend_mod
diff --git a/app/services/terraform/states/trigger_destroy_service.rb b/app/services/terraform/states/trigger_destroy_service.rb
new file mode 100644
index 00000000000..3669bdcf716
--- /dev/null
+++ b/app/services/terraform/states/trigger_destroy_service.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Terraform
+ module States
+ class TriggerDestroyService
+ def initialize(state, current_user:)
+ @state = state
+ @current_user = current_user
+ end
+
+ def execute
+ return unauthorized_response unless can_destroy_state?
+ return state_locked_response if state.locked?
+
+ state.update!(deleted_at: Time.current)
+
+ Terraform::States::DestroyWorker.perform_async(state.id)
+
+ ServiceResponse.success
+ end
+
+ private
+
+ attr_reader :state, :current_user
+
+ def can_destroy_state?
+ current_user.can?(:admin_terraform_state, state.project)
+ end
+
+ def unauthorized_response
+ error_response(s_('Terraform|You have insufficient permissions to delete this state'))
+ end
+
+ def state_locked_response
+ error_response(s_('Terraform|Cannot remove a locked state'))
+ end
+
+ def error_response(message)
+ ServiceResponse.error(message: message)
+ end
+ end
+ end
+end
diff --git a/app/services/two_factor/destroy_service.rb b/app/services/two_factor/destroy_service.rb
index b8bbe215d6e..859012c2153 100644
--- a/app/services/two_factor/destroy_service.rb
+++ b/app/services/two_factor/destroy_service.rb
@@ -8,7 +8,7 @@ module TwoFactor
result = disable_two_factor
- notification_service.disabled_two_factor(user) if result[:status] == :success
+ notify_on_success(user) if result[:status] == :success
result
end
@@ -20,5 +20,11 @@ module TwoFactor
user.disable_two_factor!
end
end
+
+ def notify_on_success(user)
+ notification_service.disabled_two_factor(user)
+ end
end
end
+
+TwoFactor::DestroyService.prepend_mod_with('TwoFactor::DestroyService')
diff --git a/app/services/user_project_access_changed_service.rb b/app/services/user_project_access_changed_service.rb
index 5bba986f4ad..ceaf21bb926 100644
--- a/app/services/user_project_access_changed_service.rb
+++ b/app/services/user_project_access_changed_service.rb
@@ -2,8 +2,10 @@
class UserProjectAccessChangedService
DELAY = 1.hour
+ MEDIUM_DELAY = 10.minutes
HIGH_PRIORITY = :high
+ MEDIUM_PRIORITY = :medium
LOW_PRIORITY = :low
def initialize(user_ids)
@@ -11,6 +13,8 @@ class UserProjectAccessChangedService
end
def execute(blocking: true, priority: HIGH_PRIORITY)
+ return if @user_ids.empty?
+
bulk_args = @user_ids.map { |id| [id] }
result =
@@ -19,6 +23,8 @@ class UserProjectAccessChangedService
else
if priority == HIGH_PRIORITY
AuthorizedProjectsWorker.bulk_perform_async(bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
+ elsif priority == MEDIUM_PRIORITY
+ AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker.bulk_perform_in(MEDIUM_DELAY, bulk_args, batch_size: 100, batch_delay: 30.seconds) # rubocop:disable Scalability/BulkPerformWithContext
else
with_related_class_context do
# We wrap the execution in `with_related_class_context`so as to obtain
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index c0727e52cc3..f2f94563e56 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -26,6 +26,12 @@ class WebHookService
end
REQUEST_BODY_SIZE_LIMIT = 25.megabytes
+ # Response body is for UI display only. It does not make much sense to save
+ # whatever the receivers throw back at us
+ RESPONSE_BODY_SIZE_LIMIT = 8.kilobytes
+ # The headers are for debugging purpose. They are displayed on the UI only.
+ RESPONSE_HEADERS_COUNT_LIMIT = 50
+ RESPONSE_HEADERS_SIZE_LIMIT = 1.kilobytes
attr_accessor :hook, :data, :hook_name, :request_options
attr_reader :uniqueness_token
@@ -98,7 +104,7 @@ class WebHookService
def async_execute
Gitlab::ApplicationContext.with_context(hook.application_context) do
- break log_rate_limited if rate_limited?
+ break log_rate_limited if rate_limit!
break log_recursion_blocked if recursion_blocked?
params = {
@@ -141,7 +147,7 @@ class WebHookService
execution_duration: execution_duration,
request_headers: build_headers,
request_data: data,
- response_headers: format_response_headers(response),
+ response_headers: safe_response_headers(response),
response_body: safe_response_body(response),
response_status: response.code,
internal_error_message: error_message
@@ -150,8 +156,21 @@ class WebHookService
if @force # executed as part of test - run log-execution inline.
::WebHooks::LogExecutionService.new(hook: hook, log_data: log_data, response_category: category).execute
else
- ::WebHooks::LogExecutionWorker
- .perform_async(hook.id, log_data, category, uniqueness_token)
+ queue_log_execution_with_retry(log_data, category)
+ end
+ end
+
+ def queue_log_execution_with_retry(log_data, category)
+ retried = false
+ begin
+ ::WebHooks::LogExecutionWorker.perform_async(hook.id, log_data, category, uniqueness_token)
+ rescue Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError
+ raise if retried
+
+ # Strip request data
+ log_data[:request_data] = ::WebHookLog::OVERSIZE_REQUEST_DATA
+ retried = true
+ retry
end
end
@@ -181,52 +200,56 @@ class WebHookService
# Make response headers more stylish
# Net::HTTPHeader has downcased hash with arrays: { 'content-type' => ['text/html; charset=utf-8'] }
# This method format response to capitalized hash with strings: { 'Content-Type' => 'text/html; charset=utf-8' }
- def format_response_headers(response)
- response.headers.each_capitalized.to_h
+ # rubocop:disable Style/HashTransformValues
+ def safe_response_headers(response)
+ response.headers.each_capitalized.first(RESPONSE_HEADERS_COUNT_LIMIT).to_h do |header_key, header_value|
+ [enforce_utf8(header_key), string_size_limit(enforce_utf8(header_value), RESPONSE_HEADERS_SIZE_LIMIT)]
+ end
end
+ # rubocop:enable Style/HashTransformValues
def safe_response_body(response)
return '' unless response.body
- response.body.encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
+ response_body = enforce_utf8(response.body)
+ string_size_limit(response_body, RESPONSE_BODY_SIZE_LIMIT)
end
- def rate_limited?
- return false if rate_limit.nil?
-
- Gitlab::ApplicationRateLimiter.throttled?(
- :web_hook_calls,
- scope: [hook],
- threshold: rate_limit
- )
+ # Increments rate-limit counter.
+ # Returns true if hook should be rate-limited.
+ def rate_limit!
+ Gitlab::WebHooks::RateLimiter.new(hook).rate_limit!
end
def recursion_blocked?
Gitlab::WebHooks::RecursionDetection.block?(hook)
end
- def rate_limit
- @rate_limit ||= hook.rate_limit
+ def log_rate_limited
+ log_auth_error('Webhook rate limit exceeded')
end
- def log_rate_limited
- Gitlab::AuthLogger.error(
- message: 'Webhook rate limit exceeded',
- hook_id: hook.id,
- hook_type: hook.type,
- hook_name: hook_name,
- **Gitlab::ApplicationContext.current
+ def log_recursion_blocked
+ log_auth_error(
+ 'Recursive webhook blocked from executing',
+ recursion_detection: ::Gitlab::WebHooks::RecursionDetection.to_log(hook)
)
end
- def log_recursion_blocked
+ def log_auth_error(message, params = {})
Gitlab::AuthLogger.error(
- message: 'Recursive webhook blocked from executing',
- hook_id: hook.id,
- hook_type: hook.type,
- hook_name: hook_name,
- recursion_detection: ::Gitlab::WebHooks::RecursionDetection.to_log(hook),
- **Gitlab::ApplicationContext.current
+ params.merge(
+ { message: message, hook_id: hook.id, hook_type: hook.type, hook_name: hook_name },
+ Gitlab::ApplicationContext.current
+ )
)
end
+
+ def string_size_limit(str, limit)
+ str.truncate_bytes(limit)
+ end
+
+ def enforce_utf8(str)
+ Gitlab::EncodingHelper.encode_utf8(str)
+ end
end
diff --git a/app/services/web_hooks/destroy_service.rb b/app/services/web_hooks/destroy_service.rb
index 58117985b56..ecb530f0d2a 100644
--- a/app/services/web_hooks/destroy_service.rb
+++ b/app/services/web_hooks/destroy_service.rb
@@ -2,77 +2,27 @@
module WebHooks
class DestroyService
- include BaseServiceUtility
-
- BATCH_SIZE = 1000
- LOG_COUNT_THRESHOLD = 10000
-
- DestroyError = Class.new(StandardError)
-
- attr_accessor :current_user, :web_hook
+ attr_accessor :current_user
def initialize(current_user)
@current_user = current_user
end
+ # Destroy the hook immediately, schedule the logs for deletion
def execute(web_hook)
- @web_hook = web_hook
-
- async = false
- # For a better user experience, it's better if the Web hook is
- # destroyed right away without waiting for Sidekiq. However, if
- # there are a lot of Web hook logs, we will need more time to
- # clean them up, so schedule a Sidekiq job to do this.
- if needs_async_destroy?
- Gitlab::AppLogger.info("User #{current_user&.id} scheduled a deletion of hook ID #{web_hook.id}")
- async_destroy(web_hook)
- async = true
- else
- sync_destroy(web_hook)
- end
-
- success({ async: async })
- end
-
- def sync_destroy(web_hook)
- @web_hook = web_hook
+ hook_id = web_hook.id
- delete_web_hook_logs
- result = web_hook.destroy
+ if web_hook.destroy
+ WebHooks::LogDestroyWorker.perform_async({ 'hook_id' => hook_id })
+ Gitlab::AppLogger.info("User #{current_user&.id} scheduled a deletion of logs for hook ID #{hook_id}")
- if result
- success({ async: false })
+ ServiceResponse.success(payload: { async: false })
else
- error("Unable to destroy #{web_hook.model_name.human}")
+ ServiceResponse.error(message: "Unable to destroy #{web_hook.model_name.human}")
end
end
- private
-
- def async_destroy(web_hook)
- WebHooks::DestroyWorker.perform_async(current_user.id, web_hook.id)
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def needs_async_destroy?
- web_hook.web_hook_logs.limit(LOG_COUNT_THRESHOLD).count == LOG_COUNT_THRESHOLD
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def delete_web_hook_logs
- loop do
- count = delete_web_hook_logs_in_batches
- break if count < BATCH_SIZE
- end
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def delete_web_hook_logs_in_batches
- # We can't use EachBatch because that does an ORDER BY id, which can
- # easily time out. We don't actually care about ordering when
- # we are deleting these rows.
- web_hook.web_hook_logs.limit(BATCH_SIZE).delete_all
- end
- # rubocop: enable CodeReuse/ActiveRecord
+ # Backwards compatibility with WebHooks::DestroyWorker
+ alias_method :sync_destroy, :execute
end
end
diff --git a/app/services/web_hooks/log_destroy_service.rb b/app/services/web_hooks/log_destroy_service.rb
new file mode 100644
index 00000000000..8a5d214a95e
--- /dev/null
+++ b/app/services/web_hooks/log_destroy_service.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module WebHooks
+ class LogDestroyService
+ BATCH_SIZE = 1000
+
+ def initialize(web_hook_id)
+ @web_hook_id = web_hook_id
+ end
+
+ def execute
+ next while WebHookLog.delete_batch_for(@web_hook_id, batch_size: BATCH_SIZE)
+
+ ServiceResponse.success
+ rescue StandardError => ex
+ ServiceResponse.error(message: ex.message)
+ end
+ end
+end
diff --git a/app/services/work_items/update_service.rb b/app/services/work_items/update_service.rb
index 5c45f4d90e5..0b420881b4b 100644
--- a/app/services/work_items/update_service.rb
+++ b/app/services/work_items/update_service.rb
@@ -2,12 +2,30 @@
module WorkItems
class UpdateService < ::Issues::UpdateService
+ def initialize(project:, current_user: nil, params: {}, spam_params: nil, widget_params: {})
+ super(project: project, current_user: current_user, params: params, spam_params: nil)
+
+ @widget_params = widget_params
+ end
+
private
- def after_update(issuable)
+ def update(work_item)
+ execute_widgets(work_item: work_item, callback: :update)
+
super
+ end
+
+ def after_update(work_item)
+ super
+
+ GraphqlTriggers.issuable_title_updated(work_item) if work_item.previous_changes.key?(:title)
+ end
- GraphqlTriggers.issuable_title_updated(issuable) if issuable.previous_changes.key?(:title)
+ def execute_widgets(work_item:, callback:)
+ work_item.widgets.each do |widget|
+ widget.try(callback, params: @widget_params[widget.class.api_symbol])
+ end
end
end
end
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index 9758d3c87aa..abe06bd97e1 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -31,6 +31,14 @@ class GitlabUploader < CarrierWave::Uploader::Base
def absolute_path(upload_record)
File.join(root, upload_record.path)
end
+
+ def version(*args, **kwargs, &block)
+ # CarrierWave uploaded file "versions" are not tracked in the uploads
+ # table. Because of this they won't get replicated to Geo secondaries.
+ # If we ever want to use versions, we need to fix that first. Also see
+ # https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1757.
+ raise "using CarrierWave alternate file version is not supported"
+ end
end
storage_options Gitlab.config.uploads
diff --git a/app/uploaders/metric_image_uploader.rb b/app/uploaders/metric_image_uploader.rb
index 0826bb251e4..d7d70518565 100644
--- a/app/uploaders/metric_image_uploader.rb
+++ b/app/uploaders/metric_image_uploader.rb
@@ -6,6 +6,10 @@ class MetricImageUploader < GitlabUploader # rubocop:disable Gitlab/NamespacedCl
prepend ObjectStorage::Extension::RecordsUploads
include UploaderHelper
+ def self.workhorse_local_upload_path
+ File.join(options.storage_path, 'uploads', TMP_UPLOAD_PATH)
+ end
+
private
def dynamic_segment
diff --git a/app/validators/json_schemas/web_hooks_url_variables.json b/app/validators/json_schemas/web_hooks_url_variables.json
new file mode 100644
index 00000000000..d23a19bf47a
--- /dev/null
+++ b/app/validators/json_schemas/web_hooks_url_variables.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "description": "WebHook#url_variables",
+ "type": "object",
+ "additionalProperties": false,
+ "maxProperties": 20,
+ "patternProperties": {
+ "^[A-Za-z_][A-Za-z0-9_]*$": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100
+ }
+ }
+}
diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml
index 78fa16c13a5..258fdb4ad9a 100644
--- a/app/views/abuse_reports/new.html.haml
+++ b/app/views/abuse_reports/new.html.haml
@@ -1,5 +1,5 @@
- page_title _("Report abuse to admin")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _("Report abuse to admin")
%p
= _("Please use this form to report to the admin users who create spam issues, comments or behave inappropriately.")
@@ -7,7 +7,7 @@
= _("A member of the abuse team will review your report as soon as possible.")
%hr
= form_for @abuse_report, html: { class: 'js-quick-submit js-requires-input'} do |f|
- = form_errors(@abuse_report)
+ = form_errors(@abuse_report, pajamas_alert: true)
= f.hidden_field :user_id
.form-group.row
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index 8b1bbbc17c7..20499a2e3bf 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -1,6 +1,6 @@
- page_title _('Abuse Reports')
-%h3.page-title= _('Abuse Reports')
+%h1.page-title.gl-font-size-h-display= _('Abuse Reports')
.row-content-block.second-block
= form_tag admin_abuse_reports_path, method: :get, class: 'filter-form' do
diff --git a/app/views/admin/application_settings/_abuse.html.haml b/app/views/admin/application_settings/_abuse.html.haml
index 96fb848b568..fbadd26d0c0 100644
--- a/app/views/admin/application_settings/_abuse.html.haml
+++ b/app/views/admin/application_settings/_abuse.html.haml
@@ -1,5 +1,5 @@
= form_for @application_setting, url: reporting_admin_application_settings_path(anchor: 'js-abuse-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index f914de138a9..e7204f635e6 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-account-settings'), html: { class: 'fieldset-form', id: 'account-settings' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index 201ca830ba4..ba2a2f34d63 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -1,6 +1,6 @@
.settings-content
= gitlab_ui_form_for @application_setting, url: ci_cd_admin_application_settings_path(anchor: 'js-ci-cd-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true )
%fieldset
.form-group
@@ -72,7 +72,7 @@
- @plans.each_with_index do |plan, index|
.tab-pane{ :id => "plan#{index}", class: index == 0 ? 'active': '' }
= form_for plan.actual_limits, url: admin_plan_limits_path(anchor: 'js-ci-cd-settings'), html: { class: 'fieldset-form' }, method: :post do |f|
- = form_errors(plan)
+ = form_errors(plan, pajamas_alert: true)
%fieldset
= f.hidden_field(:plan_id, value: plan.id)
.form-group
diff --git a/app/views/admin/application_settings/_default_branch.html.haml b/app/views/admin/application_settings/_default_branch.html.haml
index f5f45d7a6e9..4a06dcbc031 100644
--- a/app/views/admin/application_settings/_default_branch.html.haml
+++ b/app/views/admin/application_settings/_default_branch.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: repository_admin_application_settings_path(anchor: 'js-default-branch-name'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
- fallback_branch_name = "<code>#{Gitlab::DefaultBranch.value}</code>"
diff --git a/app/views/admin/application_settings/_diff_limits.html.haml b/app/views/admin/application_settings/_diff_limits.html.haml
index 6a51d2e39d4..1af4d294c1b 100644
--- a/app/views/admin/application_settings/_diff_limits.html.haml
+++ b/app/views/admin/application_settings/_diff_limits.html.haml
@@ -1,5 +1,5 @@
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-merge-request-settings'), html: { class: 'fieldset-form', id: 'merge-request-settings' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_eks.html.haml b/app/views/admin/application_settings/_eks.html.haml
index bd6ff9b426f..370d3cea07c 100644
--- a/app/views/admin/application_settings/_eks.html.haml
+++ b/app/views/admin/application_settings/_eks.html.haml
@@ -3,14 +3,14 @@
.settings-header
%h4
= _('Amazon EKS')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= _('Amazon EKS integration allows you to provision EKS clusters from GitLab.')
.settings-content
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-eks-settings'), html: { class: 'fieldset-form', id: 'eks-settings' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml
index fd65d4029f5..774c5665edd 100644
--- a/app/views/admin/application_settings/_email.html.haml
+++ b/app/views/admin/application_settings/_email.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: preferences_admin_application_settings_path(anchor: 'js-email-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_external_authorization_service_form.html.haml b/app/views/admin/application_settings/_external_authorization_service_form.html.haml
index 1abf8f78060..4d0faf69958 100644
--- a/app/views/admin/application_settings/_external_authorization_service_form.html.haml
+++ b/app/views/admin/application_settings/_external_authorization_service_form.html.haml
@@ -2,15 +2,15 @@
.settings-header
%h4
= s_('ExternalAuthorization|External authorization')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
%p
= s_('ExternalAuthorization|External classification policy authorization.')
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/external_authorization'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-external-auth-settings'), html: { class: 'fieldset-form', id: 'external-auth-settings' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_floc.html.haml b/app/views/admin/application_settings/_floc.html.haml
index 14b1a58c1ad..b5a63aa0847 100644
--- a/app/views/admin/application_settings/_floc.html.haml
+++ b/app/views/admin/application_settings/_floc.html.haml
@@ -4,7 +4,7 @@
.settings-header
%h4
= s_('FloC|Federated Learning of Cohorts')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= s_('FloC|Configure whether you want to participate in FloC.').html_safe
@@ -12,7 +12,7 @@
.settings-content
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-floc-settings'), html: { class: 'fieldset-form', id: 'floc-settings' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_git_lfs_limits.html.haml b/app/views/admin/application_settings/_git_lfs_limits.html.haml
index b8970a5bcf1..7d47ca9a139 100644
--- a/app/views/admin/application_settings/_git_lfs_limits.html.haml
+++ b/app/views/admin/application_settings/_git_lfs_limits.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-git-lfs-limits-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
%h5
diff --git a/app/views/admin/application_settings/_gitaly.html.haml b/app/views/admin/application_settings/_gitaly.html.haml
index ade6dac606a..cc2c6dbcb03 100644
--- a/app/views/admin/application_settings/_gitaly.html.haml
+++ b/app/views/admin/application_settings/_gitaly.html.haml
@@ -1,5 +1,5 @@
= form_for @application_setting, url: preferences_admin_application_settings_path(anchor: 'js-gitaly-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_gitpod.html.haml b/app/views/admin/application_settings/_gitpod.html.haml
index 515b3691324..eb47d177701 100644
--- a/app/views/admin/application_settings/_gitpod.html.haml
+++ b/app/views/admin/application_settings/_gitpod.html.haml
@@ -4,7 +4,7 @@
.settings-header
%h4
= _('Gitpod')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
#js-gitpod-settings-help-text{ data: {"message" => gitpod_enable_description, "message-url" => "https://gitpod.io/" } }
diff --git a/app/views/admin/application_settings/_help_page.html.haml b/app/views/admin/application_settings/_help_page.html.haml
index 21eb4caf579..08a4ebe5c71 100644
--- a/app/views/admin/application_settings/_help_page.html.haml
+++ b/app/views/admin/application_settings/_help_page.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: preferences_admin_application_settings_path(anchor: 'js-help-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
= render_if_exists 'admin/application_settings/help_text_setting', form: f
diff --git a/app/views/admin/application_settings/_import_export_limits.html.haml b/app/views/admin/application_settings/_import_export_limits.html.haml
index bc4a1577f90..4e774dd0a1e 100644
--- a/app/views/admin/application_settings/_import_export_limits.html.haml
+++ b/app/views/admin/application_settings/_import_export_limits.html.haml
@@ -1,5 +1,5 @@
= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-import-export-limits-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
= html_escape(_("Set any rate limit to %{code_open}0%{code_close} to disable the limit.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml
index 4362ae9cb9b..9a9038ef48e 100644
--- a/app/views/admin/application_settings/_ip_limits.html.haml
+++ b/app/views/admin/application_settings/_ip_limits.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-ip-limits-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
= _("Rate limits can help reduce request volume (like from crawlers or abusive bots).")
diff --git a/app/views/admin/application_settings/_issue_limits.html.haml b/app/views/admin/application_settings/_issue_limits.html.haml
index 431e2a64c46..64aca50cbe9 100644
--- a/app/views/admin/application_settings/_issue_limits.html.haml
+++ b/app/views/admin/application_settings/_issue_limits.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-issue-limits-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_jira_connect_application_key.html.haml b/app/views/admin/application_settings/_jira_connect_application_key.html.haml
new file mode 100644
index 00000000000..e395741dcaa
--- /dev/null
+++ b/app/views/admin/application_settings/_jira_connect_application_key.html.haml
@@ -0,0 +1,21 @@
+- expanded = integration_expanded?('jira_connect')
+
+%section.settings.no-animate#js-jira_connect-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = s_('JiraConnect|GitLab for Jira App')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = s_('JiraConnect|Configure your Jira Connect Application ID.')
+ = link_to sprite_icon('question-o'), 'https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud', target: '_blank', rel: "noopener noreferrer", class: 'has-tooltip', title: _('More information'), aria: { label: _('GitLab for Jira Cloud') }
+
+ .settings-content
+ = gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-jira-connect-application-id-settings'), html: { class: 'fieldset-form', id: 'jira-connect-application-id-settings' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :jira_connect_application_key, s_('JiraConnect|Jira Connect Application ID'), class: 'label-bold'
+ = f.text_field :jira_connect_application_key, class: 'form-control gl-form-input'
+ = f.submit _('Save changes'), class: 'gl-button btn btn-confirm'
diff --git a/app/views/admin/application_settings/_kroki.html.haml b/app/views/admin/application_settings/_kroki.html.haml
index 61469d87656..b1dd8a282ec 100644
--- a/app/views/admin/application_settings/_kroki.html.haml
+++ b/app/views/admin/application_settings/_kroki.html.haml
@@ -3,14 +3,14 @@
.settings-header
%h4
= _('Kroki')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= _('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
= gitlab_ui_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
+ = form_errors(@application_setting, pajamas_alert: true) if expanded
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_localization.html.haml b/app/views/admin/application_settings/_localization.html.haml
index a6ed48ef4fe..0477f114bdf 100644
--- a/app/views/admin/application_settings/_localization.html.haml
+++ b/app/views/admin/application_settings/_localization.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: preferences_admin_application_settings_path(anchor: 'js-localization-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_mailgun.html.haml b/app/views/admin/application_settings/_mailgun.html.haml
index 7afb35bc9cb..e84fdc56f93 100644
--- a/app/views/admin/application_settings/_mailgun.html.haml
+++ b/app/views/admin/application_settings/_mailgun.html.haml
@@ -3,7 +3,7 @@
.settings-header
%h4
= _('Mailgun')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= _('Configure the %{link} integration.').html_safe % { link: link_to(_('Mailgun events'), 'https://documentation.mailgun.com/en/latest/user_manual.html#webhooks', target: '_blank', rel: 'noopener noreferrer') }
diff --git a/app/views/admin/application_settings/_network_rate_limits.html.haml b/app/views/admin/application_settings/_network_rate_limits.html.haml
index f1857a9749a..173e830c7da 100644
--- a/app/views/admin/application_settings/_network_rate_limits.html.haml
+++ b/app/views/admin/application_settings/_network_rate_limits.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: anchor), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
= _("Rate limits can help reduce request volume (like from crawlers or abusive bots).")
diff --git a/app/views/admin/application_settings/_note_limits.html.haml b/app/views/admin/application_settings/_note_limits.html.haml
index 40760b3c45e..b783345b9df 100644
--- a/app/views/admin/application_settings/_note_limits.html.haml
+++ b/app/views/admin/application_settings/_note_limits.html.haml
@@ -1,5 +1,5 @@
= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-note-limits-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml
index 503e7d8afa6..2d91b777a0b 100644
--- a/app/views/admin/application_settings/_outbound.html.haml
+++ b/app/views/admin/application_settings/_outbound.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-outbound-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_package_registry.html.haml b/app/views/admin/application_settings/_package_registry.html.haml
index 398e63cdfdc..c0fabb1d42e 100644
--- a/app/views/admin/application_settings/_package_registry.html.haml
+++ b/app/views/admin/application_settings/_package_registry.html.haml
@@ -3,7 +3,7 @@
.settings-header
%h4
= _('Package Registry')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _("Control how the GitLab Package Registry functions.")
@@ -26,7 +26,7 @@
- @plans.each_with_index do |plan, index|
.tab-pane{ :id => "plan#{index}", class: index == 0 ? 'active': '' }
= form_for plan.actual_limits, url: admin_plan_limits_path(anchor: 'js-package-settings'), html: { class: 'fieldset-form' }, method: :post do |f|
- = form_errors(plan)
+ = form_errors(plan, pajamas_alert: true)
%fieldset
= f.hidden_field(:plan_id, value: plan.id)
.form-group
diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml
index 74903d52f25..23b0d2d2092 100644
--- a/app/views/admin/application_settings/_pages.html.haml
+++ b/app/views/admin/application_settings/_pages.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: preferences_admin_application_settings_path(anchor: 'js-pages-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
@@ -19,7 +19,7 @@
= 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_url = help_page_path('administration/pages/index', anchor: 'set-maximum-size-of-gitlab-pages-site-in-a-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
diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml
index e0ba8d93fbd..c87d166f8d9 100644
--- a/app/views/admin/application_settings/_performance.html.haml
+++ b/app/views/admin/application_settings/_performance.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-performance-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_pipeline_limits.html.haml b/app/views/admin/application_settings/_pipeline_limits.html.haml
index e93823172db..3b33c41a924 100644
--- a/app/views/admin/application_settings/_pipeline_limits.html.haml
+++ b/app/views/admin/application_settings/_pipeline_limits.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-pipeline-limits-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml
index 42914652655..57931544e65 100644
--- a/app/views/admin/application_settings/_plantuml.html.haml
+++ b/app/views/admin/application_settings/_plantuml.html.haml
@@ -3,7 +3,7 @@
.settings-header
%h4
= _('PlantUML')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= _('Render diagrams in your documents using PlantUML.')
diff --git a/app/views/admin/application_settings/_protected_paths.html.haml b/app/views/admin/application_settings/_protected_paths.html.haml
index 1f3f67c71c7..00da0f59be4 100644
--- a/app/views/admin/application_settings/_protected_paths.html.haml
+++ b/app/views/admin/application_settings/_protected_paths.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-protected-paths-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_realtime.html.haml b/app/views/admin/application_settings/_realtime.html.haml
index 6a7ec05d206..66003f31104 100644
--- a/app/views/admin/application_settings/_realtime.html.haml
+++ b/app/views/admin/application_settings/_realtime.html.haml
@@ -1,5 +1,5 @@
= form_for @application_setting, url: preferences_admin_application_settings_path(anchor: 'js-realtime-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml
index c2087efa650..ef8d3ccc8ab 100644
--- a/app/views/admin/application_settings/_repository_check.html.haml
+++ b/app/views/admin/application_settings/_repository_check.html.haml
@@ -39,4 +39,8 @@
.form-text.text-muted
= html_escape(s_('Number of Git pushes after which %{code_start}git gc%{code_end} is run.')) % { code_start: '<code>'.html_safe, code_end: '</code>'.html_safe }
+ .sub-section
+ %h4= s_("AdminSettings|Inactive project deletion")
+ .js-inactive-project-deletion-form{ data: inactive_projects_deletion_data(@application_setting) }
+
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
index b5fa08aed79..5dc2d322bb3 100644
--- a/app/views/admin/application_settings/_repository_storage.html.haml
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: repository_admin_application_settings_path(anchor: 'js-repository-storage-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.sub-section
diff --git a/app/views/admin/application_settings/_search_limits.html.haml b/app/views/admin/application_settings/_search_limits.html.haml
index 945c9397f0d..93637b59d60 100644
--- a/app/views/admin/application_settings/_search_limits.html.haml
+++ b/app/views/admin/application_settings/_search_limits.html.haml
@@ -1,5 +1,5 @@
= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-search-limits-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_sentry.html.haml b/app/views/admin/application_settings/_sentry.html.haml
index cfd34f6ca15..ece8f50151a 100644
--- a/app/views/admin/application_settings/_sentry.html.haml
+++ b/app/views/admin/application_settings/_sentry.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: metrics_and_profiling_admin_application_settings_path(anchor: 'js-sentry-settings'), html: { class: 'fieldset-form', id: 'sentry-settings' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%span.text-muted
= _('Changing any setting here requires an application restart')
diff --git a/app/views/admin/application_settings/_sidekiq_job_limits.html.haml b/app/views/admin/application_settings/_sidekiq_job_limits.html.haml
index eaf4bbf4702..a28e6e62e7f 100644
--- a/app/views/admin/application_settings/_sidekiq_job_limits.html.haml
+++ b/app/views/admin/application_settings/_sidekiq_job_limits.html.haml
@@ -1,5 +1,5 @@
= form_for @application_setting, url: preferences_admin_application_settings_path(anchor: 'js-sidekiq-job-limits-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index 48f0b9b2c31..870bfbf4184 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-signin-settings'), html: { class: 'fieldset-form', id: 'signin-settings' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml
index 85cf43ba5c2..fccf039533b 100644
--- a/app/views/admin/application_settings/_signup.html.haml
+++ b/app/views/admin/application_settings/_signup.html.haml
@@ -1,21 +1,3 @@
= form_errors(@application_setting)
-#js-signup-form{ data: { host: new_user_session_url(host: Gitlab.config.gitlab.host),
- settings_path: general_admin_application_settings_path(anchor: 'js-signup-settings'),
- signup_enabled: @application_setting[:signup_enabled].to_s,
- require_admin_approval_after_user_signup: @application_setting[:require_admin_approval_after_user_signup].to_s,
- send_user_confirmation_email: @application_setting[:send_user_confirmation_email].to_s,
- minimum_password_length: @application_setting[:minimum_password_length],
- minimum_password_length_min: ApplicationSetting::DEFAULT_MINIMUM_PASSWORD_LENGTH,
- minimum_password_length_max: Devise.password_length.max,
- minimum_password_length_help_link: 'https://about.gitlab.com/handbook/security/#gitlab-password-policy-guidelines',
- domain_allowlist_raw: @application_setting.domain_allowlist_raw,
- new_user_signups_cap: @application_setting[:new_user_signups_cap].to_s,
- domain_denylist_enabled: @application_setting[:domain_denylist_enabled].to_s,
- denylist_type_raw_selected: (@application_setting.domain_denylist.present? || @application_setting.domain_denylist.blank?).to_s,
- domain_denylist_raw: @application_setting.domain_denylist_raw,
- 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.to_s,
- after_sign_up_text: @application_setting[:after_sign_up_text].to_s,
- pending_user_count: pending_user_count } }
+#js-signup-form{ data: signup_form_data }
diff --git a/app/views/admin/application_settings/_snowplow.html.haml b/app/views/admin/application_settings/_snowplow.html.haml
index 378c1712ae0..e9387ab3f26 100644
--- a/app/views/admin/application_settings/_snowplow.html.haml
+++ b/app/views/admin/application_settings/_snowplow.html.haml
@@ -3,7 +3,7 @@
.settings-header
%h4
= _('Snowplow')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('development/snowplow/index') }
diff --git a/app/views/admin/application_settings/_sourcegraph.html.haml b/app/views/admin/application_settings/_sourcegraph.html.haml
index 391f79e431b..a0cbbecb943 100644
--- a/app/views/admin/application_settings/_sourcegraph.html.haml
+++ b/app/views/admin/application_settings/_sourcegraph.html.haml
@@ -5,7 +5,7 @@
.settings-header
%h4
= _('Sourcegraph')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: 'https://sourcegraph.com/' }
@@ -17,7 +17,7 @@
.settings-content
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-sourcegraph-settings'), html: { class: 'fieldset-form', id: 'sourcegraph-settings' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml
index a4b6e061c43..c5387db59ef 100644
--- a/app/views/admin/application_settings/_terms.html.haml
+++ b/app/views/admin/application_settings/_terms.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terms-settings'), html: { class: 'fieldset-form', id: 'terms-settings' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_third_party_offers.html.haml b/app/views/admin/application_settings/_third_party_offers.html.haml
index a62e730ee89..205e14fb8ab 100644
--- a/app/views/admin/application_settings/_third_party_offers.html.haml
+++ b/app/views/admin/application_settings/_third_party_offers.html.haml
@@ -3,7 +3,7 @@
.settings-header
%h4
= _('Customer experience improvement and third-party offers')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= _('Control whether to display customer experience improvement content and third-party offers in GitLab.')
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
index 8b4ac9b79c8..c9ed2309cec 100644
--- a/app/views/admin/application_settings/_usage.html.haml
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -21,7 +21,7 @@
checkbox_options: { disabled: !can_be_configured, data: { qa_selector: 'enable_usage_data_checkbox' } }
.form-text.gl-pl-6
- if can_be_configured
- %button.gl-button.btn.btn-default.js-payload-preview-trigger{ type: 'button', data: { payload_selector: ".#{payload_class}" } }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-preview-trigger', data: { payload_selector: ".#{payload_class}" } }) do
= gl_loading_icon(css_class: 'js-spinner gl-display-none gl-mr-2')
.js-text.gl-display-inline= s_('AdminSettings|Preview payload')
%pre.service-data-payload-container.js-syntax-highlight.code.highlight.gl-mt-2.gl-display-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
diff --git a/app/views/admin/application_settings/_users_api_limits.html.haml b/app/views/admin/application_settings/_users_api_limits.html.haml
index 3918c76b12c..f2edb81141d 100644
--- a/app/views/admin/application_settings/_users_api_limits.html.haml
+++ b/app/views/admin/application_settings/_users_api_limits.html.haml
@@ -1,5 +1,5 @@
= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-users-api-limits-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting, pajamas_alert: true)
%fieldset
.form-group
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 e3c044ff979..96dcd7e1111 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -4,8 +4,7 @@
%fieldset
= render 'shared/project_creation_levels', f: f, method: :default_project_creation, legend: s_('ProjectCreationLevel|Default project creation protection')
= 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
+ = render_if_exists 'admin/application_settings/deletion_protection_settings', form: f
.form-group.visibility-level-setting
= f.label :default_project_visibility, class: 'label-bold'
= render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
@@ -63,4 +62,6 @@
%label.label-bold= s_('AdminSettings|Feed token')
= f.gitlab_ui_checkbox_component :disable_feed_token, s_('AdminSettings|Disable feed token')
+ = render_if_exists 'admin/application_settings/globally_allowed_ips', form: f
+
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_whats_new.html.haml b/app/views/admin/application_settings/_whats_new.html.haml
index 70ba994d21e..8ae912d24b7 100644
--- a/app/views/admin/application_settings/_whats_new.html.haml
+++ b/app/views/admin/application_settings/_whats_new.html.haml
@@ -1,13 +1,8 @@
-= form_for @application_setting, url: preferences_admin_application_settings_path(anchor: 'js-whats-new-settings'), html: { class: 'fieldset-form whats-new-settings' } do |f|
- = form_errors(@application_setting)
+= gitlab_ui_form_for @application_setting, url: preferences_admin_application_settings_path(anchor: 'js-whats-new-settings'), html: { class: 'fieldset-form whats-new-settings' } do |f|
+ = form_errors(@application_setting, pajamas_alert: true)
- whats_new_variants.keys.each do |variant|
- .form-check.gl-mb-4
- = f.radio_button :whats_new_variant, variant, class: 'form-check-input'
- = f.label :whats_new_variant, value: variant, class: 'form-check-label' do
- .font-weight-bold
- = whats_new_variants_label(variant)
- .option-description
- = whats_new_variants_description(variant)
+ .gl-mb-4
+ = f.gitlab_ui_radio_component :whats_new_variant, variant, whats_new_variants_label(variant), help_text: whats_new_variants_description(variant)
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/ci/_header.html.haml b/app/views/admin/application_settings/ci/_header.html.haml
index a22e67b0522..5e3f0d6f2aa 100644
--- a/app/views/admin/application_settings/ci/_header.html.haml
+++ b/app/views/admin/application_settings/ci/_header.html.haml
@@ -3,7 +3,7 @@
%h4
= _('Variables')
-%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
diff --git a/app/views/admin/application_settings/ci_cd.html.haml b/app/views/admin/application_settings/ci_cd.html.haml
index e925175b7ea..b635e7198cb 100644
--- a/app/views/admin/application_settings/ci_cd.html.haml
+++ b/app/views/admin/application_settings/ci_cd.html.haml
@@ -16,7 +16,7 @@
.settings-header
%h4
= _('Continuous Integration and Deployment')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Customize CI/CD settings, including Auto DevOps, shared runners, and job artifacts.')
@@ -31,7 +31,7 @@
.settings-header
%h4
= _('Container Registry')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Various container registry settings.')
@@ -43,7 +43,7 @@
.settings-header
%h4
= s_('Runners|Runner registration')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? 'Collapse' : 'Expand'
.settings-content
= render 'runner_registrars_form'
diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml
index 7643f8fa7a7..36b9ad189d8 100644
--- a/app/views/admin/application_settings/general.html.haml
+++ b/app/views/admin/application_settings/general.html.haml
@@ -6,7 +6,7 @@
.settings-header
%h4
= _('Visibility and access controls')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set default and restrict visibility levels. Configure import sources and git access protocol.')
@@ -17,7 +17,7 @@
.settings-header
%h4
= _('Account and limit')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set projects and maximum size limits, session duration, user options, and check feature availability for namespace plan.')
@@ -28,7 +28,7 @@
.settings-header
%h4
= _('Diff limits')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set size limits for displaying diffs in the browser.')
@@ -39,7 +39,7 @@
.settings-header
%h4
= _('Sign-up restrictions')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Configure the way a user creates a new account.')
@@ -50,7 +50,7 @@
.settings-header
%h4
= _('Sign-in restrictions')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set sign-in restrictions for all users.')
@@ -62,7 +62,7 @@
.settings-header
%h4
= _('Terms of Service and Privacy Policy')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Add a Terms of Service agreement and Privacy Policy for users of this GitLab instance.')
@@ -76,7 +76,7 @@
.settings-header
%h4
= _('Web terminal')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set the maximum session time for a web terminal.')
@@ -88,7 +88,7 @@
.settings-header
%h4
= _('Web IDE')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Manage Web IDE features.')
@@ -111,8 +111,11 @@
= render 'admin/application_settings/plantuml'
= render 'admin/application_settings/sourcegraph'
= render_if_exists 'admin/application_settings/slack'
+-# this partial is from JiHu, see details in https://jihulab.com/gitlab-cn/gitlab/-/merge_requests/417
+= render_if_exists 'admin/application_settings/dingtalk_integration'
= render 'admin/application_settings/third_party_offers'
= render 'admin/application_settings/snowplow'
= render 'admin/application_settings/eks'
= render 'admin/application_settings/floc'
= render_if_exists 'admin/application_settings/add_license'
+= render 'admin/application_settings/jira_connect_application_key' if Feature.enabled?(:jira_connect_oauth, current_user)
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 8e4b0b53f28..7cc0ff2c28e 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -8,7 +8,7 @@
.settings-header
%h4
= _('Metrics - Prometheus')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Monitor the health and performance of GitLab with Prometheus.')
@@ -19,7 +19,7 @@
.settings-header
%h4
= _('Metrics - Grafana')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Link to your Grafana instance.')
@@ -32,7 +32,7 @@
.settings-header
%h4
= _('Profiling - Performance bar')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Enable access to the performance bar for non-administrators in a given group.')
@@ -46,7 +46,7 @@
.settings-header#usage-statistics
%h4
= _('Usage statistics')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Enable or disable version check and Service Ping.')
@@ -58,7 +58,7 @@
.settings-header
%h4
= _('Sentry')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Configure Sentry integration for error tracking')
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
index a2497fe122b..f3264f733ab 100644
--- a/app/views/admin/application_settings/network.html.haml
+++ b/app/views/admin/application_settings/network.html.haml
@@ -6,7 +6,7 @@
.settings-header
%h4
= _('Performance optimization')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Various settings that affect GitLab performance.')
@@ -17,7 +17,7 @@
.settings-header
%h4
= _('User and IP rate limits')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set limits for web and API requests.')
@@ -29,7 +29,7 @@
.settings-header
%h4
= _('Package registry rate limits')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set rate limits for package registry API requests that supersede the general user and IP rate limits.')
@@ -41,7 +41,7 @@
.settings-header
%h4
= _('Files API Rate Limits')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Configure specific limits for Files API requests that supersede the general user and IP rate limits.')
@@ -52,7 +52,7 @@
.settings-header
%h4
= _('Search rate limits')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set rate limits for searches performed by web or API requests.')
@@ -63,7 +63,7 @@
.settings-header
%h4
= _('Deprecated API rate limits')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Configure specific limits for deprecated API requests that supersede the general user and IP rate limits.')
@@ -75,7 +75,7 @@
.settings-header
%h4
= _('Git LFS Rate Limits')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Configure specific limits for Git LFS requests that supersede the general user and IP rate limits.')
@@ -88,7 +88,7 @@
%h4
= s_('OutboundRequests|Outbound requests')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= s_('OutboundRequests|Allow requests to the local network from hooks and services.')
@@ -100,7 +100,7 @@
.settings-header
%h4
= _('Protected paths')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Rate limit access to specified paths.')
@@ -113,7 +113,7 @@
.settings-header
%h4
= _('Issues Rate Limits')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Limit the number of issues and epics per minute a user can create through web and API requests.')
@@ -125,7 +125,7 @@
.settings-header
%h4
= _('Notes rate limit')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set the per-user rate limit for notes created by web or API requests.')
@@ -137,7 +137,7 @@
.settings-header
%h4
= _('Users API rate limit')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set the per-user rate limit for getting a user by ID via the API.')
@@ -149,7 +149,7 @@
.settings-header
%h4
= _('Import and export rate limits')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set per-user rate limits for imports and exports of projects and groups.')
@@ -161,7 +161,7 @@
.settings-header
%h4
= _('Pipeline creation rate limits')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Limit the number of pipeline creation requests per minute. This limit includes pipelines created through the UI, the API, and by background processing.')
diff --git a/app/views/admin/application_settings/preferences.html.haml b/app/views/admin/application_settings/preferences.html.haml
index af4bfd28a01..858f96fc0d0 100644
--- a/app/views/admin/application_settings/preferences.html.haml
+++ b/app/views/admin/application_settings/preferences.html.haml
@@ -6,7 +6,7 @@
.settings-header
%h4
= _('Email')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Various email settings.')
@@ -17,7 +17,7 @@
.settings-header
%h4
= _("What's new")
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _("Configure %{italic_start}What's new%{italic_end} drawer and content.").html_safe % { italic_start: '<i>'.html_safe, italic_end: '</i>'.html_safe }
@@ -28,7 +28,7 @@
.settings-header
%h4
= _('Sign-in and Help page')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Additional text for the sign-in and Help page.')
@@ -40,7 +40,7 @@
.settings-header
%h4
= _('Pages')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= s_('AdminSettings|Size and domain settings for Pages static sites.')
@@ -51,7 +51,7 @@
.settings-header
%h4
= _('Polling interval multiplier')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Adjust how frequently the GitLab UI polls for updates.')
@@ -63,7 +63,7 @@
.settings-header
%h4
= _('Gitaly timeouts')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Configure Gitaly timeouts.')
@@ -76,7 +76,7 @@
.settings-header
%h4
= _('Localization')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Configure the default first day of the week and time tracking units.')
@@ -87,7 +87,7 @@
.settings-header
%h4
= _('Sidekiq job size limits')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Limit the size of Sidekiq jobs stored in Redis.')
diff --git a/app/views/admin/application_settings/reporting.html.haml b/app/views/admin/application_settings/reporting.html.haml
index ae6243c3b50..b15fcd93d1a 100644
--- a/app/views/admin/application_settings/reporting.html.haml
+++ b/app/views/admin/application_settings/reporting.html.haml
@@ -6,7 +6,7 @@
.settings-header
%h4
= _('Spam and Anti-bot Protection')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Configure CAPTCHAs, IP address limits, and other anti-spam measures.')
@@ -20,7 +20,7 @@
.settings-header
%h4
= _('Abuse reports')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Receive notification of abuse reports by email.')
diff --git a/app/views/admin/application_settings/repository.html.haml b/app/views/admin/application_settings/repository.html.haml
index ce7972827d3..785261b4c7b 100644
--- a/app/views/admin/application_settings/repository.html.haml
+++ b/app/views/admin/application_settings/repository.html.haml
@@ -6,7 +6,7 @@
.settings-header
%h4
= _('Default branch')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= s_('AdminSettings|Set the initial name and protections for the default branch of new repositories created in the instance.')
@@ -17,7 +17,7 @@
.settings-header
%h4
= _('Repository mirroring')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? 'Collapse' : 'Expand'
%p
= _('Configure repository mirroring.')
@@ -29,7 +29,7 @@
.settings-header
%h4
= _('Repository storage')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Configure repository storage.')
@@ -41,7 +41,7 @@
.settings-header
%h4
= _('Repository maintenance')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- repository_checks_link_url = help_page_path('administration/repository_checks.md')
@@ -56,7 +56,7 @@
.settings-header
%h4
= _('External storage for repository static objects')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Serve repository static objects (for example, archives and blobs) from external storage.')
diff --git a/app/views/admin/application_settings/service_usage_data.html.haml b/app/views/admin/application_settings/service_usage_data.html.haml
index 55c25ca32d5..25c8bd12345 100644
--- a/app/views/admin/application_settings/service_usage_data.html.haml
+++ b/app/views/admin/application_settings/service_usage_data.html.haml
@@ -8,12 +8,12 @@
%h3= name
- if @service_ping_data_present
- %button.gl-button.btn.btn-default.js-payload-preview-trigger{ type: 'button', data: { payload_selector: ".#{payload_class}" } }
- = gl_loading_icon(css_class: 'js-spinner gl-display-none gl-mr-2')
- .js-text.gl-display-inline= _('Preview payload')
- %button.gl-button.btn.btn-default.js-payload-download-trigger{ type: 'button', data: { endpoint: usage_data_admin_application_settings_path(format: :json) } }
- = gl_loading_icon(css_class: 'js-spinner gl-display-none gl-mr-2')
- .js-text.d-inline= _('Download payload')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-preview-trigger gl-mr-2', data: { payload_selector: ".#{payload_class}" } } ) do
+ = gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true)
+ %span.js-text.gl-display-inline= _('Preview payload')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-download-trigger gl-mr-2', data: { endpoint: usage_data_admin_application_settings_path(format: :json) } } ) do
+ = gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true)
+ %span.js-text.gl-display-inline= _('Download payload')
%pre.js-syntax-highlight.code.highlight.gl-mt-2.gl-display-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
- else
= render Pajamas::AlertComponent.new(variant: :warning,
diff --git a/app/views/admin/applications/edit.html.haml b/app/views/admin/applications/edit.html.haml
index 42f7f6c3d66..10a27fb906f 100644
--- a/app/views/admin/applications/edit.html.haml
+++ b/app/views/admin/applications/edit.html.haml
@@ -2,7 +2,7 @@
- breadcrumb_title @application.name
- page_title _("Edit"), @application.name, _("Applications")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('Edit application')
- @url = admin_application_path(@application)
= render 'form', application: @application
diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml
index 890155ee604..180871e48dd 100644
--- a/app/views/admin/applications/index.html.haml
+++ b/app/views/admin/applications/index.html.haml
@@ -1,6 +1,6 @@
- page_title s_('AdminArea|Instance OAuth applications')
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= s_('AdminArea|Instance OAuth applications')
%p.light
- docs_link_path = help_page_path('integration/oauth_provider')
diff --git a/app/views/admin/applications/new.html.haml b/app/views/admin/applications/new.html.haml
index 731cb51e2e4..b7e28923057 100644
--- a/app/views/admin/applications/new.html.haml
+++ b/app/views/admin/applications/new.html.haml
@@ -1,7 +1,7 @@
- breadcrumb_title _("Add new application")
- page_title _("Add new application")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _("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 d9c683cbcc3..212e3eeb951 100644
--- a/app/views/admin/applications/show.html.haml
+++ b/app/views/admin/applications/show.html.haml
@@ -1,6 +1,6 @@
- page_title @application.name, _("Applications")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
Application: #{@application.name}
= render 'shared/doorkeeper/applications/show',
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index bab9fa02928..6d2cab06010 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -1,6 +1,6 @@
- page_title _("Background Jobs")
-%h3.page-title= _('Background Jobs')
+%h1.page-title.gl-font-size-h-display= _('Background Jobs')
%p.light
- sidekiq_link_url = 'http://sidekiq.org/'
- sidekiq_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: sidekiq_link_url }
diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml
index 8b657eda0c0..46924393a27 100644
--- a/app/views/admin/broadcast_messages/index.html.haml
+++ b/app/views/admin/broadcast_messages/index.html.haml
@@ -2,7 +2,7 @@
- page_title _("Broadcast Messages")
- targeted_broadcast_messages_enabled = Feature.enabled?(:role_targeted_broadcast_messages)
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('Broadcast Messages')
%p.light
= _('Use banners and notifications to notify your users about scheduled maintenance, recent upgrades, and more.')
@@ -43,7 +43,7 @@
= message.target_path
%td
= message.broadcast_type.capitalize
- %td.gl-white-space-nowrap
- = link_to sprite_icon('pencil-square', css_class: 'gl-icon'), edit_admin_broadcast_message_path(message), title: _('Edit'), class: 'btn btn-icon gl-button'
- = link_to sprite_icon('remove', css_class: 'gl-icon'), admin_broadcast_message_path(message), method: :delete, remote: true, title: _('Remove'), class: 'js-remove-tr btn btn-icon gl-button btn-danger ml-2'
+ %td.gl-white-space-nowrap<
+ = link_to sprite_icon('pencil', css_class: 'gl-icon'), edit_admin_broadcast_message_path(message), title: _('Edit'), class: 'btn btn-icon gl-button'
+ = link_to sprite_icon('remove', css_class: 'gl-icon'), admin_broadcast_message_path(message), method: :delete, remote: true, title: _('Remove'), class: 'js-remove-tr btn btn-icon gl-button btn-danger gl-ml-3'
= paginate @broadcast_messages, theme: 'gitlab'
diff --git a/app/views/admin/dashboard/_security_newsletter_callout.html.haml b/app/views/admin/dashboard/_security_newsletter_callout.html.haml
index 4b1303cc97c..76bfa347480 100644
--- a/app/views/admin/dashboard/_security_newsletter_callout.html.haml
+++ b/app/views/admin/dashboard/_security_newsletter_callout.html.haml
@@ -2,11 +2,11 @@
= render Pajamas::AlertComponent.new(variant: :tip,
title: s_('AdminArea|Get security updates from GitLab and stay up to date'),
- alert_class: 'js-security-newsletter-callout',
- alert_data: { feature_id: Users::CalloutsHelper::SECURITY_NEWSLETTER_CALLOUT,
- dismiss_endpoint: callouts_path,
- defer_links: 'true' },
- close_button_data: { testid: 'close-security-newsletter-callout' }) do |c|
+ alert_options: { class: 'js-security-newsletter-callout',
+ data: { feature_id: Users::CalloutsHelper::SECURITY_NEWSLETTER_CALLOUT,
+ dismiss_endpoint: callouts_path,
+ defer_links: 'true' }},
+ close_button_options: { data: { testid: 'close-security-newsletter-callout' }}) do |c|
= c.body do
= s_('AdminArea|Sign up for the GitLab Security Newsletter to get notified for security updates.')
= c.actions do
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 69033d274a2..88fbbb28201 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -3,8 +3,8 @@
- billable_users_url = help_page_path('subscriptions/self_managed/index', anchor: 'billable-users')
- billable_users_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer nofollow">'.html_safe % { url: billable_users_url }
-= render_if_exists 'shared/manual_renewal_banner'
= render_if_exists 'shared/manual_quarterly_reconciliation_banner'
+= render_if_exists 'shared/submit_license_usage_data_banner'
= render_if_exists 'shared/qrtly_reconciliation_alert'
= render 'admin/dashboard/security_newsletter_callout'
@@ -22,22 +22,24 @@
.admin-dashboard.gl-mt-3
.h3.gl-mb-5.gl-mt-0= _('Instance overview')
.row
+ - component_params = { body_options: { class: 'gl-display-flex gl-justify-content-space-between gl-align-items-center gl-p-6' },
+ footer_options: { class: 'gl-bg-transparent'} }
.col-md-4.gl-mb-6
- .gl-card
- .gl-card-body.d-flex.justify-content-between.align-items-center.gl-p-6
+ = render Pajamas::CardComponent.new(**component_params) do |c|
+ = c.body do
%span
.d-flex.align-items-center
= sprite_icon('project', size: 16, css_class: 'gl-text-gray-700')
%h3.gl-m-0.gl-ml-3= approximate_count_with_delimiters(@counts, Project)
.gl-mt-3.text-uppercase= s_('AdminArea|Projects')
= link_to(s_('AdminArea|New project'), new_project_path, class: "btn gl-button btn-default")
- .gl-card-footer.gl-bg-transparent
+ = c.footer do
.d-flex.align-items-center
= link_to(s_('AdminArea|View latest projects'), admin_projects_path)
- = sprite_icon('angle-right', size: 12, css_class: 'gl-text-gray-700 gl-ml-2')
+ = sprite_icon('chevron-right', size: 12, css_class: 'gl-text-gray-700 gl-ml-2')
.col-md-4.gl-mb-6
- .gl-card
- .gl-card-body.d-flex.justify-content-between.align-items-center.gl-p-6
+ = render Pajamas::CardComponent.new(**component_params) do |c|
+ = c.body do
%span
.d-flex.align-items-center
= sprite_icon('users', size: 16, css_class: 'gl-text-gray-700')
@@ -54,29 +56,29 @@
= s_('AdminArea|Users')
= link_to(s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: "text-capitalize gl-ml-2")
= link_to(s_('AdminArea|New user'), new_admin_user_path, class: "btn gl-button btn-default")
- .gl-card-footer.gl-bg-transparent
+ = c.footer do
.d-flex.align-items-center
= link_to(s_('AdminArea|View latest users'), admin_users_path)
- = sprite_icon('angle-right', size: 12, css_class: 'gl-text-gray-700 gl-ml-2')
+ = sprite_icon('chevron-right', size: 12, css_class: 'gl-text-gray-700 gl-ml-2')
.col-md-4.gl-mb-6
- .gl-card
- .gl-card-body.d-flex.justify-content-between.align-items-center.gl-p-6
+ = render Pajamas::CardComponent.new(**component_params) do |c|
+ = c.body do
%span
.d-flex.align-items-center
= sprite_icon('group', size: 16, css_class: 'gl-text-gray-700')
%h3.gl-m-0.gl-ml-3= approximate_count_with_delimiters(@counts, Group)
.gl-mt-3.text-uppercase= s_('AdminArea|Groups')
= link_to(s_('AdminArea|New group'), new_admin_group_path, class: "btn gl-button btn-default")
- .gl-card-footer.gl-bg-transparent
+ = c.footer do
.d-flex.align-items-center
= link_to(s_('AdminArea|View latest groups'), admin_groups_path)
- = sprite_icon('angle-right', size: 12, css_class: 'gl-text-gray-700 gl-ml-2')
+ = sprite_icon('chevron-right', size: 12, css_class: 'gl-text-gray-700 gl-ml-2')
.row
.col-md-4.gl-mb-6
#js-admin-statistics-container
.col-md-4.gl-mb-6
- .gl-card
- .gl-card-body
+ = render Pajamas::CardComponent.new do |c|
+ = c.body do
%h4= s_('AdminArea|Features')
= feature_entry(_('Sign up'),
href: general_admin_application_settings_path(anchor: 'js-signup-settings'),
@@ -114,8 +116,8 @@
href: admin_runners_path,
enabled: Gitlab.config.gitlab_ci.shared_runners_enabled)
.col-md-4.gl-mb-6
- .gl-card
- .gl-card-body
+ = render Pajamas::CardComponent.new do |c|
+ = c.body do
%h4
= s_('AdminArea|Components')
- if show_version_check?
@@ -171,8 +173,8 @@
= link_to _("Gitaly Servers"), admin_gitaly_servers_path
.row
.col-md-4.gl-mb-6
- .gl-card
- .gl-card-body
+ = render Pajamas::CardComponent.new do |c|
+ = c.body do
%h4= s_('AdminArea|Latest projects')
- @projects.each do |project|
.gl-display-flex.gl-py-3
@@ -181,8 +183,8 @@
%span.gl-white-space-nowrap.gl-text-right
#{time_ago_with_tooltip(project.created_at)}
.col-md-4.gl-mb-6
- .gl-card
- .gl-card-body
+ = render Pajamas::CardComponent.new do |c|
+ = c.body do
%h4= s_('AdminArea|Latest users')
- @users.each do |user|
.gl-display-flex.gl-py-3
@@ -192,8 +194,8 @@
%span.gl-white-space-nowrap.gl-text-right
#{time_ago_with_tooltip(user.created_at)}
.col-md-4.gl-mb-6
- .gl-card
- .gl-card-body
+ = render Pajamas::CardComponent.new do |c|
+ = c.body do
%h4= s_('AdminArea|Latest groups')
- @groups.each do |group|
.gl-display-flex.gl-py-3
diff --git a/app/views/admin/deploy_keys/edit.html.haml b/app/views/admin/deploy_keys/edit.html.haml
index f85b37b3640..12a1c0c3de2 100644
--- a/app/views/admin/deploy_keys/edit.html.haml
+++ b/app/views/admin/deploy_keys/edit.html.haml
@@ -1,5 +1,5 @@
- page_title _('Edit Deploy Key')
-%h3.page-title= _('Edit public deploy key')
+%h1.page-title.gl-font-size-h-display= _('Edit public deploy key')
%hr
%div
diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml
index fe2bc8530f7..74882900756 100644
--- a/app/views/admin/deploy_keys/new.html.haml
+++ b/app/views/admin/deploy_keys/new.html.haml
@@ -1,5 +1,5 @@
- page_title _('New Deploy Key')
-%h3.page-title= _('New public deploy key')
+%h1.page-title.gl-font-size-h-display= _('New public deploy key')
%hr
%div
diff --git a/app/views/admin/gitaly_servers/index.html.haml b/app/views/admin/gitaly_servers/index.html.haml
index 0b06f145687..5bd4e066409 100644
--- a/app/views/admin/gitaly_servers/index.html.haml
+++ b/app/views/admin/gitaly_servers/index.html.haml
@@ -1,7 +1,7 @@
- breadcrumb_title _("Gitaly Servers")
- page_title _("Gitaly Servers")
-%h3.page-title= _("Gitaly Servers")
+%h1.page-title.gl-font-size-h-display= _("Gitaly Servers")
%hr
.gitaly_servers
- if @gitaly_servers.any?
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 944d7bfced0..43a8d56d584 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for [:admin, @group] do |f|
- = form_errors(@group)
+ = form_errors(@group, pajamas_alert: true)
= render 'shared/group_form', f: f
= render 'shared/group_form_description', f: f
diff --git a/app/views/admin/groups/edit.html.haml b/app/views/admin/groups/edit.html.haml
index 8e9e1a58a17..2a1e6b8f637 100644
--- a/app/views/admin/groups/edit.html.haml
+++ b/app/views/admin/groups/edit.html.haml
@@ -1,4 +1,4 @@
- page_title _("Edit"), @group.name, _("Groups")
-%h3.page-title= _('Edit group: %{group_name}') % { group_name: @group.name }
+%h1.page-title.gl-font-size-h-display= _('Edit group: %{group_name}') % { group_name: @group.name }
%hr
= render 'form', visibility_level: @group.visibility_level
diff --git a/app/views/admin/groups/new.html.haml b/app/views/admin/groups/new.html.haml
index 553e8638e52..a98c685281d 100644
--- a/app/views/admin/groups/new.html.haml
+++ b/app/views/admin/groups/new.html.haml
@@ -1,4 +1,4 @@
- page_title _("New Group")
-%h3.page-title= _('New group')
+%h1.page-title.gl-font-size-h-display= _('New group')
%hr
= render 'form', visibility_level: default_group_visibility
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 39b2fa41c80..a57d3170cbd 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -4,11 +4,11 @@
- page_title @group.name, _("Groups")
- current_user_is_group_owner = @group && @group.has_owner?(current_user)
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('Group: %{group_name}') % { group_name: @group.full_name }
= link_to admin_group_edit_path(@group), class: "btn btn-default gl-button float-right", data: { qa_selector: 'edit_group_link' } do
- = sprite_icon('pencil-square', css_class: 'gl-icon')
+ = sprite_icon('pencil', css_class: 'gl-icon gl-mr-2')
= _('Edit')
%hr
.row
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index a289cea0d5a..98427cb6419 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -1,7 +1,7 @@
- page_title _('Health Check')
- no_errors = @errors.blank?
-%h3.page-title= page_title
+%h1.page-title.gl-font-size-h-display= page_title
.bs-callout.clearfix
.float-left
%p
@@ -23,8 +23,8 @@
%code= metrics_url(token: Gitlab::CurrentSettings.health_check_access_token)
= render_if_exists 'admin/health_check/health_check_url'
%hr
-.gl-card
- .gl-card-header
+= render Pajamas::CardComponent.new do |c|
+ = c.header do
Current Status:
- if no_errors
= sprite_icon('check', css_class: 'cgreen')
@@ -32,7 +32,7 @@
- else
= sprite_icon('warning-solid', css_class: 'cred')
#{ s_('HealthCheck|Unhealthy') }
- .gl-card-body
+ = c.body do
- if no_errors
#{ s_('HealthCheck|No Health Problems Detected') }
- else
diff --git a/app/views/admin/hook_logs/show.html.haml b/app/views/admin/hook_logs/show.html.haml
index ca2737ca56f..6fcaf2ea152 100644
--- a/app/views/admin/hook_logs/show.html.haml
+++ b/app/views/admin/hook_logs/show.html.haml
@@ -1,9 +1,12 @@
- page_title _('Request details')
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _("Request details")
%hr
-= link_to _("Resend Request"), retry_admin_hook_hook_log_path(@hook, @hook_log), method: :post, class: "btn gl-button btn-default float-right gl-ml-3"
+- if @hook_log.oversize?
+ = button_tag _("Resend Request"), class: "btn gl-button btn-default float-right gl-ml-3 has-tooltip", disabled: true, title: _("Request data is too large")
+- else
+ = link_to _("Resend Request"), retry_admin_hook_hook_log_path(@hook, @hook_log), method: :post, class: "btn gl-button btn-default float-right gl-ml-3"
= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
diff --git a/app/views/admin/hooks/_form.html.haml b/app/views/admin/hooks/_form.html.haml
index a309e874317..cf3b6e6e0e0 100644
--- a/app/views/admin/hooks/_form.html.haml
+++ b/app/views/admin/hooks/_form.html.haml
@@ -1,4 +1,4 @@
-= form_errors(hook)
+= form_errors(hook, pajamas_alert: true)
.form-group
= form.label :url, _('URL'), class: 'label-bold'
diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml
index 5c62cff27c7..ba7687db9c7 100644
--- a/app/views/admin/identities/_form.html.haml
+++ b/app/views/admin/identities/_form.html.haml
@@ -14,5 +14,5 @@
= f.text_field :extern_uid, class: 'form-control', required: true
.form-actions
- = f.submit _('Save changes'), class: "gl-button btn btn-success"
+ = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/identities/edit.html.haml b/app/views/admin/identities/edit.html.haml
index 0fd1f2f547f..54cc9139aca 100644
--- a/app/views/admin/identities/edit.html.haml
+++ b/app/views/admin/identities/edit.html.haml
@@ -2,7 +2,7 @@
- add_to_breadcrumbs @user.name, admin_user_identities_path(@user)
- breadcrumb_title _('Edit Identity')
- page_title _("Edit"), @identity.provider, _("Identities"), @user.name, _("Users")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('Edit identity for %{user_name}') % { user_name: @user.name }
%hr
diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml
index a4f1ce4afc0..b4dd92bf15c 100644
--- a/app/views/admin/identities/index.html.haml
+++ b/app/views/admin/identities/index.html.haml
@@ -3,7 +3,7 @@
- page_title _("Identities"), @user.name, _("Users")
= render 'admin/users/head'
-= link_to _('New identity'), new_admin_user_identity_path, class: 'float-right gl-button btn btn-success'
+= link_to _('New identity'), new_admin_user_identity_path, class: 'float-right gl-button btn-confirm'
- if @identities.present?
.table-holder
%table.table
diff --git a/app/views/admin/identities/new.html.haml b/app/views/admin/identities/new.html.haml
index b4f37057c51..d3e7bb8a370 100644
--- a/app/views/admin/identities/new.html.haml
+++ b/app/views/admin/identities/new.html.haml
@@ -2,6 +2,6 @@
- add_to_breadcrumbs @user.name, admin_user_identities_path(@user)
- breadcrumb_title _('New Identity')
- page_title _('New Identity')
-%h3.page-title= _('New identity')
+%h1.page-title.gl-font-size-h-display= _('New identity')
%hr
= render 'form'
diff --git a/app/views/admin/jobs/index.html.haml b/app/views/admin/jobs/index.html.haml
index 670628f7463..667c90f0228 100644
--- a/app/views/admin/jobs/index.html.haml
+++ b/app/views/admin/jobs/index.html.haml
@@ -4,14 +4,17 @@
- breadcrumb_title _("Jobs")
- page_title _("Jobs")
-.top-area.scrolling-tabs-container.inner-page-scroll-tabs
- - build_path_proc = ->(scope) { admin_jobs_path(scope: scope) }
- = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
+.top-area
+ .scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full
+ .fade-left= sprite_icon('chevron-lg-left', size: 12)
+ .fade-right= sprite_icon('chevron-lg-right', size: 12)
+ - build_path_proc = ->(scope) { admin_jobs_path(scope: scope) }
+ = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
- if @all_builds.running_or_pending.any?
#js-stop-jobs-modal
.nav-controls
- %button#js-stop-jobs-button.btn.gl-button.btn-danger{ data: { url: cancel_all_admin_jobs_path } }
+ = render Pajamas::ButtonComponent.new(variant: :danger, button_options: { id: 'js-stop-jobs-button', data: { url: cancel_all_admin_jobs_path } }) do
= s_('AdminArea|Stop all jobs')
.row-content-block.second-block
diff --git a/app/views/admin/labels/edit.html.haml b/app/views/admin/labels/edit.html.haml
index 44dd2b6a646..80112595adf 100644
--- a/app/views/admin/labels/edit.html.haml
+++ b/app/views/admin/labels/edit.html.haml
@@ -1,7 +1,7 @@
- add_to_breadcrumbs _("Labels"), admin_labels_path
- breadcrumb_title _("Edit Label")
- page_title _("Edit"), @label.name, _("Labels")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('Edit Label')
%hr
= render 'shared/labels/form', url: admin_label_path(@label), back_path: admin_labels_path
diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml
index 66fd18e1b76..21b19236683 100644
--- a/app/views/admin/labels/index.html.haml
+++ b/app/views/admin/labels/index.html.haml
@@ -3,7 +3,7 @@
%div
= link_to new_admin_label_path, class: "float-right btn gl-button btn-confirm" do
= _('New label')
- %h3.page-title
+ %h1.page-title.gl-font-size-h-display
= _('Labels')
%hr
- if @labels.present?
diff --git a/app/views/admin/labels/new.html.haml b/app/views/admin/labels/new.html.haml
index 5166bdb4d20..76f9eee717e 100644
--- a/app/views/admin/labels/new.html.haml
+++ b/app/views/admin/labels/new.html.haml
@@ -1,5 +1,5 @@
- page_title _("New Label")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('New Label')
%hr
= render 'shared/labels/form', url: admin_labels_path, back_path: admin_labels_path
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index f947e174990..f23a688dd48 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -1,14 +1,18 @@
- page_title _('Projects')
- params[:visibility_level] ||= []
-.top-area.scrolling-tabs-container.inner-page-scroll-tabs
- = gl_tabs_nav({ class: 'gl-border-b-0 gl-overflow-x-auto gl-flex-grow-1 gl-flex-nowrap gl-webkit-scrollbar-display-none' }) do
- = gl_tab_link_to _('All'), admin_projects_path(visibility_level: nil), { item_active: params[:visibility_level].empty? }
- = gl_tab_link_to _('Private'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- = gl_tab_link_to _('Internal'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- = gl_tab_link_to _('Public'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+.top-area
+ .scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full
+ .fade-left= sprite_icon('chevron-lg-left', size: 12)
+ .fade-right= sprite_icon('chevron-lg-right', size: 12)
+ = gl_tabs_nav({ class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full nav gl-tabs-nav nav gl-tabs-nav' }) do
+ = gl_tab_link_to _('All'), admin_projects_path(visibility_level: nil), { item_active: params[:visibility_level].empty? }
+ = gl_tab_link_to _('Private'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ = gl_tab_link_to _('Internal'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ = gl_tab_link_to _('Public'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- .nav-controls
+
+ .nav-controls.gl-pl-2
.search-holder
= render 'shared/projects/search_form', autofocus: true, admin_view: true
- current_namespace = _('Namespace')
@@ -22,6 +26,5 @@
= render 'shared/projects/dropdown'
= link_to new_project_path, class: 'gl-button btn btn-confirm' do
= _('New Project')
- = button_tag _("Search"), class: "gl-button btn btn-confirm btn-search hide"
= render 'projects'
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 16f6e71d79b..6921c051361 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -5,18 +5,18 @@
- @content_class = "admin-projects"
- current_user_is_group_owner = @group && @group.has_owner?(current_user)
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('Project: %{name}') % { name: @project.full_name }
= link_to edit_project_path(@project), class: "btn btn-default gl-button float-right" do
- = sprite_icon('pencil-square', css_class: 'gl-icon')
+ = sprite_icon('pencil', css_class: 'gl-icon gl-mr-2')
= _('Edit')
%hr
- if @project.last_repository_check_failed?
.row
.col-md-12
= render Pajamas::AlertComponent.new(variant: :danger,
- alert_class: 'gl-mb-5',
- alert_data: { testid: 'last-repository-check-failed-alert' }) do |c|
+ alert_options: { class: 'gl-mb-5',
+ data: { testid: 'last-repository-check-failed-alert' }}) do |c|
= c.body do
- last_check_message = _("Last repository check (%{last_check_timestamp}) failed. See the 'repocheck.log' file for error messages.")
- last_check_message = last_check_message % { last_check_timestamp: time_ago_with_tooltip(@project.last_repository_check_at) }
diff --git a/app/views/admin/spam_logs/index.html.haml b/app/views/admin/spam_logs/index.html.haml
index 2a36c991ed2..c974f455112 100644
--- a/app/views/admin/spam_logs/index.html.haml
+++ b/app/views/admin/spam_logs/index.html.haml
@@ -1,5 +1,5 @@
- page_title _("Spam Logs")
-%h3.page-title= _('Spam Logs')
+%h1.page-title.gl-font-size-h-display= _('Spam Logs')
%hr
- if @spam_logs.present?
.table-holder
diff --git a/app/views/admin/topics/_form.html.haml b/app/views/admin/topics/_form.html.haml
index 9b9d97950cc..1c1bc61aef2 100644
--- a/app/views/admin/topics/_form.html.haml
+++ b/app/views/admin/topics/_form.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for @topic, url: url, html: { multipart: true, class: 'js-project-topic-form gl-show-field-errors common-note-form js-quick-submit js-requires-input' }, authenticity_token: true do |f|
- = form_errors(@topic)
+ = form_errors(@topic, pajamas_alert: true)
.form-group
= f.label :name do
diff --git a/app/views/admin/topics/edit.html.haml b/app/views/admin/topics/edit.html.haml
index 4416bb0fe18..73796949e39 100644
--- a/app/views/admin/topics/edit.html.haml
+++ b/app/views/admin/topics/edit.html.haml
@@ -1,4 +1,4 @@
- page_title _("Edit"), @topic.name, _("Topics")
-%h3.page-title= _('Edit topic: %{topic_name}') % { topic_name: @topic.name }
+%h1.page-title.gl-font-size-h-display= _('Edit topic: %{topic_name}') % { topic_name: @topic.name }
%hr
= render 'form', url: admin_topic_path(@topic)
diff --git a/app/views/admin/topics/new.html.haml b/app/views/admin/topics/new.html.haml
index 8b4a8ac269e..aefa1bbcf95 100644
--- a/app/views/admin/topics/new.html.haml
+++ b/app/views/admin/topics/new.html.haml
@@ -1,4 +1,4 @@
- page_title _("New topic")
-%h3.page-title= _('New topic')
+%h1.page-title.gl-font-size-h-display= _('New topic')
%hr
= render 'form', url: admin_topics_path(@topic)
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
index 51e6af56377..cf951ae0265 100644
--- a/app/views/admin/users/_access_levels.html.haml
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -1,22 +1,19 @@
%fieldset
- %legend
+ %legend.gl-border-bottom-0
= s_('AdminUsers|Access')
.form-group.row
- .col-sm-2.col-form-label
+ .col-12
= f.label :projects_limit
- .col-sm-10
= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control gl-form-input'
.form-group.row
- .col-sm-2.col-form-label.gl-pt-0
+ .col-12.gl-pt-0
= f.label :can_create_group
- .col-sm-10
= f.gitlab_ui_checkbox_component :can_create_group, ''
.form-group.row
- .col-sm-2.col-form-label.gl-pt-0
+ .col-12.gl-pt-0
= f.label :access_level
- .col-sm-10
- editing_current_user = (current_user == @user)
= f.gitlab_ui_radio_component :access_level, :regular,
@@ -35,10 +32,10 @@
.form-group.row
- .col-sm-2.col-form-label.gl-pt-0
+ .col-12.gl-pt-0
= f.label :external
.hidden{ data: user_internal_regex_data }
- .col-sm-10.gl-display-flex.gl-align-items-baseline
+ .col-12.gl-display-flex.gl-align-items-baseline
= f.gitlab_ui_checkbox_component :external, s_('AdminUsers|External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects, groups, or personal snippets.')
%row.hidden#warning_external_automatically_set
= gl_badge_tag s_('AdminUsers|Automatically marked as default internal user'), variant: :warning
@@ -46,9 +43,9 @@
.form-group.row
- @user.credit_card_validation || @user.build_credit_card_validation
= f.fields_for :credit_card_validation do |ff|
- .col-sm-2.col-form-label.gl-pt-0
+ .col-12.gl-pt-0
= ff.label s_('AdminUsers|Validate user account')
- .col-sm-10.gl-display-flex.gl-align-items-baseline
+ .col-12.gl-display-flex.gl-align-items-baseline
= ff.gitlab_ui_checkbox_component :credit_card_validated_at,
s_('AdminUsers|User is validated and can use free CI minutes on shared runners.'),
help_text: s_('AdminUsers|A user can validate themselves by inputting a credit/debit card, or an admin can manually validate a user.'),
diff --git a/app/views/admin/users/_admin_notes.html.haml b/app/views/admin/users/_admin_notes.html.haml
index 7c3220e2cee..10f654e0f71 100644
--- a/app/views/admin/users/_admin_notes.html.haml
+++ b/app/views/admin/users/_admin_notes.html.haml
@@ -1,7 +1,6 @@
%fieldset
- %legend= _('Admin notes')
+ %legend.gl-border-bottom-0= _('Admin notes')
.form-group.row
- .col-sm-2.col-form-label
+ .col-12
= f.label :note, s_('Admin|Note')
- .col-sm-10
= f.text_area :note, class: 'form-control gl-form-input gl-form-textarea'
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index 3869a2b6dcd..7995bc1b6f4 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -1,49 +1,41 @@
.user_new
= gitlab_ui_form_for [:admin, @user], html: { class: 'fieldset-form' } do |f|
- = form_errors(@user)
+ = form_errors(@user, pajamas_alert: true)
%fieldset
- %legend= _('Account')
+ %legend.gl-border-bottom-0= _('Account')
.form-group.row
- .col-sm-2.col-form-label
- = f.label :name
- .col-sm-10
+ .col-12
+ = f.label "#{:name} (required)"
= f.text_field :name, required: true, autocomplete: 'off', class: 'form-control gl-form-input'
- %span.help-inline * #{_('required')}
.form-group.row
- .col-sm-2.col-form-label
- = f.label :username
- .col-sm-10
+ .col-12
+ = f.label "#{:username} (required)"
= f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control gl-form-input'
- %span.help-inline * #{_('required')}
.form-group.row
- .col-sm-2.col-form-label
- = f.label :email
- .col-sm-10
+ .col-12
+ = f.label "#{:email} (required)"
= f.text_field :email, required: true, autocomplete: 'off', class: 'form-control gl-form-input'
- %span.help-inline * #{_('required')}
- if @user.new_record?
%fieldset
- %legend= _('Password')
+ %legend.gl-border-bottom-0= _('Password')
.form-group.row
- .col-sm-2.col-form-label
- = f.label :password
- .col-sm-10
+ .col-12
%strong
= _('Reset link will be generated and sent to the user. %{break} User will be forced to set the password on first sign in.').html_safe % { break: '<br />'.html_safe }
- else
%fieldset
- %legend= _('Password')
+ %legend.gl-border-bottom-0= _('Password')
.form-group.row
- .col-sm-2.col-form-label
+ .col-12
= f.label :password
- .col-sm-10
+ .col-12
= f.password_field :password, disabled: f.object.force_random_password, autocomplete: 'new-password', class: 'form-control gl-form-input'
.form-group.row
- .col-sm-2.col-form-label
+ .col-12
= f.label :password_confirmation
- .col-sm-10
+ .col-12
= f.password_field :password_confirmation, disabled: f.object.force_random_password, autocomplete: 'new-password', class: 'form-control gl-form-input'
= render partial: 'access_levels', locals: { f: f }
@@ -53,37 +45,33 @@
= render_if_exists 'admin/users/limits', f: f
%fieldset
- %legend= _('Profile')
+ %legend.gl-border-bottom-0= _('Profile')
.form-group.row
- .col-sm-2.col-form-label
+ .col-12
= f.label :avatar
- .col-sm-10
+ .col-12
= f.file_field :avatar
.form-group.row
- .col-sm-2.col-form-label
+ .col-12
= f.label :skype
- .col-sm-10
= f.text_field :skype, class: 'form-control gl-form-input'
.form-group.row
- .col-sm-2.col-form-label
+ .col-12
= f.label :linkedin
- .col-sm-10
= f.text_field :linkedin, class: 'form-control gl-form-input'
.form-group.row
- .col-sm-2.col-form-label
+ .col-12
= f.label :twitter
- .col-sm-10
= f.text_field :twitter, class: 'form-control gl-form-input'
.form-group.row
- .col-sm-2.col-form-label
+ .col-12
= f.label :website_url
- .col-sm-10
= f.text_field :website_url, class: 'form-control gl-form-input'
= render 'admin/users/admin_notes', f: f
- .form-actions
+ %div
- if @user.new_record?
= f.submit _('Create user'), class: "btn gl-button btn-confirm"
= link_to _('Cancel'), admin_users_path, class: "gl-button btn btn-default btn-cancel"
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index e429a16d5ec..529692df0b6 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -1,6 +1,6 @@
.gl-display-flex.gl-flex-wrap.gl-justify-content-space-between.gl-align-items-center.gl-py-3.gl-mb-5.gl-border-b-solid.gl-border-gray-100.gl-border-b-1
.gl-my-3
- %h3.page-title.gl-m-0
+ %h1.page-title.gl-font-size-h-display.gl-m-0
= @user.name
- if @user.blocked_pending_approval?
%span.gl-text-red-500
@@ -32,7 +32,7 @@
- if impersonation_enabled? && @user.can?(:log_in)
= link_to _('Impersonate'), impersonate_admin_user_path(@user), method: :post, class: "btn btn-default gl-button", data: { qa_selector: 'impersonate_user_link' }
- if can_force_email_confirmation?(@user)
- %button.btn.gl-button.btn-info.js-confirm-modal-button{ data: confirm_user_data(@user) }
+ = render Pajamas::ButtonComponent.new(variant: :confirm, button_options: { class: 'js-confirm-modal-button', data: confirm_user_data(@user) }) do
= _('Confirm user')
.gl-p-2
#js-admin-user-actions{ data: admin_user_actions_data_attributes(@user) }
diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml
index a7ed7b8c052..2dbafb517be 100644
--- a/app/views/admin/users/_users.html.haml
+++ b/app/views/admin/users/_users.html.haml
@@ -1,6 +1,6 @@
- if registration_features_can_be_prompted?
= render Pajamas::AlertComponent.new(variant: :tip,
- alert_class: 'gl-my-5',
+ alert_options: { class: 'gl-my-5' },
dismissible: false) do |c|
= c.body do
= render 'shared/registration_features_discovery_message', feature_title: s_('RegistrationFeatures|send emails to users')
diff --git a/app/views/admin/users/edit.html.haml b/app/views/admin/users/edit.html.haml
index e3ebb691ba9..5507d640e04 100644
--- a/app/views/admin/users/edit.html.haml
+++ b/app/views/admin/users/edit.html.haml
@@ -1,5 +1,4 @@
- page_title _("Edit"), @user.name, _("Users")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _("Edit user: %{user_name}") % { user_name: @user.name }
-%hr
= render 'form'
diff --git a/app/views/admin/users/new.html.haml b/app/views/admin/users/new.html.haml
index 08aa7c3c9d2..425d0150d10 100644
--- a/app/views/admin/users/new.html.haml
+++ b/app/views/admin/users/new.html.haml
@@ -1,5 +1,4 @@
- page_title _("New User")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= s_('AdminUsers|New user')
-%hr
= render 'form'
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index 3b91bcdd990..6cf414dc648 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -20,7 +20,7 @@
%button.gl-button.btn.btn-default.award-control.has-tooltip.js-add-award{ type: 'button',
'aria-label': _('Add reaction'),
data: { title: _('Add reaction') } }
- %span{ class: "award-control-icon award-control-icon-neutral" }= sprite_icon('slight-smile')
- %span{ class: "award-control-icon award-control-icon-positive" }= sprite_icon('smiley')
- %span{ class: "award-control-icon award-control-icon-super-positive" }= sprite_icon('smile')
+ %span{ class: "award-control-icon award-control-icon-neutral gl-icon" }= sprite_icon('slight-smile')
+ %span{ class: "award-control-icon award-control-icon-positive gl-icon" }= sprite_icon('smiley')
+ %span{ class: "award-control-icon award-control-icon-super-positive gl-icon" }= sprite_icon('smile')
= yield
diff --git a/app/views/ci/variables/_header.html.haml b/app/views/ci/variables/_header.html.haml
index 392ff927f01..d6a9ce72d03 100644
--- a/app/views/ci/variables/_header.html.haml
+++ b/app/views/ci/variables/_header.html.haml
@@ -3,7 +3,7 @@
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Variables')
-%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index 0024526a90c..9ef2599a2a6 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -13,9 +13,9 @@
maskable_regex: ci_variable_maskable_regex,
protected_by_default: ci_variable_protected_by_default?.to_s,
aws_logo_svg_path: image_path('aws_logo.svg'),
- 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'),
+ aws_tip_deploy_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'deploy-your-application-to-ecs'),
+ aws_tip_commands_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'use-an-image-to-run-aws-commands'),
+ aws_tip_learn_link: help_page_path('ci/cloud_deployment/index.md'),
contains_variable_reference_link: help_page_path('ci/variables/index', anchor: 'use-variables-in-other-variables'),
protected_environment_variables_link: help_page_path('ci/variables/index', anchor: 'protected-cicd-variables'),
masked_environment_variables_link: help_page_path('ci/variables/index', anchor: 'mask-a-cicd-variable'),
diff --git a/app/views/clusters/clusters/_advanced_settings.html.haml b/app/views/clusters/clusters/_advanced_settings.html.haml
index 59c8fe04b09..8eba398fd13 100644
--- a/app/views/clusters/clusters/_advanced_settings.html.haml
+++ b/app/views/clusters/clusters/_advanced_settings.html.haml
@@ -35,7 +35,7 @@
= s_("ClusterIntegration|This is necessary if your integration has become out of sync. The cache is repopulated during the next CI job that requires namespace and service accounts.")
- else
= s_("ClusterIntegration|This is necessary to clear existing environment-namespace associations from clusters previously managed by GitLab.")
- = link_to(s_('ClusterIntegration|Clear cluster cache'), clusterable.clear_cluster_cache_path(@cluster), method: :delete, class: 'btn gl-button btn-info')
+ = link_to(s_('ClusterIntegration|Clear cluster cache'), clusterable.clear_cluster_cache_path(@cluster), method: :delete, class: 'btn gl-button btn-confirm')
.sub-section.form-group
%h4.text-danger
diff --git a/app/views/clusters/clusters/_banner.html.haml b/app/views/clusters/clusters/_banner.html.haml
index 720e3ff08e2..6461b71b10d 100644
--- a/app/views/clusters/clusters/_banner.html.haml
+++ b/app/views/clusters/clusters/_banner.html.haml
@@ -7,12 +7,12 @@
%span.gl-ml-2= s_('ClusterIntegration|Kubernetes cluster is being created...')
= render Pajamas::AlertComponent.new(variant: :warning,
- alert_class: 'hidden js-cluster-api-unreachable') do |c|
+ alert_options: { class: 'hidden js-cluster-api-unreachable' }) do |c|
= c.body do
= s_('ClusterIntegration|Your cluster API is unreachable. Please ensure your API URL is correct.')
= render Pajamas::AlertComponent.new(variant: :warning,
- alert_class: 'hidden js-cluster-authentication-failure js-cluster-api-unreachable') do |c|
+ alert_options: { class: 'hidden js-cluster-authentication-failure js-cluster-api-unreachable' }) do |c|
= c.body do
= s_('ClusterIntegration|There was a problem authenticating with your cluster. Please ensure your CA Certificate and Token are valid.')
diff --git a/app/views/clusters/clusters/_deprecation_alert.html.haml b/app/views/clusters/clusters/_deprecation_alert.html.haml
index 3a83efec29b..0318c0f7dfa 100644
--- a/app/views/clusters/clusters/_deprecation_alert.html.haml
+++ b/app/views/clusters/clusters/_deprecation_alert.html.haml
@@ -1,4 +1,4 @@
-= render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_class: 'gl-mt-6 gl-mb-3') do |c|
+= render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_options: { class: 'gl-mt-6 gl-mb-3' }) do |c|
= c.body do
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
- issue_link_start = link_start % { url: 'https://gitlab.com/gitlab-org/configure/general/-/issues/199' }
diff --git a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
index b130e0c7214..c3b881df98d 100644
--- a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
@@ -1,8 +1,8 @@
- link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
= render Pajamas::AlertComponent.new(title: s_('ClusterIntegration|Did you know?'),
- alert_class: 'gcp-signup-offer',
- alert_data: { feature_id: Users::CalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: callouts_path }) do |c|
+ alert_options: { class: 'gcp-signup-offer',
+ data: { feature_id: Users::CalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: callouts_path }}) do |c|
= c.body do
= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
= c.actions do
diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
index c65b947d1ba..ca9f69ab73a 100644
--- a/app/views/dashboard/_activity_head.html.haml
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -1,5 +1,5 @@
.page-title-holder.d-flex.align-items-center
- %h1.page-title= _('Activity')
+ %h1.page-title.gl-font-size-h-display= _('Activity')
.top-area
= gl_tabs_nav({ class: 'gl-border-b-0', data: { testid: 'dashboard-activity-tabs' } }) do
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index 7b1d25b9b43..f0f1413831a 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -1,5 +1,5 @@
.page-title-holder.d-flex.align-items-center
- %h1.page-title= _('Groups')
+ %h1.page-title.gl-font-size-h-display= _('Groups')
- if current_user.can_create_group?
.page-title-controls
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index b94b14bf6bd..9c492a0da34 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -5,16 +5,17 @@
= render 'shared/project_limit'
.page-title-holder.d-flex.align-items-center
- %h1.page-title= _('Projects')
+ %h1.page-title.gl-font-size-h-display= _('Projects')
- if current_user.can_create_project?
.page-title-controls
= 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)
- .fade-right= sprite_icon('chevron-lg-right', size: 12)
- = render 'dashboard/projects_nav'
+.top-area
+ .scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full
+ .fade-left= sprite_icon('chevron-lg-left', size: 12)
+ .fade-right= sprite_icon('chevron-lg-right', size: 12)
+ = render 'dashboard/projects_nav'
- unless feature_project_list_filter_bar
.nav-controls
= render 'shared/projects/search_form'
diff --git a/app/views/dashboard/_projects_nav.html.haml b/app/views/dashboard/_projects_nav.html.haml
index 90b40f3c7b7..29c820ddc58 100644
--- a/app/views/dashboard/_projects_nav.html.haml
+++ b/app/views/dashboard/_projects_nav.html.haml
@@ -1,7 +1,7 @@
- is_your_projects_path = current_page?(dashboard_projects_path) || current_page?(root_path)
- is_explore_projects_path = current_page?(explore_root_path) || current_page?(trending_explore_projects_path) || current_page?(starred_explore_projects_path) || current_page?(explore_projects_path)
-= gl_tabs_nav({ class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-flex-nowrap gl-border-0' }) do
+= gl_tabs_nav({ class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full nav gl-tabs-nav' }) do
= gl_tab_link_to dashboard_projects_path, { item_active: is_your_projects_path, class: 'shortcuts-activity', data: { placement: 'right' } } do
= _("Your projects")
= gl_tab_counter_badge(limited_counter_with_delimiter(@total_user_projects_count))
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index 8c468812e33..be2124fdd7e 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -1,5 +1,5 @@
.page-title-holder.d-flex.align-items-center
- %h1.page-title= _('Snippets')
+ %h1.page-title.gl-font-size-h-display= _('Snippets')
- if current_user && current_user.snippets.any? || @snippets.any?
.page-title-controls
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index f0216253dad..95e772f324b 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -7,7 +7,7 @@
= render_dashboard_ultimate_trial(current_user)
.page-title-holder.d-flex.align-items-center
- %h1.page-title= _('Issues')
+ %h1.page-title.gl-font-size-h-display= _('Issues')
- if current_user
.page-title-controls
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index 90fb09ed909..8a639d08a27 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -5,7 +5,7 @@
= render_dashboard_ultimate_trial(current_user)
.page-title-holder.d-flex.align-items-start.flex-column.flex-sm-row.align-items-sm-center
- %h1.page-title= _('Merge requests')
+ %h1.page-title.gl-font-size-h-display= _('Merge requests')
- if current_user
.page-title-controls.ml-0.mb-3.ml-sm-auto.mb-sm-0
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index 2bbca851dcc..39fbd9bc097 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -4,7 +4,7 @@
- add_page_specific_style 'page_bundles/milestone'
.page-title-holder.d-flex.align-items-center
- %h1.page-title= _('Milestones')
+ %h1.page-title.gl-font-size-h-display= _('Milestones')
- if current_user
.page-title-controls
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 4f6ddf10984..6bfe18fd3b2 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -7,7 +7,7 @@
- add_page_specific_style 'page_bundles/todos'
.page-title-holder.d-flex.align-items-center
- %h1.page-title= _("To-Do List")
+ %h1.page-title.gl-font-size-h-display= _("To-Do List")
- if current_user.todos.any?
.top-area
diff --git a/app/views/devise/confirmations/new.html.haml b/app/views/devise/confirmations/new.html.haml
index 2ae950f3b0d..d3bd1d58d21 100644
--- a/app/views/devise/confirmations/new.html.haml
+++ b/app/views/devise/confirmations/new.html.haml
@@ -1,5 +1,5 @@
= render 'devise/shared/tab_single', tab_title: 'Resend confirmation instructions'
-.login-box
+.login-box.gl-p-5
.login-body
= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f|
.devise-errors
diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml
index 60c3df718a1..b6acb244384 100644
--- a/app/views/devise/registrations/new.html.haml
+++ b/app/views/devise/registrations/new.html.haml
@@ -1,4 +1,5 @@
- page_title _("Sign up")
+- page_description _("Join GitLab today! You and your team can plan, build, and ship secure code all in one application. Get started here for free!")
- add_page_specific_style 'page_bundles/signup'
- content_for :page_specific_javascripts do
= render "layouts/google_tag_manager_head"
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 970e490dd72..57135c6cdfc 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -49,6 +49,7 @@
data: { qa_selector: 'new_user_email_field' },
required: true,
title: _('Please provide a valid email address.')
+ %p.gl-field-hint.text-secondary= _('We recommend a work email address.')
.form-group.gl-mb-5#password-strength
= f.label :password, class: 'label-bold'
= f.password_field :password,
diff --git a/app/views/devise/shared/_signup_omniauth_provider_list.haml b/app/views/devise/shared/_signup_omniauth_provider_list.haml
index 6688308cd71..84aabbe0efd 100644
--- a/app/views/devise/shared/_signup_omniauth_provider_list.haml
+++ b/app/views/devise/shared/_signup_omniauth_provider_list.haml
@@ -1,8 +1,10 @@
+- register_omniauth_params = Feature.enabled?(:update_oauth_registration_flow) ? { intent: :register } : {}
+
%label.gl-font-weight-bold
= _("Create an account using:")
.gl-display-flex.gl-justify-content-between.gl-flex-wrap
- providers.each do |provider|
- = link_to omniauth_authorize_path(:user, provider), method: :post, class: "btn gl-button btn-default gl-w-full gl-mb-3 js-oauth-login #{qa_class_for_provider(provider)}", data: { provider: provider }, id: "oauth-login-#{provider}" do
+ = link_to omniauth_authorize_path(:user, provider, register_omniauth_params), method: :post, class: "btn gl-button btn-default gl-w-full gl-mb-3 js-oauth-login #{qa_class_for_provider(provider)}", data: { provider: provider }, id: "oauth-login-#{provider}" do
- if provider_has_icon?(provider)
= provider_image_tag(provider)
%span.gl-button-text
diff --git a/app/views/doorkeeper/applications/edit.html.haml b/app/views/doorkeeper/applications/edit.html.haml
index 99e6a5eca19..c48e2cd4db0 100644
--- a/app/views/doorkeeper/applications/edit.html.haml
+++ b/app/views/doorkeeper/applications/edit.html.haml
@@ -1,5 +1,5 @@
- page_title _("Edit"), @application.name, _("Applications")
- @content_class = "limit-container-width" unless fluid_layout
-%h3.page-title= _('Edit application')
+%h1.page-title.gl-font-size-h-display= _('Edit application')
= render 'shared/doorkeeper/applications/form', url: doorkeeper_submit_path(@application)
diff --git a/app/views/doorkeeper/applications/new.html.haml b/app/views/doorkeeper/applications/new.html.haml
index a66fab20d7c..884d33b9a10 100644
--- a/app/views/doorkeeper/applications/new.html.haml
+++ b/app/views/doorkeeper/applications/new.html.haml
@@ -1,6 +1,6 @@
- page_title _("New Application")
-%h3.page-title= _("New Application")
+%h1.page-title.gl-font-size-h-display= _("New Application")
%hr
diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml
index 3a568421ce9..0428b9c340c 100644
--- a/app/views/doorkeeper/applications/show.html.haml
+++ b/app/views/doorkeeper/applications/show.html.haml
@@ -3,7 +3,7 @@
- page_title @application.name, _("Applications")
- @content_class = "limit-container-width" unless fluid_layout
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _("Application: %{name}") % { name: @application.name }
= render 'shared/doorkeeper/applications/show',
diff --git a/app/views/doorkeeper/authorizations/error.html.haml b/app/views/doorkeeper/authorizations/error.html.haml
index 32b4ccb0fe6..ecd011342c4 100644
--- a/app/views/doorkeeper/authorizations/error.html.haml
+++ b/app/views/doorkeeper/authorizations/error.html.haml
@@ -1,3 +1,3 @@
-%h3.page-title= _("An error has occurred")
+%h1.page-title.gl-font-size-h-display= _("An error has occurred")
%main{ :role => "main" }
%pre= @pre_auth.error_response.body[:error_description]
diff --git a/app/views/doorkeeper/authorizations/redirect.html.haml b/app/views/doorkeeper/authorizations/redirect.html.haml
index a9ac92fd087..b45db62dce1 100644
--- a/app/views/doorkeeper/authorizations/redirect.html.haml
+++ b/app/views/doorkeeper/authorizations/redirect.html.haml
@@ -1,4 +1,4 @@
-%h3.page-title= _("Redirecting")
+%h1.page-title.gl-font-size-h-display= _("Redirecting")
%div
%a{ :href => redirect_uri } Click here to redirect to #{redirect_uri}
diff --git a/app/views/doorkeeper/authorizations/show.html.haml b/app/views/doorkeeper/authorizations/show.html.haml
index e4bfd69e7f8..16d787c2f41 100644
--- a/app/views/doorkeeper/authorizations/show.html.haml
+++ b/app/views/doorkeeper/authorizations/show.html.haml
@@ -1,3 +1,3 @@
-%h3.page-title= _("Authorization code:")
+%h1.page-title.gl-font-size-h-display= _("Authorization code:")
%main{ :role => "main" }
%code#authorization_code= params[:code]
diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml
index e368ee1a75a..56c26f93402 100644
--- a/app/views/errors/access_denied.html.haml
+++ b/app/views/errors/access_denied.html.haml
@@ -11,6 +11,6 @@
%p
= s_('403|Please contact your GitLab administrator to get permission.')
.action-container.js-go-back{ hidden: true }
- %button{ type: 'button', class: 'gl-button btn btn-success' }
+ = render Pajamas::ButtonComponent.new(variant: :confirm) do
= _('Go Back')
= render "errors/footer"
diff --git a/app/views/explore/topics/_head.html.haml b/app/views/explore/topics/_head.html.haml
index f5ee95b16c3..f7d80d63c45 100644
--- a/app/views/explore/topics/_head.html.haml
+++ b/app/views/explore/topics/_head.html.haml
@@ -1,9 +1,10 @@
.page-title-holder.d-flex.align-items-center
- %h1.page-title= _('Projects')
+ %h1.page-title.gl-font-size-h-display= _('Projects')
-.top-area.scrolling-tabs-container.inner-page-scroll-tabs
- .fade-left= sprite_icon('chevron-lg-left', size: 12)
- .fade-right= sprite_icon('chevron-lg-right', size: 12)
- = render 'dashboard/projects_nav'
+.top-area
+ .scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full
+ .fade-left= sprite_icon('chevron-lg-left', size: 12)
+ .fade-right= sprite_icon('chevron-lg-right', size: 12)
+ = render 'dashboard/projects_nav'
.nav-controls
= render 'shared/topics/search_form'
diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml
index 8f50d499605..45561031083 100644
--- a/app/views/groups/_create_chat_team.html.haml
+++ b/app/views/groups/_create_chat_team.html.haml
@@ -1,3 +1,6 @@
+- bind_out_tag = content_tag(:span, nil, { data: { bind_out: :create_chat_team } })
+- checkbox_help_text = "%s %s/%s".html_safe % [_('Mattermost URL:'), Settings.mattermost.host, bind_out_tag]
+
.form-group
.col-sm-2.col-form-label
= f.label :create_chat_team do
@@ -5,13 +8,5 @@
= custom_icon('icon_mattermost')
%span.gl-ml-2= _('Mattermost')
.col-sm-12
- .form-check.js-toggle-container
- .js-toggle-button.form-check-input= f.check_box(:create_chat_team, { checked: false }, true, false)
- = f.label :create_chat_team, class: 'form-check-label' do
- = _('Create a Mattermost team for this group')
- %br
- %small.light.js-toggle-content
- = _('Mattermost URL:')
- = Settings.mattermost.host
- %span> /
- %span{ "data-bind-out" => "create_chat_team" }
+ = f.gitlab_ui_checkbox_component :create_chat_team, _('Create a Mattermost team for this group'), help_text: checkbox_help_text, checkbox_options: { checked: false }, checked_value: true, unchecked_value: false
+
diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml
index 785ca71b371..0a170ebdb24 100644
--- a/app/views/groups/_group_admin_settings.html.haml
+++ b/app/views/groups/_group_admin_settings.html.haml
@@ -2,12 +2,12 @@
.col-sm-2.col-form-label.pt-0
= f.label :lfs_enabled, _('Large File Storage')
.col-sm-10
- - label = _('Allow projects within this group to use Git LFS')
- - help_link = link_to sprite_icon('question-o'), help_page_path('topics/git/lfs/index'), class: 'gl-ml-2'
- = f.gitlab_ui_checkbox_component :lfs_enabled,
- '%{label}%{help_link}'.html_safe % { label: label, help_link: help_link },
- help_text: _('This setting can be overridden in each project.'),
- checkbox_options: { checked: @group.lfs_enabled? }
+ = f.gitlab_ui_checkbox_component :lfs_enabled, checkbox_options: { checked: @group.lfs_enabled? } do |c|
+ = c.label do
+ = _('Allow projects within this group to use Git LFS')
+ = link_to sprite_icon('question-o'), help_page_path('topics/git/lfs/index'), class: 'gl-ml-2'
+ = c.help_text do
+ = _('This setting can be overridden in each project.')
.form-group.row
.col-sm-2.col-form-label
= f.label s_('ProjectCreationLevel|Allowed to create projects')
diff --git a/app/views/groups/_invite_members_side_nav_link.html.haml b/app/views/groups/_invite_members_side_nav_link.html.haml
index 3046669b53b..978ef01984c 100644
--- a/app/views/groups/_invite_members_side_nav_link.html.haml
+++ b/app/views/groups/_invite_members_side_nav_link.html.haml
@@ -1,7 +1,8 @@
.js-invite-members-trigger{ data: { trigger_source: 'group-side-nav',
icon: 'users',
display_text: title,
- trigger_element: 'side-nav'} }
+ trigger_element: 'side-nav',
+ qa_selector: 'invite_members_sidebar_button' } }
= render partial: 'shared/nav/sidebar_submenu', locals: { sidebar_menu: sidebar_menu }
= render 'groups/invite_members_modal', group: group
diff --git a/app/views/groups/_new_group_fields.html.haml b/app/views/groups/_new_group_fields.html.haml
index fe2ee62d9be..83211505f36 100644
--- a/app/views/groups/_new_group_fields.html.haml
+++ b/app/views/groups/_new_group_fields.html.haml
@@ -1,4 +1,4 @@
-= form_errors(@group)
+= form_errors(@group, pajamas_alert: true)
= render 'shared/group_form', f: f, autofocus: true
.row
diff --git a/app/views/groups/_personalize.html.haml b/app/views/groups/_personalize.html.haml
index 07b3b29c20c..bae76952ef8 100644
--- a/app/views/groups/_personalize.html.haml
+++ b/app/views/groups/_personalize.html.haml
@@ -15,11 +15,9 @@
= f.label :setup_for_company, _('Who will be using this group?')
.gl-display-flex.gl-flex-direction-column.gl-lg-flex-direction-row
.gl-flex-grow-1.gl-display-flex.gl-align-items-center
- = f.radio_button :setup_for_company, true
- = f.label :setup_for_company, _('My company or team'), class: 'gl-font-weight-normal gl-mb-0 gl-ml-2', value: 'true'
+ = f.gitlab_ui_radio_component :setup_for_company, true, _('My company or team')
.gl-flex-grow-1.gl-display-flex.gl-align-items-center
- = f.radio_button :setup_for_company, false
- = f.label :setup_for_company, _('Just me'), class: 'gl-font-weight-normal gl-mb-0 gl-ml-2', value: 'false'
+ = f.gitlab_ui_radio_component :setup_for_company, false, _('Just me')
.row
.form-group.col-sm-4
diff --git a/app/views/groups/_subgroups_and_projects.html.haml b/app/views/groups/_subgroups_and_projects.html.haml
index 427a36aaec4..dc749af3c0c 100644
--- a/app/views/groups/_subgroups_and_projects.html.haml
+++ b/app/views/groups/_subgroups_and_projects.html.haml
@@ -1,7 +1,4 @@
#js-groups-subgroups_and_projects-tree
- .empty-state.hidden
- = render "shared/groups/empty_state"
-
%section{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } }
- .js-groups-list-holder{ data: { show_schema_markup: 'true'} }
+ .js-groups-list-holder{ data: subgroups_and_projects_list_app_data(group) }
= gl_loading_icon(size: 'md', css_class: 'gl-mt-6')
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 3dcc75ce8f4..bca1c874cc6 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -4,13 +4,12 @@
- expanded = expanded_by_default?
= render 'shared/namespaces/cascading_settings/lock_popovers'
-= render_if_exists 'shared/minute_limit_banner', namespace: @group
%section.settings.gs-general.no-animate.expanded#js-general-settings
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' }
= _('Naming, visibility')
- %button.btn.gl-button.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= _('Collapse')
%p
= _('Update your group name, description, avatar, and visibility.')
@@ -22,7 +21,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' }
= _('Permissions and group features')
- %button.btn.gl-button.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= _('Configure advanced permissions, Large File Storage, two-factor authentication, and customer relations settings.')
@@ -36,7 +35,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' }
= s_('GroupSettings|Badges')
- %button.btn.gl-button.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= s_('GroupSettings|Customize this group\'s badges.')
@@ -52,7 +51,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' }
= _('Advanced')
- %button.btn.gl-button.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= _('Perform advanced options such as changing path, transferring, exporting, or removing the group.')
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 9aa626724a8..635a74d8179 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,7 +1,7 @@
- add_page_specific_style 'page_bundles/members'
- page_title _('Group members')
-= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @group
+= render_if_exists 'shared/free_user_cap_alert', source: @group
.row.gl-mt-3
.col-lg-12
@@ -11,7 +11,7 @@
%h4
= _('Group members')
%p
- = html_escape(_('You can invite a new member to %{strong_start}%{group_name}%{strong_end}.')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ = group_member_header_subtext(@group)
.gl-w-half.gl-xs-w-full
.gl-display-flex.gl-flex-wrap.gl-justify-content-end.gl-mb-3
.js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite a group') } }
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 209faa937dc..925e7d46f14 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -1,28 +1,8 @@
-- @can_bulk_update = can?(current_user, :admin_issue, @group) && @group.licensed_feature_available?(:group_bulk_edit)
-
-- page_title _("Issues")
+- page_title _('Issues')
- add_page_specific_style 'page_bundles/issues_list'
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@group.name} issues")
-- if Feature.enabled?(:vue_issues_list, @group)
- .js-issues-list{ data: group_issues_list_data(@group, current_user) }
- - if @can_bulk_update
- = render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :issues
-- else
- .top-area
- = render 'shared/issuable/nav', type: :issues
- .nav-controls
- = render 'shared/issuable/feed_buttons'
-
- - if @can_bulk_update
- = render_if_exists 'shared/issuable/bulk_update_button', type: :issues
-
- = render 'shared/new_project_item_select', path: 'issues/new', label: _("issue"), type: :issues, with_feature_enabled: 'issues', with_shared: false, include_projects_in_subgroups: true
-
- = render 'shared/issuable/search_bar', type: :issues
-
- - if @can_bulk_update
- = render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :issues
-
- = render 'shared/issues', project_select_button: true
+.js-issues-list{ data: group_issues_list_data(@group, current_user) }
+- if can?(current_user, :admin_issue, @group) && @group.licensed_feature_available?(:group_bulk_edit)
+ = render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :issues
diff --git a/app/views/groups/labels/edit.html.haml b/app/views/groups/labels/edit.html.haml
index d9b8f99ea0c..9af842b01df 100644
--- a/app/views/groups/labels/edit.html.haml
+++ b/app/views/groups/labels/edit.html.haml
@@ -2,8 +2,7 @@
- breadcrumb_title _("Edit")
- page_title _("Edit"), @label.name, _("Labels")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('Edit Label')
-%hr
= render 'shared/labels/form', url: group_label_path(@group, @label), back_path: @previous_labels_path
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index c480123dad1..8187dda5471 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -8,13 +8,13 @@
#js-promote-label-modal
= render 'shared/labels/nav', labels_or_filters: labels_or_filters, can_admin_label: can_admin_label
- .labels-container.gl-mt-2.gl-bg-gray-10.gl-border-solid.gl-border-1.gl-border-gray-100
+ .labels-container.gl-mt-5
- if @labels.any?
- .text-muted
+ .text-muted.gl-mb-5
= _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuable_types.to_sentence }
.other-labels
- %h5= _('Labels')
- %ul.content-list.manage-labels-list.js-other-labels
+ %h4= _('Labels')
+ %ul.manage-labels-list.js-other-labels
= render partial: 'shared/label', collection: @labels, as: :label, locals: { use_label_priority: false, subject: @group }
= paginate @labels, theme: 'gitlab'
- elsif search.present?
diff --git a/app/views/groups/labels/new.html.haml b/app/views/groups/labels/new.html.haml
index 75b4ad5c795..fd9aae987d4 100644
--- a/app/views/groups/labels/new.html.haml
+++ b/app/views/groups/labels/new.html.haml
@@ -2,8 +2,7 @@
- breadcrumb_title _("New")
- page_title _("New Label")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('New Label')
-%hr
= render 'shared/labels/form', url: group_labels_path, back_path: @previous_labels_path
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index 011c76e5ae7..b33d1443706 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -10,7 +10,7 @@
- if current_user
.nav-controls
- if @can_bulk_update
- = render_if_exists 'shared/issuable/bulk_update_button', type: :merge_requests
+ = render_if_exists 'projects/merge_requests/bulk_update_button'
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: _("merge request"), type: :merge_requests, with_feature_enabled: 'merge_requests', with_shared: false, include_projects_in_subgroups: true
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index a6302569187..7da5a9e9664 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -1,5 +1,5 @@
= form_for [@group, @milestone], html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
- = form_errors(@milestone)
+ = form_errors(@milestone, pajamas_alert: true)
.form-group.row
.col-form-label.col-sm-2
= f.label :title, _("Title")
diff --git a/app/views/groups/milestones/edit.html.haml b/app/views/groups/milestones/edit.html.haml
index 187c2d24b56..931d73715d3 100644
--- a/app/views/groups/milestones/edit.html.haml
+++ b/app/views/groups/milestones/edit.html.haml
@@ -3,7 +3,7 @@
- render "header_title"
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('Edit Milestone')
%hr
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index 0d4565706d4..8bceb1ddd5c 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -2,7 +2,7 @@
- breadcrumb_title _("New")
- page_title _("Milestones"), @milestone.name, _("Milestones")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _("New Milestone")
%hr
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 58a78a8adc1..3fb2b88dadd 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -10,7 +10,7 @@
.row{ 'v-cloak': true }
#create-group-pane.tab-pane
- = form_for @group, html: { class: 'group-form gl-show-field-errors gl-mt-3' } do |f|
+ = gitlab_ui_form_for @group, html: { class: 'group-form gl-show-field-errors gl-mt-3' } do |f|
= render 'new_group_fields', f: f, group_name_id: 'create-group-name'
#import-group-pane.tab-pane
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index 3507f4574ab..d5c22d9b1f2 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -1,5 +1,6 @@
- breadcrumb_title _("Projects")
- page_title _("Projects")
+- @content_class = "limit-container-width" unless fluid_layout
.card.gl-mt-3
.card-header
diff --git a/app/views/groups/runners/_settings.html.haml b/app/views/groups/runners/_settings.html.haml
index 7716a2f125f..3e5ec3c26e2 100644
--- a/app/views/groups/runners/_settings.html.haml
+++ b/app/views/groups/runners/_settings.html.haml
@@ -1,14 +1,12 @@
-.gl-mb-6
+.gl-mb-5
#update-shared-runners-form{ data: group_shared_runners_settings_data(@group) }
-.gl-card.gl-px-8.gl-py-6.gl-line-height-20
- .gl-card-body.gl-display-flex{ :class => "gl-p-0!" }
- .gl-banner-illustration
- = image_tag('illustrations/rocket-launch-md.svg', alt: s_('Runners|Rocket launch illustration'))
- .gl-banner-content
- %h1.gl-banner-title
- = s_('Runners|New group runners view')
- %p
- = s_('Runners|The new view gives you more space and better visibility into your fleet of runners.')
- %a.btn.btn-confirm.btn-md.gl-button{ :href => group_runners_path(@group) }
- %span.gl-button-text
- = s_('Runners|Take me there!')
+- if @group.licensed_feature_available?(:stale_runner_cleanup_for_namespace)
+ .gl-mb-5
+ #stale-runner-cleanup-form{ data: { group_full_path: @group.full_path, stale_timeout_secs: ::Ci::Runner::STALE_TIMEOUT.to_i } }
+= render Pajamas::BannerComponent.new(button_text: s_('Runners|Take me there!'),
+ button_link: group_runners_path(@group),
+ svg_path: 'illustrations/rocket-launch-md.svg',
+ close_options: { class: 'gl-display-none' }) do |c|
+ - c.title do
+ = s_('Runners|New group runners view')
+ %p= s_('Runners|The new view gives you more space and better visibility into your fleet of runners.')
diff --git a/app/views/groups/runners/edit.html.haml b/app/views/groups/runners/edit.html.haml
index 04b9d88723f..c5999317597 100644
--- a/app/views/groups/runners/edit.html.haml
+++ b/app/views/groups/runners/edit.html.haml
@@ -5,7 +5,7 @@
- add_to_breadcrumbs "#{@runner.short_sha}", group_runner_path(@group, @runner)
-%h2.page-title
+%h1.page-title.gl-font-size-h-display
= s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id })
= render 'shared/runners/runner_type_badge', runner: @runner
diff --git a/app/views/groups/runners/show.html.haml b/app/views/groups/runners/show.html.haml
index b6c0c8a707f..5a9d2ca858e 100644
--- a/app/views/groups/runners/show.html.haml
+++ b/app/views/groups/runners/show.html.haml
@@ -1,3 +1,6 @@
- add_to_breadcrumbs _('Runners'), group_runners_path(@group)
-= render 'shared/runners/runner_details', runner: @runner
+- if Feature.enabled?(:group_runner_view_ui)
+ #js-group-runner-show{ data: {runner_id: @runner.id, runners_path: group_runners_path(@group)} }
+- else
+ = render 'shared/runners/runner_details', runner: @runner
diff --git a/app/views/groups/settings/_advanced.html.haml b/app/views/groups/settings/_advanced.html.haml
index ebeec2ee95a..3624ff2bcb3 100644
--- a/app/views/groups/settings/_advanced.html.haml
+++ b/app/views/groups/settings/_advanced.html.haml
@@ -4,7 +4,7 @@
.sub-section
%h4.warning-title= s_('GroupSettings|Change group URL')
= form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f|
- = form_errors(@group)
+ = form_errors(@group, pajamas_alert: true)
.form-group
%p
= s_("GroupSettings|Changing a group's URL can have unintended side effects.")
@@ -23,7 +23,7 @@
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'
+ = f.submit s_('GroupSettings|Change group URL'), class: 'btn gl-button btn-danger'
= render 'groups/settings/transfer', group: @group
= render 'groups/settings/remove', group: @group, remove_form_id: remove_form_id
diff --git a/app/views/groups/settings/_export.html.haml b/app/views/groups/settings/_export.html.haml
index 6cae416311e..66c1341fb15 100644
--- a/app/views/groups/settings/_export.html.haml
+++ b/app/views/groups/settings/_export.html.haml
@@ -3,7 +3,7 @@
.sub-section
%h4= s_('GroupSettings|Export group')
%p= _('Export this group with all related data.')
- = render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_class: 'gl-mb-4') do |c|
+ = render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_options: { class: 'gl-mb-4' }) do |c|
= c.body do
- docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index.md') }
- docs_link_end = '</a>'.html_safe
@@ -12,7 +12,7 @@
- export_information = _('After the export is complete, download the data file from a notification email or from this page. You can then import the data file from the %{strong_text_start}Create new group%{strong_text_end} page of another GitLab instance.') % { strong_text_start: '<strong>'.html_safe, strong_text_end: '</strong>'.html_safe}
= export_information.html_safe
= link_to _('Learn more.'), help_page_path('user/group/settings/import_export.md'), target: '_blank', rel: 'noopener noreferrer'
- = render Pajamas::AlertComponent.new(dismissible: false, alert_class: 'gl-mb-5') do |c|
+ = render Pajamas::AlertComponent.new(dismissible: false, alert_options: { class: 'gl-mb-5' }) do |c|
= c.body do
%p.gl-mb-0
%p= _('The following items will be exported:')
diff --git a/app/views/groups/settings/_git_access_protocols.html.haml b/app/views/groups/settings/_git_access_protocols.html.haml
new file mode 100644
index 00000000000..df798db79ad
--- /dev/null
+++ b/app/views/groups/settings/_git_access_protocols.html.haml
@@ -0,0 +1,7 @@
+- if group.root? && Feature.enabled?(:group_level_git_protocol_control, group)
+ .form-group
+ = f.label s_('Enabled Git access protocols'), class: 'label-bold'
+ = f.select :enabled_git_access_protocol, options_for_select(enabled_git_access_protocol_options_for_group, group.enabled_git_access_protocol), {}, class: 'form-control', data: { qa_selector: 'enabled_git_access_protocol_dropdown' }, disabled: !::Gitlab::CurrentSettings.enabled_git_access_protocol.blank?
+ - if !::Gitlab::CurrentSettings.enabled_git_access_protocol.blank?
+ .form-text.text-muted
+ = _("This setting has been configured at the instance level and cannot be overridden per group")
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index ecb31b37fd3..319af7be22e 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -37,6 +37,7 @@
- if @group.licensed_feature_available?(:group_wikis)
= render_if_exists 'groups/settings/wiki', f: f, group: @group
= render 'groups/settings/lfs', f: f
+ = render 'groups/settings/git_access_protocols', f: f, group: @group
= render 'groups/settings/project_creation_level', f: f, group: @group
= render 'groups/settings/subgroup_creation_level', f: f, group: @group
= render_if_exists 'groups/settings/prevent_forking', f: f, group: @group
@@ -44,12 +45,11 @@
= render_if_exists 'groups/personal_access_token_expiration_policy', f: f, group: @group
= render 'groups/settings/membership', f: f, group: @group
- - if crm_feature_available?(@group)
- %h5= _('Customer relations')
- .form-group.gl-mb-3
- = f.gitlab_ui_checkbox_component :crm_enabled,
- s_('GroupSettings|Enable customer relations'),
- checkbox_options: { checked: @group.crm_enabled? },
- help_text: s_('GroupSettings|Allows creating organizations and contacts and associating them with issues.')
+ %h5= _('Customer relations')
+ .form-group.gl-mb-3
+ = f.gitlab_ui_checkbox_component :crm_enabled,
+ s_('GroupSettings|Enable customer relations'),
+ checkbox_options: { checked: @group.crm_enabled? },
+ help_text: s_('GroupSettings|Allows creating organizations and contacts and associating them with issues.')
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm gl-mt-3 js-dirty-submit', data: { qa_selector: 'save_permissions_changes_button' }
diff --git a/app/views/groups/settings/_remove_button.html.haml b/app/views/groups/settings/_remove_button.html.haml
index e765638953a..df978f3cb96 100644
--- a/app/views/groups/settings/_remove_button.html.haml
+++ b/app/views/groups/settings/_remove_button.html.haml
@@ -1,7 +1,7 @@
- remove_form_id = local_assigns.fetch(:remove_form_id, nil)
- if group.paid?
- = render Pajamas::AlertComponent.new(dismissible: false, alert_class: 'gl-mb-5', alert_data: { testid: 'group-has-linked-subscription-alert' }) do |c|
+ = render Pajamas::AlertComponent.new(dismissible: false, alert_options: { class: 'gl-mb-5', data: { testid: 'group-has-linked-subscription-alert' }}) do |c|
= c.body do
= html_escape(_("This group can't be removed because it is linked to a subscription. To remove this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }
diff --git a/app/views/groups/settings/_transfer.html.haml b/app/views/groups/settings/_transfer.html.haml
index e65c3cd13f6..e6c88977cb1 100644
--- a/app/views/groups/settings/_transfer.html.haml
+++ b/app/views/groups/settings/_transfer.html.haml
@@ -13,7 +13,7 @@
%li= s_('GroupSettings|You will need to update your local repositories to point to the new location.')
%li= s_("GroupSettings|If the parent group's visibility is lower than the group's current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.")
- if group.paid?
- = render Pajamas::AlertComponent.new(dismissible: false, alert_class: 'gl-mb-5') do |c|
+ = render Pajamas::AlertComponent.new(dismissible: false, alert_options: { class: 'gl-mb-5' }) do |c|
= c.body do
= html_escape(_("This group can't be transferred because it is linked to a subscription. To transfer this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }
.js-transfer-group-form{ data: initial_data }
diff --git a/app/views/groups/settings/applications/edit.html.haml b/app/views/groups/settings/applications/edit.html.haml
index cba4892eef9..ee71fd5d886 100644
--- a/app/views/groups/settings/applications/edit.html.haml
+++ b/app/views/groups/settings/applications/edit.html.haml
@@ -1,5 +1,5 @@
- page_title _("Edit"), @application.name, _("Group applications")
- @content_class = "limit-container-width" unless fluid_layout
-%h3.page-title= _('Edit group application')
+%h1.page-title.gl-font-size-h-display= _('Edit group application')
= render 'shared/doorkeeper/applications/form', url: group_settings_application_path(@group, @application)
diff --git a/app/views/groups/settings/applications/show.html.haml b/app/views/groups/settings/applications/show.html.haml
index 6e7f6ce4df0..4a83d96aae4 100644
--- a/app/views/groups/settings/applications/show.html.haml
+++ b/app/views/groups/settings/applications/show.html.haml
@@ -3,7 +3,7 @@
- page_title @application.name, _("Group applications")
- @content_class = "limit-container-width" unless fluid_layout
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _("Group application: %{name}") % { name: @application.name }
= render 'shared/doorkeeper/applications/show',
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index f6dda9358f3..3b117022d1e 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -3,7 +3,6 @@
- expanded = expanded_by_default?
- general_expanded = @group.errors.empty? ? expanded : true
-= render_if_exists 'shared/minute_limit_banner', namespace: @group
-# Given we only have one field in this form which is also admin-only,
-# we don't want to show an empty section to non-admin users,
@@ -12,7 +11,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("General pipelines")
- %button.btn.gl-button.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= _("Customize your pipeline configuration.")
@@ -29,7 +28,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Runners')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: "button" }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
@@ -41,7 +40,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Auto DevOps')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: "button" }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
- auto_devops_url = help_page_path('topics/autodevops/index')
@@ -52,3 +51,5 @@
.settings-content
= render 'groups/settings/ci_cd/auto_devops_form', group: @group
+
+= render_if_exists 'groups/settings/ci_cd/protected_environments', expanded: expanded
diff --git a/app/views/groups/settings/repository/_default_branch.html.haml b/app/views/groups/settings/repository/_default_branch.html.haml
index f2644465a49..844a5f890a4 100644
--- a/app/views/groups/settings/repository/_default_branch.html.haml
+++ b/app/views/groups/settings/repository/_default_branch.html.haml
@@ -2,7 +2,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Default branch')
- %button.gl-button.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= s_('GroupSettings|Set the initial name and protections for the default branch of new repositories created in the group.')
diff --git a/app/views/groups/settings/repository/show.html.haml b/app/views/groups/settings/repository/show.html.haml
index 072c8c4d821..d3b9117c05b 100644
--- a/app/views/groups/settings/repository/show.html.haml
+++ b/app/views/groups/settings/repository/show.html.haml
@@ -1,5 +1,6 @@
- breadcrumb_title _('Repository Settings')
- page_title _('Repository')
+- @content_class = "limit-container-width" unless fluid_layout
- deploy_token_description = s_('DeployTokens|Group deploy tokens allow access to the packages, repositories, and registry images within the group.')
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 7bbc2f839f7..3614d854036 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -7,8 +7,7 @@
= render_if_exists 'shared/thanks_for_purchase_banner', plan_title: plan_title, quantity: params[:purchased_quantity].to_i
= render_if_exists 'shared/qrtly_reconciliation_alert', group: @group
-= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @group
-= render_if_exists 'shared/minute_limit_banner', namespace: @group
+= render_if_exists 'shared/free_user_cap_alert', source: @group
- if show_invite_banner?(@group)
= content_for :group_invite_members_banner do
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index 8946ab898e0..e69ca4663b4 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -1,7 +1,7 @@
- page_title _('Bitbucket import')
- header_title _('Projects'), root_path
-%h3.page-title.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
= sprite_icon('bitbucket', css_class: 'gl-mr-2')
= _('Import projects from Bitbucket')
diff --git a/app/views/import/bitbucket_server/new.html.haml b/app/views/import/bitbucket_server/new.html.haml
index 721447186a6..0d87cf66814 100644
--- a/app/views/import/bitbucket_server/new.html.haml
+++ b/app/views/import/bitbucket_server/new.html.haml
@@ -2,7 +2,7 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h3.page-title.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
= sprite_icon('bitbucket', css_class: 'gl-mr-2')
= _('Import repositories from Bitbucket Server')
diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml
index 79b2810e06d..05b42767668 100644
--- a/app/views/import/bitbucket_server/status.html.haml
+++ b/app/views/import/bitbucket_server/status.html.haml
@@ -1,6 +1,6 @@
- page_title _('Bitbucket Server import')
-%h3.page-title.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
= sprite_icon('bitbucket', css_class: 'gl-mr-2')
= _('Import projects from Bitbucket Server')
diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml
index d716d08529c..b74262f2567 100644
--- a/app/views/import/fogbugz/new.html.haml
+++ b/app/views/import/fogbugz/new.html.haml
@@ -2,7 +2,7 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h3.page-title.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
= sprite_icon('bug', css_class: 'gl-mr-2')
= _('Import projects from FogBugz')
diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml
index 93572e14a65..5caee78b9c4 100644
--- a/app/views/import/fogbugz/new_user_map.html.haml
+++ b/app/views/import/fogbugz/new_user_map.html.haml
@@ -2,7 +2,7 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h3.page-title.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
= sprite_icon('bug', css_class: 'gl-mr-2')
= _('Import projects from FogBugz')
diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml
index dcc0e94441c..3e303d3163d 100644
--- a/app/views/import/fogbugz/status.html.haml
+++ b/app/views/import/fogbugz/status.html.haml
@@ -1,5 +1,5 @@
- page_title _("FogBugz import")
-%h3.page-title.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
= sprite_icon('bug', css_class: 'gl-mr-2')
= _('Import projects from FogBugz')
diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml
index de717ce87eb..4a293bb6f4e 100644
--- a/app/views/import/gitea/new.html.haml
+++ b/app/views/import/gitea/new.html.haml
@@ -2,7 +2,7 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= custom_icon('gitea_logo')
= _('Import Projects from Gitea')
@@ -11,6 +11,7 @@
= _('To get started, please enter your Gitea Host URL and a %{link_to_personal_token}.').html_safe % { link_to_personal_token: link_to_personal_token }
= form_tag personal_access_token_import_gitea_path do
+ = hidden_field_tag(:namespace_id, params[:namespace_id])
.form-group.row
= label_tag :gitea_host_url, _('Gitea Host URL'), class: 'col-form-label col-sm-2'
.col-sm-4
diff --git a/app/views/import/gitea/status.html.haml b/app/views/import/gitea/status.html.haml
index 1bdcec0c574..c717d4848f4 100644
--- a/app/views/import/gitea/status.html.haml
+++ b/app/views/import/gitea/status.html.haml
@@ -1,6 +1,6 @@
- page_title _("Gitea Import")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= custom_icon('gitea_logo')
= _('Import Projects from Gitea')
-= render 'import/githubish_status', provider: 'gitea'
+= render 'import/githubish_status', provider: 'gitea', default_namespace: @namespace
diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml
index fbb27ba620a..7d0a46f3630 100644
--- a/app/views/import/github/new.html.haml
+++ b/app/views/import/github/new.html.haml
@@ -3,7 +3,7 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= title
%p
@@ -25,11 +25,11 @@
%label.label-bold= _('Personal Access Token')
= hidden_field_tag(:namespace_id, params[:namespace_id])
= text_field_tag :personal_access_token, '', class: 'form-control gl-form-input', placeholder: _('e.g. %{token}') % { token: '8d3f016698e...' }, data: { qa_selector: 'personal_access_token_field' }
- %span.form-text.text-muted
+ %span.form-text.gl-text-gray-600
= import_github_personal_access_token_message
= render_if_exists 'import/github/ci_cd_only'
- .form-actions.d-flex.justify-content-end
+ .form-actions.gl-display-flex.gl-justify-content-end
= link_to _('Cancel'), new_project_path, class: 'gl-button btn btn-default'
- = submit_tag _('Authenticate'), class: 'gl-button btn btn-confirm ml-2', data: { qa_selector: 'authenticate_button' }
+ = submit_tag _('Authenticate'), class: 'gl-button btn btn-confirm gl-ml-3', data: { qa_selector: 'authenticate_button' }
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 26b048c8195..1b556cd0f7f 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -1,6 +1,6 @@
- title = has_ci_cd_only_params? ? _('Connect repositories from GitHub') : _('GitHub import')
- page_title title
-%h3.page-title.mb-0.gl-display-flex
+%h1.page-title.gl-font-size-h-display.mb-0.gl-display-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
= sprite_icon('github', css_class: 'gl-mr-2')
= _('Import repositories from GitHub')
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index b7b1fae1b73..13aaa41de9b 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -1,5 +1,5 @@
- page_title _("GitLab.com import")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= sprite_icon('heart', css_class: 'gl-vertical-align-middle')
= _('Import projects from GitLab.com')
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index 533d0d13be3..42a9d2c3136 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -2,7 +2,7 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h3.page-title.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
= sprite_icon('tanuki', css_class: 'gl-mr-2')
= _('Import an exported GitLab project')
diff --git a/app/views/import/manifest/_form.html.haml b/app/views/import/manifest/_form.html.haml
index 365184537cc..096d2543502 100644
--- a/app/views/import/manifest/_form.html.haml
+++ b/app/views/import/manifest/_form.html.haml
@@ -6,7 +6,7 @@
.input-group-prepend.has-tooltip{ title: root_url }
.input-group-text
= root_url
- = select_tag :group_id, namespaces_options(nil, display_path: true, groups_only: true), { class: 'select2 js-select-namespace' }
+ = select_tag :group_id, namespaces_options(params[:namespace_id], display_path: true, groups_only: true), { class: 'select2 js-select-namespace' }
.form-text.text-muted
= _('Choose the top-level group for your repository imports.')
diff --git a/app/views/import/manifest/new.html.haml b/app/views/import/manifest/new.html.haml
index a949e14e273..3d33e229f8f 100644
--- a/app/views/import/manifest/new.html.haml
+++ b/app/views/import/manifest/new.html.haml
@@ -3,7 +3,7 @@
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('Manifest file import')
= render 'import/shared/errors'
diff --git a/app/views/import/manifest/status.html.haml b/app/views/import/manifest/status.html.haml
index 45d03575713..ced507de271 100644
--- a/app/views/import/manifest/status.html.haml
+++ b/app/views/import/manifest/status.html.haml
@@ -1,6 +1,6 @@
- page_title _("Manifest import")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('Manifest file import')
= render 'import/githubish_status', provider: 'manifest'
diff --git a/app/views/import/phabricator/new.html.haml b/app/views/import/phabricator/new.html.haml
index 0249dc446e4..d4fdd107043 100644
--- a/app/views/import/phabricator/new.html.haml
+++ b/app/views/import/phabricator/new.html.haml
@@ -2,7 +2,7 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h3.page-title.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
= sprite_icon('issues', css_class: 'gl-mr-2')
= _('Import tasks from Phabricator into issues')
diff --git a/app/views/import/shared/_errors.html.haml b/app/views/import/shared/_errors.html.haml
index ae54d0544f3..2dbb54a9a0e 100644
--- a/app/views/import/shared/_errors.html.haml
+++ b/app/views/import/shared/_errors.html.haml
@@ -1,7 +1,7 @@
- if @errors.present?
= render Pajamas::AlertComponent.new(variant: :danger,
dismissible: false,
- alert_class: 'gl-mb-5') do |c|
+ alert_options: { class: 'gl-mb-5' }) do |c|
= c.body do
- @errors.each do |error|
= error
diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml
index 3622fc46983..c1ee12bb6c8 100644
--- a/app/views/invites/show.html.haml
+++ b/app/views/invites/show.html.haml
@@ -1,5 +1,5 @@
- page_title _("Invitation")
-%h3.page-title= _("Invitation")
+%h1.page-title.gl-font-size-h-display= _("Invitation")
- if current_user_matches_invite?
- if member?
diff --git a/app/views/jira_connect/subscriptions/index.html.haml b/app/views/jira_connect/subscriptions/index.html.haml
index 3319137551b..d4ced15b869 100644
--- a/app/views/jira_connect/subscriptions/index.html.haml
+++ b/app/views/jira_connect/subscriptions/index.html.haml
@@ -1,8 +1,4 @@
-%header.jira-connect-header.gl-display-flex.gl-align-items-center.gl-justify-content-center.gl-px-5.gl-border-b-solid.gl-border-b-gray-100.gl-border-b-1.gl-bg-white
- = link_to brand_header_logo, Gitlab.config.gitlab.url, target: '_blank', rel: 'noopener noreferrer'
-
-%main.jira-connect-app.gl-px-5.gl-pt-7.gl-mx-auto
- .js-jira-connect-app{ data: jira_connect_app_data(@subscriptions) }
+.js-jira-connect-app{ data: jira_connect_app_data(@subscriptions) }
= webpack_bundle_tag 'performance_bar' if performance_bar_enabled?
= webpack_bundle_tag 'jira_connect_app'
diff --git a/app/views/kaminari/gitlab/_keyset_paginator.html.haml b/app/views/kaminari/gitlab/_keyset_paginator.html.haml
index f64c70dadfc..aed8b912fc6 100644
--- a/app/views/kaminari/gitlab/_keyset_paginator.html.haml
+++ b/app/views/kaminari/gitlab/_keyset_paginator.html.haml
@@ -9,22 +9,22 @@
%li.page-item
- first_page_path = url_for(page_params.merge(cursor: paginator.cursor_for_first_page))
= link_to first_page_path, rel: 'first', class: 'page-link' do
- = sprite_icon('angle-double-left', size: 8)
+ = sprite_icon('chevron-double-lg-left', size: 8)
= s_('Pagination|First')
%li.page-item.prev
= link_to previous_path, rel: 'prev', class: 'page-link' do
- = sprite_icon('angle-left', size: 8)
+ = sprite_icon('chevron-lg-left', size: 8)
= s_('Pagination|Prev')
- if paginator.has_next_page?
%li.page-item.next
= link_to next_path, rel: 'next', class: 'page-link' do
= s_('Pagination|Next')
- = sprite_icon('angle-right', size: 8)
+ = sprite_icon('chevron-lg-right', size: 8)
- unless without_first_and_last_pages
%li.page-item
- last_page_path = url_for(page_params.merge(cursor: paginator.cursor_for_last_page))
= link_to last_page_path, rel: 'last', class: 'page-link' do
= s_('Pagination|Last')
- = sprite_icon('angle-double-right', size: 8)
+ = sprite_icon('chevron-double-lg-right', size: 8)
diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml
index 9572dd91330..3ddd9fe655f 100644
--- a/app/views/kaminari/gitlab/_next_page.html.haml
+++ b/app/views/kaminari/gitlab/_next_page.html.haml
@@ -11,4 +11,4 @@
%li.page-item.js-next-button{ class: ('disabled' if current_page.last?) }
= link_to page_url, rel: 'next', remote: remote, class: 'page-link' do
= s_('Pagination|Next')
- = sprite_icon('angle-right', size: 8)
+ = sprite_icon('chevron-lg-right', size: 8)
diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml
index 4ba7ab6488a..5fb11c975de 100644
--- a/app/views/kaminari/gitlab/_prev_page.html.haml
+++ b/app/views/kaminari/gitlab/_prev_page.html.haml
@@ -10,5 +10,5 @@
%li.page-item.js-previous-button{ class: ('disabled' if current_page.first?) }
= link_to page_url, rel: 'prev', remote: remote, class: 'page-link' do
- = sprite_icon('angle-left', size: 8)
+ = sprite_icon('chevron-lg-left', size: 8)
= s_('Pagination|Prev')
diff --git a/app/views/kaminari/gitlab/_without_count.html.haml b/app/views/kaminari/gitlab/_without_count.html.haml
index dc9dcbeed1d..8b29cb1988f 100644
--- a/app/views/kaminari/gitlab/_without_count.html.haml
+++ b/app/views/kaminari/gitlab/_without_count.html.haml
@@ -3,10 +3,10 @@
- if previous_path
%li.page-item.prev
= link_to previous_path, rel: 'prev', class: 'page-link' do
- = sprite_icon('angle-left', size: 8)
+ = sprite_icon('chevron-lg-left', size: 8)
= s_('Pagination|Prev')
- if next_path
%li.page-item.next
= link_to next_path, rel: 'next', class: 'page-link' do
= s_('Pagination|Next')
- = sprite_icon('angle-right', size: 8)
+ = sprite_icon('chevron-lg-right', size: 8)
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 55c66454d0b..84eb2706929 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -68,6 +68,7 @@
%meta{ name: "description", content: page_description }
+ %link{ rel: 'manifest', href: manifest_path(format: :json) }
%meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' }
%meta{ name: 'theme-color', content: user_theme_primary_color }
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 3c4b612f33f..b7cf7b7468f 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -8,6 +8,7 @@
= dispensable_render 'shared/outdated_browser'
= dispensable_render_if_exists "layouts/header/licensed_user_count_threshold"
= dispensable_render_if_exists "layouts/header/token_expiry_notification"
+ = dispensable_render "shared/projects/inactive_project_deletion_alert"
= dispensable_render "layouts/broadcast"
= dispensable_render "layouts/header/read_only_banner"
= dispensable_render "layouts/header/registration_enabled_callout"
@@ -15,12 +16,12 @@
= yield :flash_message
= dispensable_render "shared/service_ping_consent"
= dispensable_render_if_exists "layouts/header/ee_subscribable_banner"
- = dispensable_render_if_exists "layouts/header/seats_count_alert"
+ = dispensable_render_if_exists "layouts/header/seat_count_alert"
= dispensable_render_if_exists "shared/namespace_storage_limit_alert"
= dispensable_render_if_exists "shared/namespace_user_cap_reached_alert"
= dispensable_render_if_exists "shared/new_user_signups_cap_reached_alert"
= yield :page_level_alert
- = yield :user_over_limit_free_plan_alert
+ = yield :free_user_cap_alert
= yield :group_invite_members_banner
- unless @hide_breadcrumbs
= render "layouts/nav/breadcrumbs"
diff --git a/app/views/layouts/_visual_review.html.haml b/app/views/layouts/_visual_review.html.haml
new file mode 100644
index 00000000000..73da841964a
--- /dev/null
+++ b/app/views/layouts/_visual_review.html.haml
@@ -0,0 +1 @@
+= javascript_tag "", visual_review_toolbar_options
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index bdab5d7ea07..455d18a5ae8 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -8,6 +8,7 @@
%body{ class: body_classes, data: body_data }
= render "layouts/init_auto_complete" if @gfm_form
= render "layouts/init_client_detection_flags"
+ = render "layouts/visual_review" if ENV['REVIEW_APPS_ENABLED']
= render 'peek/bar'
= header_message
= render partial: "layouts/header/default", locals: { project: @project, group: @group }
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 3cae8186750..911cb85de53 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -6,13 +6,10 @@
.container-fluid
.header-content.js-header-content
.title-container.hide-when-top-nav-responsive-open.gl-transition-medium.gl-display-flex.gl-align-items-stretch.gl-pt-0
- %h1.title
+ .title
%span.gl-sr-only GitLab
= link_to root_path, title: _('Dashboard'), id: 'logo', **tracking_attrs('main_navigation', 'click_gitlab_logo_link', 'navigation') do
- %span{ :class => "gl-display-none gl-lg-display-flex" }
- = brand_header_logo({add_gitlab_white_text: true})
- %span{ :class => "gl-lg-display-none! gl-display-flex" }
- = brand_header_logo
+ = brand_header_logo
- if Gitlab.com_and_canary?
= link_to Gitlab::Saas.canary_toggle_com_url, class: 'canary-badge bg-transparent', data: { qa_selector: 'canary_badge_link' }, target: :_blank, rel: 'noopener noreferrer' do
= gl_badge_tag({ variant: :success, size: :sm }) do
@@ -32,12 +29,12 @@
= render "layouts/nav/top_nav"
.navbar-collapse.gl-transition-medium.collapse
- %ul.nav.navbar-nav.gl-w-full
+ %ul.nav.navbar-nav.gl-w-full.gl-align-items-center.gl-justify-content-end
- if current_user
= render 'layouts/header/new_dropdown', class: 'gl-display-none gl-sm-display-block gl-white-space-nowrap gl-text-right'
- if top_nav_show_search
- search_menu_item = top_nav_search_menu_item_attrs
- %li.nav-item.header-search-new.gl-display-none.gl-lg-display-block.m-auto.gl-w-full
+ %li.nav-item.header-search-new.gl-display-none.gl-lg-display-block.gl-w-full
- unless current_controller?(:search)
- if Feature.enabled?(:new_header_search)
= render 'layouts/header_search'
diff --git a/app/views/layouts/header/_registration_enabled_callout.html.haml b/app/views/layouts/header/_registration_enabled_callout.html.haml
index 03e961bda8f..dd3d14a5678 100644
--- a/app/views/layouts/header/_registration_enabled_callout.html.haml
+++ b/app/views/layouts/header/_registration_enabled_callout.html.haml
@@ -2,10 +2,10 @@
= render Pajamas::AlertComponent.new(title: _('Anyone can register for an account.'),
variant: :warning,
- alert_class: 'js-registration-enabled-callout',
- alert_data: { feature_id: Users::CalloutsHelper::REGISTRATION_ENABLED_CALLOUT,
- dismiss_endpoint: callouts_path },
- close_button_data: { testid: 'close-registration-enabled-callout' }) do |c|
+ alert_options: { class: 'js-registration-enabled-callout',
+ data: { feature_id: Users::CalloutsHelper::REGISTRATION_ENABLED_CALLOUT,
+ dismiss_endpoint: callouts_path }},
+ close_button_options: { data: { testid: 'close-registration-enabled-callout' }}) do |c|
= c.body do
= _('Only allow anyone to register for accounts on GitLab instances that you intend to be used by anyone. Allowing anyone to register makes GitLab instances more vulnerable.')
= c.actions do
diff --git a/app/views/layouts/header/_storage_enforcement_banner.html.haml b/app/views/layouts/header/_storage_enforcement_banner.html.haml
index 6613130fdf3..c117f22a402 100644
--- a/app/views/layouts/header/_storage_enforcement_banner.html.haml
+++ b/app/views/layouts/header/_storage_enforcement_banner.html.haml
@@ -4,11 +4,11 @@
- return unless banner_info.present?
= render Pajamas::AlertComponent.new(variant: :warning,
- alert_class: 'js-storage-enforcement-banner',
- alert_data: { feature_id: banner_info[:callouts_feature_name],
- dismiss_endpoint: banner_info[:callouts_path],
- group_id: namespace.id,
- defer_links: "true" }) do |c|
+ alert_options: { class: 'js-storage-enforcement-banner',
+ data: { feature_id: banner_info[:callouts_feature_name],
+ dismiss_endpoint: banner_info[:callouts_path],
+ group_id: namespace.id,
+ defer_links: "true" }}) do |c|
= c.body do
= banner_info[:text]
= banner_info[:learn_more_link]
diff --git a/app/views/layouts/nav/breadcrumbs/_collapsed_inline_list.html.haml b/app/views/layouts/nav/breadcrumbs/_collapsed_inline_list.html.haml
index ccb660c050e..dd2d23320be 100644
--- a/app/views/layouts/nav/breadcrumbs/_collapsed_inline_list.html.haml
+++ b/app/views/layouts/nav/breadcrumbs/_collapsed_inline_list.html.haml
@@ -4,8 +4,8 @@
%li.expander
%button.text-expander.has-tooltip.js-breadcrumbs-collapsed-expander{ type: "button", data: { container: "body" }, "aria-label": button_tooltip, title: button_tooltip }
= sprite_icon("ellipsis_h", size: 12)
- = sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle")
+ = sprite_icon("chevron-lg-right", size: 8, css_class: "breadcrumbs-list-angle")
- @breadcrumb_collapsed_links[dropdown_location].each_with_index do |link, index|
%li{ :class => "gl-display-none! breadcrumbs-detail-item" }
= link
- = sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle")
+ = sprite_icon("chevron-lg-right", size: 8, css_class: "breadcrumbs-list-angle")
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 3b979f69cac..d9f16a89fbc 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -5,6 +5,10 @@
GitLab
- if Feature.enabled?(:enhanced_notify_css)
= stylesheet_link_tag 'notify_enhanced'
+ %style{ type: 'text/css', 'data-premailer': 'ignore' }
+ -# The MUA automatically turns some text into links.
+ -# Match the color of explicit links ($blue-600 from typography.scss).
+ a { color: #1068bf; }
- else
= stylesheet_link_tag 'notify'
= yield :head
diff --git a/app/views/layouts/service_desk.html.haml b/app/views/layouts/service_desk.html.haml
index a838ba91d26..bab7bc6b9da 100644
--- a/app/views/layouts/service_desk.html.haml
+++ b/app/views/layouts/service_desk.html.haml
@@ -7,6 +7,10 @@
-# haml-lint:enable NoPlainNodes
- if Feature.enabled?(:enhanced_notify_css)
= stylesheet_link_tag 'notify_enhanced'
+ %style{ type: 'text/css', 'data-premailer': 'ignore' }
+ -# The MUA automatically turns some text into links.
+ -# Match the color of explicit links ($blue-600 from typography.scss).
+ a { color: #1068bf; }
- else
= stylesheet_link_tag 'notify'
= yield :head
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
index 91301e1e226..c9baf0cd2b8 100644
--- a/app/views/layouts/terms.html.haml
+++ b/app/views/layouts/terms.html.haml
@@ -16,16 +16,17 @@
%div{ class: "#{container_class} limit-container-width" }
.content{ id: "content-body" }
- .gl-card
- .gl-card-header
+ = render Pajamas::CardComponent.new do |c|
+ = c.header do
= brand_header_logo({add_gitlab_black_text: true})
- - if header_link?(:user_dropdown)
- .navbar-collapse
- %ul.nav.navbar-nav
- %li.header-user.dropdown
- = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
- = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar", data: { qa_selector: 'user_avatar' }
- = sprite_icon('angle-down', css_class: 'caret-down')
- .dropdown-menu.dropdown-menu-right
- = render 'layouts/header/current_user_dropdown'
- = yield
+ = c.body do
+ - if header_link?(:user_dropdown)
+ .navbar-collapse
+ %ul.nav.navbar-nav
+ %li.header-user.dropdown
+ = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
+ = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar", data: { qa_selector: 'user_avatar' }
+ = sprite_icon('chevron-down')
+ .dropdown-menu.dropdown-menu-right
+ = render 'layouts/header/current_user_dropdown'
+ = yield
diff --git a/app/views/notify/approved_merge_request_email.html.haml b/app/views/notify/approved_merge_request_email.html.haml
index 4393186a8ad..c51fe02370d 100644
--- a/app/views/notify/approved_merge_request_email.html.haml
+++ b/app/views/notify/approved_merge_request_email.html.haml
@@ -61,7 +61,7 @@
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }
%tr.header
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
- %img{ alt: "GitLab", height: "55", src: image_url('mailers/gitlab_logo.png'), width: "55" }/
+ = header_logo
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
diff --git a/app/views/notify/merge_when_pipeline_succeeds_email.html.haml b/app/views/notify/merge_when_pipeline_succeeds_email.html.haml
index e4d138cce96..550d386c843 100644
--- a/app/views/notify/merge_when_pipeline_succeeds_email.html.haml
+++ b/app/views/notify/merge_when_pipeline_succeeds_email.html.haml
@@ -61,7 +61,7 @@
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }
%tr.header
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
- %img{ alt: "GitLab", height: "55", src: image_url('mailers/gitlab_logo.png'), width: "55" }
+ = header_logo
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
diff --git a/app/views/notify/unapproved_merge_request_email.html.haml b/app/views/notify/unapproved_merge_request_email.html.haml
index 8a4138b7515..ae58ccd3995 100644
--- a/app/views/notify/unapproved_merge_request_email.html.haml
+++ b/app/views/notify/unapproved_merge_request_email.html.haml
@@ -61,7 +61,7 @@
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }
%tr.header
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
- %img{ alt: "GitLab", height: "55", src: image_url('mailers/gitlab_logo.png'), width: "55" }/
+ = header_logo
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
diff --git a/app/views/notify/user_auto_banned_email.html.haml b/app/views/notify/user_auto_banned_email.html.haml
new file mode 100644
index 00000000000..d88c06526eb
--- /dev/null
+++ b/app/views/notify/user_auto_banned_email.html.haml
@@ -0,0 +1,9 @@
+- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
+- link_end = '</a>'.html_safe
+= email_default_heading(_("We've detected some unusual activity"))
+%p
+ = _('We want to let you know %{username} has been banned from your GitLab instance due to them downloading more than %{max_project_downloads} project repositories within %{within_minutes} minutes.') % { username: sanitize_name(@user.name), max_project_downloads: @max_project_downloads, within_minutes: @within_minutes }
+%p
+ = _('If this is a mistake, you can %{link_start}unban them%{link_end}.').html_safe % { link_start: link_start % { url: admin_users_url(filter: 'banned') }, link_end: link_end }
+%p
+ = _('You can adjust rules on auto-banning %{link_start}here%{link_end}.').html_safe % { link_start: link_start % { url: network_admin_application_settings_url(anchor: 'js-ip-limits-settings') }, link_end: link_end }
diff --git a/app/views/notify/user_auto_banned_email.text.erb b/app/views/notify/user_auto_banned_email.text.erb
new file mode 100644
index 00000000000..0469ee9788c
--- /dev/null
+++ b/app/views/notify/user_auto_banned_email.text.erb
@@ -0,0 +1,7 @@
+<%= _("We've detected some unusual activity") %>
+
+<%= _('We want to let you know %{username} has been banned from your GitLab instance due to them downloading more than %{max_project_downloads} project repositories within %{within_minutes} minutes.') % { username: sanitize_name(@user.name), max_project_downloads: @max_project_downloads, within_minutes: @within_minutes } %>
+
+<%= _('If this is a mistake, you can unban them: %{url}.') % { url: admin_users_url(filter: 'banned') } %>
+
+<%= _('You can adjust rules on auto-banning here: %{url}.') % { url: network_admin_application_settings_url(anchor: 'js-ip-limits-settings') } %>
diff --git a/app/views/profiles/_email_settings.html.haml b/app/views/profiles/_email_settings.html.haml
index 35cad79b6fd..457d6690a78 100644
--- a/app/views/profiles/_email_settings.html.haml
+++ b/app/views/profiles/_email_settings.html.haml
@@ -7,7 +7,7 @@
.form-group.gl-form-group
= form.label :email, _('Email')
- = form.text_field :email, required: true, class: 'gl-form-input form-control gl-form-input-lg', value: (@user.email unless @user.temp_oauth_email?), readonly: readonly || email_change_disabled
+ = form.text_field :email, required: true, class: 'gl-form-input form-control gl-md-form-input-lg', value: (@user.email unless @user.temp_oauth_email?), readonly: readonly || email_change_disabled
%small.form-text.text-gl-muted
= help_text.html_safe
@@ -16,7 +16,7 @@
.form-group.gl-form-group
= form.label :public_email, s_('Profiles|Public email')
- .gl-form-input-lg
+ .gl-md-form-input-lg
= form.select :public_email,
options_for_select(@user.public_verified_emails, selected: @user.public_email),
{ include_blank: s_("Profiles|Do not show on profile") },
@@ -29,7 +29,7 @@
- commit_email_link_start = '<a href="%{url}">'.html_safe % { url: commit_email_link_url }
- commit_email_docs_link = s_('Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more%{commit_email_link_end}').html_safe % { commit_email_link_start: commit_email_link_start, commit_email_link_end: '</a>'.html_safe }
= form.label :commit_email, s_('Profiles|Commit email')
- .gl-form-input-lg
+ .gl-md-form-input-lg
= form.select :commit_email,
options_for_select(commit_email_select_options(@user), selected: @user.commit_email),
{},
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index bbbb8154c51..745d3c62c5d 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -2,15 +2,15 @@
- @content_class = "limit-container-width" unless fluid_layout
- if current_user.ldap_user?
- = render Pajamas::AlertComponent.new(alert_class: 'gl-my-5',
+ = render Pajamas::AlertComponent.new(alert_options: { class: 'gl-my-5' },
dismissible: false) do |c|
= c.body do
= s_('Profiles|Some options are unavailable for LDAP accounts')
- if params[:two_factor_auth_enabled_successfully]
= render Pajamas::AlertComponent.new(variant: :success,
- alert_class: 'gl-my-5',
- close_button_class: 'js-close-2fa-enabled-success-alert') do |c|
+ alert_options: { class: 'gl-my-5' },
+ close_button_options: { class: 'js-close-2fa-enabled-success-alert' }) do |c|
= c.body do
= html_escape(_('You have set up 2FA for your account! If you lose access to your 2FA device, you can use your recovery codes to access your account. Alternatively, if you upload an SSH key, you can %{anchorOpen}use that key to generate additional recovery codes%{anchorClose}.')) % { anchorOpen: '<a href="%{href}">'.html_safe % { href: help_page_path('user/profile/account/two_factor_authentication', anchor: 'generate-new-recovery-codes-using-ssh') }, anchorClose: '</a>'.html_safe }
@@ -85,7 +85,7 @@
%p
= s_('Profiles|GitLab is unable to verify your identity automatically. For security purposes, you must set a password by %{openingTag}resetting your password%{closingTag} to delete your account.').html_safe % { openingTag: "<a href='#{reset_profile_password_path}' rel=\"nofollow\" data-method=\"put\">".html_safe, closingTag: '</a>'.html_safe}
%p
- = s_('Profiles|If after setting a password, the option to delete your account is still not available, please email %{data_request} to begin the account deletion process.').html_safe % { data_request: mail_to('personal-data-request@gitlab.com') }
+ = s_('Profiles|If after setting a password, the option to delete your account is still not available, please %{link_start}submit a request%{link_end} to begin the account deletion process.').html_safe % { link_start: '<a href="https://support.gitlab.io/account-deletion/" rel="nofollow noreferrer noopener" target="_blank">'.html_safe, link_end: '</a>'.html_safe}
- else
%p
= s_("Profiles|You don't have access to delete this user.")
diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml
index 8f80c9fdc6c..0b45869bdf9 100644
--- a/app/views/profiles/chat_names/_chat_name.html.haml
+++ b/app/views/profiles/chat_names/_chat_name.html.haml
@@ -6,11 +6,11 @@
- if can?(current_user, :read_project, project)
= link_to project.full_name, project_path(project)
- else
- .light= _('N/A')
+ .light= _('Not applicable.')
%td
%strong
- if can?(current_user, :admin_project, project)
- = link_to integration.title, edit_project_integration_path(project, integration)
+ = link_to integration.title, edit_project_settings_integration_path(project, integration)
- else
= integration.title
%td
diff --git a/app/views/profiles/chat_names/new.html.haml b/app/views/profiles/chat_names/new.html.haml
index f008abf376d..303b8b10027 100644
--- a/app/views/profiles/chat_names/new.html.haml
+++ b/app/views/profiles/chat_names/new.html.haml
@@ -1,4 +1,4 @@
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _("Authorization required")
%main{ :role => "main" }
%p.h4
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index fef55f5d384..69f765ee163 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -56,7 +56,7 @@
= gl_badge_tag s_('Profiles|Notification email'), variant: :info
- unless email.confirmed?
- confirm_title = "#{email.confirmation_sent_at ? _('Resend confirmation email') : _('Send confirmation email')}"
- = link_to confirm_title, resend_confirmation_instructions_profile_email_path(email), method: :put, class: 'gl-button btn btn-sm btn-warning gl-ml-3'
+ = link_to confirm_title, resend_confirmation_instructions_profile_email_path(email), method: :put, class: 'gl-button btn btn-sm btn-default gl-ml-3'
= link_to profile_email_path(email), data: { confirm: _('Are you sure?'), qa_selector: 'delete_email_link'}, method: :delete, class: 'gl-button btn btn-sm btn-danger gl-ml-3' do
%span.sr-only= _('Remove')
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index 941b8545745..a749fbd1eec 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -1,7 +1,7 @@
- max_date = ::Gitlab::CurrentSettings.max_ssh_key_lifetime_from_now.to_date if ssh_key_expiration_policy_enabled?
%div
= form_for [:profile, @key], html: { class: 'js-requires-input' } do |f|
- = form_errors(@key)
+ = form_errors(@key, pajamas_alert: true)
.form-group
= f.label :key, s_('Profiles|Key'), class: 'label-bold'
@@ -23,7 +23,8 @@
%strong= _('Oops, are you sure?')
%p= s_("Profiles|Publicly visible private SSH keys can compromise your system.")
- %button.btn.gl-button.btn-confirm.js-add-ssh-key-validation-confirm-submit= _("Yes, add it")
-
+ = render Pajamas::ButtonComponent.new(variant: :confirm,
+ button_options: { class: 'js-add-ssh-key-validation-confirm-submit' }) do
+ = _("Yes, add it")
.gl-mt-3
= f.submit s_('Profiles|Add key'), class: "gl-button btn btn-confirm js-add-ssh-key-validation-original-submit qa-add-key-button"
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index 8016d989ff1..8f7ccadd108 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -19,7 +19,7 @@
%strong= @key.last_used_at.try(:to_s, :medium) || _('Never')
.col-md-8
- = form_errors(@key, type: 'key') unless @key.valid?
+ = form_errors(@key, type: 'key', pajamas_alert: true) unless @key.valid?
%pre.well-pre
= @key.key
.card
diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml
index 9154c94abb6..a2180dc68a6 100644
--- a/app/views/profiles/passwords/new.html.haml
+++ b/app/views/profiles/passwords/new.html.haml
@@ -1,7 +1,7 @@
- page_title _('New Password')
- breadcrumb_title _('New Password')
-%h3.page-title= _('Set up new password')
+%h1.page-title.gl-font-size-h-display= _('Set up new password')
%hr
= form_for @user, url: profile_password_path, method: :post do |f|
%p.slead
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 887d07f7a20..636defb3f10 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -15,22 +15,16 @@
= s_('AccessTokens|They are the only accepted password when you have Two-Factor Authentication (2FA) enabled.')
.col-lg-8
- - if @new_personal_access_token
- = render 'shared/access_tokens/created_container',
- type: type,
- new_token_value: @new_personal_access_token
+ #js-new-access-token-app{ data: { access_token_type: type } }
= render 'shared/access_tokens/form',
+ ajax: true,
type: type,
path: profile_personal_access_tokens_path,
token: @personal_access_token,
scopes: @scopes,
help_path: help_page_path('user/profile/personal_access_tokens.md', anchor: 'personal-access-token-scopes')
- = render 'shared/access_tokens/table',
- type: type,
- type_plural: type_plural,
- active_tokens: @active_personal_access_tokens,
- revoke_route_helper: ->(token) { revoke_profile_personal_access_token_path(token) }
+ #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_personal_access_tokens.to_json } }
#js-tokens-app{ data: { tokens_data: tokens_app_data } }
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 8c799a5e3fe..a63e02fca1d 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -1,6 +1,7 @@
- page_title _('Preferences')
- @content_class = "limit-container-width" unless fluid_layout
- user_theme_id = Gitlab::Themes.for_user(@user).id
+- user_color_schema_id = Gitlab::ColorSchemes.for_user(@user).id
- user_fields = { theme: user_theme_id, gitpod_enabled: @user.gitpod_enabled, sourcegraph_enabled: @user.sourcegraph_enabled }.to_json
- @themes = Gitlab::Themes::available_themes.to_json
- data_attributes = { themes: @themes, integration_views: integration_views.to_json, user_fields: user_fields, body_classes: Gitlab::Themes.body_classes, profile_preferences_path: profile_preferences_path }
@@ -12,16 +13,17 @@
.row.gl-mt-3.js-preferences-form.js-search-settings-section
.col-lg-4.application-theme#navigation-theme
%h4.gl-mt-0
- = s_('Preferences|Navigation theme')
+ = s_('Preferences|Color theme')
%p
- = s_('Preferences|Customize the appearance of the application header and navigation sidebar.')
+ = s_('Preferences|Customize the color of GitLab.')
.col-lg-8.application-theme
.row
- Gitlab::Themes.each do |theme|
%label.col-6.col-sm-4.col-md-3.gl-mb-5.gl-text-center
.preview{ class: theme.css_class }
- = f.radio_button :theme_id, theme.id, checked: user_theme_id == theme.id
- = theme.name
+ = f.gitlab_ui_radio_component :theme_id, theme.id,
+ theme.name,
+ radio_options: { checked: user_theme_id == theme.id }
.col-sm-12
%hr
@@ -38,8 +40,9 @@
- Gitlab::ColorSchemes.each do |scheme|
= label_tag do
.preview= image_tag "#{scheme.css_class}-scheme-preview.png"
- = f.radio_button :color_scheme_id, scheme.id
- = scheme.name
+ = f.gitlab_ui_radio_component :color_scheme_id, scheme.id,
+ scheme.name,
+ radio_options: { checked: user_color_schema_id == scheme.id }
.col-sm-12
%hr
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 107c7cebc61..d1f1ff892d5 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -109,44 +109,44 @@
= f.text_field :id, class: 'gl-form-input form-control', readonly: true
.form-group.gl-form-group
= f.label :pronouns, s_('Profiles|Pronouns')
- = f.text_field :pronouns, class: 'gl-form-input form-control gl-form-input-lg'
+ = f.text_field :pronouns, class: 'gl-form-input form-control gl-md-form-input-lg'
%small.form-text.text-gl-muted
= s_("Profiles|Enter your pronouns to let people know how to refer to you")
.form-group.gl-form-group
= f.label :pronunciation, s_('Profiles|Pronunciation')
- = f.text_field :pronunciation, class: 'gl-form-input form-control gl-form-input-lg'
+ = f.text_field :pronunciation, class: 'gl-form-input form-control gl-md-form-input-lg'
%small.form-text.text-gl-muted
= s_("Profiles|Enter how your name is pronounced to help people address you correctly")
= render_if_exists 'profiles/extra_settings', form: f
= render_if_exists 'profiles/email_settings', form: f
.form-group.gl-form-group
= f.label :skype
- = f.text_field :skype, class: 'gl-form-input form-control gl-form-input-lg', placeholder: s_("Profiles|username")
+ = f.text_field :skype, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|username")
.form-group.gl-form-group
= f.label :linkedin
- = f.text_field :linkedin, class: 'gl-form-input form-control gl-form-input-lg'
+ = f.text_field :linkedin, class: 'gl-form-input form-control gl-md-form-input-lg'
%small.form-text.text-gl-muted
= s_("Profiles|Your LinkedIn profile name from linkedin.com/in/profilename")
.form-group.gl-form-group
= f.label :twitter
- = f.text_field :twitter, class: 'gl-form-input form-control gl-form-input-lg', placeholder: s_("Profiles|@username")
+ = f.text_field :twitter, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|@username")
.form-group.gl-form-group
= f.label :website_url, s_('Profiles|Website url')
- = f.text_field :website_url, class: 'gl-form-input form-control gl-form-input-lg', placeholder: s_("Profiles|https://website.com")
+ = f.text_field :website_url, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|https://website.com")
.form-group.gl-form-group
= f.label :location, s_('Profiles|Location')
- if @user.read_only_attribute?(:location)
- = f.text_field :location, class: 'gl-form-input form-control gl-form-input-lg', readonly: true
+ = f.text_field :location, class: 'gl-form-input form-control gl-md-form-input-lg', readonly: true
%small.form-text.text-gl-muted
= s_("Profiles|Your location was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:location) }
- else
- = f.text_field :location, class: 'gl-form-input form-control gl-form-input-lg', placeholder: s_("Profiles|City, country")
+ = f.text_field :location, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|City, country")
.form-group.gl-form-group
= f.label :job_title, s_('Profiles|Job title')
- = f.text_field :job_title, class: 'gl-form-input form-control gl-form-input-lg'
+ = f.text_field :job_title, class: 'gl-form-input form-control gl-md-form-input-lg'
.form-group.gl-form-group
= f.label :organization, s_('Profiles|Organization')
- = f.text_field :organization, class: 'gl-form-input form-control gl-form-input-lg'
+ = f.text_field :organization, class: 'gl-form-input form-control gl-md-form-input-lg'
%small.form-text.text-gl-muted
= s_("Profiles|Who you represent or work for")
.form-group.gl-form-group
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index ace644a493b..845baae3bb2 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -24,8 +24,9 @@
- register_2fa_token = _('We recommend cloud-based mobile authenticator apps such as Authy, Duo Mobile, and LastPass. They can restore access if you lose your hardware device.')
= register_2fa_token.html_safe
.row.gl-mb-3
- .col-md-4.gl-pt-2{ style: 'background: #fff' }
- = raw @qr_code
+ .col-md-4.gl-min-w-fit-content
+ .gl-p-2.gl-mb-3{ style: 'background: #fff' }
+ = raw @qr_code
.col-md-8
.account-well
%p.gl-mt-0.gl-mb-0
diff --git a/app/views/projects/_clusters_deprecation_alert.html.haml b/app/views/projects/_clusters_deprecation_alert.html.haml
new file mode 100644
index 00000000000..67e65b0e81b
--- /dev/null
+++ b/app/views/projects/_clusters_deprecation_alert.html.haml
@@ -0,0 +1,2 @@
+- if show_clusters_alert?(@project)
+ .js-clusters-deprecation-alert{ data: { message: clusters_deprecation_alert_message } }
diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml
index d987c4b1033..659bca25533 100644
--- a/app/views/projects/_commit_button.html.haml
+++ b/app/views/projects/_commit_button.html.haml
@@ -1,7 +1,8 @@
.form-actions.gl-display-flex
- = button_tag 'Commit changes', id: 'commit-changes', class: 'gl-button btn btn-confirm js-commit-button qa-commit-button'
+ = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { id: 'commit-changes', class: 'js-commit-button qa-commit-button' }) do
+ = _('Commit changes')
- = link_to _('Cancel'), cancel_path,
- id: 'cancel-changes', class: 'gl-button btn btn-default gl-ml-3', data: {confirm: leave_edit_message, confirm_btn_variant: "danger"}, aria: { label: _('Discard changes') }
+ = render Pajamas::ButtonComponent.new(href: cancel_path, button_options: { class: 'gl-ml-3', id: 'cancel-changes', aria: { label: _('Discard changes') }, data: { confirm: leave_edit_message, confirm_btn_variant: "danger" } }) do
+ = _('Cancel')
= render 'shared/projects/edit_information'
diff --git a/app/views/projects/_deletion_failed.html.haml b/app/views/projects/_deletion_failed.html.haml
index 85a7b9eb22b..489d303c5b9 100644
--- a/app/views/projects/_deletion_failed.html.haml
+++ b/app/views/projects/_deletion_failed.html.haml
@@ -3,7 +3,7 @@
= render Pajamas::AlertComponent.new(variant: :warning,
dismissible: false,
- alert_class: 'project-deletion-failed-message') do |c|
+ alert_options: { class: 'project-deletion-failed-message' }) do |c|
= c.body do
This project was scheduled for deletion, but failed with the following message:
= project.delete_error
diff --git a/app/views/projects/_errors.html.haml b/app/views/projects/_errors.html.haml
index 2dba22d3be6..5982aaf3622 100644
--- a/app/views/projects/_errors.html.haml
+++ b/app/views/projects/_errors.html.haml
@@ -1 +1 @@
-= form_errors(@project)
+= form_errors(@project, pajamas_alert: true)
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
index 6dfb338a916..cb15858a935 100644
--- a/app/views/projects/_import_project_pane.html.haml
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -53,7 +53,7 @@
- if gitea_import_enabled?
%div
- = link_to new_import_gitea_path, class: 'gl-button btn-default btn import_gitea js-import-project-btn', data: { platform: 'gitea', **tracking_attrs_data(track_label, 'click_button', 'gitea') } do
+ = link_to new_import_gitea_path(namespace_id: namespace_id), class: 'gl-button btn-default btn import_gitea js-import-project-btn', data: { platform: 'gitea', **tracking_attrs_data(track_label, 'click_button', 'gitea') } do
.gl-button-icon
= custom_icon('gitea_logo')
Gitea
@@ -63,18 +63,18 @@
%button.gl-button.btn-default.btn.btn-svg.js-toggle-button.js-import-git-toggle-button.js-import-project-btn{ type: "button", data: { platform: 'repo_url', toggle_open_class: 'active', **tracking_attrs_data(track_label, 'click_button', 'repo_url') } }
.gl-button-icon
= sprite_icon('link', css_class: 'gl-icon')
- = _('Repo by URL')
+ = _('Repository by URL')
- if manifest_import_enabled?
%div
- = link_to new_import_manifest_path, class: 'gl-button btn-default btn import_manifest js-import-project-btn', data: { platform: 'manifest_file', **tracking_attrs_data(track_label, 'click_button', 'manifest_file') } do
+ = link_to new_import_manifest_path(namespace_id: namespace_id), class: 'gl-button btn-default btn import_manifest js-import-project-btn', data: { platform: 'manifest_file', **tracking_attrs_data(track_label, 'click_button', 'manifest_file') } do
.gl-button-icon
= sprite_icon('doc-text')
Manifest file
- if phabricator_import_enabled?
%div
- = link_to new_import_phabricator_path, class: 'gl-button btn-default btn import_phabricator js-import-project-btn', data: { platform: 'phabricator', track_label: "#{track_label}", track_action: "click_button", track_property: "phabricator" } do
+ = link_to new_import_phabricator_path(namespace_id: namespace_id), class: 'gl-button btn-default btn import_phabricator js-import-project-btn', data: { platform: 'phabricator', track_label: "#{track_label}", track_action: "click_button", track_property: "phabricator" } do
.gl-button-icon
= custom_icon('issues')
= _("Phabricator Tasks")
diff --git a/app/views/projects/_invite_members_modal.html.haml b/app/views/projects/_invite_members_modal.html.haml
index 6375a56bf5d..16288f4357a 100644
--- a/app/views/projects/_invite_members_modal.html.haml
+++ b/app/views/projects/_invite_members_modal.html.haml
@@ -1,5 +1,5 @@
- return unless can_admin_project_member?(project)
.js-invite-members-modal{ data: { is_project: 'true',
- access_levels: ProjectMember.access_level_roles.to_json,
+ access_levels: ProjectMember.permissible_access_level_roles(current_user, project).to_json,
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/_invite_members_side_nav_link.html.haml b/app/views/projects/_invite_members_side_nav_link.html.haml
index fae681b1a71..b96a7608ce2 100644
--- a/app/views/projects/_invite_members_side_nav_link.html.haml
+++ b/app/views/projects/_invite_members_side_nav_link.html.haml
@@ -1,7 +1,8 @@
.js-invite-members-trigger{ data: { trigger_source: 'project-side-nav',
icon: 'users',
display_text: title,
- trigger_element: 'side-nav'} }
+ trigger_element: 'side-nav',
+ qa_selector: 'invite_members_sidebar_button' } }
= render partial: 'shared/nav/sidebar_submenu', locals: { sidebar_menu: sidebar_menu }
= render 'projects/invite_members_modal', project: project
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 5a2add9de1e..9845de17a11 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -1,8 +1,8 @@
- event = last_push_event
- if event && show_last_push_widget?(event)
= render Pajamas::AlertComponent.new(variant: :success,
- alert_class: 'gl-mt-3',
- close_button_class: 'js-close-banner') do |c|
+ alert_options: { class: 'gl-mt-3' },
+ close_button_options: { class: 'js-close-banner' }) do |c|
= c.body do
%span= s_("LastPushEvent|You pushed to")
%strong.gl-display-inline-flex.gl-max-w-50p{ data: { toggle: 'tooltip' }, title: event.ref_name }
diff --git a/app/views/projects/_merge_request_merge_method_settings.html.haml b/app/views/projects/_merge_request_merge_method_settings.html.haml
index cb660750632..f205fe2b9bf 100644
--- a/app/views/projects/_merge_request_merge_method_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_method_settings.html.haml
@@ -1,38 +1,34 @@
- form = local_assigns.fetch(:form)
+- labelMerge = s_('ProjectSettings|Merge commit')
+- everyMergeCommit = s_('ProjectSettings|Every merge creates a merge commit.')
+
+- labelRebase = s_('ProjectSettings|Merge commit with semi-linear history')
+- rebaseUpToDate = s_('ProjectSettings|Merging is only allowed when the source branch is up-to-date with its target.')
+- rebaseSemiLinear = s_('ProjectSettings|When semi-linear merge is not possible, the user is given the option to rebase.')
+
+- labelFastForward = s_('ProjectSettings|Fast-forward merge')
+- noMergeCommit = s_('ProjectSettings|No merge commits are created.')
+- ffOnly = s_('ProjectSettings|Fast-forward merges only.')
+- ffConflictRebase = s_('ProjectSettings|When there is a merge conflict, the user is given the option to rebase.')
+- ffTrains = s_('ProjectSettings|If merge trains are enabled, merging is only possible if the branch can be rebased without conflicts.')
+- ffTrainsHelp = link_to s_('ProjectSettings|What are merge trains?'), help_page_path('ci/pipelines/merge_trains.md', anchor: 'enable-merge-trains'), target: '_blank', rel: 'noopener noreferrer'
+
.form-group
%b= s_('ProjectSettings|Merge method')
%p.text-secondary
= s_('ProjectSettings|Determine what happens to the commit history when you merge a merge request.')
= link_to s_('ProjectSettings|How do they differ?'), help_page_path('user/project/merge_requests/methods/index.md'), target: '_blank', rel: 'noopener noreferrer'
- .form-check.mb-2
- = form.radio_button :merge_method, :merge, class: "js-merge-method-radio form-check-input"
- = label_tag :project_merge_method_merge, class: 'form-check-label' do
- = s_('ProjectSettings|Merge commit')
- .text-secondary
- = s_('ProjectSettings|Every merge creates a merge commit.')
-
- .form-check.mb-2
- = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio form-check-input"
- = label_tag :project_merge_method_rebase_merge, class: 'form-check-label' do
- = s_('ProjectSettings|Merge commit with semi-linear history')
- .text-secondary
- = s_('ProjectSettings|Every merge creates a merge commit.')
- %br
- = s_('ProjectSettings|Merging is only allowed when the source branch is up-to-date with its target.')
- %br
- = s_('ProjectSettings|When semi-linear merge is not possible, the user is given the option to rebase.')
-
- .form-check.mb-2
- = form.radio_button :merge_method, :ff, class: "js-merge-method-radio form-check-input", data: { qa_selector: 'merge_ff_radio' }
- = label_tag :project_merge_method_ff, class: 'form-check-label' do
- = s_('ProjectSettings|Fast-forward merge')
- .text-secondary
- = s_('ProjectSettings|No merge commits are created.')
- %br
- = s_('ProjectSettings|Fast-forward merges only.')
- %br
- = s_('ProjectSettings|When there is a merge conflict, the user is given the option to rebase.')
- %div
- = s_('ProjectSettings|If merge trains are enabled, merging is only possible if the branch can be rebased without conflicts.')
- = link_to s_('ProjectSettings|What are merge trains?'), help_page_path('ci/pipelines/merge_trains.md', anchor: 'enable-merge-trains'), target: '_blank', rel: 'noopener noreferrer'
+ = form.gitlab_ui_radio_component :merge_method,
+ :merge,
+ labelMerge,
+ help_text: everyMergeCommit
+ = form.gitlab_ui_radio_component :merge_method,
+ :rebase_merge,
+ labelRebase,
+ help_text: (everyMergeCommit + "<br />" + rebaseUpToDate + "<br />" + rebaseSemiLinear).html_safe
+ = form.gitlab_ui_radio_component :merge_method,
+ :ff,
+ labelFastForward,
+ help_text: (noMergeCommit + "<br />" + ffOnly + "<br />" + ffConflictRebase + "<br />" + ffTrains + " " + ffTrainsHelp).html_safe,
+ radio_options: { data: { qa_selector: 'merge_ff_radio' } }
diff --git a/app/views/projects/_merge_request_squash_options_settings.html.haml b/app/views/projects/_merge_request_squash_options_settings.html.haml
index bf3000f2b5e..4b428363646 100644
--- a/app/views/projects/_merge_request_squash_options_settings.html.haml
+++ b/app/views/projects/_merge_request_squash_options_settings.html.haml
@@ -7,34 +7,19 @@
= s_('ProjectSettings|Set the default behavior of this option in merge requests. Changes to this are also applied to existing merge requests.')
= link_to "What is squashing?", help_page_path('user/project/merge_requests/squash_and_merge.md'), target: '_blank', rel: 'noopener noreferrer'
- .form-check.gl-mb-2
- = settings.radio_button :squash_option, :never, class: "form-check-input"
- = label_tag :project_project_setting_attributes_squash_option_never, class: 'form-check-label' do
- .gl-font-weight-bold
- = s_('ProjectSettings|Do not allow')
- .text-secondary
- = s_('ProjectSettings|Squashing is never performed and the checkbox is hidden.')
-
- .form-check.gl-mb-2
- = settings.radio_button :squash_option, :default_off, class: "form-check-input"
- = label_tag :project_project_setting_attributes_squash_option_default_off, class: 'form-check-label' do
- .gl-font-weight-bold
- = s_('ProjectSettings|Allow')
- .text-secondary
- = s_('ProjectSettings|Checkbox is visible and unselected by default.')
-
- .form-check.gl-mb-2
- = settings.radio_button :squash_option, :default_on, class: "form-check-input"
- = label_tag :project_project_setting_attributes_squash_option_default_on, class: 'form-check-label' do
- .gl-font-weight-bold
- = s_('ProjectSettings|Encourage')
- .text-secondary
- = s_('ProjectSettings|Checkbox is visible and selected by default.')
-
- .form-check.gl-mb-2
- = settings.radio_button :squash_option, :always, class: "form-check-input"
- = label_tag :project_project_setting_attributes_squash_option_always, class: 'form-check-label' do
- .gl-font-weight-bold
- = s_('ProjectSettings|Require')
- .text-secondary
- = s_('ProjectSettings|Squashing is always performed. Checkbox is visible and selected, and users cannot change it.')
+ = settings.gitlab_ui_radio_component :squash_option,
+ :never,
+ s_('ProjectSettings|Do not allow'),
+ help_text: s_('ProjectSettings|Squashing is never performed and the checkbox is hidden.')
+ = settings.gitlab_ui_radio_component :squash_option,
+ :default_off,
+ s_('ProjectSettings|Allow'),
+ help_text: s_('ProjectSettings|Checkbox is visible and unselected by default.')
+ = settings.gitlab_ui_radio_component :squash_option,
+ :default_on,
+ s_('ProjectSettings|Encourage'),
+ help_text: s_('ProjectSettings|Checkbox is visible and selected by default.')
+ = settings.gitlab_ui_radio_component :squash_option,
+ :always,
+ s_('ProjectSettings|Require'),
+ help_text: s_('ProjectSettings|Squashing is always performed. Checkbox is visible and selected, and users cannot change it.')
diff --git a/app/views/projects/_merge_request_target_project_settings.html.haml b/app/views/projects/_merge_request_target_project_settings.html.haml
index 41d37884ac9..6f2917f24e0 100644
--- a/app/views/projects/_merge_request_target_project_settings.html.haml
+++ b/app/views/projects/_merge_request_target_project_settings.html.haml
@@ -8,16 +8,11 @@
%p.text-secondary
= s_('ProjectSettings|The default target project for merge requests created in this fork project.')
- .form-check.gl-mb-2
- = settings.radio_button :mr_default_target_self, false, class: "form-check-input"
- = label_tag :project_project_setting_attributes_mr_default_target_self_false, class: 'form-check-label' do
- .gl-font-weight-bold
- = s_('ProjectSettings|Upstream project')
- = @project.forked_from_project.full_name
-
- .form-check.gl-mb-2
- = settings.radio_button :mr_default_target_self, true, class: "form-check-input"
- = label_tag :project_project_setting_attributes_mr_default_target_self_true, class: 'form-check-label' do
- .gl-font-weight-bold
- = s_('ProjectSettings|This project')
- = @project.full_name
+ = settings.gitlab_ui_radio_component :mr_default_target_self,
+ false,
+ s_('ProjectSettings|Upstream project'),
+ help_text: @project.forked_from_project.full_name
+ = settings.gitlab_ui_radio_component :mr_default_target_self,
+ true,
+ s_('ProjectSettings|This project'),
+ help_text: @project.full_name
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index 66fa1a69ef9..2cbb9758703 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -37,7 +37,7 @@
- link_start_group_path = '<a href="%{path}">' % { path: new_group_path }
- project_tip = s_('ProjectsNew|Want to house several dependent projects under the same namespace? %{link_start}Create a group.%{link_end}') % { link_start: link_start_group_path, link_end: '</a>' }
= project_tip.html_safe
-= render Pajamas::AlertComponent.new(alert_class: "gl-mb-4 gl-display-none js-user-readme-repo",
+= render Pajamas::AlertComponent.new(alert_options: { class: "gl-mb-4 gl-display-none js-user-readme-repo" },
dismissible: false,
variant: :success) do |c|
= c.body do
diff --git a/app/views/projects/_service_desk_settings.html.haml b/app/views/projects/_service_desk_settings.html.haml
index 63cf4dfe0ab..cee3d9071b6 100644
--- a/app/views/projects/_service_desk_settings.html.haml
+++ b/app/views/projects/_service_desk_settings.html.haml
@@ -2,7 +2,7 @@
%section.settings.js-service-desk-setting-wrapper.no-animate#js-service-desk{ class: ('expanded' if expanded), data: { qa_selector: 'service_desk_settings_content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Service Desk')
- %button.btn.gl-button.btn-default.js-settings-toggle
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
- link_start = "<a href='#{help_page_path('user/project/service_desk')}' target='_blank' rel='noopener noreferrer'>".html_safe
%p= _('Enable and disable Service Desk. Some additional configuration might be required. %{link_start}Learn more%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
diff --git a/app/views/projects/_visibility_modal.html.haml b/app/views/projects/_visibility_modal.html.haml
index db98b978f04..66066ceb5b2 100644
--- a/app/views/projects/_visibility_modal.html.haml
+++ b/app/views/projects/_visibility_modal.html.haml
@@ -5,7 +5,7 @@
.modal-dialog
.modal-content
.modal-header
- %h3.page-title= _('Reduce this project’s visibility?')
+ %h1.page-title.gl-font-size-h-display= _('Reduce this project’s visibility?')
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": "true" }= sprite_icon("close")
.modal-body
diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml
index 6a4760c3954..e69c4f51ec4 100644
--- a/app/views/projects/activity.html.haml
+++ b/app/views/projects/activity.html.haml
@@ -1,4 +1,5 @@
- page_title _("Activity")
+= render_if_exists 'shared/minute_limit_banner', namespace: @project
= render 'projects/last_push'
= render 'projects/activity'
diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml
index 3cc9fea56e2..3ae7741d24d 100644
--- a/app/views/projects/blob/_new_dir.html.haml
+++ b/app/views/projects/blob/_new_dir.html.haml
@@ -2,7 +2,7 @@
.modal-dialog.modal-lg
.modal-content
.modal-header
- %h3.page-title= _('Create New Directory')
+ %h1.page-title.gl-font-size-h-display= _('Create New Directory')
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": "true" } &times;
.modal-body
diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml
index 1463fcf8052..7511de76223 100644
--- a/app/views/projects/blob/_remove.html.haml
+++ b/app/views/projects/blob/_remove.html.haml
@@ -2,7 +2,7 @@
.modal-dialog
.modal-content
.modal-header
- %h3.page-title Delete #{@blob.name}
+ %h1.page-title.gl-font-size-h-display Delete #{@blob.name}
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": "true" } &times;
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
deleted file mode 100644
index 629fa9c0e8a..00000000000
--- a/app/views/projects/blob/_upload.html.haml
+++ /dev/null
@@ -1,30 +0,0 @@
-#modal-upload-blob.modal
- .modal-dialog.modal-lg
- .modal-content
- .modal-header
- %h3.page-title= title
- %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
- %span{ "aria-hidden": "true" } &times;
- .modal-body
- = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form', data: { method: method } do
- .dropzone
- .dropzone-previews.blob-upload-dropzone-previews
- %p.dz-message.light
- - upload_link = link_to s_('UploadLink|click to upload'), '#', class: "markdown-selector"
- - dropzone_text = _('Attach a file by drag &amp; drop or %{upload_link}') % { upload_link: upload_link }
- #{ dropzone_text.html_safe }
-
- %br
- = render Pajamas::AlertComponent.new(variant: :danger,
- alert_class: 'dropzone-alerts gl-alert gl-alert-danger gl-mb-5 data gl-display-none',
- dismissible: false)
-
- = render 'shared/new_commit_form', placeholder: placeholder, ref: local_assigns[:ref]
-
- .form-actions
- = button_tag class: 'btn gl-button btn-confirm btn-upload-file gl-mr-2', id: 'submit-all', type: 'button' do
- = gl_loading_icon(inline: true, css_class: 'gl-mr-2 js-loading-icon hidden')
- = button_title
- = link_to _("Cancel"), '#', class: "btn gl-button btn-default btn-cancel", "data-dismiss" => "modal"
-
- = render 'shared/projects/edit_information'
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index f80601ef221..220319d31b5 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -4,7 +4,7 @@
- webpack_preload_asset_tag('monaco')
- if @conflict
- = render Pajamas::AlertComponent.new(alert_class: 'gl-mb-5 gl-mt-5',
+ = render Pajamas::AlertComponent.new(alert_options: { class: 'gl-mb-5 gl-mt-5' },
variant: :danger,
dismissible: false) do |c|
- blob_url = project_blob_path(@project, @id)
@@ -15,7 +15,7 @@
= _('Someone edited the file the same time you did. Please check out %{link_start}the file %{icon}%{link_end} and make sure your changes will not unintentionally remove theirs.').html_safe % { link_start: blob_link_start, link_end: '</a>'.html_safe , icon: external_link_icon }
-%h3.page-title.blob-edit-page-title
+%h1.page-title.gl-font-size-h-display.blob-edit-page-title
Edit file
.file-editor
= gl_tabs_nav({ class: 'js-edit-mode nav-links gl-border-0'}) do
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 60877db581f..27f64104cf4 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,7 +1,7 @@
- breadcrumb_title _("Repository")
- page_title _("New File"), @path.presence, @ref
-%h3.page-title.blob-new-page-title
+%h1.page-title.blob-new-page-title.gl-font-size-h-display
= _('New file')
.file-editor
= form_tag(project_create_blob_path(@project, @id), method: :post, class: 'js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths(@project)) do
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index d4e7ee90a84..a91c0d63b00 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -14,8 +14,5 @@
- if can_modify_blob?(@blob)
= render 'projects/blob/remove'
- - title = _("Replace %{blob_name}") % { blob_name: @blob.name }
- = render 'projects/blob/upload', title: title, placeholder: title, button_title: _('Replace file'), form_path: project_update_blob_path(@project, @id), method: :put
-
= render partial: 'pipeline_tour_success' if show_suggest_pipeline_creation_celebration?
= render 'shared/web_ide_path'
diff --git a/app/views/projects/branch_rules/_show.html.haml b/app/views/projects/branch_rules/_show.html.haml
new file mode 100644
index 00000000000..af0e656d301
--- /dev/null
+++ b/app/views/projects/branch_rules/_show.html.haml
@@ -0,0 +1,12 @@
+- expanded = expanded_by_default?
+
+%section.settings.no-animate#branch-rules{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Branch rules')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Define rules for who can push, merge, and the required approvals for each branch.')
+
+ .settings-content.gl-pr-0
+ #js-branch-rules
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index c06f60bd05d..f8bee5a69e9 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -5,7 +5,7 @@
= render Pajamas::AlertComponent.new(variant: :danger) do |c|
= c.body do
= @error
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('New Branch')
%hr
@@ -26,7 +26,8 @@
= render 'shared/ref_dropdown', dropdown_class: 'wide'
.form-text.text-muted Existing branch name, tag, or commit SHA
.form-actions
- = button_tag 'Create branch', class: 'gl-button btn btn-confirm'
+ = render Pajamas::ButtonComponent.new(variant: :confirm, button_options: { type: 'submit', class: 'gl-mr-3' }) do
+ = _('Create branch')
= link_to _('Cancel'), project_branches_path(@project), class: 'gl-button btn btn-default btn-cancel'
-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/buttons/_remove_tag.html.haml b/app/views/projects/buttons/_remove_tag.html.haml
index 58af0d91f30..060a854d4e4 100644
--- a/app/views/projects/buttons/_remove_tag.html.haml
+++ b/app/views/projects/buttons/_remove_tag.html.haml
@@ -1,6 +1,11 @@
- project = local_assigns.fetch(:project, nil)
- tag = local_assigns.fetch(:tag, nil)
- return unless project && tag
+- title = s_('TagsPage|Delete tag')
+- if protected_tag?(project, tag)
+ - title = s_('TagsPage|Delete protected tag')
+ - if !can?(current_user, :maintainer_access, project)
+ - title = s_('TagsPage|Only a project maintainer or owner can delete a protected tag')
+ - disabled = true
-%button{ type: "button", class: "js-remove-tag js-confirm-modal-button gl-button btn btn-default btn-icon has-tooltip gl-ml-3\! #{protected_tag?(project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), data: { container: 'body', path: project_tag_path(@project, tag.name), modal_attributes: delete_tag_modal_attributes(tag.name) } }
- = sprite_icon('remove', css_class: 'gl-icon')
+= render Pajamas::ButtonComponent.new(variant: :default, icon: 'remove', button_options: { class: "js-delete-tag-button gl-ml-3\!", 'aria-label': s_('TagsPage|Delete tag'), title: title, disabled: disabled, data: { toggle: 'tooltip', container: 'body', path: project_tag_path(@project, tag.name), tag_name: tag.name, is_protected: protected_tag?(project, tag).to_s } })
diff --git a/app/views/projects/cleanup/_show.html.haml b/app/views/projects/cleanup/_show.html.haml
index 5e14b6dacfd..c53205b6c58 100644
--- a/app/views/projects/cleanup/_show.html.haml
+++ b/app/views/projects/cleanup/_show.html.haml
@@ -3,7 +3,7 @@
%section.settings.no-animate#cleanup{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Repository cleanup')
- %button.btn.gl-button.btn-default.js-settings-toggle
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
- link_url = 'https://github.com/newren/git-filter-repo'
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index c26f24dd52c..feaac255d8c 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.commits-container{ class: [limited_container_width, container_class] }
+.container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box"
= render "ci_menu"
= render "projects/diffs/diffs",
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index c6fb3bcd559..764ddace0ad 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -23,7 +23,7 @@
%li.commit-header.js-commit-header
%span.font-weight-bold= n_("%d previously merged commit", "%d previously merged commits", context_commits.count) % context_commits.count
- if can_update_merge_request
- %button.gl-button.btn.btn-default.ml-3.add-review-item-modal-trigger{ type: "button", data: { context_commits_empty: 'false' } }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'gl-ml-3 add-review-item-modal-trigger', data: { context_commits_empty: 'false' } }) do
= _('Add/remove')
%li.commits-row
@@ -41,7 +41,7 @@
= n_('%s additional commit has been omitted to prevent performance issues.', '%s additional commits have been omitted to prevent performance issues.', hidden) % number_with_delimiter(hidden)
- if can_update_merge_request && context_commits&.empty?
- %button.gl-button.btn.btn-default.mt-3.add-review-item-modal-trigger{ type: "button", data: { context_commits_empty: 'true' } }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'gl-mt-5', data: { context_commits_empty: 'true' } }) do
= _('Add previously merged commits')
- if commits.size == 0 && context_commits.nil?
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index 12d3f28dc20..b3590eea631 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -1,9 +1,9 @@
- breadcrumb_title _("Compare Revisions")
- page_title _("Compare")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _("Compare Git revisions")
-.sub-header-block
+%div
- example_branch = capture do
%code.ref-name= @project.default_branch_or_main
- example_sha = capture do
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index cb2c2d488e8..a6be6695b75 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -17,7 +17,7 @@
paginate_diffs: true,
paginate_diffs_per_page: Projects::CompareController::COMMIT_DIFFS_PER_PAGE
- else
- .card.bg-light
+ .card.gl-bg-gray-50.gl-border-none.gl-p-2
.center
%h4
= s_("CompareBranches|There isn't anything to compare.")
diff --git a/app/views/projects/confluences/show.html.haml b/app/views/projects/confluences/show.html.haml
index 413de90b67b..6fec9b501ea 100644
--- a/app/views/projects/confluences/show.html.haml
+++ b/app/views/projects/confluences/show.html.haml
@@ -8,6 +8,6 @@
- wiki_confluence_epic_link_url = 'https://gitlab.com/groups/gitlab-org/-/epics/3629'
- wiki_confluence_epic_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: wiki_confluence_epic_link_url }
= html_escape(s_("WikiEmpty|You've enabled the Confluence Workspace integration. Your wiki will be viewable directly within Confluence. We are hard at work integrating Confluence more seamlessly into GitLab. If you'd like to stay up to date, follow our %{wiki_confluence_epic_link_start}Confluence epic%{wiki_confluence_epic_link_end}.")) % { wiki_confluence_epic_link_start: wiki_confluence_epic_link_start, wiki_confluence_epic_link_end: '</a>'.html_safe }
- = link_to @project.confluence_integration.confluence_url, target: '_blank', rel: 'noopener noreferrer', class: 'gl-button btn btn-success external-url', title: s_('WikiEmpty|Go to Confluence') do
+ = link_to @project.confluence_integration.confluence_url, target: '_blank', rel: 'noopener noreferrer', class: 'gl-button btn btn-confirm external-url', title: s_('WikiEmpty|Go to Confluence') do
= s_('WikiEmpty|Go to Confluence')
= sprite_icon('external-link')
diff --git a/app/views/projects/default_branch/_show.html.haml b/app/views/projects/default_branch/_show.html.haml
index 2d3d36a9157..b1fb9c70a54 100644
--- a/app/views/projects/default_branch/_show.html.haml
+++ b/app/views/projects/default_branch/_show.html.haml
@@ -3,7 +3,7 @@
%section.settings.no-animate#default-branch-settings{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Default branch')
- %button.btn.gl-button.btn-default.js-settings-toggle
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= _('Set the default branch for this project. All merge requests and commits are made against this branch unless you specify a different one.')
diff --git a/app/views/projects/deploy_keys/edit.html.haml b/app/views/projects/deploy_keys/edit.html.haml
index 263b0025fe8..04e364d6b15 100644
--- a/app/views/projects/deploy_keys/edit.html.haml
+++ b/app/views/projects/deploy_keys/edit.html.haml
@@ -1,5 +1,5 @@
- page_title _('Edit Deploy Key')
-%h3.page-title= _('Edit Deploy Key')
+%h1.page-title.gl-font-size-h-display= _('Edit Deploy Key')
%hr
%div
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 92dbde07709..41d6b7086c1 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -10,14 +10,16 @@
%section.settings.general-settings.no-animate.expanded#js-general-settings
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Naming, topics, avatar')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }= _('Collapse')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = _('Collapse')
%p= _('Update your project name, topics, description, and avatar.')
.settings-content= render 'projects/settings/general'
%section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded), data: { qa_selector: 'visibility_features_permissions_content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Visibility, project features, permissions')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
%p= _('Choose visibility level, enable/disable project features and their permissions, disable email notifications, and show default award emoji.')
.settings-content
@@ -29,7 +31,8 @@
%section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { qa_selector: 'merge_request_settings_content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Merge requests')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
= render_if_exists 'projects/merge_request_settings_description_text'
.settings-content
@@ -47,7 +50,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= s_('ProjectSettings|Badges')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= s_('ProjectSettings|Customize this project\'s badges.')
@@ -64,7 +67,8 @@
%section.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded), data: { qa_selector: 'advanced_settings_content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Advanced')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
%p= s_('ProjectSettings|Housekeeping, export, archive, change path, transfer, and delete.')
.settings-content
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index b2338fa6c55..ce6d021ce2f 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -3,8 +3,10 @@
- escaped_default_branch_name = default_branch_name.shellescape
- @skip_current_level_breadcrumb = true
-= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
+= render_if_exists 'projects/free_user_cap_alert', project: @project
+
= render partial: 'flash_messages', locals: { project: @project }
+= render 'clusters_deprecation_alert'
= render "home_panel"
= render "archived_notice", project: @project
@@ -74,6 +76,3 @@
%span><
git push -u origin --all
git push -u origin --tags
-
-- if @project.upload_anchor_data.present?
- = render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, default_branch_name), ref: default_branch_name, method: :post
diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml
index ee31985eaf0..7c837d4ded0 100644
--- a/app/views/projects/environments/terminal.html.haml
+++ b/app/views/projects/environments/terminal.html.haml
@@ -6,7 +6,7 @@
.top-area
.row
.col-sm-6
- %h3.page-title
+ %h1.page-title.gl-font-size-h-display
= _("Terminal for environment")
= @environment.name
diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml
index 13fd4cee0cc..022a96b15a7 100644
--- a/app/views/projects/forks/error.html.haml
+++ b/app/views/projects/forks/error.html.haml
@@ -2,7 +2,7 @@
- if @forked_project && !@forked_project.saved?
= render Pajamas::AlertComponent.new(title: _('Fork Error!'),
variant: :danger,
- alert_class: 'gl-mt-5',
+ alert_options: { class: 'gl-mt-5' },
dismissible: false) do |c|
= c.body do
%p
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index 1973b23a062..c7639eec75d 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,7 +1,7 @@
- page_title _('Contributors')
-.sub-header-block.bg-gray-light.gl-p-5
- .tree-ref-holder.inline.vertical-align-middle
+.sub-header-block.gl-bg-gray-10.gl-p-5
+ .tree-ref-holder.gl-display-inline-block.gl-vertical-align-middle.gl-mr-3>
= render 'shared/ref_switcher', destination: 'graphs'
= link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn gl-button btn-default'
diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml
index f6861e4119e..d610ef21400 100644
--- a/app/views/projects/hook_logs/show.html.haml
+++ b/app/views/projects/hook_logs/show.html.haml
@@ -2,11 +2,14 @@
- add_to_breadcrumbs _('Webhook Settings'), namespace_project_hooks_path
- page_title _('Webhook Logs')
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _("Request details")
%hr
-= link_to _("Resend Request"), @hook_log.present.retry_path, method: :post, class: "btn gl-button btn-default float-right gl-ml-3"
+- if @hook_log.oversize?
+ = button_tag _("Resend Request"), class: "btn gl-button btn-default float-right gl-ml-3 has-tooltip", disabled: true, title: _("Request data is too large")
+- else
+ = link_to _("Resend Request"), @hook_log.present.retry_path, method: :post, class: "btn gl-button btn-default float-right gl-ml-3"
= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
diff --git a/app/views/projects/import/jira/show.html.haml b/app/views/projects/import/jira/show.html.haml
index 1feae7baa02..2605ebc544f 100644
--- a/app/views/projects/import/jira/show.html.haml
+++ b/app/views/projects/import/jira/show.html.haml
@@ -1,6 +1,6 @@
.js-jira-import-root{ data: { project_path: @project.full_path,
issues_path: project_issues_path(@project),
- jira_integration_path: edit_project_integration_path(@project, :jira),
+ jira_integration_path: edit_project_settings_integration_path(@project, :jira),
is_jira_configured: @project.jira_integration&.configured?.to_s,
in_progress_illustration: image_path('illustrations/export-import.svg'),
project_id: @project.id,
diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml
index dc2bcfa33bb..bcfa32566fb 100644
--- a/app/views/projects/imports/new.html.haml
+++ b/app/views/projects/imports/new.html.haml
@@ -1,5 +1,5 @@
- page_title _("Import repository")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('Import repository')
%hr
diff --git a/app/views/projects/issuable/_show.html.haml b/app/views/projects/issuable/_show.html.haml
index f311ed2d8ae..0898e0ae52d 100644
--- a/app/views/projects/issuable/_show.html.haml
+++ b/app/views/projects/issuable/_show.html.haml
@@ -4,7 +4,7 @@
- if issuable.relocation_target
- page_canonical_link issuable.relocation_target.present(current_user: current_user).web_url
-= render "projects/issues/alert_moved_from_service_desk", issue: issuable
+= render "projects/issues/service_desk/alert_moved_from_service_desk", issue: issuable
= render 'shared/issue_type/details_header', issuable: issuable
= render 'shared/issue_type/details_content', issuable: issuable, api_awards_path: api_awards_path
diff --git a/app/views/projects/issues/_by_email_description.html.haml b/app/views/projects/issues/_by_email_description.html.haml
deleted file mode 100644
index aeed5fb69c9..00000000000
--- a/app/views/projects/issues/_by_email_description.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-The subject will be used as the title of the new issue, and the message will be the description.
-
-= link_to 'Quick actions', help_page_path('user/project/quick_actions'), target: '_blank', rel: 'noopener noreferrer'
-and styling with
-= link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', rel: 'noopener noreferrer'
-are supported.
diff --git a/app/views/projects/issues/_work_item_links.html.haml b/app/views/projects/issues/_work_item_links.html.haml
new file mode 100644
index 00000000000..55a8eb720b6
--- /dev/null
+++ b/app/views/projects/issues/_work_item_links.html.haml
@@ -0,0 +1,2 @@
+- if Feature.enabled?(:work_items_hierarchy, @project)
+ .js-work-item-links-root{ data: { issuable_id: @issue.id } }
diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml
index 353ff9c1cc2..c2b620280d8 100644
--- a/app/views/projects/issues/edit.html.haml
+++ b/app/views/projects/issues/edit.html.haml
@@ -1,6 +1,6 @@
- page_title _("Edit"), "#{@issue.title} (#{@issue.to_reference})", _("Issues")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
Edit Issue ##{@issue.iid}
%hr
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index fe2be0f73c9..b730eb5072e 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -1,10 +1,5 @@
-- @can_bulk_update = can?(current_user, :admin_issue, @project)
-
-- page_title _("Issues")
-- new_issue_email = @project.new_issuable_address(current_user, 'issue')
+- page_title _('Issues')
- add_page_specific_style 'page_bundles/issues_list'
-- issuable_type = 'issue'
-
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@project.name} issues")
@@ -13,24 +8,6 @@
issues_path: project_issues_path(@project),
project_path: @project.full_path } }
-- if Feature.enabled?(:vue_issues_list, @project&.group)
- .js-issues-list{ data: project_issues_list_data(@project, current_user) }
- - if @can_bulk_update
- = render 'shared/issuable/bulk_update_sidebar', type: :issues
-- elsif project_issues(@project).exists?
- .top-area
- = render 'shared/issuable/nav', type: :issues
- = render "projects/issues/nav_btns"
- = render 'shared/issuable/search_bar', type: :issues
-
- - if @can_bulk_update
- = render 'shared/issuable/bulk_update_sidebar', type: :issues
-
- .issues-holder
- = render 'issues'
- - if new_issue_email
- .gl-text-center.gl-pt-5.gl-pb-7
- .js-issuable-by-email{ data: { initial_email: new_issue_email, issuable_type: issuable_type, emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'), quick_actions_help_path: help_page_path('user/project/quick_actions'), markdown_help_path: help_page_path('user/markdown'), reset_path: new_issuable_address_project_path(@project, issuable_type: issuable_type) } }
-- else
- - new_project_issue_button_path = @project.archived? ? false : new_project_issue_path(@project)
- = render 'shared/empty_states/issues', new_project_issue_button_path: new_project_issue_button_path, show_import_button: true
+.js-issues-list{ data: project_issues_list_data(@project, current_user) }
+- if can?(current_user, :admin_issue, @project)
+ = render 'shared/issuable/bulk_update_sidebar', type: :issues
diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml
index b18027f0f25..617579cdd6f 100644
--- a/app/views/projects/issues/new.html.haml
+++ b/app/views/projects/issues/new.html.haml
@@ -2,7 +2,7 @@
- breadcrumb_title _("New")
- page_title _("New Issue")
-.top-area.flex-lg-row
- %h3.page-title= _("New Issue")
+.top-area.gl-lg-flex-direction-row.gl-border-bottom-0
+ %h1.page-title.gl-font-size-h-display= _("New Issue")
= render "form"
diff --git a/app/views/projects/issues/service_desk.html.haml b/app/views/projects/issues/service_desk.html.haml
index fb5880f633a..93cb5ddd7e2 100644
--- a/app/views/projects/issues/service_desk.html.haml
+++ b/app/views/projects/issues/service_desk.html.haml
@@ -3,7 +3,7 @@
- page_title _("Service Desk")
- add_page_specific_style 'page_bundles/issues_list'
- content_for :breadcrumbs_extra do
- = render "projects/issues/nav_btns", show_export_button: false, show_rss_button: false
+ = render "projects/issues/service_desk/nav_btns", show_export_button: false, show_rss_button: false
- support_bot_attrs = { service_desk_enabled: @project.service_desk_enabled?, **UserSerializer.new.represent(User.support_bot) }.to_json
@@ -11,12 +11,12 @@
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls.d-block.d-sm-none
- = render "projects/issues/nav_btns", show_feed_buttons: false, show_import_button: false, show_export_button: false
+ = render "projects/issues/service_desk/nav_btns", show_feed_buttons: false, show_import_button: false, show_export_button: false
- if @issues.present?
= render 'shared/issuable/search_bar', type: :issues
- if Gitlab::ServiceDesk.supported?
- = render 'service_desk_info_content'
+ = render 'projects/issues/service_desk/service_desk_info_content'
.issues-holder
- = render 'projects/issues/issues', empty_state_path: 'service_desk_empty_state'
+ = render 'projects/issues/issues', empty_state_path: 'projects/issues/service_desk/service_desk_empty_state'
diff --git a/app/views/projects/issues/_alert_moved_from_service_desk.html.haml b/app/views/projects/issues/service_desk/_alert_moved_from_service_desk.html.haml
index 291edf014c3..cc8d5bdaeec 100644
--- a/app/views/projects/issues/_alert_moved_from_service_desk.html.haml
+++ b/app/views/projects/issues/service_desk/_alert_moved_from_service_desk.html.haml
@@ -3,6 +3,6 @@
- service_desk_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: service_desk_link_url }
= render Pajamas::AlertComponent.new(variant: :warning,
- alert_class: 'hide js-alert-moved-from-service-desk-warning gl-mt-5') do |c|
+ alert_options: { class: 'hide js-alert-moved-from-service-desk-warning gl-mt-5' }) do |c|
= c.body do
= s_('This project does not have %{service_desk_link_start}Service Desk%{service_desk_link_end} enabled, so the user who created the issue will no longer receive email notifications about new activity.').html_safe % { service_desk_link_start: service_desk_link_start, service_desk_link_end: '</a>'.html_safe }
diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/service_desk/_nav_btns.html.haml
index 8d16c3d978f..8d16c3d978f 100644
--- a/app/views/projects/issues/_nav_btns.html.haml
+++ b/app/views/projects/issues/service_desk/_nav_btns.html.haml
diff --git a/app/views/projects/issues/_service_desk_empty_state.html.haml b/app/views/projects/issues/service_desk/_service_desk_empty_state.html.haml
index efc319ed8df..1c9143c633d 100644
--- a/app/views/projects/issues/_service_desk_empty_state.html.haml
+++ b/app/views/projects/issues/service_desk/_service_desk_empty_state.html.haml
@@ -21,7 +21,7 @@
- if can_edit_project_settings && !service_desk_enabled
.text-center
- = link_to s_("ServiceDesk|Enable Service Desk"), edit_project_path(@project), class: 'gl-button btn btn-success'
+ = link_to s_("ServiceDesk|Enable Service Desk"), edit_project_path(@project), class: 'gl-button btn btn-confirm'
- else
.empty-state
.svg-content
diff --git a/app/views/projects/issues/_service_desk_info_content.html.haml b/app/views/projects/issues/service_desk/_service_desk_info_content.html.haml
index f0ec68ba54b..bad75ac2cd9 100644
--- a/app/views/projects/issues/_service_desk_info_content.html.haml
+++ b/app/views/projects/issues/service_desk/_service_desk_info_content.html.haml
@@ -21,4 +21,4 @@
- if can_edit_project_settings && !service_desk_enabled
.gl-mt-3
- = link_to s_("ServiceDesk|Enable Service Desk"), edit_project_path(@project), class: 'gl-button btn btn-success'
+ = link_to s_("ServiceDesk|Enable Service Desk"), edit_project_path(@project), class: 'gl-button btn btn-confirm'
diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml
index e725e8e6889..dfea4db4d07 100644
--- a/app/views/projects/jobs/index.html.haml
+++ b/app/views/projects/jobs/index.html.haml
@@ -1,5 +1,3 @@
-= render_if_exists 'shared/minute_limit_banner', namespace: @project
-
- page_title _("Jobs")
- add_page_specific_style 'page_bundles/ci_status'
- admin = local_assigns.fetch(:admin, false)
diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml
index 8023fb93c64..4f4609e6016 100644
--- a/app/views/projects/labels/edit.html.haml
+++ b/app/views/projects/labels/edit.html.haml
@@ -2,7 +2,7 @@
- breadcrumb_title _("Edit")
- page_title _("Edit"), @label.name, _("Labels")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('Edit Label')
-%hr
+
= render 'shared/labels/form', url: project_label_path(@project, @label), back_path: project_labels_path(@project)
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index e034e9c71ab..dd63e854a36 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -4,11 +4,12 @@
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present?
+= render_if_exists 'shared/minute_limit_banner', namespace: @project
- if labels_or_filters
#js-promote-label-modal
= render 'shared/labels/nav', labels_or_filters: labels_or_filters, can_admin_label: can_admin_label
- .labels-container.gl-mt-3.gl-bg-gray-10.gl-border-solid.gl-border-1.gl-border-gray-100
+ .labels-container.gl-mt-5
- if can_admin_label && search.blank?
%p.text-muted
= _('Labels can be applied to issues and merge requests.')
@@ -18,8 +19,8 @@
-# Only show it in the first page
- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
.prioritized-labels.gl-mb-7{ class: [('hide' if hide), ('is-not-draggable' unless can_admin_label)] }
- %h5.gl-mt-3= _('Prioritized Labels')
- .content-list.manage-labels-list.js-prioritized-labels{ data: { url: set_priorities_project_labels_path(@project), sortable: can_admin_label } }
+ %h4.gl-mt-3= _('Prioritized Labels')
+ .manage-labels-list.js-prioritized-labels{ data: { url: set_priorities_project_labels_path(@project), sortable: can_admin_label } }
#js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty? && search.blank?}" }
= render 'shared/empty_states/priority_labels'
- if @prioritized_labels.any?
@@ -30,14 +31,14 @@
- if @labels.any?
.other-labels
- %h5{ class: ('hide' if hide) }= _('Other Labels')
- .content-list.manage-labels-list.js-other-labels
+ %h4{ class: ('hide' if hide) }= _('Other Labels')
+ .manage-labels-list.js-other-labels
= render partial: 'shared/label', collection: @labels, as: :label, locals: { subject: @project }
= paginate @labels, theme: 'gitlab'
- elsif search.present?
.other-labels
- if @available_labels.any?
- %h5
+ %h4
= _('Other Labels')
.nothing-here-block
= _('No other labels with such name or description')
diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml
index 1ae87bf93d1..7002da0b76a 100644
--- a/app/views/projects/labels/new.html.haml
+++ b/app/views/projects/labels/new.html.haml
@@ -2,7 +2,7 @@
- breadcrumb_title _("New")
- page_title _("New Label")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('New Label')
-%hr
+
= render 'shared/labels/form', url: project_labels_path(@project), back_path: project_labels_path(@project)
diff --git a/app/views/projects/mattermosts/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml
index 1f008496a34..5886c0565b1 100644
--- a/app/views/projects/mattermosts/_no_teams.html.haml
+++ b/app/views/projects/mattermosts/_no_teams.html.haml
@@ -9,4 +9,4 @@
and try again.
%hr
.clearfix
- = link_to 'Go back', edit_project_integration_path(@project, @integration), class: 'gl-button btn btn-lg float-right'
+ = link_to 'Go back', edit_project_settings_integration_path(@project, @integration), class: 'gl-button btn btn-lg float-right'
diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml
index d52d980c364..98221125443 100644
--- a/app/views/projects/mattermosts/_team_selection.html.haml
+++ b/app/views/projects/mattermosts/_team_selection.html.haml
@@ -42,5 +42,5 @@
%hr
.clearfix
.float-right
- = link_to _('Cancel'), edit_project_integration_path(@project, @integration), class: 'gl-button btn btn-lg'
- = f.submit 'Install', class: 'gl-button btn btn-success btn-lg'
+ = link_to _('Cancel'), edit_project_settings_integration_path(@project, @integration), class: 'gl-button btn btn-lg'
+ = f.submit 'Install', class: 'gl-button btn btn-confirm btn-lg'
diff --git a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
index 282faf7714e..a0810cfe37d 100644
--- a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
+++ b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
@@ -20,7 +20,7 @@
%li.gl-new-dropdown-item
= link_to toggle_draft_merge_request_path(@merge_request), method: :put, class: 'dropdown-item js-draft-toggle-button' do
.gl-new-dropdown-item-text-wrapper
- = @merge_request.work_in_progress? ? _('Mark as ready') : _('Mark as draft')
+ = @merge_request.draft? ? _('Mark as ready') : _('Mark as draft')
%li.gl-new-dropdown-item.js-close-item
= link_to close_issuable_path(@merge_request), method: :put, class: 'dropdown-item' do
.gl-new-dropdown-item-text-wrapper
@@ -33,14 +33,15 @@
= _('Reopen')
= display_issuable_type
- - if current_user && moved_mr_sidebar_enabled?
- %li.gl-new-dropdown-divider
- %hr.dropdown-divider
- %li.gl-new-dropdown-item.js-sidebar-subscriptions-entry-point
- - unless issuable_author_is_current_user(@merge_request)
- %li.gl-new-dropdown-item
- = link_to new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)), class: 'dropdown-item' do
- .gl-new-dropdown-item-text-wrapper
- = _('Report abuse')
- - if moved_mr_sidebar_enabled?
- %li.gl-new-dropdown-item#js-lock-entry-point
+ - unless current_controller?('conflicts')
+ - if current_user && moved_mr_sidebar_enabled?
+ %li.gl-new-dropdown-divider
+ %hr.dropdown-divider
+ %li.gl-new-dropdown-item.js-sidebar-subscriptions-entry-point
+ - unless issuable_author_is_current_user(@merge_request)
+ %li.gl-new-dropdown-item
+ = link_to new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)), class: 'dropdown-item' do
+ .gl-new-dropdown-item-text-wrapper
+ = _('Report abuse')
+ - if moved_mr_sidebar_enabled?
+ %li.gl-new-dropdown-item#js-lock-entry-point
diff --git a/app/views/projects/merge_requests/_code_dropdown.html.haml b/app/views/projects/merge_requests/_code_dropdown.html.haml
index 0bd28e315d9..bb42c3067d9 100644
--- a/app/views/projects/merge_requests/_code_dropdown.html.haml
+++ b/app/views/projects/merge_requests/_code_dropdown.html.haml
@@ -34,6 +34,6 @@
.gl-new-dropdown-item-text-wrapper
= _('Email patches')
%li.gl-new-dropdown-item
- = link_to merge_request_path(@merge_request, format: :diff), class: 'dropdown-item', data: { qa_selector: 'download_plain_diff_menu_item' } do
+ = link_to merge_request_path(@merge_request, format: :diff), class: 'dropdown-item', download: '', data: { qa_selector: 'download_plain_diff_menu_item' } do
.gl-new-dropdown-item-text-wrapper
= _('Plain diff')
diff --git a/app/views/projects/merge_requests/_mr_box.html.haml b/app/views/projects/merge_requests/_mr_box.html.haml
index e16631b4943..4fc405c63ff 100644
--- a/app/views/projects/merge_requests/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/_mr_box.html.haml
@@ -1,7 +1,3 @@
-.detail-page-description.py-2
- - if Feature.enabled?(:updated_mr_header, @project)
- = render 'shared/issuable/status_box', issuable: @merge_request
- = merge_request_header(@project, @merge_request)
- - else
- %h2.title.mb-0{ data: { qa_selector: 'title_content' } }
- = markdown_field(@merge_request, :title)
+.detail-page-description.py-2{ class: "#{'is-merge-request' if moved_mr_sidebar_enabled? && !fluid_layout}" }
+ = render 'shared/issuable/status_box', issuable: @merge_request
+ = merge_request_header(@project, @merge_request)
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 638c520e210..4f4acb6103f 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -2,35 +2,30 @@
- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
- can_reopen_merge_request = can?(current_user, :reopen_merge_request, @merge_request)
- are_close_and_open_buttons_hidden = merge_request_button_hidden?(@merge_request, true) && merge_request_button_hidden?(@merge_request, false)
-- updated_mr_header_enabled = Feature.enabled?(:updated_mr_header, @project)
-- cache_key = [@project, @merge_request, can_update_merge_request, can_reopen_merge_request, are_close_and_open_buttons_hidden, current_user&.preferred_language, updated_mr_header_enabled]
+- hide_gutter_toggle = local_assigns.fetch(:hide_gutter_toggle, false)
+- cache_key = [@project, @merge_request, can_update_merge_request, can_reopen_merge_request, are_close_and_open_buttons_hidden, current_user&.preferred_language, "1.1-updated_header", moved_mr_sidebar_enabled?, hide_gutter_toggle]
= cache(cache_key, expires_in: 1.day) do
- if @merge_request.closed_or_merged_without_fork?
- = render Pajamas::AlertComponent.new(alert_class: 'gl-mb-5',
+ = render Pajamas::AlertComponent.new(alert_options: { class: 'gl-mb-5' },
variant: :danger,
dismissible: false) do |c|
= c.body do
= _('The source project of this merge request has been removed.')
- .detail-page-header.border-bottom-0.pt-0.pb-0{ class: "#{'gl-display-block gl-md-display-flex!' if updated_mr_header_enabled}" }
+ .detail-page-header.border-bottom-0.pt-0.pb-0.gl-display-block{ class: "gl-md-display-flex! #{'is-merge-request' if moved_mr_sidebar_enabled? && !fluid_layout}" }
.detail-page-header-body
- - unless updated_mr_header_enabled
- = render "shared/issuable/status_box", issuable: @merge_request
- .issuable-meta{ class: "#{'gl-display-flex' if updated_mr_header_enabled}" }
- - if updated_mr_header_enabled
- #js-issuable-header-warnings
- %h2.title.gl-my-0.gl-display-inline-block{ data: { qa_selector: 'title_content' } }
- = markdown_field(@merge_request, :title)
- - else
- #js-issuable-header-warnings
- = issuable_meta(@merge_request, @project)
+ .issuable-meta.gl-display-flex
+ #js-issuable-header-warnings
+ %h1.title.page-title.gl-font-size-h-display.gl-my-0.gl-display-inline-block{ data: { qa_selector: 'title_content' } }
+ = markdown_field(@merge_request, :title)
- %div
- %button.gl-button.btn.btn-default.btn-icon.float-right.gl-display-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ type: 'button', class: "#{'gl-md-display-none!' if moved_mr_sidebar_enabled? } #{'gl-sm-display-none!' unless moved_mr_sidebar_enabled?}" }
- = sprite_icon('chevron-double-lg-left')
+ - unless hide_gutter_toggle
+ %div
+ - display_class = moved_mr_sidebar_enabled? ? 'gl-md-display-none!' : 'gl-sm-display-none!'
+ = render Pajamas::ButtonComponent.new(icon: "chevron-double-lg-left", button_options: { class: "btn-icon float-right gl-display-block gutter-toggle issuable-gutter-toggle js-sidebar-toggle #{display_class}" })
- .detail-page-header-actions.js-issuable-actions{ class: "#{'gl-align-self-start is-merge-request' if updated_mr_header_enabled}" }
+ .detail-page-header-actions.gl-align-self-start.is-merge-request.js-issuable-actions
- if can_update_merge_request
= link_to _('Edit'), edit_project_merge_request_path(@project, @merge_request), class: "gl-display-none gl-md-display-block btn gl-button btn-default btn-grouped js-issuable-edit", data: { qa_selector: "edit_button" }
diff --git a/app/views/projects/merge_requests/conflicts/show.html.haml b/app/views/projects/merge_requests/conflicts/show.html.haml
index 5ba42ca7610..a882196ffa2 100644
--- a/app/views/projects/merge_requests/conflicts/show.html.haml
+++ b/app/views/projects/merge_requests/conflicts/show.html.haml
@@ -1,13 +1,11 @@
- page_title _("Merge Conflicts"), "#{@merge_request.title} (#{@merge_request.to_reference}", _("Merge requests")
- add_page_specific_style 'page_bundles/merge_conflicts'
-= render "projects/merge_requests/mr_title"
+= render "projects/merge_requests/mr_title", hide_gutter_toggle: true
.merge-request-details.issuable-details
= render "projects/merge_requests/mr_box"
-= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, reviewers: @merge_request.reviewers, source_branch: @merge_request.source_branch
-
#conflicts{ data: { conflicts_path: conflicts_project_merge_request_path(@merge_request.project, @merge_request, format: :json),
resolve_conflicts_path: resolve_conflicts_project_merge_request_path(@merge_request.project, @merge_request),
source_branch_path: project_tree_path(@merge_request.project, @merge_request.source_branch),
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index 811b45ef8af..8cd0d2f9e32 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -1,4 +1,4 @@
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('New merge request')
= form_for [@project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form js-requires-input" } do |f|
@@ -6,60 +6,66 @@
= hidden_field_tag(:nav_source, params[:nav_source])
.js-merge-request-new-compare.row{ 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
.col-lg-6
- .card.card-new-merge-request
- .card-header
+ .card-new-merge-request
+ %h2.gl-font-size-h2
Source branch
- .card-body.clearfix
+ .clearfix
.merge-request-select.dropdown
= f.hidden_field :source_project_id
- = dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-source-project" }
+ = dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted?, default_text: _("Select source project") }, { toggle_class: "js-compare-dropdown js-source-project" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-project
- = dropdown_title("Select source project")
- = dropdown_filter("Search projects")
+ = dropdown_title(_("Select source project"))
+ = dropdown_filter(_("Search projects"))
= dropdown_content do
= render 'projects/merge_requests/dropdowns/project',
projects: [@merge_request.source_project],
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
- = dropdown_toggle f.object.source_branch.presence || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch, qa_selector: "source_branch_dropdown" }, { toggle_class: "js-compare-dropdown js-source-branch monospace" }
+ = dropdown_toggle f.object.source_branch.presence || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch, default_text: _("Select target branch"), qa_selector: "source_branch_dropdown" }, { toggle_class: "js-compare-dropdown js-source-branch monospace" }
.dropdown-menu.dropdown-menu-selectable.js-source-branch-dropdown.git-revision-dropdown
= dropdown_title(_("Select source branch"))
= dropdown_filter(_("Search branches"))
= dropdown_content
= dropdown_loading
- .card-footer
- = gl_loading_icon(css_class: 'js-source-loading gl-my-3')
+ .gl-bg-gray-50.gl-rounded-base.gl-mx-2.gl-my-4
+ .compare-commit-empty.js-source-commit-empty.gl-display-flex.gl-align-items-center.gl-p-5{ style: 'display: none;' }
+ = sprite_icon('branch', size: 16, css_class: 'gl-mr-3')
+ = _('Select a branch to compare')
+ = gl_loading_icon(css_class: 'js-source-loading gl-py-3')
%ul.list-unstyled.mr_source_commit
.col-lg-6
- .card.card-new-merge-request
- .card-header
+ .card-new-merge-request
+ %h2.gl-font-size-h2
Target branch
- .card-body.clearfix
+ .clearfix
- projects = target_projects(@project)
.merge-request-select.dropdown
= f.hidden_field :target_project_id
- = dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" }
+ = dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted?, default_text: _("Select target project") }, { toggle_class: "js-compare-dropdown js-target-project" }
.dropdown-menu.dropdown-menu-selectable.dropdown-target-project
- = dropdown_title("Select target project")
- = dropdown_filter("Search projects")
+ = dropdown_title(_("Select target project"))
+ = dropdown_filter(_("Search projects"))
= dropdown_content do
= render 'projects/merge_requests/dropdowns/project',
projects: projects,
selected: f.object.target_project_id
.merge-request-select.dropdown
= f.hidden_field :target_branch
- = dropdown_toggle f.object.target_branch.presence || _("Select target branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch }, { toggle_class: "js-compare-dropdown js-target-branch monospace" }
+ = dropdown_toggle f.object.target_branch.presence || _("Select target branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch, default_text: _("Select target branch") }, { toggle_class: "js-compare-dropdown js-target-branch monospace" }
.dropdown-menu.dropdown-menu-selectable.js-target-branch-dropdown.git-revision-dropdown
= dropdown_title(_("Select target branch"))
= dropdown_filter(_("Search branches"))
= dropdown_content
= dropdown_loading
- .card-footer
- = gl_loading_icon(css_class: 'js-target-loading gl-my-3')
+ .gl-bg-gray-50.gl-rounded-base.gl-mx-2.gl-my-4
+ .compare-commit-empty.js-target-commit-empty.gl-display-flex.gl-align-items-center.gl-p-5{ style: 'display: none;' }
+ = sprite_icon('branch', size: 16, css_class: 'gl-mr-3')
+ = _('Select a branch to compare')
+ = gl_loading_icon(css_class: 'js-target-loading gl-py-3')
%ul.list-unstyled.mr_target_commit
- if @merge_request.errors.any?
= form_errors(@merge_request)
- = f.submit 'Compare branches and continue', class: "gl-button btn btn-confirm mr-compare-btn", data: { qa_selector: "compare_branches_button" }
+ = f.submit 'Compare branches and continue', class: "gl-button btn btn-confirm mr-compare-btn gl-mt-4", data: { qa_selector: "compare_branches_button" }
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 ef6e930bf23..ef3174efcc7 100644
--- a/app/views/projects/merge_requests/creations/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml
@@ -1,4 +1,4 @@
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('New merge request')
= gitlab_ui_form_for [@project, @merge_request], html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request, commits: @commits, presenter: @mr_presenter
@@ -18,11 +18,11 @@
= custom_icon ('illustration_no_commits')
- else
.merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
- .merge-request-tabs-container
+ .merge-request-tabs-container.gl-display-flex.gl-justify-content-space-between
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
- %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom.js-tabs-affix
+ %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom.gl-display-flex.gl-flex-nowrap.gl-m-0.gl-p-0.js-tabs-affix
%li.commits-tab.new-tab
= link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tabvue'} do
Commits
diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml
index 5fcb5d3f876..77cc69f32ab 100644
--- a/app/views/projects/merge_requests/edit.html.haml
+++ b/app/views/projects/merge_requests/edit.html.haml
@@ -2,6 +2,6 @@
- breadcrumb_title @merge_request.to_reference
- page_title _("Edit"), "#{@merge_request.title} (#{@merge_request.to_reference}", _("Merge requests")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
Edit merge request #{@merge_request.to_reference}
= render 'form'
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 13e5451df98..99b84339058 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -21,8 +21,8 @@
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
= render "projects/merge_requests/mr_box"
.merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
- .merge-request-tabs-container
- %ul.merge-request-tabs.nav-tabs.nav.nav-links
+ .merge-request-tabs-container.gl-display-flex.gl-justify-content-space-between{ class: "#{'is-merge-request' if Feature.enabled?(:moved_mr_sidebar, @project) && !fluid_layout}" }
+ %ul.merge-request-tabs.nav-tabs.nav.nav-links.gl-display-flex.gl-flex-nowrap.gl-m-0.gl-p-0{ class: "#{'gl-w-full gl-lg-w-auto!' if Feature.enabled?(:moved_mr_sidebar, @project)}" }
= render "projects/merge_requests/tabs/tab", class: "notes-tab", qa_selector: "notes_tab" do
= tab_link_for @merge_request, :show, force_link: @commit.present? do
= _("Overview")
@@ -41,6 +41,12 @@
= tab_link_for @merge_request, :diffs do
= _("Changes")
= gl_badge_tag @diffs_count, { size: :sm }
+ - if Feature.enabled?(:moved_mr_sidebar, @project)
+ .gl-ml-auto.gl-align-items-center.gl-display-none.gl-md-display-flex.js-expand-sidebar{ class: "gl-lg-display-none!" }
+ = render Pajamas::ButtonComponent.new(size: 'small',
+ icon: 'angle-double-left',
+ button_options: { class: 'js-sidebar-toggle' }) do
+ = _('Expand')
.d-flex.flex-wrap.align-items-center.justify-content-lg-end
#js-vue-discussion-counter{ data: { blocks_merge: @project.only_allow_merge_if_all_discussions_are_resolved?.to_s } }
@@ -57,13 +63,10 @@
= render "projects/merge_requests/widget"
= render "projects/merge_requests/awards_block"
- if mr_action === "show"
- - if Feature.enabled?(:paginated_notes, @project)
- - add_page_startup_api_call notes_url
- - else
- - add_page_startup_api_call discussions_path(@merge_request)
+ - add_page_startup_api_call Feature.enabled?(:paginated_mr_discussions, @project) ? discussions_path(@merge_request, per_page: 20) : discussions_path(@merge_request)
- add_page_startup_api_call widget_project_json_merge_request_path(@project, @merge_request, format: :json)
- add_page_startup_api_call cached_widget_project_json_merge_request_path(@project, @merge_request, format: :json)
- #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request, Feature.enabled?(:paginated_notes, @project)).to_json,
+ #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request).to_json,
endpoint_metadata: @endpoint_metadata_url,
noteable_data: serialize_issuable(@merge_request, serializer: 'noteable'),
noteable_type: 'MergeRequest',
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index 0d040a5cdb3..8d4ea2c3c21 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -2,7 +2,7 @@
- add_to_breadcrumbs _('Milestones'), project_milestones_path(@project)
- page_title _('Edit'), @milestone.title, _('Milestones')
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('Edit Milestone')
%hr
diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml
index 721506a2201..70ec1b0008a 100644
--- a/app/views/projects/milestones/new.html.haml
+++ b/app/views/projects/milestones/new.html.haml
@@ -2,7 +2,7 @@
- breadcrumb_title _('New')
- page_title _('New Milestone')
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('New Milestone')
%hr
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 4ec72176202..8ff7fe6da71 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -13,8 +13,8 @@
- if can?(current_user, :read_issue, @project) && @milestone.total_issues_count == 0
= render Pajamas::AlertComponent.new(dismissible: false,
- alert_data: { testid: 'no-issues-alert' },
- alert_class: 'gl-mt-3 gl-mb-5') do |c|
+ alert_options: { class: 'gl-mt-3 gl-mb-5',
+ data: { testid: 'no-issues-alert' }}) do |c|
= c.body do
= _('Assign some issues to this milestone.')
- else
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index d689b54678e..339042eb703 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -6,7 +6,7 @@
%section.settings.project-mirror-settings.no-animate#js-push-remote-settings{ class: mirror_settings_class, data: { qa_selector: 'mirroring_repositories_settings_content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Mirroring repositories')
- %button.btn.gl-button.btn-default.js-settings-toggle
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= _('Set up your project to automatically push and/or pull changes to/from another repository. Branches, tags, and commits will be synced automatically.')
@@ -71,8 +71,10 @@
= gl_badge_tag _('Error'), { variant: :danger }, { data: { toggle: 'tooltip', html: 'true', qa_selector: 'mirror_error_badge' }, title: html_escape(mirror.last_error.try(:strip)) }
%td.gl-display-flex
- if mirror_settings_enabled
- %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')
+ = clipboard_button(text: mirror.ssh_public_key, class: 'gl-button btn btn-default btn-icon', title: _('Copy SSH public key'), qa_selector: 'copy_public_key_button')
= render 'shared/remote_mirror_update_button', remote_mirror: mirror
+ = render Pajamas::ButtonComponent.new(variant: :danger,
+ icon: 'remove',
+ button_options: { class: 'js-delete-mirror qa-delete-mirror rspec-delete-mirror', title: _('Remove'), data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' } })
diff --git a/app/views/projects/mirrors/_ssh_host_keys.html.haml b/app/views/projects/mirrors/_ssh_host_keys.html.haml
index 3abab0281a0..e3fe098c807 100644
--- a/app/views/projects/mirrors/_ssh_host_keys.html.haml
+++ b/app/views/projects/mirrors/_ssh_host_keys.html.haml
@@ -3,7 +3,7 @@
- verified_at = mirror.ssh_known_hosts_verified_at
.form-group.js-ssh-host-keys-section{ class: ('collapse' unless mirror.ssh_mirror_url?) }
- %button.btn.gl-button.btn-inverted.btn-secondary.inline.js-detect-host-keys.gl-mr-3{ type: 'button', data: { qa_selector: 'detect_host_keys' } }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-detect-host-keys gl-mr-3', data: { qa_selector: 'detect_host_keys' } }) do
= gl_loading_icon(inline: true, css_class: 'js-spinner gl-display-none gl-mr-2')
= _('Detect host keys')
.fingerprint-ssh-info.js-fingerprint-ssh-info.gl-mt-3.gl-mb-3{ class: ('collapse' unless mirror.ssh_mirror_url?) }
@@ -23,7 +23,7 @@
#{time_ago_in_words(verified_at)} ago
.js-ssh-hosts-advanced.inline
- %button.btn.gl-button.btn-default.btn-show-advanced.show-advanced{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'btn-show-advanced show-advanced' }) do
%span.label-show
= _('Input host keys manually')
%span.label-hide
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 4e4738ebd25..511adf37b39 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -16,8 +16,8 @@
= render 'new_project_fields', f: f, project_name_id: "blank-project-name"
#create-from-template-pane.tab-pane
- .gl-card.gl-my-5
- .gl-card-body
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-my-5' }) do |c|
+ = c.body do
%div
- contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing'
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: contributing_templates_url }
@@ -30,7 +30,7 @@
#import-project-pane.tab-pane.js-toggle-container
- if import_sources_enabled?
- = render 'import_project_pane'
+ = render 'import_project_pane', destination_namespace_id: @namespace&.id
- else
.nothing-here-block
%h4= s_('ProjectsNew|No import options available')
diff --git a/app/views/projects/no_repo.html.haml b/app/views/projects/no_repo.html.haml
index 1331ed24307..a8a30d73000 100644
--- a/app/views/projects/no_repo.html.haml
+++ b/app/views/projects/no_repo.html.haml
@@ -1,7 +1,7 @@
- page_title _('No repository')
- @skip_current_level_breadcrumb = true
-= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
+= render_if_exists 'projects/free_user_cap_alert', project: @project
%h2.gl-display-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml
index 31c14aaad50..9a8b83649de 100644
--- a/app/views/projects/notes/_actions.html.haml
+++ b/app/views/projects/notes/_actions.html.haml
@@ -9,15 +9,14 @@
- if can?(current_user, :award_emoji, note)
- if note.emoji_awardable?
.note-actions-item
- = button_tag title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip btn gl-button btn-icon btn-default-tertiary btn-transparent", data: { position: 'right', container: 'body' } do
- = sprite_icon('slight-smile', css_class: 'link-highlight award-control-icon-neutral gl-button-icon gl-icon gl-text-gray-400')
- = sprite_icon('smiley', css_class: 'link-highlight award-control-icon-positive gl-button-icon gl-icon gl-left-3!')
- = sprite_icon('smile', css_class: 'link-highlight award-control-icon-super-positive gl-button-icon gl-icon gl-left-3! ')
+ = button_tag title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip btn gl-button btn-icon btn-default-tertiary", data: { position: 'right', container: 'body' } do
+ = sprite_icon('slight-smile', css_class: 'award-control-icon-neutral gl-button-icon gl-icon')
+ = sprite_icon('smiley', css_class: 'award-control-icon-positive gl-button-icon gl-icon gl-left-3!')
+ = sprite_icon('smile', css_class: 'award-control-icon-super-positive gl-button-icon gl-icon gl-left-3! ')
- if note_editable
.note-actions-item.gl-ml-0
- = button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn gl-button btn-default-tertiary btn-transparent gl-px-2!', data: { container: 'body', qa_selector: 'edit_comment_button' } do
- %span.link-highlight
- = sprite_icon('pencil', css_class: 'gl-button-icon gl-icon gl-text-gray-400 s16')
+ = button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn gl-button btn-default-tertiary btn-icon', data: { container: 'body', qa_selector: 'edit_comment_button' } do
+ = sprite_icon('pencil', css_class: 'gl-button-icon gl-icon')
= render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml
index 5385c6a4cc6..5f70e25f802 100644
--- a/app/views/projects/notes/_more_actions_dropdown.html.haml
+++ b/app/views/projects/notes/_more_actions_dropdown.html.haml
@@ -2,8 +2,8 @@
- if note_editable || !is_current_user
%div{ class: "dropdown more-actions note-actions-item gl-ml-0!" }
- = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn gl-button btn-default-tertiary btn-transparent gl-pl-2! gl-pr-0!', data: { toggle: 'dropdown', container: 'body', qa_selector: 'more_actions_dropdown' } do
- = sprite_icon('ellipsis_v', css_class: 'gl-button-icon gl-icon gl-text-gray-400')
+ = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn gl-button btn-default-tertiary btn-icon', data: { toggle: 'dropdown', container: 'body', qa_selector: 'more_actions_dropdown' } do
+ = sprite_icon('ellipsis_v', css_class: 'gl-button-icon gl-icon')
%ul.dropdown-menu.more-actions-dropdown.dropdown-open-left
%li
= clipboard_button(text: noteable_note_url(note), title: _('Copy reference'), button_text: _('Copy link'), class: 'btn-clipboard', hide_tooltip: true, hide_button_icon: true)
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
index 04178804de4..0ddf105ef60 100644
--- a/app/views/projects/pages/_list.html.haml
+++ b/app/views/projects/pages/_list.html.haml
@@ -4,20 +4,21 @@
.card
.card-header
Domains (#{@domains.size})
- %ul.list-group.list-group-flush.pages-domain-list{ class: ("has-verification-status" if verification_enabled) }
+ %ul.list-group.list-group-flush
- @domains.each do |domain|
- %li.pages-domain-list-item.list-group-item.d-flex.justify-content-between
- - if verification_enabled
- - tooltip, status = domain.unverified? ? [s_('GitLabPages|Unverified'), 'failed'] : [s_('GitLabPages|Verified'), 'success']
- .domain-status.ci-status-icon.has-tooltip{ class: "ci-status-icon-#{status}", title: tooltip }
- = sprite_icon("status_#{status}" )
- .domain-name
- = external_link(domain.url, domain.url)
- - if domain.certificate
- %div
- = gl_badge_tag(s_('GitLabPages|Certificate: %{subject}') % { subject: domain.pages_domain.subject })
- - if domain.expired?
- = gl_badge_tag s_('GitLabPages|Expired'), variant: :danger
+ %li.list-group-item.gl-display-flex.gl-justify-content-space-between.gl-align-items-center
+ .gl-display-flex.gl-align-items-center
+ - if verification_enabled
+ - tooltip, status = domain.unverified? ? [s_('GitLabPages|Unverified'), 'failed'] : [s_('GitLabPages|Verified'), 'success']
+ .domain-status.ci-status-icon.has-tooltip{ class: "gl-mr-5 ci-status-icon-#{status}", title: tooltip }
+ = sprite_icon("status_#{status}" )
+ .domain-name
+ = external_link(domain.url, domain.url)
+ - if domain.certificate
+ %div
+ = gl_badge_tag(s_('GitLabPages|Certificate: %{subject}') % { subject: domain.pages_domain.subject })
+ - if domain.expired?
+ = gl_badge_tag s_('GitLabPages|Expired'), variant: :danger
%div
= link_to s_('GitLabPages|Edit'), project_pages_domain_path(@project, domain), class: "btn gl-button btn-sm btn-grouped btn-confirm btn-inverted"
= link_to s_('GitLabPages|Remove'), project_pages_domain_path(@project, domain), data: { confirm: s_('GitLabPages|Are you sure?'), 'confirm-btn-variant': 'danger'}, "aria-label": s_("GitLabPages|Remove domain"), method: :delete, class: "btn gl-button btn-danger btn-sm btn-grouped"
diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml
index 64760d8972f..3fea9f9ff1b 100644
--- a/app/views/projects/pages/show.html.haml
+++ b/app/views/projects/pages/show.html.haml
@@ -1,7 +1,7 @@
- page_title _('Pages')
- if @project.pages_enabled?
- %h3.page-title.with-button
+ %h1.page-title.gl-font-size-h-display.with-button
= s_('GitLabPages|Pages')
- if can?(current_user, :update_pages, @project) && (Gitlab.config.pages.external_http || Gitlab.config.pages.external_https)
diff --git a/app/views/projects/pages_domains/_dns.html.haml b/app/views/projects/pages_domains/_dns.html.haml
index 6943469aaac..2c6b808eb1c 100644
--- a/app/views/projects/pages_domains/_dns.html.haml
+++ b/app/views/projects/pages_domains/_dns.html.haml
@@ -19,10 +19,10 @@
.col-sm-2
= _("Verification status")
.col-sm-10
- .status-badge
+ .gl-mb-3
- text, status = domain_presenter.unverified? ? [_('Unverified'), :danger] : [_('Verified'), :success]
= gl_badge_tag text, variant: status
- = link_to sprite_icon("redo"), verify_project_pages_domain_path(@project, domain_presenter), method: :post, class: "gl-ml-2 gl-button btn btn-default has-tooltip", title: _("Retry verification")
+ = link_to sprite_icon("redo"), verify_project_pages_domain_path(@project, domain_presenter), method: :post, class: "gl-ml-2 gl-button btn btn-sm btn-default has-tooltip", title: _("Retry verification")
.input-group
= text_field_tag :domain_verification, verification_record, class: "monospace js-select-on-focus form-control", readonly: true
.input-group-append
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
index 0b794226c7f..6de8117df6b 100644
--- a/app/views/projects/pages_domains/new.html.haml
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -1,6 +1,6 @@
- add_to_breadcrumbs _("Pages"), project_pages_path(@project)
- page_title _('New Pages Domain')
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _("New Pages Domain")
= render 'projects/pages_domains/helper_text'
%div
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
index d16821c3940..0edf75c9abc 100644
--- a/app/views/projects/pages_domains/show.html.haml
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -6,11 +6,12 @@
- if verification_enabled && domain_presenter.unverified?
= content_for :flash_message do
- .gl-alert.gl-alert-warning
- .container-fluid.container-limited
- = _("This domain is not verified. You will need to verify ownership before access is enabled.")
+ = render Pajamas::AlertComponent.new(variant: :warning, dismissible: false) do |c|
+ = c.body do
+ .container-fluid.container-limited
+ = _("This domain is not verified. You will need to verify ownership before access is enabled.")
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('Pages Domain')
= render 'projects/pages_domains/helper_text'
%div
diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml
index 5ff0e2ccac3..d29030f992f 100644
--- a/app/views/projects/pipeline_schedules/_form.html.haml
+++ b/app/views/projects/pipeline_schedules/_form.html.haml
@@ -1,5 +1,5 @@
= gitlab_ui_form_for [@project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "js-pipeline-schedule-form pipeline-schedule-form" } do |f|
- = form_errors(@schedule)
+ = form_errors(@schedule, pajamas_alert: true)
.form-group.row
.col-md-9
= f.label :description, _('Description'), class: 'label-bold'
@@ -28,7 +28,7 @@
= render 'ci/variables/variable_row', form_field: 'schedule', variable: variable
= render 'ci/variables/variable_row', form_field: 'schedule'
- if @schedule.variables.size > 0
- %button.gl-button.btn.btn-confirm-secondary.gl-mt-3.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: "#{@schedule.variables.size == 0}" } }
+ = render Pajamas::ButtonComponent.new(category: :secondary, variant: :confirm, button_options: { class: 'gl-mt-3 js-secret-value-reveal-button', data: { secret_reveal_status: "#{@schedule.variables.size == 0}" }}) do
- if @schedule.variables.size == 0
= n_('Hide value', 'Hide values', @schedule.variables.size)
- else
@@ -38,6 +38,6 @@
= f.label :active, s_('PipelineSchedules|Activated'), class: 'label-bold'
%div
= f.gitlab_ui_checkbox_component :active, _('Active'), checkbox_options: { value: @schedule.active, required: false }
- .footer-block.row-content-block
+ .footer-block
= f.submit _('Save pipeline schedule'), class: 'btn gl-button btn-confirm'
= link_to _('Cancel'), pipeline_schedules_path(@project), class: 'btn gl-button btn-default btn-cancel'
diff --git a/app/views/projects/pipeline_schedules/edit.html.haml b/app/views/projects/pipeline_schedules/edit.html.haml
index 51f0c58330d..642b458eea6 100644
--- a/app/views/projects/pipeline_schedules/edit.html.haml
+++ b/app/views/projects/pipeline_schedules/edit.html.haml
@@ -3,7 +3,7 @@
- page_title _("Edit"), @schedule.description, _("Pipeline Schedule")
- add_page_specific_style 'page_bundles/pipeline_schedules'
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _("Edit Pipeline Schedule")
%hr
diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml
index 10a49fbd779..a56e8f7f5c7 100644
--- a/app/views/projects/pipeline_schedules/index.html.haml
+++ b/app/views/projects/pipeline_schedules/index.html.haml
@@ -1,5 +1,3 @@
-= render_if_exists 'shared/minute_limit_banner', namespace: @project
-
- breadcrumb_title _("Schedules")
- page_title _("Pipeline Schedules")
- add_page_specific_style 'page_bundles/pipeline_schedules'
diff --git a/app/views/projects/pipeline_schedules/new.html.haml b/app/views/projects/pipeline_schedules/new.html.haml
index 1b50090e445..3b4acf5b8c5 100644
--- a/app/views/projects/pipeline_schedules/new.html.haml
+++ b/app/views/projects/pipeline_schedules/new.html.haml
@@ -5,8 +5,7 @@
- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _("Schedule a new pipeline")
-%hr
= render "form"
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 6b26c9f3f00..a8ad53db8c2 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -33,44 +33,7 @@
- if @pipeline.failed_builds.present?
#js-tab-failures.tab-pane
- - if Feature.enabled?(:failed_jobs_tab_vue, @project)
- #js-pipeline-failed-jobs-vue{ data: { full_path: @project.full_path, pipeline_iid: @pipeline.iid, failed_jobs_summary_data: prepare_failed_jobs_summary_data(@pipeline.failed_builds) } }
- - else
- .build-failures.build-page
- %table.table.gl-table.responsive-table.ci-table.responsive-table-sm-rounded
- %thead
- %th
- %th= _('Name')
- %th= _('Stage')
- %th= _('Failure')
- %th
-
- %tbody
- - @pipeline.failed_builds.each_with_index do |build, index|
- - job = build.present(current_user: current_user)
- %tr.build-state.responsive-table-border-start
- %td.responsive-table-cell.ci-status-icon-failed{ data: { column: _('Status')} }
- .d-none.d-md-block.build-icon
- = sprite_icon("status_#{build.status}")
- .d-md-none.build-badge
- = render "ci/status/badge", link: false, status: job.detailed_status(current_user)
- %td.responsive-table-cell.build-name{ data: { column: _('Name')} }
- = link_to build.name, pipeline_job_url(pipeline, build)
- %td.responsive-table-cell.build-stage{ data: { column: _('Stage')} }
- = build.stage.titleize
- %td.responsive-table-cell.build-failure{ data: { column: _('Failure')} }
- = build.present.callout_failure_message
- %td.responsive-table-cell.build-actions
- - if can?(current_user, :update_build, job) && job.retryable?
- = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'gl-button btn btn-default btn-icon' do
- = sprite_icon('repeat', css_class: 'gl-icon')
- - if can?(current_user, :read_build, job)
- %tr.build-log-row.responsive-table-border-end
- %td
- %td.responsive-table-cell.build-log-container{ colspan: 4 }
- %pre.build-log.build-log-rounded
- %code.bash.js-build-output
- = build_summary(build)
+ #js-pipeline-failed-jobs-vue{ data: { full_path: @project.full_path, pipeline_iid: @pipeline.iid, failed_jobs_summary_data: prepare_failed_jobs_summary_data(@pipeline.failed_builds) } }
#js-tab-dag.tab-pane
#js-pipeline-dag-vue{ data: { pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, empty_svg_path: image_path('illustrations/empty-state/empty-dag-md.svg'), about_dag_doc_path: help_page_path('ci/directed_acyclic_graph/index.md'), dag_doc_path: help_page_path('ci/yaml/index.md', anchor: 'needs')} }
@@ -80,5 +43,6 @@
suite_endpoint: project_pipeline_test_path(@project, @pipeline, suite_name: 'suite', format: :json),
blob_path: project_blob_path(@project, @pipeline.sha),
has_test_report: @pipeline.has_reports?(Ci::JobArtifact.test_reports).to_s,
- empty_state_image_path: image_path('illustrations/empty-state/empty-test-cases-lg.svg') } }
+ empty_state_image_path: image_path('illustrations/empty-state/empty-test-cases-lg.svg'),
+ artifacts_expired_image_path: image_path('illustrations/pipeline.svg') } }
= render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 817cc6d6e6c..f4b242ffc40 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -1,5 +1,3 @@
-= render_if_exists 'shared/minute_limit_banner', namespace: @project
-
- page_title _('Pipelines')
- add_page_specific_style 'page_bundles/pipelines'
- add_page_specific_style 'page_bundles/ci_status'
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index e92f14fcc63..a4144f8ab0d 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -1,7 +1,7 @@
- breadcrumb_title _('Pipelines')
- page_title s_('Pipeline|Run pipeline')
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= s_('Pipeline|Run pipeline')
%hr
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 30b224a60da..10ff9c31c3e 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -26,7 +26,6 @@
- lint_link_start = '<a href="%{url}" class="gl-text-blue-500!">'.html_safe % { url: lint_link_url }
= s_('You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}').html_safe % { gitlab_ci_yml: '.gitlab-ci.yml', lint_link_start: lint_link_start, lint_link_end: '</a>'.html_safe }
- #js-pipeline-notification{ data: { deprecated_keywords_doc_path: help_page_path('ci/yaml/index.md', anchor: 'deprecated-keywords'), full_path: @project.full_path, pipeline_iid: @pipeline.iid } }
- if Feature.enabled?(:pipeline_tabs_vue, @project)
#js-pipeline-tabs{ data: js_pipeline_tabs_data(@project, @pipeline) }
- else
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 298c2074062..8c616b89658 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,7 +1,8 @@
- add_page_specific_style 'page_bundles/members'
- page_title _("Members")
-= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
+= render_if_exists 'projects/free_user_cap_alert', project: @project
+= render_if_exists 'shared/minute_limit_banner', namespace: @project
.row.gl-mt-3
.col-lg-12
@@ -12,11 +13,8 @@
%h4
= _("Project members")
.gl-justify-content-bottom.gl-display-flex.align-items-center
- - 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 }
+ %p
+ = project_member_header_subtext(@project)
.col-md-12.col-lg-6
.gl-display-flex.gl-flex-wrap.gl-justify-content-end
- if can_admin_project_member?(@project)
@@ -36,7 +34,8 @@
%h4
= _("Project members")
- if can?(current_user, :admin_project_member, @project)
- %p= share_project_description(@project)
+ %p
+ = project_member_header_subtext(@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 }
diff --git a/app/views/projects/prometheus/metrics/edit.html.haml b/app/views/projects/prometheus/metrics/edit.html.haml
index 146bf6b6853..212d625d292 100644
--- a/app/views/projects/prometheus/metrics/edit.html.haml
+++ b/app/views/projects/prometheus/metrics/edit.html.haml
@@ -1,6 +1,6 @@
- add_to_breadcrumbs _("Settings"), edit_project_path(@project)
- add_to_breadcrumbs _("Integrations"), project_settings_integrations_path(@project)
-- add_to_breadcrumbs "Prometheus", edit_project_integration_path(@project, ::Integrations::Prometheus)
+- add_to_breadcrumbs "Prometheus", edit_project_settings_integration_path(@project, ::Integrations::Prometheus)
- breadcrumb_title s_('Metrics|Edit metric')
- page_title @metric.title, s_('Metrics|Edit metric')
= render 'form', project: @project, metric: @metric
diff --git a/app/views/projects/prometheus/metrics/new.html.haml b/app/views/projects/prometheus/metrics/new.html.haml
index ad8463d1804..c04e5f385d9 100644
--- a/app/views/projects/prometheus/metrics/new.html.haml
+++ b/app/views/projects/prometheus/metrics/new.html.haml
@@ -1,6 +1,6 @@
- add_to_breadcrumbs _("Settings"), edit_project_path(@project)
- add_to_breadcrumbs _("Integrations"), project_settings_integrations_path(@project)
-- add_to_breadcrumbs "Prometheus", edit_project_integration_path(@project, ::Integrations::Prometheus)
+- add_to_breadcrumbs "Prometheus", edit_project_settings_integration_path(@project, ::Integrations::Prometheus)
- breadcrumb_title s_('Metrics|New metric')
- page_title s_('Metrics|New metric')
= render 'form', project: @project, metric: @metric
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index 2e9a9357fb0..1d60791eae2 100644
--- a/app/views/projects/protected_branches/shared/_index.html.haml
+++ b/app/views/projects/protected_branches/shared/_index.html.haml
@@ -4,8 +4,8 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= s_("ProtectedBranch|Protected branches")
- %button.btn.gl-button.btn-default.js-settings-toggle.qa-expand-protected-branches{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle qa-expand-protected-branches' }) do
+ = expanded ? _('Collapse') : _('Expand')
%p
= s_("ProtectedBranch|Keep stable branches secure and force developers to use merge requests.")
= link_to s_("ProtectedBranch|What are protected branches?"), help_page_path("user/project/protected_branches")
diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml
index 8f5ce798dc7..11e09d843e0 100644
--- a/app/views/projects/protected_tags/shared/_index.html.haml
+++ b/app/views/projects/protected_tags/shared/_index.html.haml
@@ -4,8 +4,8 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= s_("ProtectedTag|Protected tags")
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
%p
= s_("ProtectedTag|Limit access to creating and updating tags.")
= link_to s_("ProtectedTag|What are protected tags?"), help_page_path("user/project/protected_tags")
diff --git a/app/views/projects/readme_templates/default.md.tt b/app/views/projects/readme_templates/default.md.tt
index d5fef29b290..cd0b2db1d31 100644
--- a/app/views/projects/readme_templates/default.md.tt
+++ b/app/views/projects/readme_templates/default.md.tt
@@ -47,7 +47,7 @@ Use the built-in continuous integration in GitLab.
# Editing this README
-When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
+When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index 88ca64f2af0..1d53726e25c 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -1,3 +1,5 @@
- page_title _('Edit Release')
+- add_to_breadcrumbs _('Releases'), project_releases_path(@project)
+- add_to_breadcrumbs @release.name, project_release_path(@project, @release)
#js-edit-release-page{ data: data_for_edit_release_page }
diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml
index e87c52ff1a8..ce56b160187 100644
--- a/app/views/projects/runners/edit.html.haml
+++ b/app/views/projects/runners/edit.html.haml
@@ -3,7 +3,7 @@
- add_to_breadcrumbs _('CI/CD Settings'), project_settings_ci_cd_path(@project)
- add_to_breadcrumbs "#{@runner.short_sha}", project_runner_path(@project, @runner)
-%h2.page-title
+%h1.page-title.gl-font-size-h-display
= s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id })
= render 'shared/runners/runner_type_badge', runner: @runner
diff --git a/app/views/projects/settings/_archive.html.haml b/app/views/projects/settings/_archive.html.haml
index 1b0294bc967..8a080241513 100644
--- a/app/views/projects/settings/_archive.html.haml
+++ b/app/views/projects/settings/_archive.html.haml
@@ -8,14 +8,14 @@
= _('Archive project')
- if @project.archived?
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'unarchiving-a-project') }
- %p= _("Unarchiving the project will restore its members' ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe }
+ %p= _("Unarchiving the project restores its members' ability to make commits, and create issues, comments, and other entities. %{strong_start}After you unarchive the project, it displays in the search and on the dashboard.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe }
= link_to _('Unarchive project'), unarchive_project_path(@project),
aria: { label: _('Unarchive project') },
data: { confirm: _("Are you sure that you want to unarchive this project?"), qa_selector: 'unarchive_project_link' },
method: :post, class: "gl-button btn btn-confirm"
- else
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'archiving-a-project') }
- %p= _("Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe }
+ %p= _("Archiving the project makes it entirely read-only. It is hidden from the dashboard and doesn't display in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe }
= link_to _('Archive project'), archive_project_path(@project),
aria: { label: _('Archive project') },
data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link', 'confirm-btn-variant': 'confirm' },
diff --git a/app/views/projects/settings/access_tokens/index.html.haml b/app/views/projects/settings/access_tokens/index.html.haml
index e4b027fcc44..359e34d8918 100644
--- a/app/views/projects/settings/access_tokens/index.html.haml
+++ b/app/views/projects/settings/access_tokens/index.html.haml
@@ -36,7 +36,7 @@
resource: @project,
token: @resource_access_token,
scopes: @scopes,
- access_levels: ProjectMember.access_level_roles,
+ access_levels: ProjectMember.permissible_access_level_roles(current_user, @project),
default_access_level: Gitlab::Access::MAINTAINER,
prefix: :resource_access_token,
help_path: help_page_path('user/project/settings/project_access_tokens', anchor: 'scopes-for-a-project-access-token')
diff --git a/app/views/projects/settings/branch_rules/index.html.haml b/app/views/projects/settings/branch_rules/index.html.haml
new file mode 100644
index 00000000000..384d504e51f
--- /dev/null
+++ b/app/views/projects/settings/branch_rules/index.html.haml
@@ -0,0 +1,6 @@
+- add_to_breadcrumbs _('Repository Settings'), project_settings_repository_path(@project)
+- page_title _('Branch rules')
+
+%h3= _('Branch rules')
+
+#js-branch-rules{ data: { project_path: @project.full_path } }
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index 7783e83b88f..96564e44cf2 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -9,6 +9,10 @@
- base_domain_path = help_page_path('user/project/clusters/gitlab_managed_clusters', anchor: 'base-domain')
- base_domain_link_start = link_start % { url: base_domain_path }
+- help_link_continouos = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/stages.md', anchor: 'auto-deploy'), target: '_blank', rel: 'noopener noreferrer'
+- help_link_timed = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/customize.md', anchor: 'timed-incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
+- help_link_incremental = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/customize.md', anchor: 'incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
+
.row
.col-lg-12
= gitlab_ui_form_for @project, url: project_settings_ci_cd_path(@project, anchor: 'autodevops-settings') do |f|
@@ -33,22 +37,8 @@
= s_('CICD|Add a %{base_domain_link_start}base domain%{link_end} to your %{kubernetes_cluster_link_start}Kubernetes cluster%{link_end} for your deployment strategy to work.').html_safe % { base_domain_link_start: base_domain_link_start, kubernetes_cluster_link_start: kubernetes_cluster_link_start, link_end: link_end }
%label.gl-mt-3
%strong= s_('CICD|Deployment strategy')
- .form-check
- = form.radio_button :deploy_strategy, 'continuous', class: 'form-check-input'
- = form.label :deploy_strategy_continuous, class: 'form-check-label' do
- = s_('CICD|Continuous deployment to production')
- = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/stages.md', anchor: 'auto-deploy'), target: '_blank', rel: 'noopener noreferrer'
-
- .form-check
- = form.radio_button :deploy_strategy, 'timed_incremental', class: 'form-check-input'
- = form.label :deploy_strategy_timed_incremental, class: 'form-check-label' do
- = s_('CICD|Continuous deployment to production using timed incremental rollout')
- = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/customize.md', anchor: 'timed-incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
-
- .form-check
- = form.radio_button :deploy_strategy, 'manual', class: 'form-check-input'
- = form.label :deploy_strategy_manual, class: 'form-check-label' do
- = s_('CICD|Automatic deployment to staging, manual deployment to production')
- = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/customize.md', anchor: 'incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
+ = form.gitlab_ui_radio_component :deploy_strategy, 'continuous', (s_('CICD|Continuous deployment to production') + ' ' + help_link_continouos).html_safe
+ = form.gitlab_ui_radio_component :deploy_strategy, 'timed_incremental', (s_('CICD|Continuous deployment to production using timed incremental rollout') + ' ' + help_link_timed).html_safe
+ = form.gitlab_ui_radio_component :deploy_strategy, 'manual', (s_('CICD|Automatic deployment to staging, manual deployment to production') + ' ' + help_link_incremental).html_safe
= f.submit _('Save changes'), class: "btn gl-button btn-confirm gl-mt-5", data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 508e63f77d8..9419dacc16f 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -39,25 +39,19 @@
%hr
.form-group
- %h5.gl-mt-0
+ %h5.gl-mt-0.gl-mb-3
= _("Git strategy")
- %p
+ .gl-mb-3
= _("Choose which Git strategy to use when fetching the project.")
= link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'choose-the-default-git-strategy'), target: '_blank', rel: 'noopener noreferrer'
- .form-check
- = f.radio_button :build_allow_git_fetch, 'false', { class: 'form-check-input' }
- = f.label :build_allow_git_fetch_false, class: 'form-check-label' do
- %strong git clone
- %br
- %span
- = _("For each job, clone the repository.")
- .form-check
- = f.radio_button :build_allow_git_fetch, 'true', { class: 'form-check-input' }
- = f.label :build_allow_git_fetch_true, class: 'form-check-label' do
- %strong git fetch
- %br
- %span
- = html_escape(_("For each job, re-use the project workspace. If the workspace doesn't exist, use %{code_open}git clone%{code_close}.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+ = f.gitlab_ui_radio_component :build_allow_git_fetch,
+ false,
+ "git clone",
+ help_text: _("For each job, clone the repository.")
+ = f.gitlab_ui_radio_component :build_allow_git_fetch,
+ true,
+ "git fetch",
+ help_text: html_escape(_("For each job, re-use the project workspace. If the workspace doesn't exist, use %{code_open}git clone%{code_close}.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
.form-group
= f.fields_for :ci_cd_settings_attributes, @project.ci_cd_settings do |form|
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 87ca13a7bd6..5da3d2b891c 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -1,5 +1,3 @@
-= render_if_exists 'shared/minute_limit_banner', namespace: @project
-
- @content_class = "limit-container-width" unless fluid_layout
- page_title _("CI/CD Settings")
- page_title _("CI/CD")
@@ -11,7 +9,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("General pipelines")
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= _("Customize your pipeline configuration.")
@@ -22,7 +20,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= s_('CICD|Auto DevOps')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
- auto_devops_url = help_page_path('topics/autodevops/index')
@@ -39,7 +37,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("Runners")
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
@@ -52,7 +50,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("Artifacts")
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= _("A job artifact is an archive of files and directories saved by a job when it finishes.")
@@ -69,7 +67,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("Pipeline triggers")
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= _("Trigger a pipeline for a branch or tag by generating a trigger token and using it with an API call. The token impersonates a user's project access and permissions.")
@@ -84,7 +82,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("Deploy freezes")
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
- freeze_period_docs = help_page_path('user/project/releases/index', anchor: 'prevent-unintentional-releases-by-setting-a-deploy-freeze')
@@ -102,7 +100,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("Token Access")
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p
= _("Control which projects can be accessed by API requests authenticated with this project's CI_JOB_TOKEN CI/CD variable. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API.")
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/settings/integrations/_form.html.haml
index 9d74f99bb19..9d74f99bb19 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/settings/integrations/_form.html.haml
diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/settings/integrations/edit.html.haml
index a250daafdbb..a250daafdbb 100644
--- a/app/views/projects/services/edit.html.haml
+++ b/app/views/projects/settings/integrations/edit.html.haml
diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/index.html.haml
index 84635941436..84635941436 100644
--- a/app/views/projects/settings/integrations/show.html.haml
+++ b/app/views/projects/settings/integrations/index.html.haml
diff --git a/app/views/projects/settings/operations/_alert_management.html.haml b/app/views/projects/settings/operations/_alert_management.html.haml
index 34255af9cc6..d80f1e4597c 100644
--- a/app/views/projects/settings/operations/_alert_management.html.haml
+++ b/app/views/projects/settings/operations/_alert_management.html.haml
@@ -7,7 +7,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Alerts')
- %button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= _('Expand')
%p
= _('Display alerts from all configured monitoring tools.')
diff --git a/app/views/projects/settings/operations/_error_tracking.html.haml b/app/views/projects/settings/operations/_error_tracking.html.haml
index 23b1ec4dea3..5d89790ef9f 100644
--- a/app/views/projects/settings/operations/_error_tracking.html.haml
+++ b/app/views/projects/settings/operations/_error_tracking.html.haml
@@ -6,7 +6,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Error tracking')
- %button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= _('Expand')
%p
= _('Link Sentry to GitLab to discover and view the errors your application generates.')
diff --git a/app/views/projects/settings/operations/_tracing.html.haml b/app/views/projects/settings/operations/_tracing.html.haml
index 343fd22c051..3c8ebe3fb20 100644
--- a/app/views/projects/settings/operations/_tracing.html.haml
+++ b/app/views/projects/settings/operations/_tracing.html.haml
@@ -4,7 +4,7 @@
.settings-header{ :class => 'border-top' }
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Tracing')
- %button.btn.btn-default.gl-button.js-settings-toggle{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= _('Expand')
%p
= _('Embed an image of your existing Jaeger server in GitLab.')
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index 24fc137fd29..500cfdcb62b 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -4,6 +4,8 @@
- deploy_token_description = s_('DeployTokens|Deploy tokens allow access to packages, your repository, and registry images.')
= render "projects/default_branch/show"
+- if Feature.enabled?(:branch_rules, @project)
+ = render "projects/branch_rules/show"
= render_if_exists "projects/push_rules/index"
= render "projects/mirrors/mirror_repos"
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 1934f293b0f..290ef79f261 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -6,9 +6,10 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity")
-= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
+= render_if_exists 'projects/free_user_cap_alert', project: @project
= render_if_exists 'shared/minute_limit_banner', namespace: @project
= render partial: 'flash_messages', locals: { project: @project }
+= render 'clusters_deprecation_alert'
= render "projects/last_push"
diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml
index 9f5af1cfe1e..d9bf064ad24 100644
--- a/app/views/projects/snippets/edit.html.haml
+++ b/app/views/projects/snippets/edit.html.haml
@@ -3,7 +3,7 @@
- page_title _("Edit"), "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
- @content_class = "limit-container-width" unless fluid_layout
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _("Edit Snippet")
%hr
= render "shared/snippets/form", url: project_snippet_path(@project, @snippet)
diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml
index d55a1160d48..5086b5eaa3d 100644
--- a/app/views/projects/snippets/new.html.haml
+++ b/app/views/projects/snippets/new.html.haml
@@ -3,7 +3,7 @@
- page_title _("New Snippet")
- @content_class = "limit-container-width" unless fluid_layout
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _("New Snippet")
%hr
= render "shared/snippets/form", url: project_snippets_path(@project, @snippet)
diff --git a/app/views/projects/static_site_editor/show.html.haml b/app/views/projects/static_site_editor/show.html.haml
deleted file mode 100644
index cbe27cefba3..00000000000
--- a/app/views/projects/static_site_editor/show.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-#static-site-editor{ data: @data }
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 0ee3b89b629..7654150509e 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -21,7 +21,7 @@
.text-secondary
= sprite_icon("rocket", size: 12)
= _("Release")
- = link_to release.name, project_releases_path(@project, anchor: release.tag), class: 'gl-text-blue-600!'
+ = link_to release.name, project_release_path(@project, release), class: 'gl-text-blue-600!'
- if tag.message.present?
%pre.wrap
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index a654d0a8863..2721f94134c 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -38,3 +38,6 @@
= s_('TagsPage|Use git tag command to add a new one:')
%br
%span.monospace git tag -a v1.4 -m 'version 1.4'
+
+- if can?(current_user, :admin_tag, @project)
+ .js-delete-tag-modal
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 4281152225a..3b546888375 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -2,13 +2,11 @@
- default_ref = params[:ref] || @project.default_branch
- if @error
- .gl-alert.gl-alert-danger
- = sprite_icon('error', 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')
- = @error
+ = render Pajamas::AlertComponent.new(variant: :danger, dismissible: true, close_button_options: { class: 'gl-alert-dismiss' }) do |c|
+ = c.body do
+ = @error
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= s_('TagsPage|New Tag')
%hr
@@ -52,7 +50,9 @@
= render 'shared/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here…'), current_text: @release_description, qa_selector: 'release_notes_field'
= render 'shared/notes/hints'
.form-actions.gl-display-flex
- = button_tag s_('TagsPage|Create tag'), class: 'gl-button btn btn-confirm gl-mr-3', data: { qa_selector: "create_tag_button" }
- = link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'gl-button btn btn-default btn-cancel'
+ = render Pajamas::ButtonComponent.new(variant: :confirm, button_options: { class: 'gl-mr-3', data: { qa_selector: "create_tag_button" }, type: 'submit' }) do
+ = s_('TagsPage|Create tag')
+ = render Pajamas::ButtonComponent.new(href: project_tags_path(@project)) do
+ = s_('TagsPage|Cancel')
-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index c1b78d3258d..2a68ad37c1e 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -63,3 +63,6 @@
= markdown_field(@release, :description)
- else
= s_('TagsPage|This tag has no release notes.')
+
+- if can?(current_user, :admin_tag, @project)
+ .js-delete-tag-modal
diff --git a/app/views/projects/tracings/show.html.haml b/app/views/projects/tracings/show.html.haml
index c9aac68b19d..61f2cd8ac7f 100644
--- a/app/views/projects/tracings/show.html.haml
+++ b/app/views/projects/tracings/show.html.haml
@@ -17,7 +17,7 @@
= html_escape(s_('Deprecations|The logs and tracing features were deprecated in GitLab 14.7, and are %{removal_link_start} scheduled for removal %{link_end} in GitLab 15.0. For information on a possible replacement, %{opstrace_link_start} learn more about Opstrace %{link_end}.')) % {removal_link_start: removal_epic_link_start, opstrace_link_start: opstrace_link_start, link_end: link_end }
- if @project.tracing_external_url.present?
- %h3.page-title= _('Tracing')
+ %h1.page-title.gl-font-size-h-display= _('Tracing')
.gl-alert.gl-alert-info.gl-mb-5
.gl-alert-container
= sprite_icon('information-o', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
diff --git a/app/views/projects/usage_quotas/index.html.haml b/app/views/projects/usage_quotas/index.html.haml
index 5b4edc92d1d..3de9bce14d4 100644
--- a/app/views/projects/usage_quotas/index.html.haml
+++ b/app/views/projects/usage_quotas/index.html.haml
@@ -1,6 +1,14 @@
- page_title s_("UsageQuota|Usage")
-%h3.page-title
+= render_if_exists 'namespaces/free_user_cap/projects/usage_quota_limitations_banner'
+
+= render Pajamas::AlertComponent.new(title: _('Repository usage recalculation started'),
+ variant: :info,
+ alert_options: { class: 'js-recalculation-started-alert gl-mt-4 gl-mb-5 gl-display-none' }) do |c|
+ = c.body do
+ = _('To view usage, refresh this page in a few minutes.')
+
+%h1.page-title.gl-font-size-h-display
= s_('UsageQuota|Usage Quotas')
.row
diff --git a/app/views/projects/work_items/index.html.haml b/app/views/projects/work_items/index.html.haml
index 356f93c6ed5..1f36afc48aa 100644
--- a/app/views/projects/work_items/index.html.haml
+++ b/app/views/projects/work_items/index.html.haml
@@ -1,3 +1,3 @@
- page_title s_('WorkItem|Work Items')
-#js-work-items{ data: { full_path: @project.full_path, issues_list_path: project_issues_path(@project) } }
+#js-work-items{ data: work_items_index_data(@project) }
diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb
new file mode 100644
index 00000000000..557a39ee157
--- /dev/null
+++ b/app/views/pwa/manifest.json.erb
@@ -0,0 +1,27 @@
+{
+ "name": "GitLab",
+ "short_name": "GitLab",
+ "description": "<%= _("The complete DevOps platform. One application with endless possibilities. Organizations rely on GitLab’s source code management, CI/CD, security, and more to deliver software rapidly.") %>",
+ "start_url": "<%= explore_projects_path %>",
+ "scope": "<%= root_path %>",
+ "display": "browser",
+ "orientation": "any",
+ "background_color": "#fff",
+ "theme_color": "<%= user_theme_primary_color %>",
+ "icons": [{
+ "src": "<%= Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/-/pwa-icons/logo-192.png') %>",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "<%= Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/-/pwa-icons/logo-512.png') %>",
+ "sizes": "512x512",
+ "type": "image/png"
+ },
+ {
+ "src": "<%= Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/-/pwa-icons/maskable-logo.png') %>",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }]
+}
diff --git a/app/views/registrations/welcome/show.html.haml b/app/views/registrations/welcome/show.html.haml
index 62499f1a6b6..911ba5e8042 100644
--- a/app/views/registrations/welcome/show.html.haml
+++ b/app/views/registrations/welcome/show.html.haml
@@ -24,11 +24,6 @@
.form-group.col-sm-12
= f.label :role, _('Role'), class: 'label-bold'
= f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, { include_blank: _('Select a role') }, class: 'form-control js-user-role-dropdown', autofocus: true, required: true, data: { qa_selector: 'role_dropdown' }
- - if Feature.enabled?(:user_other_role_details)
- .row
- .form-group.col-sm-12.js-other-role-group.hidden
- = f.label :other_role, _('What is your job title? (optional)')
- = f.text_field :other_role, class: 'form-control'
= render_if_exists "registrations/welcome/jobs_to_be_done", f: f
= render_if_exists "registrations/welcome/setup_for_company", f: f
= render_if_exists "registrations/welcome/joining_project"
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index ab5ca0cd90f..5a45e512579 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -15,8 +15,8 @@
- 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
- %h1.page-title.mr-3= _('Search')
+.page-title-holder.gl-display-flex.gl-flex-wrap.gl-justify-content-space-between
+ %h1.page-title.gl-font-size-h-display.gl-mr-5= _('Search')
= render_if_exists 'search/form_elasticsearch', attrs: { class: 'mb-2 mb-sm-0 align-self-center' }
.gl-mt-3
diff --git a/app/views/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml
index cacfd601d4d..03b030eb257 100644
--- a/app/views/sent_notifications/unsubscribe.html.haml
+++ b/app/views/sent_notifications/unsubscribe.html.haml
@@ -6,7 +6,7 @@
- noteable_url = show_project_path ? url_for([@sent_notification.project, noteable]) : breadcrumb_title_link
- page_title _('Unsubscribe'), noteable_text, noteable_type.pluralize, project_path
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _("Unsubscribe from %{type}") % { type: noteable_type }
%p
diff --git a/app/views/shared/_alert_info.html.haml b/app/views/shared/_alert_info.html.haml
index e47c100909a..30dfc87b9bf 100644
--- a/app/views/shared/_alert_info.html.haml
+++ b/app/views/shared/_alert_info.html.haml
@@ -1,6 +1,3 @@
-.gl-alert.gl-alert-info
- = sprite_icon('information-o', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
- %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
- = sprite_icon('close', css_class: 'gl-icon')
- .gl-alert-body
+= render Pajamas::AlertComponent.new(variant: :info, dismissible: true) do |c|
+ = c.body do
= body
diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml
index d6d84b2181f..c2b941c6106 100644
--- a/app/views/shared/_auto_devops_callout.html.haml
+++ b/app/views/shared/_auto_devops_callout.html.haml
@@ -1,15 +1,13 @@
-%section.js-autodevops-banner.gl-banner{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
- .gl-banner-illustration
- = image_tag('illustrations/autodevops.svg')
+= render Pajamas::BannerComponent.new(button_text: s_('AutoDevOps|Enable in settings'),
+ button_link: project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'),
+ svg_path: 'illustrations/autodevops.svg',
+ banner_options: { class: 'js-autodevops-banner', data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } },
+ close_options: { 'aria-label' => s_('AutoDevOps|Dismiss Auto DevOps box'), class: 'js-close-callout' }) do |c|
+ - c.title do
+ = s_('AutoDevOps|Auto DevOps')
- .gl-banner-content
- %h1.gl-banner-title= s_('AutoDevOps|Auto DevOps')
- %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
- %p
- - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
- = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
- = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn btn-md btn-default gl-button js-close-callout'
+ %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
- %button.gl-banner-close.close.js-close-callout{ type: 'button',
- 'aria-label' => s_('AutoDevOps|Dismiss Auto DevOps box') }
- = sprite_icon('close', size: 16, css_class: 'dismiss-icon')
+ %p
+ - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
+ = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
diff --git a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
index d69f54608e9..1f37e33a037 100644
--- a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
+++ b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
@@ -1,7 +1,7 @@
- if show_auto_devops_implicitly_enabled_banner?(project, current_user)
- = render Pajamas::AlertComponent.new(alert_class: 'qa-auto-devops-banner auto-devops-implicitly-enabled-banner',
- close_button_class: 'hide-auto-devops-implicitly-enabled-banner',
- close_button_data: { project_id: project.id }) do |c|
+ = render Pajamas::AlertComponent.new(alert_options: { class: 'qa-auto-devops-banner auto-devops-implicitly-enabled-banner' },
+ close_button_options: { class: 'hide-auto-devops-implicitly-enabled-banner',
+ data: { project_id: project.id }}) do |c|
= c.body do
= s_("AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found.")
- unless Gitlab.config.registry.enabled
diff --git a/app/views/shared/_broadcast_message.html.haml b/app/views/shared/_broadcast_message.html.haml
index ab6423e9ade..f7794677dc1 100644
--- a/app/views/shared/_broadcast_message.html.haml
+++ b/app/views/shared/_broadcast_message.html.haml
@@ -13,8 +13,11 @@
- else
= yield
- if dismissable && !preview
- %button.btn.gl-close-btn-color-inherit.gl-broadcast-message-dismiss.btn-default.btn-sm.gl-button.btn-default-tertiary.btn-icon.js-dismiss-current-broadcast-notification{ 'aria-label' => _('Close'), :type => 'button', data: { id: message.id, expire_date: message.ends_at.iso8601 } }
- = sprite_icon('close', size: 16, css_class: "gl-icon gl-mx-3! gl-text-white")
+ = render Pajamas::ButtonComponent.new(category: :tertiary,
+ icon: 'close',
+ size: :small,
+ button_options: { class: 'gl-close-btn-color-inherit gl-broadcast-message-dismiss js-dismiss-current-broadcast-notification', 'aria-label': _('Close'), data: { id: message.id, expire_date: message.ends_at.iso8601 } },
+ icon_classes: 'gl-mx-3! gl-text-white')
- else
- notification_class = "js-broadcast-notification-#{message.id}"
- notification_class << ' preview' if preview
@@ -25,5 +28,8 @@
- else
= yield
- if !preview
- %button.js-dismiss-current-broadcast-notification.btn.btn-link.gl-button{ 'aria-label' => _('Close'), :type => 'button', data: { id: message.id, expire_date: message.ends_at.iso8601 } }
- = sprite_icon('close', size: 16, css_class: "gl-icon gl-mx-3! gl-text-gray-700")
+ = render Pajamas::ButtonComponent.new(variant: :link,
+ icon: 'close',
+ size: :small,
+ button_options: { class: 'js-dismiss-current-broadcast-notification', 'aria-label': _('Close'), data: { id: message.id, expire_date: message.ends_at.iso8601 } },
+ icon_classes: 'gl-mx-3! gl-text-gray-700')
diff --git a/app/views/shared/_captcha_check.html.haml b/app/views/shared/_captcha_check.html.haml
index 3d611c22491..a10ae655ea6 100644
--- a/app/views/shared/_captcha_check.html.haml
+++ b/app/views/shared/_captcha_check.html.haml
@@ -3,7 +3,7 @@
- script = local_assigns.fetch(:script, true)
- method = params[:action] == 'create' ? :post : :put
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _('Anti-spam verification')
%hr
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index ee8cfe3abb6..5ae99474c70 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -2,38 +2,42 @@
- group_path = root_url
- group_path << parent.full_path + '/' if parent
-.row
- .form-group.group-name-holder.col-sm-12
- = f.label :name, class: 'label-bold' do
- = s_('Groups|Group name')
- = f.text_field :name, placeholder: _('My awesome group'), class: 'js-autofill-group-name form-control input-lg', data: { qa_selector: 'group_name_field' },
- required: true,
- title: s_('Groups|Enter a descriptive name for your group.'),
- autofocus: true
- .text-muted
- = s_('Groups|Must start with letter, digit, emoji, or underscore. Can also contain periods, dashes, spaces, and parentheses.')
-.row
- .form-group.col-xs-12.col-sm-8
- = f.label :path, class: 'label-bold' do
- = s_('Groups|Group URL')
- .input-group.gl-field-error-anchor
- .group-root-path.input-group-prepend.has-tooltip{ title: group_path, :'data-placement' => 'bottom' }
- .input-group-text
- %span>= root_url
- - if parent
- %strong= parent.full_path + '/'
- = f.hidden_field :parent_id
- = f.text_field :path, placeholder: _('my-awesome-group'), class: 'form-control js-validate-group-path js-autofill-group-path', data: { qa_selector: 'group_path_field' },
- autofocus: local_assigns[:autofocus] || false, required: true,
- pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
- 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
- = s_('Groups|Group path is unavailable. Path has been replaced with a suggested available path.')
- %p.validation-success.gl-field-success.field-validation.hide= s_('Groups|Group path is available.')
- %p.validation-pending.gl-field-error-ignore.field-validation.hide= s_('Groups|Checking group URL availability...')
+- if Feature::enabled?(:group_name_path_vue, current_user)
+ = render 'shared/groups/group_name_and_path_fields', f: f
+- else
+ .row
+ .form-group.group-name-holder.col-sm-12
+ = f.label :name, class: 'label-bold' do
+ = s_('Groups|Group name')
+ = f.text_field :name, placeholder: _('My awesome group'), class: 'js-autofill-group-name form-control input-lg', data: { qa_selector: 'group_name_field' },
+ required: true,
+ title: s_('Groups|Enter a descriptive name for your group.'),
+ autofocus: true
+ .text-muted
+ = s_('Groups|Must start with letter, digit, emoji, or underscore. Can also contain periods, dashes, spaces, and parentheses.')
+
+ .row
+ .form-group.col-xs-12.col-sm-8
+ = f.label :path, class: 'label-bold' do
+ = s_('Groups|Group URL')
+ .input-group.gl-field-error-anchor
+ .group-root-path.input-group-prepend.has-tooltip{ title: group_path, :'data-placement' => 'bottom' }
+ .input-group-text
+ %span>= root_url
+ - if parent
+ %strong= parent.full_path + '/'
+ = f.hidden_field :parent_id
+ = f.text_field :path, placeholder: _('my-awesome-group'), class: 'form-control js-validate-group-path js-autofill-group-path', data: { qa_selector: 'group_path_field' },
+ autofocus: local_assigns[:autofocus] || false, required: true,
+ pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
+ 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
+ = s_('Groups|Group path is unavailable. Path has been replaced with a suggested available path.')
+ %p.validation-success.gl-field-success.field-validation.hide= s_('Groups|Group path is available.')
+ %p.validation-pending.gl-field-error-ignore.field-validation.hide= s_('Groups|Checking group URL availability...')
- if @group.persisted?
.gl-alert.gl-alert-warning.gl-mt-3.gl-mb-3
@@ -43,9 +47,9 @@
= succeed '.' do
= link_to s_('Groups|Learn more'), help_page_path('user/group/index', anchor: 'change-a-groups-path'), target: '_blank', class: 'gl-link'
-- if @group.persisted?
- .row
- .form-group.group-name-holder.col-sm-8
- = f.label :id, class: 'label-bold' do
- = s_('Groups|Group ID')
- = f.text_field :id, class: 'form-control', readonly: true
+ - if @group.persisted?
+ .row
+ .form-group.group-name-holder.col-sm-8
+ = f.label :id, class: 'label-bold' do
+ = s_('Groups|Group ID')
+ = f.text_field :id, class: 'form-control', readonly: true
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 7248403d6c9..d10f514dc58 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -22,9 +22,9 @@
= f.text_field :import_url, value: import_url.sanitized_url,
autocomplete: 'off', class: 'form-control gl-form-input', placeholder: 'https://gitlab.company.com/group/project.git', required: true
= render Pajamas::AlertComponent.new(variant: :danger,
- alert_class: 'gl-mt-3 js-import-url-error hide',
+ alert_options: { class: 'gl-mt-3 js-import-url-error hide' },
dismissible: false,
- close_button_class: 'js-close-2fa-enabled-success-alert') do |c|
+ close_button_options: { class: 'js-close-2fa-enabled-success-alert' }) do |c|
= c.body do
= s_('Import|There is not a valid Git repository at this URL. If your HTTP repository is not publicly accessible, verify your credentials.')
= render_if_exists 'shared/ee/import_form', f: f, ci_cd_only: ci_cd_only
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 9428813f6b0..af5657e0e14 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -6,25 +6,27 @@
- toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user
- tooltip_title = label_status_tooltip(label, status) if status
-%li.label-list-item{ id: label_css_id, data: { id: label.id } }
+%li.label-list-item{ id: label_css_id, class: "gl-p-5 gl-border-b", data: { id: label.id } }
= render "shared/label_row", label: label, force_priority: force_priority
%ul.label-actions-list
- if can?(current_user, :admin_label, @project)
%li.gl-display-inline-block.js-toggle-priority.gl-ml-3{ data: { url: remove_priority_project_label_path(@project, label),
dom_id: dom_id(label), type: label.type } }
- %button.add-priority.btn.gl-button.btn-default-tertiary.btn-sm.has-tooltip{ title: _('Prioritize'), type: 'button', data: { placement: 'bottom' }, aria_label: _('Prioritize label') }
- = sprite_icon('star-o')
- %button.remove-priority.btn.gl-button.btn-default-tertiary.btn-sm.has-tooltip{ title: _('Remove priority'), type: 'button', data: { placement: 'bottom' }, aria_label: _('Deprioritize label') }
- = sprite_icon('star')
+ = render Pajamas::ButtonComponent.new(category: :tertiary,
+ icon: 'star',
+ button_options: { class: 'remove-priority has-tooltip', 'title': _('Remove priority'), 'aria_label': _('Deprioritize label'), data: { placement: 'bottom' } })
+ = render Pajamas::ButtonComponent.new(category: :tertiary,
+ icon: 'star-o',
+ button_options: { class: 'add-priority has-tooltip', title: _('Prioritize'), aria_label: _('Prioritize label'), data: { placement: 'bottom' } })
- if can?(current_user, :admin_label, label)
%li.gl-display-inline-block
- = link_to label.edit_path, class: 'btn gl-button btn-default-tertiary btn-sm edit has-tooltip', title: _('Edit'), data: { placement: 'bottom' }, aria_label: _('Edit') do
- = sprite_icon('pencil')
+ = render Pajamas::ButtonComponent.new(href: label.edit_path, category: :tertiary, icon: 'pencil', button_options: { class: 'edit has-tooltip', 'title': _('Edit'), 'aria_label': _('Edit'), data: { placement: 'bottom' } })
- if can?(current_user, :admin_label, label)
%li.gl-display-inline-block
.dropdown
- %button{ type: 'button', class: 'btn gl-button btn-default-tertiary btn-sm js-label-options-dropdown', data: { toggle: 'dropdown' }, aria_label: _('Label actions dropdown') }
- = sprite_icon('ellipsis_v')
+ = render Pajamas::ButtonComponent.new(category: :tertiary,
+ icon: 'ellipsis_v',
+ button_options: { class: 'js-label-options-dropdown', 'aria_label': _('Label actions dropdown'), data: { toggle: 'dropdown' } })
.dropdown-menu.dropdown-open-left
%ul
- if label.project_label? && label.project.group && can?(current_user, :admin_label, label.project.group)
@@ -46,10 +48,9 @@
%button.js-unsubscribe-button.gl-button.btn.btn-default.gl-w-full{ class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
%span.gl-button-text= _('Unsubscribe')
.dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) }
- %button.gl-button.btn.btn-default.gl-w-full{ data: { toggle: 'dropdown' } }
- %span.gl-button-text
- = _('Subscribe')
- = sprite_icon('chevron-down')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'gl-w-full', data: { toggle: 'dropdown' } }) do
+ = _('Subscribe')
+ = sprite_icon('chevron-down')
.dropdown-menu.dropdown-open-left
%ul
%li
diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml
index 91cd91ec38b..76830230cf6 100644
--- a/app/views/shared/_no_password.html.haml
+++ b/app/views/shared/_no_password.html.haml
@@ -1,7 +1,7 @@
- if show_no_password_message?
= render Pajamas::AlertComponent.new(variant: :warning,
- alert_class: 'js-no-password-message',
- close_button_class: 'js-hide-no-password-message') do |c|
+ alert_options: { class: 'js-no-password-message' },
+ close_button_options: { class: 'js-hide-no-password-message' }) do |c|
= c.body do
= no_password_message
= c.actions do
diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml
index c4d8cb092dc..be1df54a432 100644
--- a/app/views/shared/_no_ssh.html.haml
+++ b/app/views/shared/_no_ssh.html.haml
@@ -1,7 +1,7 @@
- if show_no_ssh_key_message?
= render Pajamas::AlertComponent.new(variant: :warning,
- alert_class: 'js-no-ssh-message',
- close_button_class: 'js-hide-no-ssh-message') do |c|
+ alert_options: { class: 'js-no-ssh-message' },
+ close_button_options: { class: 'js-hide-no-ssh-message'}) do |c|
= c.body do
= s_("MissingSSHKeyWarningLink|You can't push or pull repositories using SSH until you add an SSH key to your profile.")
= c.actions do
diff --git a/app/views/shared/_project_limit.html.haml b/app/views/shared/_project_limit.html.haml
index b630c829c76..60be03c6631 100644
--- a/app/views/shared/_project_limit.html.haml
+++ b/app/views/shared/_project_limit.html.haml
@@ -1,7 +1,7 @@
- if cookies[:hide_project_limit_message].blank? && !current_user.hide_project_limit && !current_user.can_create_project? && current_user.projects_limit > 0
= render Pajamas::AlertComponent.new(variant: :warning,
dismissible: false,
- alert_class: 'project-limit-message') do |c|
+ alert_options: { class: 'project-limit-message' }) do |c|
= c.body do
= _("You won't be able to create new projects because you have reached your project limit.")
= c.actions do
diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml
index 70b72f74ab3..f3942aa5dc2 100644
--- a/app/views/shared/_remote_mirror_update_button.html.haml
+++ b/app/views/shared/_remote_mirror_update_button.html.haml
@@ -1,6 +1,7 @@
- if remote_mirror.update_in_progress?
- %button.btn.btn-icon.gl-button.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' }, title: _('Updating') }
- = sprite_icon("retry", css_class: "spin")
+ = render Pajamas::ButtonComponent.new(icon: 'retry',
+ button_options: { class: 'disabled', title: _('Updating'), data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' } },
+ icon_classes: 'spin')
- elsif remote_mirror.enabled?
= link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn btn-icon gl-button qa-update-now-button rspec-update-now-button", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do
= sprite_icon("retry")
diff --git a/app/views/shared/_service_ping_consent.html.haml b/app/views/shared/_service_ping_consent.html.haml
index 8de7552c39a..700ffa7aa12 100644
--- a/app/views/shared/_service_ping_consent.html.haml
+++ b/app/views/shared/_service_ping_consent.html.haml
@@ -1,5 +1,5 @@
- if session[:ask_for_usage_stats_consent]
- = render Pajamas::AlertComponent.new(alert_class: 'service-ping-consent-message') do |c|
+ = render Pajamas::AlertComponent.new(alert_options: { class: 'service-ping-consent-message' }) do |c|
= c.body do
- 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'
@@ -7,5 +7,5 @@
= c.actions do
- 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 _("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-confirm'
= 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/_sidebar_toggle_button.html.haml b/app/views/shared/_sidebar_toggle_button.html.haml
index b3d6c4c327b..0a74e47fa4c 100644
--- a/app/views/shared/_sidebar_toggle_button.html.haml
+++ b/app/views/shared/_sidebar_toggle_button.html.haml
@@ -1,5 +1,5 @@
%a.toggle-sidebar-button.js-toggle-sidebar.qa-toggle-sidebar.rspec-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
- = sprite_icon('chevron-double-lg-left', css_class: 'icon-chevron-double-lg-left')
+ = sprite_icon('chevron-double-lg-left', size: 12, css_class: 'icon-chevron-double-lg-left')
%span.collapse-text.gl-ml-3= _("Collapse sidebar")
= button_tag class: 'close-nav-button', type: 'button' do
diff --git a/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml b/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml
index 0899756d088..ff4b2de2286 100644
--- a/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml
+++ b/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml
@@ -1,9 +1,9 @@
= render Pajamas::AlertComponent.new(variant: :warning,
- alert_class: 'js-recovery-settings-callout gl-mt-5',
- alert_data: { feature_id: Users::CalloutsHelper::TWO_FACTOR_AUTH_RECOVERY_SETTINGS_CHECK,
- dismiss_endpoint: callouts_path,
- defer_links: 'true' },
- close_button_data: { testid: 'close-account-recovery-regular-check-callout' }) do |c|
+ alert_options: { class: 'js-recovery-settings-callout gl-mt-5',
+ data: { feature_id: Users::CalloutsHelper::TWO_FACTOR_AUTH_RECOVERY_SETTINGS_CHECK,
+ dismiss_endpoint: callouts_path,
+ defer_links: 'true' }},
+ close_button_options: { data: { testid: 'close-account-recovery-regular-check-callout' }}) do |c|
= c.body do
= 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'
diff --git a/app/views/shared/access_tokens/_form.html.haml b/app/views/shared/access_tokens/_form.html.haml
index d4106ba4e5d..0f6fc860883 100644
--- a/app/views/shared/access_tokens/_form.html.haml
+++ b/app/views/shared/access_tokens/_form.html.haml
@@ -1,3 +1,4 @@
+- ajax = local_assigns.fetch(:ajax, false)
- title = local_assigns.fetch(:title, _('Add a %{type}') % { type: type })
- prefix = local_assigns.fetch(:prefix, :personal_access_token)
- help_path = local_assigns.fetch(:help_path)
@@ -10,9 +11,9 @@
%p.profile-settings-content
= _("Enter the name of your application, and we'll return a unique %{type}.") % { type: type }
-= gitlab_ui_form_for token, as: prefix, url: path, method: :post, html: { class: 'js-requires-input' } do |f|
+= gitlab_ui_form_for token, as: prefix, url: path, method: :post, html: { id: 'js-new-access-token-form', class: 'js-requires-input' }, remote: ajax do |f|
- = form_errors(token)
+ = form_errors(token, pajamas_alert: true)
.row
.form-group.col
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
index 3bbd7a32bda..8e4b8d6d428 100644
--- a/app/views/shared/builds/_tabs.html.haml
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -1,6 +1,6 @@
- count_badge_classes = 'gl-display-none gl-sm-display-inline-flex'
-= gl_tabs_nav( {class: 'gl-border-b-0 gl-flex-grow-1', data: { testid: 'jobs-tabs' } } ) do
+= gl_tabs_nav( {class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full nav gl-border-b-0', data: { testid: 'jobs-tabs' } } ) do
= gl_tab_link_to build_path_proc.call(nil), { item_active: scope.nil? } do
= _('All')
= gl_tab_counter_badge(limited_counter_with_delimiter(all_builds), { class: count_badge_classes })
diff --git a/app/views/shared/deploy_keys/_form.html.haml b/app/views/shared/deploy_keys/_form.html.haml
index b60d433bafa..4ab93030638 100644
--- a/app/views/shared/deploy_keys/_form.html.haml
+++ b/app/views/shared/deploy_keys/_form.html.haml
@@ -2,7 +2,7 @@
- deploy_key = local_assigns.fetch(:deploy_key)
- deploy_keys_project = deploy_key.deploy_keys_project_for(@project)
-= form_errors(deploy_key)
+= form_errors(deploy_key, pajamas_alert: true)
.form-group
= form.label :title, class: 'col-form-label col-sm-2'
diff --git a/app/views/shared/deploy_keys/_index.html.haml b/app/views/shared/deploy_keys/_index.html.haml
index 388fe75e833..1cd2a590653 100644
--- a/app/views/shared/deploy_keys/_index.html.haml
+++ b/app/views/shared/deploy_keys/_index.html.haml
@@ -2,8 +2,8 @@
%section.rspec-deploy-keys-settings.settings.no-animate#js-deploy-keys-settings{ class: ('expanded' if expanded), data: { qa_selector: 'deploy_keys_settings_content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Deploy keys')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
%p
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/deploy_keys/index') }
= _("Add deploy keys to grant read/write access to this repository. %{link_start}What are deploy keys?%{link_end}").html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
diff --git a/app/views/shared/deploy_tokens/_index.html.haml b/app/views/shared/deploy_tokens/_index.html.haml
index 860fb5614af..aa4a3deaac4 100644
--- a/app/views/shared/deploy_tokens/_index.html.haml
+++ b/app/views/shared/deploy_tokens/_index.html.haml
@@ -3,8 +3,8 @@
%section.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded), data: { qa_selector: 'deploy_tokens_settings_content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= s_('DeployTokens|Deploy tokens')
- %button.btn.gl-button.btn-default.js-settings-toggle
- = expanded ? 'Collapse' : 'Expand'
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
%p
= description
.settings-content
diff --git a/app/views/shared/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml
index c3120774826..552b100d5dd 100644
--- a/app/views/shared/empty_states/_wikis.html.haml
+++ b/app/views/shared/empty_states/_wikis.html.haml
@@ -13,7 +13,7 @@
= create_link
- if show_enable_confluence_integration?(@wiki.container)
= link_to s_('WikiEmpty|Enable the Confluence Wiki integration'),
- edit_project_integration_path(@project, :confluence),
+ edit_project_settings_integration_path(@project, :confluence),
class: 'btn gl-button', title: s_('WikiEmpty|Enable the Confluence Wiki integration')
- elsif @project && can?(current_user, :read_issue, @project)
diff --git a/app/views/shared/errors/_gitaly_unavailable.html.haml b/app/views/shared/errors/_gitaly_unavailable.html.haml
index c9d7920b9c2..ba3293a3f75 100644
--- a/app/views/shared/errors/_gitaly_unavailable.html.haml
+++ b/app/views/shared/errors/_gitaly_unavailable.html.haml
@@ -1,4 +1,4 @@
-= render Pajamas::AlertComponent.new(alert_class: 'gl-my-5',
+= render Pajamas::AlertComponent.new(alert_options: { class: 'gl-my-5' },
variant: :danger,
dismissible: false,
title: reason) do |c|
diff --git a/app/views/shared/form_elements/_description.html.haml b/app/views/shared/form_elements/_description.html.haml
index 94818c13f76..a87aa8de679 100644
--- a/app/views/shared/form_elements/_description.html.haml
+++ b/app/views/shared/form_elements/_description.html.haml
@@ -6,23 +6,22 @@
- supports_quick_actions = true
- preview_url = preview_markdown_path(project, target_type: model.class.name)
-.form-group.row.detail-page-description
- = form.label :description, _('Description'), class: 'col-form-label col-sm-2'
- .col-sm-10
- - if model.is_a?(MergeRequest)
- = hidden_field_tag :merge_request_diff_head_sha, model.diff_head_sha
+.form-group
+ = form.label :description, _('Description'), class: 'gl-display-block'
+ - if model.is_a?(MergeRequest)
+ = hidden_field_tag :merge_request_diff_head_sha, model.diff_head_sha
- - if model.is_a?(Issuable)
- = render 'shared/issuable/form/template_selector', issuable: model
+ - if model.is_a?(Issuable)
+ = render 'shared/issuable/form/template_selector', issuable: model
- = render 'shared/form_elements/apply_template_warning', issuable: model
+ = render 'shared/form_elements/apply_template_warning', issuable: model
- = render layout: 'shared/md_preview', locals: { url: preview_url, referenced_users: true } do
- = render 'shared/zen', f: form, attr: :description,
- classes: 'note-textarea rspec-issuable-form-description',
- placeholder: placeholder,
- supports_quick_actions: supports_quick_actions,
- qa_selector: 'issuable_form_description'
- = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
- .clearfix
- .error-alert
+ = render layout: 'shared/md_preview', locals: { url: preview_url, referenced_users: true } do
+ = render 'shared/zen', f: form, attr: :description,
+ classes: 'note-textarea rspec-issuable-form-description',
+ placeholder: placeholder,
+ supports_quick_actions: supports_quick_actions,
+ qa_selector: 'issuable_form_description'
+ = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
+ .clearfix
+ .error-alert
diff --git a/app/views/shared/groups/_group_name_and_path_fields.html.haml b/app/views/shared/groups/_group_name_and_path_fields.html.haml
new file mode 100644
index 00000000000..709130a47d3
--- /dev/null
+++ b/app/views/shared/groups/_group_name_and_path_fields.html.haml
@@ -0,0 +1,5 @@
+.js-group-name-and-path{ data: group_name_and_path_app_data(@group) }
+ = f.hidden_field :name, data: { js_name: 'name' }
+ = f.hidden_field :path, maxlength: ::Namespace::URL_MAX_LENGTH, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, data: { js_name: 'path' }
+ = f.hidden_field :parent_id, data: { js_name: 'parentId' }
+ = f.hidden_field :id, data: { js_name: 'groupId' }
diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml
index 932971402a2..8b5b4b6e5fa 100644
--- a/app/views/shared/hook_logs/_content.html.haml
+++ b/app/views/shared/hook_logs/_content.html.haml
@@ -30,8 +30,11 @@
%h4.gl-mt-6= _('Request')
%pre
- :escaped
- #{Gitlab::Json.pretty_generate(hook_log.request_data)}
+ - if hook_log.oversize?
+ = _('Request data is too large')
+ - else
+ :escaped
+ #{Gitlab::Json.pretty_generate(hook_log.request_data)}
%h5= _('Headers')
%pre
diff --git a/app/views/shared/integrations/overrides.html.haml b/app/views/shared/integrations/overrides.html.haml
index 4619675cfef..a63053bde0a 100644
--- a/app/views/shared/integrations/overrides.html.haml
+++ b/app/views/shared/integrations/overrides.html.haml
@@ -3,7 +3,7 @@
- page_title @integration.title, _('Integrations')
- @content_class = 'limit-container-width' unless fluid_layout
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= @integration.title
.js-vue-integration-overrides{ data: integration_overrides_data(@integration, project: @project, group: @group) }
diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
index 3a526a9f306..8409f224158 100644
--- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml
+++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
@@ -6,8 +6,10 @@
= form_tag [:bulk_update, @project, type], method: :post, class: "bulk-update" do
.block.issuable-sidebar-header
.filter-item.inline.update-issues-btn.float-left
- = button_tag _('Update all'), class: "gl-button btn js-update-selected-issues btn-confirm", disabled: true
- = button_tag _('Cancel'), class: "gl-button btn btn-default js-bulk-update-menu-hide float-right"
+ = render Pajamas::ButtonComponent.new(variant: :confirm, button_options: { type: 'submit', disabled: true, class: 'js-update-selected-issues' }) do
+ = _('Update all')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-bulk-update-menu-hide float-right' }) do
+ = _('Cancel')
- if params[:state] != 'merged'
.block
.title
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 62e1a930ee6..da49a301087 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -8,7 +8,7 @@
- if @conflict
= render Pajamas::AlertComponent.new(variant: :danger,
dismissible: false,
- alert_class: 'gl-mb-5') do |c|
+ alert_options: { class: 'gl-mb-5' }) do |c|
= c.body do
Someone edited the #{issuable.class.model_name.human.downcase} the same time you did.
Please check out
@@ -17,12 +17,11 @@
= render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form
-.form-group.row
- = form.label :title, class: 'col-form-label col-sm-2' do
- = _('Title')
- %i{ aria: { hidden: true } }= '*'
+.form-group
+ = form.label :title do
+ = _('Title (required)')
- = render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?)
+ = render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:draft?)
#js-suggestions{ data: { project_path: @project.full_path } }
= render 'shared/issuable/form/type_selector', issuable: issuable, form: form
@@ -36,27 +35,26 @@
= render 'shared/issuable/form/contribution', issuable: issuable, form: form
- if @merge_request_to_resolve_discussions_of
- .form-group.row
- .col-sm-10.offset-sm-2
- = sprite_icon('information-o')
- - if @merge_request_to_resolve_discussions_of.discussions_can_be_resolved_by?(current_user)
- = hidden_field_tag 'merge_request_to_resolve_discussions_of', @merge_request_to_resolve_discussions_of.iid
- - if @discussion_to_resolve
- = hidden_field_tag 'discussion_to_resolve', @discussion_to_resolve.id
- Creating this issue will resolve the thread in
- - else
- Creating this issue will resolve all threads in
- = link_to_discussions_to_resolve(@merge_request_to_resolve_discussions_of, @discussion_to_resolve)
+ .form-group
+ = sprite_icon('information-o')
+ - if @merge_request_to_resolve_discussions_of.discussions_can_be_resolved_by?(current_user)
+ = hidden_field_tag 'merge_request_to_resolve_discussions_of', @merge_request_to_resolve_discussions_of.iid
+ - if @discussion_to_resolve
+ = hidden_field_tag 'discussion_to_resolve', @discussion_to_resolve.id
+ Creating this issue will resolve the thread in
- else
- The
- = @discussion_to_resolve ? 'thread' : 'threads'
- at
- = link_to_discussions_to_resolve(@merge_request_to_resolve_discussions_of, @discussion_to_resolve)
- will stay unresolved. Ask someone with permission to resolve
- = @discussion_to_resolve ? 'it.' : 'them.'
+ Creating this issue will resolve all threads in
+ = link_to_discussions_to_resolve(@merge_request_to_resolve_discussions_of, @discussion_to_resolve)
+ - else
+ The
+ = @discussion_to_resolve ? 'thread' : 'threads'
+ at
+ = link_to_discussions_to_resolve(@merge_request_to_resolve_discussions_of, @discussion_to_resolve)
+ will stay unresolved. Ask someone with permission to resolve
+ = @discussion_to_resolve ? 'it.' : 'them.'
- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
-.row-content-block{ class: (is_footer ? "footer-block" : "middle-block") }
+.gl-mt-5{ class: (is_footer ? "footer-block" : "middle-block") }
- if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = issuable.project.present.contribution_guide_path)
.gl-mb-5
Please review the
diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml
index 7ab82362e85..ec78b3f7ce3 100644
--- a/app/views/shared/issuable/_label_page_create.html.haml
+++ b/app/views/shared/issuable/_label_page_create.html.haml
@@ -6,7 +6,7 @@
.dropdown-page-two.dropdown-new-label
= dropdown_title(create_label_title(subject), options: { back: true, close: show_close })
= dropdown_content do
- = render Pajamas::AlertComponent.new(variant: :danger, alert_class: 'js-label-error gl-mb-3', dismissible: false)
+ = render Pajamas::AlertComponent.new(variant: :danger, alert_options: { class: 'js-label-error gl-mb-3' }, dismissible: false)
%input#new_label_name.default-dropdown-input{ type: "text", placeholder: _('Name new label') }
.suggest-colors.suggest-colors-dropdown
= render_suggested_colors
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index feffc7eb011..55f5dce8b37 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -13,7 +13,7 @@
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in }, issuable_type: issuable_type }, class: "#{sidebar_gutter_collapsed_class} #{'right-sidebar-merge-requests' if moved_sidebar_enabled}", 'aria-live' => 'polite', 'aria-label': issuable_type }
.issuable-sidebar{ class: "#{'is-merge-request' if moved_sidebar_enabled}" }
- .issuable-sidebar-header{ class: "#{'gl-pb-2! gl-md-display-flex gl-justify-content-end gl-md-display-none!' if moved_sidebar_enabled}" }
+ .issuable-sidebar-header{ class: "#{'gl-pb-2! gl-md-display-flex gl-justify-content-end gl-lg-display-none!' if moved_sidebar_enabled}" }
%a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", class: "#{'gl-display-block' if moved_sidebar_enabled}", href: "#", "aria-label" => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
= sidebar_gutter_toggle_icon
- if signed_in && !moved_sidebar_enabled
@@ -48,7 +48,7 @@
.js-milestone-select{ data: { can_edit: can_edit_issuable.to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid] } }
- if in_group_context_with_iterations
- .block{ class: 'gl-pt-0! gl-collapse-empty', data: { qa_selector: 'iteration_container', testid: 'iteration_container' } }<
+ .block.gl-collapse-empty{ data: { qa_selector: 'iteration_container', testid: 'iteration_container' } }<
= render_if_exists 'shared/issuable/iteration_select', can_edit: can_edit_issuable.to_s, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type
- if issuable_sidebar[:show_crm_contacts]
@@ -106,8 +106,7 @@
.sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') }
= sprite_icon('long-arrow')
.dropdown.sidebar-move-issue-dropdown.hide-collapsed
- %button.gl-button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button',
- data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_action: "click_button", track_value: "" } }
+ = render Pajamas::ButtonComponent.new(block: true, button_options: { class: 'js-sidebar-dropdown-toggle js-move-issue', data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_action: "click_button", track_value: "" } } ) do
= _('Move issue')
.dropdown-menu.dropdown-menu-selectable.dropdown-extended-height
= dropdown_title(_('Move issue'))
diff --git a/app/views/shared/issuable/_status_box.html.haml b/app/views/shared/issuable/_status_box.html.haml
index 4fda1f11545..125ef921cfa 100644
--- a/app/views/shared/issuable/_status_box.html.haml
+++ b/app/views/shared/issuable/_status_box.html.haml
@@ -2,8 +2,7 @@
- badge_icon = state_name_with_icon(issuable)[1]
- badge_variant = issuable.open? ? :success : issuable.merged? ? :info : :danger
- badge_status_class = issuable.open? ? 'issuable-status-badge-open' : issuable.merged? ? 'issuable-status-badge-merged' : 'issuable-status-badge-closed'
-- updated_mr_header_enabled = Feature.enabled?(:updated_mr_header, @project) && issuable.is_a?(MergeRequest)
-- badge_classes = "js-mr-status-box issuable-status-badge gl-mr-3 #{badge_status_class} #{'gl-vertical-align-bottom' if updated_mr_header_enabled}"
+- badge_classes = "js-mr-status-box issuable-status-badge gl-mr-3 #{badge_status_class} #{'gl-vertical-align-bottom' if issuable.is_a?(MergeRequest)}"
= gl_badge_tag({ variant: badge_variant, icon: badge_icon, icon_classes: 'gl-mr-0!' }, { class: badge_classes, data: { project_path: issuable.project.path_with_namespace, iid: issuable.iid, issuable_type: 'merge_request', state: issuable.state } }) do
%span.gl-display-none.gl-sm-display-block.gl-ml-2
diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml
index 46df9b4ef9a..8ab002f755f 100644
--- a/app/views/shared/issuable/form/_branch_chooser.html.haml
+++ b/app/views/shared/issuable/form/_branch_chooser.html.haml
@@ -37,12 +37,8 @@
data: { placeholder: _('Select branch'), endpoint: refs_project_path(@project, sort: 'updated_desc', find: 'branches') }})
- if source_level < target_level
- .gl-alert.gl-alert-warning.gl-alert-not-dismissible.gl-max-content.gl-mt-4
- .gl-alert-container
- .gl-alert-content{ role: 'alert' }
- = sprite_icon('warning', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
- .gl-alert-body
- = visibilityMismatchString
- %br
- = _('Review the target project before submitting to avoid exposing %{source} changes.') % { source: source_visibility }
-%hr
+ = render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_options: { class: 'gl-mb-4' }) do |c|
+ = c.body do
+ = visibilityMismatchString
+ %br
+ = _('Review the target project before submitting to avoid exposing %{source} changes.') % { source: source_visibility }
diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml
index 76feb4d1613..5831460d59a 100644
--- a/app/views/shared/issuable/form/_merge_params.html.haml
+++ b/app/views/shared/issuable/form/_merge_params.html.haml
@@ -4,11 +4,10 @@
- return unless issuable.is_a?(MergeRequest)
- return if issuable.closed_or_merged_without_fork?
-.form-group.row
- .col-sm-2.col-form-label.pt-sm-0
+.form-group.row.gl-mb-7
+ .col-12
%label
= _('Merge options')
- .col-sm-10
- if issuable.can_remove_source_branch?(current_user)
.form-check.gl-mb-3
= hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index e941eaadbc9..61cc408f6b3 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -5,24 +5,21 @@
- form = local_assigns.fetch(:form)
- if @add_related_issue
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = check_box_tag :add_related_issue, @add_related_issue.iid, true, class: 'form-check-input'
- = label_tag :add_related_issue, class: 'form-check-label' do
- - add_related_issue_link = link_to "\##{@add_related_issue.iid}", issue_path(@add_related_issue), class: ['has-tooltip'], title: @add_related_issue.title
- #{_('Relate to %{issuable_type} %{add_related_issue_link}').html_safe % { issuable_type: @add_related_issue.issue_type, add_related_issue_link: add_related_issue_link }}
- %p.text-muted= _('Adds this %{issuable_type} as related to the %{issuable_type} it was created from') % { issuable_type: @add_related_issue.issue_type }
+ .form-group
+ .form-check
+ = check_box_tag :add_related_issue, @add_related_issue.iid, true, class: 'form-check-input'
+ = label_tag :add_related_issue, class: 'form-check-label' do
+ - add_related_issue_link = link_to "\##{@add_related_issue.iid}", issue_path(@add_related_issue), class: ['has-tooltip'], title: @add_related_issue.title
+ #{_('Relate to %{issuable_type} %{add_related_issue_link}').html_safe % { issuable_type: @add_related_issue.issue_type, add_related_issue_link: add_related_issue_link }}
+ %p.text-muted= _('Adds this %{issuable_type} as related to the %{issuable_type} it was created from') % { issuable_type: @add_related_issue.issue_type }
- if issuable.respond_to?(:confidential) && can?(current_user, :set_confidentiality, issuable)
- .form-group.row
- .offset-sm-2.col-sm-10
- = form.gitlab_ui_checkbox_component :confidential,
- _('This issue is confidential and should only be visible to team members with at least Reporter access.')
+ .form-group
+ = form.gitlab_ui_checkbox_component :confidential,
+ _('This issue is confidential and should only be visible to team members with at least Reporter access.')
- if can?(current_user, :"set_#{issuable.to_ability_name}_metadata", issuable)
- %hr
- .row
+ .row.gl-pt-4
%div{ class: (has_due_date ? "col-lg-6" : "col-12") }
.form-group.row.merge-request-assignee
= render "shared/issuable/form/metadata_issuable_assignee", issuable: issuable, form: form, has_due_date: has_due_date
@@ -35,15 +32,15 @@
- if issuable.supports_milestone?
.form-group.row.issue-milestone
- = form.label :milestone_id, _('Milestone'), class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
- .col-sm-10{ class: ("col-md-8" if has_due_date) }
+ = form.label :milestone_id, _('Milestone'), class: "col-12"
+ .col-12
.issuable-form-select-holder
= render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "qa-issuable-milestone-dropdown js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: _('Select milestone')
.form-group.row
- = form.label :label_ids, _('Labels'), class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
+ = form.label :label_ids, _('Labels'), class: "col-12"
= form.hidden_field :label_ids, multiple: true, value: ''
- .col-sm-10{ class: "#{"col-md-8" if has_due_date}" }
+ .col-12
.issuable-form-select-holder
= render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label"
@@ -53,7 +50,7 @@
.col-lg-6
= render_if_exists "shared/issuable/form/weight", issuable: issuable, form: form
.form-group.row
- = form.label :due_date, _('Due date'), class: "col-form-label col-md-2 col-lg-4"
- .col-8
+ = form.label :due_date, _('Due date'), class: "col-12"
+ .col-12
.issuable-form-select-holder
= form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: _('Select due date'), autocomplete: 'off'
diff --git a/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
index 781ee8b5f80..f9c3c11eed8 100644
--- a/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
+++ b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
@@ -1,5 +1,5 @@
-= form.label :assignee_id, issuable.allows_multiple_assignees? ? _('Assignees') : _('Assignee'), class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
-.col-sm-10{ class: ("col-md-8" if has_due_date) }
+= form.label :assignee_id, issuable.allows_multiple_assignees? ? _('Assignees') : _('Assignee'), class: "col-12"
+.col-12
.issuable-form-select-holder.selectbox
- issuable.assignees.each do |assignee|
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { meta: assignee.name, avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }
@@ -8,4 +8,4 @@
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' }
= dropdown_tag(users_dropdown_label(issuable.assignees), options: assignees_dropdown_options(issuable.to_ability_name))
- = link_to _('Assign to me'), '#', class: "assign-to-me-link qa-assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}"
+ = link_to _('Assign to me'), '#', class: "assign-to-me-link gl-white-space-nowrap gl-pl-4 qa-assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}"
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 fad13c78e26..0e3383acfce 100644
--- a/app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml
+++ b/app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml
@@ -1,5 +1,5 @@
-= form.label :reviewer_id, issuable.allows_multiple_reviewers? ? _('Reviewers') : _('Reviewer'), class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
-.col-sm-10.gl-mb-2{ class: ("col-md-8" if has_due_date) }
+= form.label :reviewer_id, issuable.allows_multiple_reviewers? ? _('Reviewers') : _('Reviewer'), class: "col-12"
+.col-12
.issuable-form-select-holder.selectbox
- issuable.reviewers.each do |reviewer|
= hidden_field_tag "#{issuable.to_ability_name}[reviewer_ids][]", reviewer.id, id: nil, data: { meta: reviewer.name, avatar_url: reviewer.avatar_url, name: reviewer.name, username: reviewer.username }
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
index 6b00cdc5e24..e7c0833de0f 100644
--- a/app/views/shared/issuable/form/_title.html.haml
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -2,17 +2,16 @@
- has_wip_commits = local_assigns.fetch(:has_wip_commits)
- form = local_assigns.fetch(:form)
- no_issuable_templates = issuable_templates(ref_project, issuable.to_ability_name).empty?
-- div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8'
- toggle_wip_link_start = '<a href="" class="js-toggle-wip">'
- toggle_wip_link_end = '</a>'
- add_wip_text = (_('%{link_start}Start the title with %{draft_snippet}%{link_end} to prevent a merge request draft from merging before it\'s ready.') % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: '<code>Draft:</code>'.html_safe } ).html_safe
- remove_wip_text = (_('%{link_start}Remove the %{draft_snippet} prefix%{link_end} from the title to allow this merge request to be merged when it\'s ready.' ) % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: '<code>Draft</code>'.html_safe } ).html_safe
-%div{ class: div_class, data: { testid: 'issue-title-input-field' } }
+%div{ data: { testid: 'issue-title-input-field' } }
= form.text_field :title, required: true, aria: { required: true }, maxlength: 255, autofocus: true,
- autocomplete: 'off', class: 'form-control pad qa-issuable-form-title', placeholder: _('Title'), dir: 'auto'
+ autocomplete: 'off', class: 'form-control pad qa-issuable-form-title', dir: 'auto'
- - if issuable.respond_to?(:work_in_progress?)
+ - if issuable.respond_to?(:draft?)
.form-text.text-muted
.js-wip-explanation{ style: "display: none;" }
= remove_wip_text
diff --git a/app/views/shared/issuable/form/_type_selector.html.haml b/app/views/shared/issuable/form/_type_selector.html.haml
index 0d86aa8c8e7..d5c696b1698 100644
--- a/app/views/shared/issuable/form/_type_selector.html.haml
+++ b/app/views/shared/issuable/form/_type_selector.html.haml
@@ -1,33 +1,32 @@
- return unless issuable.supports_issue_type? && can?(current_user, :create_issue, @project)
-.form-group.row.gl-mb-0
- = form.label :type, _('Type'), class: 'col-form-label col-sm-2'
- .col-sm-10
- .gl-display-flex.gl-align-items-center
- .issuable-form-select-holder.selectbox.form-group.gl-mb-0
- .dropdown.js-issuable-type-filter-dropdown-wrap
- %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
- %span.dropdown-toggle-text.is-default
- = issuable.issue_type.capitalize || _("Select type")
- = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
- .dropdown-menu.dropdown-menu-selectable.dropdown-select
- .dropdown-title.gl-display-flex
- %span.gl-ml-auto
- = _("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{ data: { testid: 'issue-type-select-dropdown' } }
- %ul
- - if create_issue_type_allowed?(@project, :issue)
- %li.js-filter-issuable-type
- = link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do
- #{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_('Issue')}
- - if create_issue_type_allowed?(@project, :incident)
- %li.js-filter-issuable-type{ data: { track: { action: "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
- #{sprite_icon(work_item_type_icon(:incident), css_class: 'gl-icon')} #{_('Incident')}
+.form-group
+ = form.label :type, _('Type')
+ .gl-display-flex.gl-align-items-center
+ .issuable-form-select-holder.selectbox.form-group.gl-mb-0
+ .dropdown.js-issuable-type-filter-dropdown-wrap
+ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
+ %span.dropdown-toggle-text.is-default
+ = issuable.issue_type.capitalize || _("Select type")
+ = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
+ .dropdown-menu.dropdown-menu-selectable.dropdown-select
+ .dropdown-title.gl-display-flex
+ %span.gl-ml-auto
+ = _("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{ data: { testid: 'issue-type-select-dropdown' } }
+ %ul
+ - if create_issue_type_allowed?(@project, :issue)
+ %li.js-filter-issuable-type
+ = link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do
+ #{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_('Issue')}
+ - if create_issue_type_allowed?(@project, :incident)
+ %li.js-filter-issuable-type{ data: { track: { action: "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
+ #{sprite_icon(work_item_type_icon(:incident), css_class: 'gl-icon')} #{_('Incident')}
- #js-type-popover
+ #js-type-popover
- if issuable.incident?
%p.form-text.text-muted
diff --git a/app/views/shared/issue_type/_details_content.html.haml b/app/views/shared/issue_type/_details_content.html.haml
index 7276175db59..7c5b3fd4b3c 100644
--- a/app/views/shared/issue_type/_details_content.html.haml
+++ b/app/views/shared/issue_type/_details_content.html.haml
@@ -3,9 +3,9 @@
.issue-details.issuable-details.js-issue-details
.detail-page-description.content-block.js-detail-page-description
- #js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json, full_path: @project.full_path } }
+ #js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json, issuable_id: issuable.id, full_path: @project.full_path } }
.title-container
- %h1.title= markdown_field(issuable, :title)
+ %h1.title.page-title.gl-font-size-h-display= markdown_field(issuable, :title)
- if issuable.description.present?
.description
.md= markdown_field(issuable, :description)
@@ -17,6 +17,7 @@
= render 'projects/issues/design_management'
+ = render_if_exists 'projects/issues/work_item_links'
= render_if_exists 'projects/issues/related_issues'
#js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: issuable.iid)), project_namespace: @project.namespace.path, project_path: @project.path } }
diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml
index 29f6dc02749..f768b63afff 100644
--- a/app/views/shared/labels/_form.html.haml
+++ b/app/views/shared/labels/_form.html.haml
@@ -2,21 +2,18 @@
= form_errors(@label)
.form-group.row
- .col-sm-2.col-form-label
+ .col-12
= f.label :title
- .col-sm-10
= f.text_field :title, class: "gl-form-input form-control js-label-title qa-label-title", required: true, autofocus: true
= render_if_exists 'shared/labels/create_label_help_text'
.form-group.row
- .col-sm-2.col-form-label
+ .col-12
= f.label :description
- .col-sm-10
= f.text_field :description, class: "gl-form-input form-control js-quick-submit qa-label-description"
.form-group.row
- .col-sm-2.col-form-label
+ .col-12
= f.label :color, _("Background color")
- .col-sm-10
.input-group
.input-group-prepend
.input-group-text.label-color-preview &nbsp;
@@ -26,13 +23,13 @@
%br
= _("Or you can choose one of the suggested colors below")
= render_suggested_colors
- .gl-display-flex.gl-justify-content-space-between.gl-p-5.gl-bg-gray-10.gl-border-t-solid.gl-border-t-gray-100.gl-border-t-1
+ .gl-display-flex.gl-justify-content-space-between
%div
- if @label.persisted?
- = f.submit _('Save changes'), class: 'btn gl-button btn-confirm js-save-button'
+ = f.submit _('Save changes'), class: 'btn gl-button btn-confirm js-save-button gl-mr-2'
- else
- = f.submit _('Create label'), class: 'btn gl-button btn-confirm js-save-button qa-label-create-button'
- = link_to _('Cancel'), back_path, class: 'btn gl-button btn-default btn-cancel'
+ = f.submit _('Create label'), class: 'btn gl-button btn-confirm js-save-button qa-label-create-button gl-mr-2'
+ = link_to _('Cancel'), back_path, class: 'btn gl-button btn-default btn-cancel gl-mr-2'
- if @label.persisted?
- presented_label = @label.present
%button.btn.btn-danger.gl-button.btn-danger-secondary.js-delete-label-modal-button{ type: 'button', data: { label_name: presented_label.name, subject_name: presented_label.subject_name, destroy_path: presented_label.destroy_path } }
diff --git a/app/views/shared/labels/_nav.html.haml b/app/views/shared/labels/_nav.html.haml
index 47e9d9b0e4a..622ad9db425 100644
--- a/app/views/shared/labels/_nav.html.haml
+++ b/app/views/shared/labels/_nav.html.haml
@@ -11,10 +11,11 @@
.input-group
= search_field_tag :search, params[:search], { placeholder: _('Filter'), id: 'label-search', class: 'form-control search-text-input input-short', spellcheck: false, autofocus: true }
%span.input-group-append
- %button.btn.gl-button.btn-default{ type: "submit", "aria-label" => _('Submit search') }
- = sprite_icon('search')
+ = render Pajamas::ButtonComponent.new(icon: 'search', button_options: { type: "submit", "aria-label" => _('Submit search') })
= render 'shared/labels/sort_dropdown'
- if labels_or_filters && can_admin_label && @project
- = link_to _('New label'), new_project_label_path(@project), class: "btn gl-button btn-confirm qa-label-create-new"
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_project_label_path(@project), button_options: { class: 'qa-label-create-new' }) do
+ = _('New label')
- if labels_or_filters && can_admin_label && @group
- = link_to _('New label'), new_group_label_path(@group), class: "btn gl-button btn-confirm qa-label-create-new"
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_group_label_path(@group), button_options: { class: 'qa-label-create-new' }) do
+ = _('New label')
diff --git a/app/views/shared/members/_manage_access_button.html.haml b/app/views/shared/members/_manage_access_button.html.haml
index 13509a7480a..c88198ec380 100644
--- a/app/views/shared/members/_manage_access_button.html.haml
+++ b/app/views/shared/members/_manage_access_button.html.haml
@@ -2,6 +2,6 @@
.gl-float-right
= link_to path, class: 'btn btn-default btn-sm gl-button' do
- = sprite_icon('pencil-square', css_class: 'gl-icon gl-button-icon')
+ = sprite_icon('pencil', css_class: 'gl-icon gl-button-icon')
%span.gl-button-text
= _('Manage access')
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 45699808b6b..3082c6bb4db 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -48,14 +48,7 @@
.milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end
- if @project # if in milestones list on project level
- if can_admin_group_milestones?
- %button.js-promote-project-milestone-button.btn.gl-button.btn-icon.btn-default-tertiary.btn-sm.has-tooltip{ title: s_('Milestones|Promote to Group Milestone'),
- disabled: true,
- type: 'button',
- data: { url: promote_project_milestone_path(milestone.project, milestone),
- milestone_title: milestone.title,
- group_name: @project.group.name } }
- = sprite_icon('level-up', size: 14, css_class: 'gl-button-icon gl-icon')
-
+ = render Pajamas::ButtonComponent.new(icon: 'level-up', category: :tertiary, size: :small, button_options: { class: 'js-promote-project-milestone-button', title: s_('Milestones|Promote to Group Milestone'), disabled: true, data: { toggle: 'tooltip', container: 'body', url: promote_project_milestone_path(milestone.project, milestone), milestone_title: milestone.title, group_name: @project.group.name } })
- if can?(current_user, :admin_milestone, milestone)
- if milestone.closed?
= link_to s_('Milestones|Reopen Milestone'), milestone_path(milestone, milestone: { state_event: :activate }), method: :put, class: "btn gl-button btn-sm gl-ml-3"
diff --git a/app/views/shared/milestones/_milestone_complete_alert.html.haml b/app/views/shared/milestones/_milestone_complete_alert.html.haml
index 86f9193cc7a..bde8a0b91b0 100644
--- a/app/views/shared/milestones/_milestone_complete_alert.html.haml
+++ b/app/views/shared/milestones/_milestone_complete_alert.html.haml
@@ -2,7 +2,7 @@
- if milestone.complete? && milestone.active?
= render Pajamas::AlertComponent.new(variant: :success,
- alert_data: { testid: 'all-issues-closed-alert' },
+ alert_options: { data: { testid: 'all-issues-closed-alert' }},
dismissible: false) do |c|
= c.body do
= yield
diff --git a/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml
index d167ffb5582..68a4d010872 100644
--- a/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml
+++ b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml
@@ -3,6 +3,7 @@
- form = local_assigns.fetch(:form, nil)
- setting_locked = local_assigns.fetch(:setting_locked, false)
- help_text = local_assigns.fetch(:help_text, s_('CascadingSettings|Subgroups cannot change this setting.'))
+- label = local_assigns.fetch(:label, s_('CascadingSettings|Enforce for all subgroups'))
- return unless attribute && group && form
- return if setting_locked
@@ -10,6 +11,6 @@
- lock_attribute = "lock_#{attribute}"
= form.gitlab_ui_checkbox_component lock_attribute,
- s_('CascadingSettings|Enforce for all subgroups'),
+ label,
help_text: help_text,
checkbox_options: { checked: group.namespace_settings.public_send(lock_attribute), data: { testid: 'enforce-for-all-subgroups-checkbox' } }
diff --git a/app/views/shared/namespaces/cascading_settings/_lock_icon.html.haml b/app/views/shared/namespaces/cascading_settings/_lock_icon.html.haml
index 4e3b6b2afc4..ed835af6524 100644
--- a/app/views/shared/namespaces/cascading_settings/_lock_icon.html.haml
+++ b/app/views/shared/namespaces/cascading_settings/_lock_icon.html.haml
@@ -1,4 +1,3 @@
-%button.position-absolute.gl-top-3.gl-right-0.gl-translate-y-n50.gl-cursor-default.btn.btn-default.btn-sm.gl-button.btn-default-tertiary.js-cascading-settings-lock-popover-target{ class: 'gl-p-1! gl-text-gray-600! gl-bg-transparent!',
- type: 'button',
- data: cascading_namespace_settings_popover_data(attribute, group, settings_path_helper) }
- = sprite_icon('lock', size: 16)
+- class_list = local_assigns.fetch(:class_list, '')
+
+= render Pajamas::ButtonComponent.new(category: 'tertiary', icon: 'lock', button_options: { class: "gl-absolute gl-top-3 gl-right-0 gl-translate-y-n50 gl-p-1! gl-bg-transparent! gl-cursor-default! js-cascading-settings-lock-popover-target #{class_list}", data: cascading_namespace_settings_popover_data(attribute, group, settings_path_helper) })
diff --git a/app/views/shared/notes/_edit_form.html.haml b/app/views/shared/notes/_edit_form.html.haml
index 63c895a5a03..cbf0b6f1051 100644
--- a/app/views/shared/notes/_edit_form.html.haml
+++ b/app/views/shared/notes/_edit_form.html.haml
@@ -9,6 +9,6 @@
.note-form-actions.clearfix
.settings-message.note-edit-warning.js-finish-edit-warning
= _("Finish editing this message first!")
- = submit_tag _('Save comment'), class: 'gl-button btn btn-success js-comment-save-button', data: { qa_selector: 'save_comment_button' }
- %button.btn.gl-button.btn-cancel.note-edit-cancel{ type: 'button' }
+ = submit_tag _('Save comment'), class: 'gl-button btn btn-confirm js-comment-save-button', data: { qa_selector: 'save_comment_button' }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'note-edit-cancel' }) do
= _("Cancel")
diff --git a/app/views/shared/projects/_inactive_project_deletion_alert.html.haml b/app/views/shared/projects/_inactive_project_deletion_alert.html.haml
new file mode 100644
index 00000000000..0030265f007
--- /dev/null
+++ b/app/views/shared/projects/_inactive_project_deletion_alert.html.haml
@@ -0,0 +1,7 @@
+- if show_inactive_project_deletion_banner?(@project)
+ - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('administration/inactive_project_deletion') }
+ - link_end = '</a>'.html_safe
+ - deletion_date = inactive_project_deletion_date(@project)
+ - title = _('Due to inactivity, this project is scheduled to be deleted on %{deletion_date}. %{link_start}Why is this scheduled?%{link_end}').html_safe % { deletion_date: deletion_date, link_start: link_start, link_end: link_end }
+
+ = render Pajamas::AlertComponent.new(title: title, variant: :warning, alert_options: { class: 'gl-pb-3' }, dismissible: false)
diff --git a/app/views/shared/projects/_search_bar.html.haml b/app/views/shared/projects/_search_bar.html.haml
index 8ec11d9cfbb..5271a5fac09 100644
--- a/app/views/shared/projects/_search_bar.html.haml
+++ b/app/views/shared/projects/_search_bar.html.haml
@@ -13,8 +13,7 @@
.filtered-search-box.m-0
.filtered-search-box-input-container.pl-2
= render 'shared/projects/search_form', admin_view: false, search_form_placeholder: _("Search projects...")
- %button.btn.gl-button.btn-icon.btn-secondary{ type: 'submit', form: 'project-filter-form' }
- = sprite_icon('search', css_class: 'search-icon ')
+ = render Pajamas::ButtonComponent.new(icon: 'search', icon_classes: 'search-icon', button_options: { type: 'submit', form: 'project-filter-form' })
.filtered-search-dropdown.flex-row.align-items-center.mb-2.m-sm-0#filtered-search-visibility-dropdown{ class: flex_grow_and_shrink_xs }
.filtered-search-dropdown-label.p-0.pl-sm-3.font-weight-bold
%span
diff --git a/app/views/shared/promotions/_promote_servicedesk.html.haml b/app/views/shared/promotions/_promote_servicedesk.html.haml
index a2da23de2b9..57ac1370f8d 100644
--- a/app/views/shared/promotions/_promote_servicedesk.html.haml
+++ b/app/views/shared/promotions/_promote_servicedesk.html.haml
@@ -1,11 +1,8 @@
-.user-callout.promotion-callout.js-service-desk-callout#promote_service_desk{ data: { uid: 'promote_service_desk_dismissed' } }
- .bordered-box.content-block
- %button.gl-button.btn.btn-default.close.js-close-callout{ type: 'button', 'aria-label' => 'Dismiss Service Desk promotion' }
- = sprite_icon('close', size: 16, css_class: 'dismiss-icon')
- .svg-container
- = custom_icon('icon_service_desk')
- .user-callout-copy
- %h4
- = _("Improve customer support with Service Desk")
- %p
- = _("Service Desk allows people to create issues in your GitLab instance without their own user account. It provides a unique email address for end users to create issues in a project. Replies can be sent either through the GitLab interface or by email. End users only see threads through email.")
+= render Pajamas::BannerComponent.new(banner_options: {class: 'js-service-desk-callout', data: {uid: 'promote_service_desk_dismissed'}, id: 'promote_service_desk'},
+ close_options: {'aria-label' => s_('Promotions|Dismiss Service Desk promotion'), class: 'js-close-callout'},
+ svg_path: 'illustrations/service_desk_callout.svg',
+ button_text: s_('Promotions|Configure Service Desk'), button_link: help_page_path('user/project/service_desk.html', anchor: 'configuring-service-desk')) do |c|
+ - c.title do
+ = _('Improve customer support with Service Desk')
+ %p
+ = _('Service Desk allows people to create issues in your GitLab instance without their own user account. It provides a unique email address for end users to create issues in a project. Replies can be sent either through the GitLab interface or by email. End users only see threads through email.')
diff --git a/app/views/shared/runners/_runner_details.html.haml b/app/views/shared/runners/_runner_details.html.haml
index 7a35b1cec0a..f6396168cb3 100644
--- a/app/views/shared/runners/_runner_details.html.haml
+++ b/app/views/shared/runners/_runner_details.html.haml
@@ -1,7 +1,7 @@
- breadcrumb_title runner.short_sha
- page_title "##{runner.id} (#{runner.short_sha})"
-%h2.page-title
+%h1.page-title.gl-font-size-h-display
= s_('Runners|Runner #%{runner_id}' % { runner_id: runner.id })
= render 'shared/runners/runner_type_badge', runner: runner
diff --git a/app/views/shared/runners/_runner_type_alert.html.haml b/app/views/shared/runners/_runner_type_alert.html.haml
index 4bf02b71109..9736780c436 100644
--- a/app/views/shared/runners/_runner_type_alert.html.haml
+++ b/app/views/shared/runners/_runner_type_alert.html.haml
@@ -1,14 +1,14 @@
-- alert_class = 'gl-mb-5'
+- alert_options = { class: 'gl-mb-5' }
- if runner.group_type?
- = render Pajamas::AlertComponent.new(alert_class: alert_class,
+ = render Pajamas::AlertComponent.new(alert_options: alert_options,
title: s_('Runners|This runner is available to all projects and subgroups in a group.'),
dismissible: false) do |c|
= c.body do
= s_('Runners|Use Group runners when you want all projects in a group to have access to a set of runners.')
= link_to _('Learn more.'), help_page_path('ci/runners/runners_scope', anchor: 'group-runners'), target: '_blank', rel: 'noopener noreferrer'
- else
- = render Pajamas::AlertComponent.new(alert_class: alert_class,
+ = render Pajamas::AlertComponent.new(alert_options: alert_options,
title: s_('Runners|This runner is associated with specific projects.'),
dismissible: false) do |c|
= c.body do
diff --git a/app/views/shared/snippets/_embed.html.haml b/app/views/shared/snippets/_embed.html.haml
index 5744fc9fba6..05fe307318f 100644
--- a/app/views/shared/snippets/_embed.html.haml
+++ b/app/views/shared/snippets/_embed.html.haml
@@ -14,8 +14,8 @@
.file-actions.d-none.d-sm-block
.btn-group{ role: "group" }<
+ = embedded_copy_snippet_button(blob)
= embedded_raw_snippet_button(@snippet, blob)
-
= embedded_snippet_download_button(@snippet, blob)
%figure.file-holder.snippet-file-content{ "aria-label" => _('Code snippet') }
= render 'projects/blob/viewer', viewer: blob.simple_viewer, load_async: false, external_embed: true
diff --git a/app/views/shared/snippets/show.js.haml b/app/views/shared/snippets/show.js.haml
index 23cebc97f63..f61f162be10 100644
--- a/app/views/shared/snippets/show.js.haml
+++ b/app/views/shared/snippets/show.js.haml
@@ -1,2 +1,3 @@
+function copyToClipboard(queryEl) { const copyText = document.querySelector(queryEl).textContent; navigator.clipboard.writeText(copyText); }
document.write('#{escape_javascript(stylesheet_link_tag("#{stylesheet_url 'snippets'}"))}');
document.write('#{escape_javascript(render(partial: 'shared/snippets/embed', collection: @blobs, as: :blob))}');
diff --git a/app/views/shared/topics/_topic.html.haml b/app/views/shared/topics/_topic.html.haml
index ca1098511da..83d5ecdb833 100644
--- a/app/views/shared/topics/_topic.html.haml
+++ b/app/views/shared/topics/_topic.html.haml
@@ -2,8 +2,9 @@
- detail_page_link = topic_explore_projects_path(topic_name: topic.name)
.col-lg-3.col-md-4.col-sm-12
- .gl-card.gl-mb-5
- .gl-card-body.gl-display-flex.gl-align-items-center
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' },
+ body_options: { class: 'gl-display-flex gl-align-items-center' }) do |c|
+ = c.body do
.avatar-container.rect-avatar.s40.gl-flex-shrink-0
= link_to detail_page_link do
= topic_icon(topic, class: "avatar s40")
diff --git a/app/views/shared/users/_user.html.haml b/app/views/shared/users/_user.html.haml
index 7f7cd31591e..93b3ce5f319 100644
--- a/app/views/shared/users/_user.html.haml
+++ b/app/views/shared/users/_user.html.haml
@@ -1,8 +1,8 @@
- user = local_assigns.fetch(:user)
.col-lg-3.col-md-4.col-sm-12
- .gl-card.gl-mb-5
- .gl-card-body
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }) do |c|
+ = c.body do
= image_tag avatar_icon_for_user(user, 40), class: "avatar s40", alt: ''
.user-info
diff --git a/app/views/shared/wikis/_form.html.haml b/app/views/shared/wikis/_form.html.haml
index 34bedbd928a..0d5e59919cb 100644
--- a/app/views/shared/wikis/_form.html.haml
+++ b/app/views/shared/wikis/_form.html.haml
@@ -1,6 +1,6 @@
- page_info = { last_commit_sha: @page.last_commit_sha, persisted: @page.persisted?, title: @page.title, content: @page.content || '', format: @page.format.to_s, uploads_path: uploads_path, path: wiki_page_path(@wiki, @page), wiki_path: wiki_path(@wiki), help_path: help_page_path('user/project/wiki/index'), markdown_help_path: help_page_path('user/markdown'), markdown_preview_path: wiki_page_path(@wiki, @page, action: :preview_markdown), create_path: wiki_path(@wiki, action: :create) }
.gl-mt-3
- = form_errors(@page, truncate: :title)
+ = form_errors(@page, truncate: :title, pajamas_alert: true)
#js-wiki-form{ data: { page_info: page_info.to_json, format_options: wiki_markup_hash_by_name_id.to_json } }
diff --git a/app/views/shared/wikis/_sidebar.html.haml b/app/views/shared/wikis/_sidebar.html.haml
index 5f181371663..cb6a67bd8d4 100644
--- a/app/views/shared/wikis/_sidebar.html.haml
+++ b/app/views/shared/wikis/_sidebar.html.haml
@@ -16,7 +16,7 @@
- edit_sidebar_url = wiki_page_path(@wiki, Wiki::SIDEBAR, action: :edit)
- link_class = (editing && @page&.slug == Wiki::SIDEBAR) ? 'active' : ''
= link_to edit_sidebar_url, class: link_class, data: { qa_selector: 'edit_sidebar_link' } do
- = sprite_icon('pencil-square', css_class: 'gl-mr-2')
+ = sprite_icon('pencil', css_class: 'gl-mr-2')
%span= _("Edit sidebar")
- if @sidebar_error.present?
diff --git a/app/views/shared/wikis/diff.html.haml b/app/views/shared/wikis/diff.html.haml
index 0eeceac28c8..c39739ac422 100644
--- a/app/views/shared/wikis/diff.html.haml
+++ b/app/views/shared/wikis/diff.html.haml
@@ -5,7 +5,7 @@
.wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row
= wiki_sidebar_toggle_button
- %h3.page-title.gl-flex-grow-1
+ %h1.page-title.gl-font-size-h-display.gl-flex-grow-1
= link_to_wiki_page @page
%span.light
&middot;
diff --git a/app/views/shared/wikis/edit.html.haml b/app/views/shared/wikis/edit.html.haml
index e0860bc473d..6bbce6b80d8 100644
--- a/app/views/shared/wikis/edit.html.haml
+++ b/app/views/shared/wikis/edit.html.haml
@@ -7,7 +7,7 @@
.js-wiki-edit-page.wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row
= wiki_sidebar_toggle_button
- %h3.page-title.gl-flex-grow-1
+ %h1.page-title.gl-font-size-h-display.gl-flex-grow-1
- if @page.persisted?
= link_to_wiki_page @page
%span.light
diff --git a/app/views/shared/wikis/history.html.haml b/app/views/shared/wikis/history.html.haml
index afbed3b0f42..052bbb3b410 100644
--- a/app/views/shared/wikis/history.html.haml
+++ b/app/views/shared/wikis/history.html.haml
@@ -4,7 +4,7 @@
.wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row
= wiki_sidebar_toggle_button
- %h3.page-title
+ %h1.page-title.gl-font-size-h-display
= link_to_wiki_page @page
%span.light
&middot;
diff --git a/app/views/shared/wikis/pages.html.haml b/app/views/shared/wikis/pages.html.haml
index abe7753b9f1..21d63a6db3d 100644
--- a/app/views/shared/wikis/pages.html.haml
+++ b/app/views/shared/wikis/pages.html.haml
@@ -5,7 +5,7 @@
- wiki_sort_options = [{ text: s_("Wiki|Title"), value: 'title', href: wiki_path(@wiki, action: :pages, sort: Wiki::TITLE_ORDER)}, { text: s_("Wiki|Created date"), value: 'created_at', href: wiki_path(@wiki, action: :pages, sort: Wiki::CREATED_AT_ORDER) }]
.wiki-page-header.top-area.flex-column.flex-lg-row
- %h3.page-title.gl-flex-grow-1
+ %h1.page-title.gl-font-size-h-display.gl-flex-grow-1
= s_("Wiki|Wiki Pages")
.nav-controls.pb-md-3.pb-lg-0
diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml
index f737e347c39..5fa4a6775f9 100644
--- a/app/views/snippets/edit.html.haml
+++ b/app/views/snippets/edit.html.haml
@@ -3,7 +3,7 @@
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco')
-%h3.page-title
+%h1.page-title.gl-font-size-h-display
= _("Edit Snippet")
%hr
= render 'shared/snippets/form', url: gitlab_snippet_path(@snippet)
diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml
index 2669754cc3a..418f96a1024 100644
--- a/app/views/snippets/new.html.haml
+++ b/app/views/snippets/new.html.haml
@@ -4,7 +4,7 @@
- @content_class = "limit-container-width" unless fluid_layout
.page-title-holder.d-flex.align-items-center
- %h1.page-title= _('New Snippet')
+ %h1.page-title.gl-font-size-h-display= _('New Snippet')
.gl-mt-3
= render "shared/snippets/form", url: snippets_path(@snippet)
diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml
index 2e94bbe4baf..e3a14b0454e 100644
--- a/app/views/snippets/notes/_actions.html.haml
+++ b/app/views/snippets/notes/_actions.html.haml
@@ -1,15 +1,13 @@
- if current_user
- if note.emoji_awardable?
.note-actions-item
- = link_to '#', title: _('Add reaction'), class: "note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip", data: { position: 'right' } do
- %span{ class: 'link-highlight award-control-icon-neutral' }= sprite_icon('slight-smile')
- %span{ class: 'link-highlight award-control-icon-positive' }= sprite_icon('smiley')
- %span{ class: 'link-highlight award-control-icon-super-positive' }= sprite_icon('smile')
+ = render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { title: _('Add reaction'), class: 'btn-icon note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip' }) do
+ = sprite_icon('slight-smile', css_class: 'award-control-icon-neutral gl-button-icon gl-icon')
+ = sprite_icon('smiley', css_class: 'award-control-icon-positive gl-button-icon gl-icon gl-left-3!')
+ = sprite_icon('smile', css_class: 'award-control-icon-super-positive gl-button-icon gl-icon gl-left-3! ')
- if note_editable
- .note-actions-item
- = button_tag title: _('Edit comment'), class: 'note-action-button js-note-edit has-tooltip gl-button btn btn-transparent', data: { container: 'body', qa_selector: 'edit_comment_button' } do
- %span.link-highlight
- = custom_icon('icon_pencil')
+ .note-actions-item.gl-ml-0
+ = render Pajamas::ButtonComponent.new(category: :tertiary, icon: 'pencil', button_options: { title: _('Edit comment'), class: 'note-action-button js-note-edit has-tooltip', data: { container: 'body', qa_selector: 'edit_comment_button' } })
= render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
diff --git a/app/views/users/unsubscribes/show.html.haml b/app/views/users/unsubscribes/show.html.haml
index 8b3dc69f3a7..df8989ad979 100644
--- a/app/views/users/unsubscribes/show.html.haml
+++ b/app/views/users/unsubscribes/show.html.haml
@@ -1,5 +1,5 @@
- page_title _("Unsubscribe"), _("Admin Notifications")
-%h3.page-title Unsubscribe from Admin notifications
+%h1.page-title.gl-font-size-h-display Unsubscribe from Admin notifications
%hr
= form_tag unsubscribe_path(Base64.urlsafe_encode64(@email)) do
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 8a525e455fd..ab75abff9ba 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -6,7 +6,7 @@
- :name: authorized_project_update:authorized_project_update_project_recalculate
:worker_name: AuthorizedProjectUpdate::ProjectRecalculateWorker
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 1
@@ -15,7 +15,7 @@
- :name: authorized_project_update:authorized_project_update_project_recalculate_per_user
:worker_name: AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 1
@@ -24,7 +24,7 @@
- :name: authorized_project_update:authorized_project_update_user_refresh_from_replica
:worker_name: AuthorizedProjectUpdate::UserRefreshFromReplicaWorker
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -33,7 +33,7 @@
- :name: authorized_project_update:authorized_project_update_user_refresh_over_user_range
:worker_name: AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -42,79 +42,79 @@
- :name: authorized_project_update:authorized_project_update_user_refresh_with_low_urgency
:worker_name: AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
- :weight: 1
+ :weight: 2
:idempotent: true
:tags: []
- :name: auto_devops:auto_devops_disable
:worker_name: AutoDevops::DisableWorker
:feature_category: :auto_devops
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: auto_merge:auto_merge_process
:worker_name: AutoMergeProcessWorker
:feature_category: :continuous_delivery
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 3
- :idempotent:
+ :idempotent: false
:tags: []
- :name: chaos:chaos_cpu_spin
:worker_name: Chaos::CpuSpinWorker
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: chaos:chaos_db_spin
:worker_name: Chaos::DbSpinWorker
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: chaos:chaos_kill
:worker_name: Chaos::KillWorker
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: chaos:chaos_leak_mem
:worker_name: Chaos::LeakMemWorker
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: chaos:chaos_sleep
:worker_name: Chaos::SleepWorker
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cluster_agent:clusters_agents_delete_expired_events
:worker_name: Clusters::Agents::DeleteExpiredEventsWorker
:feature_category: :kubernetes_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -123,7 +123,7 @@
- :name: container_repository:cleanup_container_repository
:worker_name: CleanupContainerRepositoryWorker
:feature_category: :container_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -132,7 +132,7 @@
- :name: container_repository:container_expiration_policies_cleanup_container_repository
:worker_name: ContainerExpirationPolicies::CleanupContainerRepositoryWorker
:feature_category: :container_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -141,25 +141,25 @@
- :name: container_repository:delete_container_repository
:worker_name: DeleteContainerRepositoryWorker
:feature_category: :container_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:admin_email
:worker_name: AdminEmailWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:analytics_usage_trends_count_job_trigger
:worker_name: Analytics::UsageTrends::CountJobTriggerWorker
:feature_category: :devops_reports
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -168,7 +168,7 @@
- :name: cronjob:authorized_project_update_periodic_recalculate
:worker_name: AuthorizedProjectUpdate::PeriodicRecalculateWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -177,7 +177,7 @@
- :name: cronjob:bulk_imports_stuck_import
:worker_name: BulkImports::StuckImportWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -186,16 +186,16 @@
- :name: cronjob:ci_archive_traces_cron
:worker_name: Ci::ArchiveTracesCronWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:ci_delete_unit_tests
:worker_name: Ci::DeleteUnitTestsWorker
:feature_category: :code_testing
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -204,7 +204,7 @@
- :name: cronjob:ci_pipeline_artifacts_expire_artifacts
:worker_name: Ci::PipelineArtifacts::ExpireArtifactsWorker
:feature_category: :build_artifacts
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -213,16 +213,16 @@
- :name: cronjob:ci_platform_metrics_update_cron
:worker_name: CiPlatformMetricsUpdateCronWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:ci_schedule_delete_objects_cron
:worker_name: Ci::ScheduleDeleteObjectsCronWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -231,7 +231,7 @@
- :name: cronjob:ci_stuck_builds_drop_running
:worker_name: Ci::StuckBuilds::DropRunningWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -240,7 +240,7 @@
- :name: cronjob:ci_stuck_builds_drop_scheduled
:worker_name: Ci::StuckBuilds::DropScheduledWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -249,11 +249,11 @@
- :name: cronjob:ci_update_locked_unknown_artifacts
:worker_name: Ci::UpdateLockedUnknownArtifactsWorker
:feature_category: :build_artifacts
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :throttled
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:clusters_integrations_check_prometheus_health
:worker_name: Clusters::Integrations::CheckPrometheusHealthWorker
@@ -267,16 +267,16 @@
- :name: cronjob:container_expiration_policy
:worker_name: ContainerExpirationPolicyWorker
:feature_category: :container_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:container_registry_migration_enqueuer
:worker_name: ContainerRegistry::Migration::EnqueuerWorker
:feature_category: :container_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -285,7 +285,7 @@
- :name: cronjob:container_registry_migration_guard
:worker_name: ContainerRegistry::Migration::GuardWorker
:feature_category: :container_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -294,7 +294,7 @@
- :name: cronjob:container_registry_migration_observer
:worker_name: ContainerRegistry::Migration::ObserverWorker
:feature_category: :container_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -303,7 +303,7 @@
- :name: cronjob:database_batched_background_migration
:worker_name: Database::BatchedBackgroundMigrationWorker
:feature_category: :database
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -312,7 +312,7 @@
- :name: cronjob:database_batched_background_migration_ci_database
:worker_name: Database::BatchedBackgroundMigration::CiDatabaseWorker
:feature_category: :database
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -320,8 +320,8 @@
:tags: []
- :name: cronjob:database_ci_namespace_mirrors_consistency_check
:worker_name: Database::CiNamespaceMirrorsConsistencyCheckWorker
- :feature_category: :sharding
- :has_external_dependencies:
+ :feature_category: :pods
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -329,8 +329,8 @@
:tags: []
- :name: cronjob:database_ci_project_mirrors_consistency_check
:worker_name: Database::CiProjectMirrorsConsistencyCheckWorker
- :feature_category: :sharding
- :has_external_dependencies:
+ :feature_category: :pods
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -339,7 +339,7 @@
- :name: cronjob:database_drop_detached_partitions
:worker_name: Database::DropDetachedPartitionsWorker
:feature_category: :database
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -348,7 +348,7 @@
- :name: cronjob:database_partition_management
:worker_name: Database::PartitionManagementWorker
:feature_category: :database
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -357,7 +357,7 @@
- :name: cronjob:dependency_proxy_cleanup_dependency_proxy
:worker_name: DependencyProxy::CleanupDependencyProxyWorker
:feature_category: :dependency_proxy
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -366,16 +366,16 @@
- :name: cronjob:dependency_proxy_image_ttl_group_policy
:worker_name: DependencyProxy::ImageTtlGroupPolicyWorker
:feature_category: :dependency_proxy
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:environments_auto_delete_cron
:worker_name: Environments::AutoDeleteCronWorker
:feature_category: :continuous_delivery
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -384,61 +384,61 @@
- :name: cronjob:environments_auto_stop_cron
:worker_name: Environments::AutoStopCronWorker
:feature_category: :continuous_delivery
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:expire_build_artifacts
:worker_name: ExpireBuildArtifactsWorker
:feature_category: :build_artifacts
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:gitlab_service_ping
:worker_name: GitlabServicePingWorker
:feature_category: :service_ping
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:import_export_project_cleanup
:worker_name: ImportExportProjectCleanupWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:import_stuck_project_import_jobs
:worker_name: Gitlab::Import::StuckProjectImportJobsWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:issue_due_scheduler
:worker_name: IssueDueSchedulerWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:issues_reschedule_stuck_issue_rebalances
:worker_name: Issues::RescheduleStuckIssueRebalancesWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -447,16 +447,16 @@
- :name: cronjob:jira_import_stuck_jira_import_jobs
:worker_name: Gitlab::JiraImport::StuckJiraImportJobsWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:loose_foreign_keys_cleanup
:worker_name: LooseForeignKeys::CleanupWorker
- :feature_category: :sharding
- :has_external_dependencies:
+ :feature_category: :pods
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -465,16 +465,16 @@
- :name: cronjob:member_invitation_reminder_emails
:worker_name: MemberInvitationReminderEmailsWorker
:feature_category: :subgroups
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:metrics_dashboard_schedule_annotations_prune
:worker_name: Metrics::Dashboard::ScheduleAnnotationsPruneWorker
:feature_category: :metrics
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -483,25 +483,25 @@
- :name: cronjob:namespaces_in_product_marketing_emails
:worker_name: Namespaces::InProductMarketingEmailsWorker
:feature_category: :experimentation_activation
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:namespaces_prune_aggregation_schedules
:worker_name: Namespaces::PruneAggregationSchedulesWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:packages_cleanup_package_registry
:worker_name: Packages::CleanupPackageRegistryWorker
:feature_category: :package_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -510,7 +510,7 @@
- :name: cronjob:packages_composer_cache_cleanup
:worker_name: Packages::Composer::CacheCleanupWorker
:feature_category: :package_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -519,34 +519,34 @@
- :name: cronjob:pages_domain_removal_cron
:worker_name: PagesDomainRemovalCronWorker
:feature_category: :pages
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:pages_domain_ssl_renewal_cron
:worker_name: PagesDomainSslRenewalCronWorker
:feature_category: :pages
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:pages_domain_verification_cron
:worker_name: PagesDomainVerificationCronWorker
:feature_category: :pages
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:partition_creation
:worker_name: PartitionCreationWorker
:feature_category: :database
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -555,34 +555,34 @@
- :name: cronjob:personal_access_tokens_expired_notification
:worker_name: PersonalAccessTokens::ExpiredNotificationWorker
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:personal_access_tokens_expiring
:worker_name: PersonalAccessTokens::ExpiringWorker
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:pipeline_schedule
:worker_name: PipelineScheduleWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:projects_inactive_projects_deletion_cron
:worker_name: Projects::InactiveProjectsDeletionCronWorker
:feature_category: :compliance_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -591,7 +591,7 @@
- :name: cronjob:projects_schedule_refresh_build_artifacts_size_statistics
:worker_name: Projects::ScheduleRefreshBuildArtifactsSizeStatisticsWorker
:feature_category: :build_artifacts
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -600,43 +600,43 @@
- :name: cronjob:prune_old_events
:worker_name: PruneOldEventsWorker
:feature_category: :users
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:releases_manage_evidence
:worker_name: Releases::ManageEvidenceWorker
:feature_category: :release_evidence
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:remove_expired_group_links
:worker_name: RemoveExpiredGroupLinksWorker
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:remove_expired_members
:worker_name: RemoveExpiredMembersWorker
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:remove_unaccepted_member_invites
:worker_name: RemoveUnacceptedMemberInvitesWorker
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -645,7 +645,7 @@
- :name: cronjob:remove_unreferenced_lfs_objects
:worker_name: RemoveUnreferencedLfsObjectsWorker
:feature_category: :git_lfs
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -654,25 +654,25 @@
- :name: cronjob:repository_archive_cache
:worker_name: RepositoryArchiveCacheWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:repository_check_dispatch
:worker_name: RepositoryCheck::DispatchWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:schedule_merge_request_cleanup_refs
:worker_name: ScheduleMergeRequestCleanupRefsWorker
:feature_category: :code_review
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -681,16 +681,16 @@
- :name: cronjob:schedule_migrate_external_diffs
:worker_name: ScheduleMigrateExternalDiffsWorker
:feature_category: :code_review
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:ssh_keys_expired_notification
:worker_name: SshKeys::ExpiredNotificationWorker
:feature_category: :compliance_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -699,7 +699,7 @@
- :name: cronjob:ssh_keys_expiring_soon_notification
:worker_name: SshKeys::ExpiringSoonNotificationWorker
:feature_category: :compliance_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -708,43 +708,43 @@
- :name: cronjob:stuck_ci_jobs
:worker_name: StuckCiJobsWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:stuck_export_jobs
:worker_name: StuckExportJobsWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:stuck_merge_jobs
:worker_name: StuckMergeJobsWorker
:feature_category: :code_review
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:trending_projects
:worker_name: TrendingProjectsWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:update_container_registry_info
:worker_name: UpdateContainerRegistryInfoWorker
:feature_category: :container_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -753,7 +753,7 @@
- :name: cronjob:user_status_cleanup_batch
:worker_name: UserStatusCleanup::BatchWorker
:feature_category: :users
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -762,20 +762,20 @@
- :name: cronjob:users_create_statistics
:worker_name: Users::CreateStatisticsWorker
:feature_category: :users
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:users_deactivate_dormant_users
:worker_name: Users::DeactivateDormantUsersWorker
:feature_category: :utilization
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: cronjob:x509_issuer_crl_check
:worker_name: X509IssuerCrlCheckWorker
@@ -789,7 +789,7 @@
- :name: dependency_proxy:purge_dependency_proxy_cache
:worker_name: PurgeDependencyProxyCacheWorker
:feature_category: :dependency_proxy
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -798,7 +798,7 @@
- :name: dependency_proxy_blob:dependency_proxy_cleanup_blob
:worker_name: DependencyProxy::CleanupBlobWorker
:feature_category: :dependency_proxy
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -807,7 +807,7 @@
- :name: dependency_proxy_manifest:dependency_proxy_cleanup_manifest
:worker_name: DependencyProxy::CleanupManifestWorker
:feature_category: :dependency_proxy
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -816,7 +816,7 @@
- :name: deployment:deployments_archive_in_project
:worker_name: Deployments::ArchiveInProjectWorker
:feature_category: :continuous_delivery
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 3
@@ -825,25 +825,25 @@
- :name: deployment:deployments_drop_older_deployments
:worker_name: Deployments::DropOlderDeploymentsWorker
:feature_category: :continuous_delivery
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 3
- :idempotent:
+ :idempotent: false
:tags: []
- :name: deployment:deployments_hooks
:worker_name: Deployments::HooksWorker
:feature_category: :continuous_delivery
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 3
- :idempotent:
+ :idempotent: false
:tags: []
- :name: deployment:deployments_link_merge_request
:worker_name: Deployments::LinkMergeRequestWorker
:feature_category: :continuous_delivery
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 3
@@ -852,7 +852,7 @@
- :name: deployment:deployments_update_environment
:worker_name: Deployments::UpdateEnvironmentWorker
:feature_category: :continuous_delivery
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 3
@@ -865,7 +865,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: gcp_cluster:cluster_install_app
:worker_name: ClusterInstallAppWorker
@@ -874,7 +874,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: gcp_cluster:cluster_patch_app
:worker_name: ClusterPatchAppWorker
@@ -883,7 +883,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: gcp_cluster:cluster_provision
:worker_name: ClusterProvisionWorker
@@ -892,16 +892,16 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: gcp_cluster:cluster_update_app
:worker_name: ClusterUpdateAppWorker
:feature_category: :kubernetes_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: gcp_cluster:cluster_upgrade_app
:worker_name: ClusterUpgradeAppWorker
@@ -910,7 +910,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: gcp_cluster:cluster_wait_for_app_installation
:worker_name: ClusterWaitForAppInstallationWorker
@@ -919,16 +919,16 @@
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: gcp_cluster:cluster_wait_for_app_update
:worker_name: ClusterWaitForAppUpdateWorker
:feature_category: :kubernetes_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: gcp_cluster:cluster_wait_for_ingress_ip_address
:worker_name: ClusterWaitForIngressIpAddressWorker
@@ -937,25 +937,43 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
+ :tags: []
+- :name: gcp_cluster:clusters_applications_activate_integration
+ :worker_name: Clusters::Applications::ActivateIntegrationWorker
+ :feature_category: :kubernetes_management
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
:tags: []
- :name: gcp_cluster:clusters_applications_activate_service
:worker_name: Clusters::Applications::ActivateServiceWorker
:feature_category: :kubernetes_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
+ :tags: []
+- :name: gcp_cluster:clusters_applications_deactivate_integration
+ :worker_name: Clusters::Applications::DeactivateIntegrationWorker
+ :feature_category: :kubernetes_management
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
:tags: []
- :name: gcp_cluster:clusters_applications_deactivate_service
:worker_name: Clusters::Applications::DeactivateServiceWorker
:feature_category: :kubernetes_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: gcp_cluster:clusters_applications_uninstall
:worker_name: Clusters::Applications::UninstallWorker
@@ -964,7 +982,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: gcp_cluster:clusters_applications_wait_for_uninstall_app
:worker_name: Clusters::Applications::WaitForUninstallAppWorker
@@ -973,7 +991,7 @@
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: gcp_cluster:clusters_cleanup_project_namespace
:worker_name: Clusters::Cleanup::ProjectNamespaceWorker
@@ -982,7 +1000,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: gcp_cluster:clusters_cleanup_service_account
:worker_name: Clusters::Cleanup::ServiceAccountWorker
@@ -991,7 +1009,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: gcp_cluster:wait_for_cluster_creation
:worker_name: WaitForClusterCreationWorker
@@ -1000,7 +1018,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: github_importer:github_import_import_diff_note
:worker_name: Gitlab::GithubImport::ImportDiffNoteWorker
@@ -1009,7 +1027,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: github_importer:github_import_import_issue
:worker_name: Gitlab::GithubImport::ImportIssueWorker
@@ -1018,7 +1036,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: github_importer:github_import_import_lfs_object
:worker_name: Gitlab::GithubImport::ImportLfsObjectWorker
@@ -1027,7 +1045,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: github_importer:github_import_import_note
:worker_name: Gitlab::GithubImport::ImportNoteWorker
@@ -1036,7 +1054,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: github_importer:github_import_import_pull_request
:worker_name: Gitlab::GithubImport::ImportPullRequestWorker
@@ -1045,7 +1063,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: github_importer:github_import_import_pull_request_merged_by
:worker_name: Gitlab::GithubImport::ImportPullRequestMergedByWorker
@@ -1054,7 +1072,7 @@
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: github_importer:github_import_import_pull_request_review
:worker_name: Gitlab::GithubImport::ImportPullRequestReviewWorker
@@ -1063,156 +1081,156 @@
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: github_importer:github_import_refresh_import_jid
:worker_name: Gitlab::GithubImport::RefreshImportJidWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: github_importer:github_import_stage_finish_import
:worker_name: Gitlab::GithubImport::Stage::FinishImportWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: github_importer:github_import_stage_import_base_data
:worker_name: Gitlab::GithubImport::Stage::ImportBaseDataWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: github_importer:github_import_stage_import_issues_and_diff_notes
:worker_name: Gitlab::GithubImport::Stage::ImportIssuesAndDiffNotesWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: github_importer:github_import_stage_import_lfs_objects
:worker_name: Gitlab::GithubImport::Stage::ImportLfsObjectsWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: github_importer:github_import_stage_import_notes
:worker_name: Gitlab::GithubImport::Stage::ImportNotesWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: github_importer:github_import_stage_import_pull_requests
:worker_name: Gitlab::GithubImport::Stage::ImportPullRequestsWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: github_importer:github_import_stage_import_pull_requests_merged_by
:worker_name: Gitlab::GithubImport::Stage::ImportPullRequestsMergedByWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: github_importer:github_import_stage_import_pull_requests_reviews
:worker_name: Gitlab::GithubImport::Stage::ImportPullRequestsReviewsWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: github_importer:github_import_stage_import_repository
:worker_name: Gitlab::GithubImport::Stage::ImportRepositoryWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: hashed_storage:hashed_storage_migrator
:worker_name: HashedStorage::MigratorWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: hashed_storage:hashed_storage_project_migrate
:worker_name: HashedStorage::ProjectMigrateWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: hashed_storage:hashed_storage_project_rollback
:worker_name: HashedStorage::ProjectRollbackWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: hashed_storage:hashed_storage_rollbacker
:worker_name: HashedStorage::RollbackerWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: incident_management:incident_management_add_severity_system_note
:worker_name: IncidentManagement::AddSeveritySystemNoteWorker
:feature_category: :incident_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: incident_management:incident_management_pager_duty_process_incident
:worker_name: IncidentManagement::PagerDuty::ProcessIncidentWorker
:feature_category: :incident_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: incident_management:incident_management_process_alert_worker_v2
:worker_name: IncidentManagement::ProcessAlertWorkerV2
:feature_category: :incident_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 2
@@ -1225,7 +1243,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: jira_connect:jira_connect_retry_request
:worker_name: JiraConnect::RetryRequestWorker
@@ -1234,7 +1252,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: jira_connect:jira_connect_sync_branch
:worker_name: JiraConnect::SyncBranchWorker
@@ -1243,7 +1261,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: jira_connect:jira_connect_sync_builds
:worker_name: JiraConnect::SyncBuildsWorker
@@ -1252,7 +1270,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: jira_connect:jira_connect_sync_deployments
:worker_name: JiraConnect::SyncDeploymentsWorker
@@ -1261,7 +1279,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: jira_connect:jira_connect_sync_feature_flags
:worker_name: JiraConnect::SyncFeatureFlagsWorker
@@ -1270,7 +1288,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: jira_connect:jira_connect_sync_merge_request
:worker_name: JiraConnect::SyncMergeRequestWorker
@@ -1279,7 +1297,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: jira_connect:jira_connect_sync_project
:worker_name: JiraConnect::SyncProjectWorker
@@ -1288,156 +1306,156 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: jira_importer:jira_import_advance_stage
:worker_name: Gitlab::JiraImport::AdvanceStageWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: jira_importer:jira_import_import_issue
:worker_name: Gitlab::JiraImport::ImportIssueWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: jira_importer:jira_import_stage_finish_import
:worker_name: Gitlab::JiraImport::Stage::FinishImportWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: jira_importer:jira_import_stage_import_attachments
:worker_name: Gitlab::JiraImport::Stage::ImportAttachmentsWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: jira_importer:jira_import_stage_import_issues
:worker_name: Gitlab::JiraImport::Stage::ImportIssuesWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: jira_importer:jira_import_stage_import_labels
:worker_name: Gitlab::JiraImport::Stage::ImportLabelsWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: jira_importer:jira_import_stage_import_notes
:worker_name: Gitlab::JiraImport::Stage::ImportNotesWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: jira_importer:jira_import_stage_start_import
:worker_name: Gitlab::JiraImport::Stage::StartImportWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: mail_scheduler:mail_scheduler_issue_due
:worker_name: MailScheduler::IssueDueWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: mail_scheduler:mail_scheduler_notification_service
:worker_name: MailScheduler::NotificationServiceWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: object_pool:object_pool_create
:worker_name: ObjectPool::CreateWorker
:feature_category: :gitaly
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: object_pool:object_pool_destroy
:worker_name: ObjectPool::DestroyWorker
:feature_category: :gitaly
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: object_pool:object_pool_join
:worker_name: ObjectPool::JoinWorker
:feature_category: :gitaly
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: object_pool:object_pool_schedule_join
:worker_name: ObjectPool::ScheduleJoinWorker
:feature_category: :gitaly
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: object_storage:object_storage_background_move
:worker_name: ObjectStorage::BackgroundMoveWorker
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: object_storage:object_storage_migrate_uploads
:worker_name: ObjectStorage::MigrateUploadsWorker
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: package_cleanup:packages_cleanup_package_file
:worker_name: Packages::CleanupPackageFileWorker
:feature_category: :package_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1446,7 +1464,7 @@
- :name: package_cleanup:packages_mark_package_files_for_destruction
:worker_name: Packages::MarkPackageFilesForDestructionWorker
:feature_category: :package_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1455,7 +1473,7 @@
- :name: package_repositories:packages_debian_generate_distribution
:worker_name: Packages::Debian::GenerateDistributionWorker
:feature_category: :package_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1464,7 +1482,7 @@
- :name: package_repositories:packages_debian_process_changes
:worker_name: Packages::Debian::ProcessChangesWorker
:feature_category: :package_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1473,7 +1491,7 @@
- :name: package_repositories:packages_go_sync_packages
:worker_name: Packages::Go::SyncPackagesWorker
:feature_category: :package_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1482,7 +1500,7 @@
- :name: package_repositories:packages_helm_extraction
:worker_name: Packages::Helm::ExtractionWorker
:feature_category: :package_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1491,7 +1509,7 @@
- :name: package_repositories:packages_maven_metadata_sync
:worker_name: Packages::Maven::Metadata::SyncWorker
:feature_category: :package_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1500,43 +1518,43 @@
- :name: package_repositories:packages_nuget_extraction
:worker_name: Packages::Nuget::ExtractionWorker
:feature_category: :package_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: package_repositories:packages_rubygems_extraction
:worker_name: Packages::Rubygems::ExtractionWorker
:feature_category: :package_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_background:archive_trace
:worker_name: ArchiveTraceWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_background:ci_archive_trace
:worker_name: Ci::ArchiveTraceWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_background:ci_build_trace_chunk_flush
:worker_name: Ci::BuildTraceChunkFlushWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1545,7 +1563,7 @@
- :name: pipeline_background:ci_daily_build_group_report_results
:worker_name: Ci::DailyBuildGroupReportResultsWorker
:feature_category: :code_testing
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1554,7 +1572,7 @@
- :name: pipeline_background:ci_pending_builds_update_group
:worker_name: Ci::PendingBuilds::UpdateGroupWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1563,7 +1581,7 @@
- :name: pipeline_background:ci_pending_builds_update_project
:worker_name: Ci::PendingBuilds::UpdateProjectWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1572,7 +1590,7 @@
- :name: pipeline_background:ci_pipeline_artifacts_coverage_report
:worker_name: Ci::PipelineArtifacts::CoverageReportWorker
:feature_category: :code_testing
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1581,7 +1599,7 @@
- :name: pipeline_background:ci_pipeline_artifacts_create_quality_report
:worker_name: Ci::PipelineArtifacts::CreateQualityReportWorker
:feature_category: :code_quality
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1590,7 +1608,7 @@
- :name: pipeline_background:ci_pipeline_success_unlock_artifacts
:worker_name: Ci::PipelineSuccessUnlockArtifactsWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1599,7 +1617,7 @@
- :name: pipeline_background:ci_ref_delete_unlock_artifacts
:worker_name: Ci::RefDeleteUnlockArtifactsWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1608,52 +1626,34 @@
- :name: pipeline_background:ci_test_failure_history
:worker_name: Ci::TestFailureHistoryWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
-- :name: pipeline_cache:expire_job_cache
- :worker_name: ExpireJobCacheWorker
- :feature_category: :continuous_integration
- :has_external_dependencies:
- :urgency: :high
- :resource_boundary: :unknown
- :weight: 3
- :idempotent: true
- :tags: []
-- :name: pipeline_cache:expire_pipeline_cache
- :worker_name: ExpirePipelineCacheWorker
- :feature_category: :continuous_integration
- :has_external_dependencies:
- :urgency: :high
- :resource_boundary: :cpu
- :weight: 3
- :idempotent:
- :tags: []
- :name: pipeline_creation:ci_external_pull_requests_create_pipeline
:worker_name: Ci::ExternalPullRequests::CreatePipelineWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 4
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_creation:create_pipeline
:worker_name: CreatePipelineWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 4
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_creation:merge_requests_create_pipeline
:worker_name: MergeRequests::CreatePipelineWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 4
@@ -1662,34 +1662,34 @@
- :name: pipeline_creation:run_pipeline_schedule
:worker_name: RunPipelineScheduleWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 4
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_default:ci_create_cross_project_pipeline
:worker_name: Ci::CreateCrossProjectPipelineWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 3
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_default:ci_create_downstream_pipeline
:worker_name: Ci::CreateDownstreamPipelineWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 3
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_default:ci_drop_pipeline
:worker_name: Ci::DropPipelineWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 3
@@ -1698,7 +1698,7 @@
- :name: pipeline_default:ci_merge_requests_add_todo_when_build_fails
:worker_name: Ci::MergeRequests::AddTodoWhenBuildFailsWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 3
@@ -1707,115 +1707,115 @@
- :name: pipeline_default:ci_pipeline_bridge_status
:worker_name: Ci::PipelineBridgeStatusWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 3
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_default:ci_retry_pipeline
:worker_name: Ci::RetryPipelineWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 3
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_default:pipeline_metrics
:worker_name: PipelineMetricsWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 3
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_default:pipeline_notification
:worker_name: PipelineNotificationWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 3
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_hooks:build_hooks
:worker_name: BuildHooksWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_hooks:pipeline_hooks
:worker_name: PipelineHooksWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_processing:build_finished
:worker_name: BuildFinishedWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 5
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_processing:build_queue
:worker_name: BuildQueueWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 5
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_processing:build_success
:worker_name: BuildSuccessWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 5
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_processing:ci_build_finished
:worker_name: Ci::BuildFinishedWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 5
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_processing:ci_build_prepare
:worker_name: Ci::BuildPrepareWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 5
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_processing:ci_build_schedule
:worker_name: Ci::BuildScheduleWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 5
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pipeline_processing:ci_initial_pipeline_process
:worker_name: Ci::InitialPipelineProcessWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 5
@@ -1824,7 +1824,7 @@
- :name: pipeline_processing:ci_resource_groups_assign_resource_from_resource_group
:worker_name: Ci::ResourceGroups::AssignResourceFromResourceGroupWorker
:feature_category: :continuous_delivery
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 5
@@ -1833,7 +1833,7 @@
- :name: pipeline_processing:pipeline_process
:worker_name: PipelineProcessWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 5
@@ -1842,7 +1842,7 @@
- :name: pipeline_processing:stage_update
:worker_name: StageUpdateWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 5
@@ -1851,7 +1851,7 @@
- :name: pipeline_processing:update_head_pipeline_for_merge_request
:worker_name: UpdateHeadPipelineForMergeRequestWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 5
@@ -1860,43 +1860,52 @@
- :name: repository_check:repository_check_batch
:worker_name: RepositoryCheck::BatchWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: repository_check:repository_check_clear
:worker_name: RepositoryCheck::ClearWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: repository_check:repository_check_single_repository
:worker_name: RepositoryCheck::SingleRepositoryWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
+ :tags: []
+- :name: terraform:terraform_states_destroy
+ :worker_name: Terraform::States::DestroyWorker
+ :feature_category: :infrastructure_as_code
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
:tags: []
- :name: todos_destroyer:todos_destroyer_confidential_issue
:worker_name: TodosDestroyer::ConfidentialIssueWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: todos_destroyer:todos_destroyer_destroyed_designs
:worker_name: TodosDestroyer::DestroyedDesignsWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1905,7 +1914,7 @@
- :name: todos_destroyer:todos_destroyer_destroyed_issuable
:worker_name: TodosDestroyer::DestroyedIssuableWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1914,43 +1923,43 @@
- :name: todos_destroyer:todos_destroyer_entity_leave
:worker_name: TodosDestroyer::EntityLeaveWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: todos_destroyer:todos_destroyer_group_private
:worker_name: TodosDestroyer::GroupPrivateWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: todos_destroyer:todos_destroyer_private_features
:worker_name: TodosDestroyer::PrivateFeaturesWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: todos_destroyer:todos_destroyer_project_private
:worker_name: TodosDestroyer::ProjectPrivateWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: unassign_issuables:members_destroyer_unassign_issuables
:worker_name: MembersDestroyer::UnassignIssuablesWorker
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1959,7 +1968,7 @@
- :name: update_namespace_statistics:namespaces_root_statistics
:worker_name: Namespaces::RootStatisticsWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1968,7 +1977,7 @@
- :name: update_namespace_statistics:namespaces_schedule_aggregation
:worker_name: Namespaces::ScheduleAggregationWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1977,7 +1986,7 @@
- :name: analytics_usage_trends_counter_job
:worker_name: Analytics::UsageTrends::CounterJobWorker
:feature_category: :devops_reports
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1986,7 +1995,7 @@
- :name: approve_blocked_pending_approval_users
:worker_name: ApproveBlockedPendingApprovalUsersWorker
:feature_category: :users
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1995,7 +2004,7 @@
- :name: authorized_keys
:worker_name: AuthorizedKeysWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 2
@@ -2004,7 +2013,7 @@
- :name: authorized_projects
:worker_name: AuthorizedProjectsWorker
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 2
@@ -2013,29 +2022,29 @@
- :name: background_migration
:worker_name: BackgroundMigrationWorker
:feature_category: :database
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :throttled
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: background_migration_ci_database
:worker_name: BackgroundMigration::CiDatabaseWorker
:feature_category: :database
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :throttled
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: bulk_import
:worker_name: BulkImportWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: bulk_imports_entity
:worker_name: BulkImports::EntityWorker
@@ -2062,12 +2071,12 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: bulk_imports_relation_export
:worker_name: BulkImports::RelationExportWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2080,12 +2089,12 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: ci_delete_objects
:worker_name: Ci::DeleteObjectsWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2094,7 +2103,7 @@
- :name: ci_job_artifacts_expire_project_build_artifacts
:worker_name: Ci::JobArtifacts::ExpireProjectBuildArtifactsWorker
:feature_category: :build_artifacts
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2103,7 +2112,7 @@
- :name: create_commit_signature
:worker_name: CreateCommitSignatureWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 2
@@ -2112,11 +2121,11 @@
- :name: create_note_diff_file
:worker_name: CreateNoteDiffFileWorker
:feature_category: :code_review
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: default
:worker_name:
@@ -2130,43 +2139,43 @@
- :name: delete_diff_files
:worker_name: DeleteDiffFilesWorker
:feature_category: :code_review
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: delete_merged_branches
:worker_name: DeleteMergedBranchesWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: delete_stored_files
:worker_name: DeleteStoredFilesWorker
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: delete_user
:worker_name: DeleteUserWorker
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: design_management_copy_design_collection
:worker_name: DesignManagement::CopyDesignCollectionWorker
:feature_category: :design_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2175,16 +2184,16 @@
- :name: design_management_new_version
:worker_name: DesignManagement::NewVersionWorker
:feature_category: :design_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :memory
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: destroy_pages_deployments
:worker_name: DestroyPagesDeploymentsWorker
:feature_category: :pages
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2193,16 +2202,16 @@
- :name: detect_repository_languages
:worker_name: DetectRepositoryLanguagesWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: disallow_two_factor_for_group
:worker_name: DisallowTwoFactorForGroupWorker
:feature_category: :subgroups
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2211,7 +2220,7 @@
- :name: disallow_two_factor_for_subgroups
:worker_name: DisallowTwoFactorForSubgroupsWorker
:feature_category: :subgroups
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2220,26 +2229,26 @@
- :name: email_receiver
:worker_name: EmailReceiverWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: false
:tags:
- :needs_own_queue
- :name: emails_on_push
:worker_name: EmailsOnPushWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: environments_auto_stop
:worker_name: Environments::AutoStopWorker
:feature_category: :continuous_delivery
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2261,12 +2270,12 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: experiments_record_conversion_event
:worker_name: Experiments::RecordConversionEventWorker
:feature_category: :users
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2275,11 +2284,11 @@
- :name: export_csv
:worker_name: ExportCsvWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: external_service_reactive_caching
:worker_name: ExternalServiceReactiveCachingWorker
@@ -2288,21 +2297,21 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: file_hook
:worker_name: FileHookWorker
:feature_category: :integrations
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: flush_counter_increments
:worker_name: FlushCounterIncrementsWorker
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2311,16 +2320,16 @@
- :name: github_import_advance_stage
:worker_name: Gitlab::GithubImport::AdvanceStageWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: gitlab_performance_bar_stats
:worker_name: GitlabPerformanceBarStatsWorker
:feature_category: :metrics
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
@@ -2329,43 +2338,43 @@
- :name: gitlab_shell
:worker_name: GitlabShellWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: group_destroy
:worker_name: GroupDestroyWorker
:feature_category: :subgroups
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: group_export
:worker_name: GroupExportWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: group_import
:worker_name: GroupImportWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: groups_update_statistics
:worker_name: Groups::UpdateStatisticsWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
@@ -2374,7 +2383,7 @@
- :name: import_issues_csv
:worker_name: ImportIssuesCsvWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 2
@@ -2383,43 +2392,61 @@
- :name: integrations_create_external_cross_reference
:worker_name: Integrations::CreateExternalCrossReferenceWorker
:feature_category: :integrations
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
+- :name: integrations_execute
+ :worker_name: Integrations::ExecuteWorker
+ :feature_category: :integrations
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
+- :name: integrations_irker
+ :worker_name: Integrations::IrkerWorker
+ :feature_category: :integrations
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
- :name: invalid_gpg_signature_update
:worker_name: InvalidGpgSignatureUpdateWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: irker
:worker_name: IrkerWorker
:feature_category: :integrations
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: issuable_export_csv
:worker_name: IssuableExportCsvWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: issuable_label_links_destroy
:worker_name: Issuable::LabelLinksDestroyWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2428,25 +2455,7 @@
- :name: issuables_clear_groups_issue_counter
:worker_name: Issuables::ClearGroupsIssueCounterWorker
:feature_category: :team_planning
- :has_external_dependencies:
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: true
- :tags: []
-- :name: issue_placement
- :worker_name: IssuePlacementWorker
- :feature_category: :team_planning
- :has_external_dependencies:
- :urgency: :high
- :resource_boundary: :cpu
- :weight: 2
- :idempotent: true
- :tags: []
-- :name: issue_rebalancing
- :worker_name: IssueRebalancingWorker
- :feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2455,7 +2464,7 @@
- :name: issues_placement
:worker_name: Issues::PlacementWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 2
@@ -2464,7 +2473,7 @@
- :name: issues_rebalancing
:worker_name: Issues::RebalancingWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2482,7 +2491,7 @@
- :name: merge
:worker_name: MergeWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 5
@@ -2491,7 +2500,7 @@
- :name: merge_request_cleanup_refs
:worker_name: MergeRequestCleanupRefsWorker
:feature_category: :code_review
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2500,7 +2509,7 @@
- :name: merge_request_mergeability_check
:worker_name: MergeRequestMergeabilityCheckWorker
:feature_category: :code_review
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2518,7 +2527,7 @@
- :name: merge_requests_delete_source_branch
:worker_name: MergeRequests::DeleteSourceBranchWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 1
@@ -2527,7 +2536,7 @@
- :name: merge_requests_handle_assignees_change
:worker_name: MergeRequests::HandleAssigneesChangeWorker
:feature_category: :code_review
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 1
@@ -2536,7 +2545,7 @@
- :name: merge_requests_resolve_todos
:worker_name: MergeRequests::ResolveTodosWorker
:feature_category: :code_review
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 1
@@ -2545,7 +2554,7 @@
- :name: merge_requests_update_head_pipeline
:worker_name: MergeRequests::UpdateHeadPipelineWorker
:feature_category: :code_review
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 1
@@ -2554,7 +2563,7 @@
- :name: metrics_dashboard_prune_old_annotations
:worker_name: Metrics::Dashboard::PruneOldAnnotationsWorker
:feature_category: :metrics
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2563,7 +2572,7 @@
- :name: metrics_dashboard_sync_dashboards
:worker_name: Metrics::Dashboard::SyncDashboardsWorker
:feature_category: :metrics
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2572,25 +2581,16 @@
- :name: migrate_external_diffs
:worker_name: MigrateExternalDiffsWorker
:feature_category: :code_review
- :has_external_dependencies:
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent:
- :tags: []
-- :name: namespaceless_project_destroy
- :worker_name: NamespacelessProjectDestroyWorker
- :feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: namespaces_onboarding_issue_created
:worker_name: Namespaces::OnboardingIssueCreatedWorker
:feature_category: :onboarding
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2599,7 +2599,7 @@
- :name: namespaces_onboarding_pipeline_created
:worker_name: Namespaces::OnboardingPipelineCreatedWorker
:feature_category: :onboarding
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2608,7 +2608,7 @@
- :name: namespaces_onboarding_progress
:worker_name: Namespaces::OnboardingProgressWorker
:feature_category: :onboarding
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
@@ -2617,7 +2617,7 @@
- :name: namespaces_onboarding_user_added
:worker_name: Namespaces::OnboardingUserAddedWorker
:feature_category: :onboarding
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2625,8 +2625,8 @@
:tags: []
- :name: namespaces_process_sync_events
:worker_name: Namespaces::ProcessSyncEventsWorker
- :feature_category: :sharding
- :has_external_dependencies:
+ :feature_category: :pods
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 1
@@ -2635,7 +2635,7 @@
- :name: namespaces_update_root_statistics
:worker_name: Namespaces::UpdateRootStatisticsWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2644,34 +2644,34 @@
- :name: new_issue
:worker_name: NewIssueWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: new_merge_request
:worker_name: NewMergeRequestWorker
:feature_category: :code_review
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: new_note
:worker_name: NewNoteWorker
:feature_category: :team_planning
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: packages_composer_cache_update
:worker_name: Packages::Composer::CacheUpdateWorker
:feature_category: :package_registry
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2680,52 +2680,52 @@
- :name: pages
:worker_name: PagesWorker
:feature_category: :pages
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pages_domain_ssl_renewal
:worker_name: PagesDomainSslRenewalWorker
:feature_category: :pages
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pages_domain_verification
:worker_name: PagesDomainVerificationWorker
:feature_category: :pages
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: pages_transfer
:worker_name: PagesTransferWorker
:feature_category: :pages
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: phabricator_import_import_tasks
:worker_name: Gitlab::PhabricatorImport::ImportTasksWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: post_receive
:worker_name: PostReceive
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 5
@@ -2734,7 +2734,7 @@
- :name: process_commit
:worker_name: ProcessCommitWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 3
@@ -2743,38 +2743,29 @@
- :name: project_cache
:worker_name: ProjectCacheWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
-- :name: project_daily_statistics
- :worker_name: ProjectDailyStatisticsWorker
- :feature_category: :source_code_management
- :has_external_dependencies:
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent:
- :tags: []
- :name: project_destroy
:worker_name: ProjectDestroyWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: project_export
:worker_name: ProjectExportWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :memory
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: project_service
:worker_name: ProjectServiceWorker
@@ -2783,12 +2774,12 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: projects_after_import
:worker_name: Projects::AfterImportWorker
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2797,16 +2788,16 @@
- :name: projects_git_garbage_collect
:worker_name: Projects::GitGarbageCollectWorker
:feature_category: :gitaly
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: projects_inactive_projects_deletion_notification
:worker_name: Projects::InactiveProjectsDeletionNotificationWorker
:feature_category: :compliance_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2815,7 +2806,7 @@
- :name: projects_post_creation
:worker_name: Projects::PostCreationWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2823,8 +2814,8 @@
:tags: []
- :name: projects_process_sync_events
:worker_name: Projects::ProcessSyncEventsWorker
- :feature_category: :sharding
- :has_external_dependencies:
+ :feature_category: :pods
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 1
@@ -2833,7 +2824,7 @@
- :name: projects_record_target_platforms
:worker_name: Projects::RecordTargetPlatformsWorker
:feature_category: :experimentation_activation
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2842,7 +2833,7 @@
- :name: projects_refresh_build_artifacts_size_statistics
:worker_name: Projects::RefreshBuildArtifactsSizeStatisticsWorker
:feature_category: :build_artifacts
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2851,7 +2842,7 @@
- :name: projects_schedule_bulk_repository_shard_moves
:worker_name: Projects::ScheduleBulkRepositoryShardMovesWorker
:feature_category: :gitaly
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :throttled
:resource_boundary: :unknown
:weight: 1
@@ -2860,25 +2851,16 @@
- :name: projects_update_repository_storage
:worker_name: Projects::UpdateRepositoryStorageWorker
:feature_category: :gitaly
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :throttled
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
-- :name: prometheus_create_default_alerts
- :worker_name: Prometheus::CreateDefaultAlertsWorker
- :feature_category: :incident_management
- :has_external_dependencies:
- :urgency: :high
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: true
- :tags: []
- :name: propagate_integration
:worker_name: PropagateIntegrationWorker
:feature_category: :integrations
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2887,7 +2869,7 @@
- :name: propagate_integration_group
:worker_name: PropagateIntegrationGroupWorker
:feature_category: :integrations
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2896,7 +2878,7 @@
- :name: propagate_integration_inherit
:worker_name: PropagateIntegrationInheritWorker
:feature_category: :integrations
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2905,7 +2887,7 @@
- :name: propagate_integration_inherit_descendant
:worker_name: PropagateIntegrationInheritDescendantWorker
:feature_category: :integrations
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2914,7 +2896,7 @@
- :name: propagate_integration_project
:worker_name: PropagateIntegrationProjectWorker
:feature_category: :integrations
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2923,56 +2905,56 @@
- :name: reactive_caching
:worker_name: ReactiveCachingWorker
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: rebase
:worker_name: RebaseWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: releases_create_evidence
:worker_name: Releases::CreateEvidenceWorker
:feature_category: :release_evidence
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: remote_mirror_notification
:worker_name: RemoteMirrorNotificationWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: repository_cleanup
:worker_name: RepositoryCleanupWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: repository_fork
:worker_name: RepositoryForkWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: repository_import
:worker_name: RepositoryImportWorker
@@ -2981,16 +2963,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
- :tags: []
-- :name: repository_remove_remote
- :worker_name: RepositoryRemoveRemoteWorker
- :feature_category: :source_code_management
- :has_external_dependencies:
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: repository_update_remote_mirror
:worker_name: RepositoryUpdateRemoteMirrorWorker
@@ -3004,35 +2977,35 @@
- :name: self_monitoring_project_create
:worker_name: SelfMonitoringProjectCreateWorker
:feature_category: :metrics
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: self_monitoring_project_delete
:worker_name: SelfMonitoringProjectDeleteWorker
:feature_category: :metrics
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: false
:tags: []
- :name: service_desk_email_receiver
:worker_name: ServiceDeskEmailReceiverWorker
:feature_category: :service_desk
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
- :weight: 1
- :idempotent:
+ :weight: 2
+ :idempotent: false
:tags:
- :needs_own_queue
- :name: snippets_schedule_bulk_repository_shard_moves
:worker_name: Snippets::ScheduleBulkRepositoryShardMovesWorker
:feature_category: :gitaly
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :throttled
:resource_boundary: :unknown
:weight: 1
@@ -3041,7 +3014,7 @@
- :name: snippets_update_repository_storage
:worker_name: Snippets::UpdateRepositoryStorageWorker
:feature_category: :gitaly
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :throttled
:resource_boundary: :unknown
:weight: 1
@@ -3050,16 +3023,16 @@
- :name: system_hook_push
:worker_name: SystemHookPushWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: tasks_to_be_done_create
:worker_name: TasksToBeDone::CreateWorker
:feature_category: :onboarding
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
:weight: 1
@@ -3068,16 +3041,16 @@
- :name: update_external_pull_requests
:worker_name: UpdateExternalPullRequestsWorker
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 3
- :idempotent:
+ :idempotent: false
:tags: []
- :name: update_highest_role
:worker_name: UpdateHighestRoleWorker
:feature_category: :utilization
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 2
@@ -3086,29 +3059,29 @@
- :name: update_merge_requests
:worker_name: UpdateMergeRequestsWorker
:feature_category: :code_review
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :cpu
:weight: 3
- :idempotent:
+ :idempotent: false
:tags: []
- :name: update_project_statistics
:worker_name: UpdateProjectStatisticsWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: upload_checksum
:worker_name: UploadChecksumWorker
:feature_category: :geo_replication
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: web_hook
:worker_name: WebHookWorker
@@ -3117,21 +3090,30 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: web_hooks_destroy
:worker_name: WebHooks::DestroyWorker
:feature_category: :integrations
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
+- :name: web_hooks_log_destroy
+ :worker_name: WebHooks::LogDestroyWorker
+ :feature_category: :integrations
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: web_hooks_log_execution
:worker_name: WebHooks::LogExecutionWorker
:feature_category: :integrations
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -3140,16 +3122,16 @@
- :name: wikis_git_garbage_collect
:worker_name: Wikis::GitGarbageCollectWorker
:feature_category: :gitaly
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: false
:tags: []
- :name: x509_certificate_revoke
:worker_name: X509CertificateRevokeWorker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
diff --git a/app/workers/background_migration/single_database_worker.rb b/app/workers/background_migration/single_database_worker.rb
index f3a2165c41e..2f797a24468 100644
--- a/app/workers/background_migration/single_database_worker.rb
+++ b/app/workers/background_migration/single_database_worker.rb
@@ -7,6 +7,7 @@ module BackgroundMigration
include ApplicationWorker
MAX_LEASE_ATTEMPTS = 5
+ BACKGROUND_MIGRATIONS_DELAY = 4.hours.freeze
included do
data_consistency :always
@@ -44,6 +45,18 @@ module BackgroundMigration
# lease on the class before giving up. See MR for more discussion.
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45298#note_434304956
def perform(class_name, arguments = [], lease_attempts = MAX_LEASE_ATTEMPTS)
+ unless Feature.enabled?(:execute_background_migrations, type: :ops)
+ # Delay execution of background migrations
+ self.class.perform_in(BACKGROUND_MIGRATIONS_DELAY, class_name, arguments, lease_attempts)
+
+ Sidekiq.logger.info(
+ class: self.class.name,
+ database: self.class.tracking_database,
+ message: 'skipping execution, migration rescheduled')
+
+ return
+ end
+
job_coordinator.with_shared_connection do
perform_with_connection(class_name, arguments, lease_attempts)
end
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index 114bced0b22..247105d2a1a 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -13,13 +13,13 @@ class BuildSuccessWorker # rubocop:disable Scalability/IdempotentWorker
def perform(build_id)
Ci::Build.find_by_id(build_id).try do |build|
- stop_environment(build) if build.stops_environment?
+ stop_environment(build) if build.stops_environment? && build.stop_action_successful?
end
end
private
def stop_environment(build)
- build.persisted_environment.fire_state_event(:stop)
+ build.persisted_environment.fire_state_event(:stop_complete)
end
end
diff --git a/app/workers/bulk_import_worker.rb b/app/workers/bulk_import_worker.rb
index 157586ca397..c7efc92b25e 100644
--- a/app/workers/bulk_import_worker.rb
+++ b/app/workers/bulk_import_worker.rb
@@ -20,7 +20,7 @@ class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
@bulk_import.start! if @bulk_import.created?
created_entities.find_each do |entity|
- entity.create_pipeline_trackers!
+ BulkImports::CreatePipelineTrackersService.new(entity).execute!
BulkImports::ExportRequestWorker.perform_async(entity.id)
BulkImports::EntityWorker.perform_async(entity.id)
diff --git a/app/workers/bulk_imports/pipeline_worker.rb b/app/workers/bulk_imports/pipeline_worker.rb
index b515f0fa202..9c95e25e2e8 100644
--- a/app/workers/bulk_imports/pipeline_worker.rb
+++ b/app/workers/bulk_imports/pipeline_worker.rb
@@ -12,7 +12,7 @@ module BulkImports
worker_has_external_dependencies!
def perform(pipeline_tracker_id, stage, entity_id)
- pipeline_tracker = ::BulkImports::Tracker
+ @pipeline_tracker = ::BulkImports::Tracker
.with_status(:enqueued)
.find_by_id(pipeline_tracker_id)
@@ -24,7 +24,7 @@ module BulkImports
)
)
- run(pipeline_tracker)
+ run
else
logger.error(
structured_payload(
@@ -41,48 +41,29 @@ module BulkImports
private
- def run(pipeline_tracker)
- if pipeline_tracker.entity.failed?
- raise(Entity::FailedError, 'Failed entity status')
- end
-
- if file_extraction_pipeline?(pipeline_tracker)
- export_status = ExportStatus.new(pipeline_tracker, pipeline_tracker.pipeline_class.relation)
+ attr_reader :pipeline_tracker
- raise(Pipeline::ExpiredError, 'Pipeline timeout') if job_timeout?(pipeline_tracker)
- raise(Pipeline::FailedError, export_status.error) if export_status.failed?
+ def run
+ raise(Entity::FailedError, 'Failed entity status') if pipeline_tracker.entity.failed?
+ raise(Pipeline::ExpiredError, 'Pipeline timeout') if job_timeout?
+ raise(Pipeline::FailedError, export_status.error) if export_failed?
- return reenqueue(pipeline_tracker) if export_status.started?
- end
+ return re_enqueue if export_empty? || export_started?
pipeline_tracker.update!(status_event: 'start', jid: jid)
-
- context = ::BulkImports::Pipeline::Context.new(pipeline_tracker)
-
pipeline_tracker.pipeline_class.new(context).run
-
pipeline_tracker.finish!
rescue BulkImports::NetworkError => e
if e.retriable?(pipeline_tracker)
- logger.error(
- structured_payload(
- entity_id: pipeline_tracker.entity.id,
- pipeline_name: pipeline_tracker.pipeline_name,
- message: "Retrying error: #{e.message}"
- )
- )
-
- pipeline_tracker.update!(status_event: 'retry', jid: jid)
-
- reenqueue(pipeline_tracker, delay: e.retry_delay)
+ retry_tracker(e)
else
- fail_tracker(pipeline_tracker, e)
+ fail_tracker(e)
end
rescue StandardError => e
- fail_tracker(pipeline_tracker, e)
+ fail_tracker(e)
end
- def fail_tracker(pipeline_tracker, exception)
+ def fail_tracker(exception)
pipeline_tracker.update!(status_event: 'fail_op', jid: jid)
logger.error(
@@ -98,21 +79,22 @@ module BulkImports
entity_id: pipeline_tracker.entity.id,
pipeline_name: pipeline_tracker.pipeline_name
)
+
+ BulkImports::Failure.create(
+ bulk_import_entity_id: context.entity.id,
+ pipeline_class: pipeline_tracker.pipeline_name,
+ pipeline_step: 'pipeline_worker_run',
+ exception_class: exception.class.to_s,
+ exception_message: exception.message.truncate(255),
+ correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id
+ )
end
def logger
@logger ||= Gitlab::Import::Logger.build
end
- def file_extraction_pipeline?(pipeline_tracker)
- pipeline_tracker.pipeline_class.file_extraction_pipeline?
- end
-
- def job_timeout?(pipeline_tracker)
- (Time.zone.now - pipeline_tracker.entity.created_at) > Pipeline::NDJSON_EXPORT_TIMEOUT
- end
-
- def reenqueue(pipeline_tracker, delay: FILE_EXTRACTION_PIPELINE_PERFORM_DELAY)
+ def re_enqueue(delay = FILE_EXTRACTION_PIPELINE_PERFORM_DELAY)
self.class.perform_in(
delay,
pipeline_tracker.id,
@@ -120,5 +102,55 @@ module BulkImports
pipeline_tracker.entity.id
)
end
+
+ def context
+ @context ||= ::BulkImports::Pipeline::Context.new(pipeline_tracker)
+ end
+
+ def export_status
+ @export_status ||= ExportStatus.new(pipeline_tracker, pipeline_tracker.pipeline_class.relation)
+ end
+
+ def file_extraction_pipeline?
+ pipeline_tracker.file_extraction_pipeline?
+ end
+
+ def job_timeout?
+ return false unless file_extraction_pipeline?
+
+ (Time.zone.now - pipeline_tracker.entity.created_at) > Pipeline::NDJSON_EXPORT_TIMEOUT
+ end
+
+ def export_failed?
+ return false unless file_extraction_pipeline?
+
+ export_status.failed?
+ end
+
+ def export_started?
+ return false unless file_extraction_pipeline?
+
+ export_status.started?
+ end
+
+ def export_empty?
+ return false unless file_extraction_pipeline?
+
+ export_status.empty?
+ end
+
+ def retry_tracker(exception)
+ logger.error(
+ structured_payload(
+ entity_id: pipeline_tracker.entity.id,
+ pipeline_name: pipeline_tracker.pipeline_name,
+ message: "Retrying error: #{exception.message}"
+ )
+ )
+
+ pipeline_tracker.update!(status_event: 'retry', jid: jid)
+
+ re_enqueue(exception.retry_delay)
+ end
end
end
diff --git a/app/workers/ci/archive_trace_worker.rb b/app/workers/ci/archive_trace_worker.rb
index 5a22a5c74ee..47d77c15b4a 100644
--- a/app/workers/ci/archive_trace_worker.rb
+++ b/app/workers/ci/archive_trace_worker.rb
@@ -4,13 +4,19 @@ module Ci
class ArchiveTraceWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- data_consistency :always
+ data_consistency :sticky, feature_flag: :sticky_ci_archive_trace_worker
sidekiq_options retry: 3
include PipelineBackgroundQueue
def perform(job_id)
- Ci::Build.without_archived_trace.find_by_id(job_id).try do |job|
+ archivable_jobs = Ci::Build.without_archived_trace
+
+ if Feature.enabled?(:sticky_ci_archive_trace_worker)
+ archivable_jobs = archivable_jobs.eager_load_for_archiving_trace
+ end
+
+ archivable_jobs.find_by_id(job_id).try do |job|
Ci::ArchiveTraceService.new.execute(job, worker_name: self.class.name)
end
end
diff --git a/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb
index 16c4744eae1..8ee518e3ae6 100644
--- a/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb
+++ b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb
@@ -16,7 +16,7 @@ module Ci
def perform(pipeline_id)
Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
- Ci::PipelineArtifacts::CoverageReportService.new.execute(pipeline)
+ Ci::PipelineArtifacts::CoverageReportService.new(pipeline).execute
end
end
end
diff --git a/app/workers/clusters/applications/activate_integration_worker.rb b/app/workers/clusters/applications/activate_integration_worker.rb
new file mode 100644
index 00000000000..29813afd168
--- /dev/null
+++ b/app/workers/clusters/applications/activate_integration_worker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class ActivateIntegrationWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ data_consistency :always
+
+ sidekiq_options retry: 3
+ include ClusterQueue
+
+ loggable_arguments 1
+
+ def perform(cluster_id, integration_name)
+ cluster = Clusters::Cluster.find_by_id(cluster_id)
+ return unless cluster
+
+ cluster.all_projects.find_each do |project|
+ project.find_or_initialize_integration(integration_name).update!(active: true)
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/clusters/applications/activate_service_worker.rb b/app/workers/clusters/applications/activate_service_worker.rb
index 55e224887f4..abc84bcd093 100644
--- a/app/workers/clusters/applications/activate_service_worker.rb
+++ b/app/workers/clusters/applications/activate_service_worker.rb
@@ -1,25 +1,12 @@
# frozen_string_literal: true
+# This worker was renamed in 15.1, we can delete it in 15.2.
+# See: https://gitlab.com/gitlab-org/gitlab/-/issues/364112
+#
+# rubocop:disable Scalability/IdempotentWorker
module Clusters
module Applications
- class ActivateServiceWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
- include ClusterQueue
-
- loggable_arguments 1
-
- def perform(cluster_id, service_name)
- cluster = Clusters::Cluster.find_by_id(cluster_id)
- return unless cluster
-
- cluster.all_projects.find_each do |project|
- project.find_or_initialize_integration(service_name).update!(active: true)
- end
- end
+ class ActivateServiceWorker < ActivateIntegrationWorker
end
end
end
diff --git a/app/workers/clusters/applications/deactivate_integration_worker.rb b/app/workers/clusters/applications/deactivate_integration_worker.rb
new file mode 100644
index 00000000000..d1db99d21af
--- /dev/null
+++ b/app/workers/clusters/applications/deactivate_integration_worker.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class DeactivateIntegrationWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ data_consistency :always
+
+ sidekiq_options retry: 3
+ include ClusterQueue
+
+ loggable_arguments 1
+
+ def perform(cluster_id, integration_name)
+ cluster = Clusters::Cluster.find_by_id(cluster_id)
+ raise cluster_missing_error(integration_name) unless cluster
+
+ integration_class = Integration.integration_name_to_model(integration_name)
+ integration_association_name = ::Project.integration_association_name(integration_name).to_sym
+
+ projects = cluster.all_projects
+ .with_integration(integration_class)
+ .include_integration(integration_association_name)
+
+ projects.find_each do |project|
+ project.public_send(integration_association_name).update!(active: false) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ def cluster_missing_error(integration_name)
+ ActiveRecord::RecordNotFound.new(
+ "Can't deactivate #{integration_name} integrations, host cluster not found! " \
+ "Some inconsistent records may be left in database."
+ )
+ end
+ end
+ end
+end
diff --git a/app/workers/clusters/applications/deactivate_service_worker.rb b/app/workers/clusters/applications/deactivate_service_worker.rb
index 4c8d21a7c4d..88219b8b17e 100644
--- a/app/workers/clusters/applications/deactivate_service_worker.rb
+++ b/app/workers/clusters/applications/deactivate_service_worker.rb
@@ -1,32 +1,12 @@
# frozen_string_literal: true
+# This worker was renamed in 15.1, we can delete it in 15.2.
+# See: https://gitlab.com/gitlab-org/gitlab/-/issues/364112
+#
+# rubocop:disable Scalability/IdempotentWorker
module Clusters
module Applications
- class DeactivateServiceWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
- include ClusterQueue
-
- loggable_arguments 1
-
- def perform(cluster_id, integration_name)
- cluster = Clusters::Cluster.find_by_id(cluster_id)
- raise cluster_missing_error(integration_name) unless cluster
-
- 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
-
- def cluster_missing_error(integration_name)
- ActiveRecord::RecordNotFound.new("Can't deactivate #{integration_name} integrations, host cluster not found! Some inconsistent records may be left in database.")
- end
+ class DeactivateServiceWorker < DeactivateIntegrationWorker
end
end
end
diff --git a/app/workers/concerns/limited_capacity/job_tracker.rb b/app/workers/concerns/limited_capacity/job_tracker.rb
index 47b13cd5bf6..a1eb4e45027 100644
--- a/app/workers/concerns/limited_capacity/job_tracker.rb
+++ b/app/workers/concerns/limited_capacity/job_tracker.rb
@@ -62,7 +62,7 @@ module LimitedCapacity
end
def with_redis(&block)
- Gitlab::Redis::Queues.with(&block) # rubocop: disable CodeReuse/ActiveRecord
+ Gitlab::Redis::SharedState.with(&block) # rubocop: disable CodeReuse/ActiveRecord
end
end
end
diff --git a/app/workers/concerns/worker_attributes.rb b/app/workers/concerns/worker_attributes.rb
index 5d7251e9a98..8a135bc1853 100644
--- a/app/workers/concerns/worker_attributes.rb
+++ b/app/workers/concerns/worker_attributes.rb
@@ -67,7 +67,7 @@ module WorkerAttributes
end
def get_urgency
- class_attributes[:urgency] || :low
+ get_class_attribute(:urgency) || :low
end
# Allows configuring worker's data_consistency.
@@ -98,13 +98,13 @@ module WorkerAttributes
end
def get_data_consistency
- class_attributes[:data_consistency] || DEFAULT_DATA_CONSISTENCY
+ get_class_attribute(:data_consistency) || DEFAULT_DATA_CONSISTENCY
end
def get_data_consistency_feature_flag_enabled?
- return true unless class_attributes[:data_consistency_feature_flag]
+ return true unless get_class_attribute(:data_consistency_feature_flag)
- Feature.enabled?(class_attributes[:data_consistency_feature_flag])
+ Feature.enabled?(get_class_attribute(:data_consistency_feature_flag))
end
# Set this attribute on a job when it will call to services outside of the
@@ -115,11 +115,11 @@ module WorkerAttributes
set_class_attribute(:external_dependencies, true)
end
- # Returns a truthy value if the worker has external dependencies.
+ # Returns true if the worker has external dependencies.
# See doc/development/sidekiq_style_guide.md#jobs-with-external-dependencies
# for details
def worker_has_external_dependencies?
- class_attributes[:external_dependencies]
+ !!get_class_attribute(:external_dependencies)
end
def worker_resource_boundary(boundary)
@@ -129,7 +129,7 @@ module WorkerAttributes
end
def get_worker_resource_boundary
- class_attributes[:resource_boundary] || :unknown
+ get_class_attribute(:resource_boundary) || :unknown
end
def idempotent!
@@ -137,7 +137,7 @@ module WorkerAttributes
end
def idempotent?
- class_attributes[:idempotent]
+ !!get_class_attribute(:idempotent)
end
def weight(value)
@@ -145,7 +145,7 @@ module WorkerAttributes
end
def get_weight
- class_attributes[:weight] ||
+ get_class_attribute(:weight) ||
NAMESPACE_WEIGHTS[queue_namespace] ||
1
end
@@ -155,7 +155,7 @@ module WorkerAttributes
end
def get_tags
- Array(class_attributes[:tags])
+ Array(get_class_attribute(:tags))
end
def deduplicate(strategy, options = {})
@@ -164,12 +164,12 @@ module WorkerAttributes
end
def get_deduplicate_strategy
- class_attributes[:deduplication_strategy] ||
+ get_class_attribute(:deduplication_strategy) ||
Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DEFAULT_STRATEGY
end
def get_deduplication_options
- class_attributes[:deduplication_options] || {}
+ get_class_attribute(:deduplication_options) || {}
end
def deduplication_enabled?
@@ -183,7 +183,7 @@ module WorkerAttributes
end
def big_payload?
- class_attributes[:big_payload]
+ !!get_class_attribute(:big_payload)
end
end
end
diff --git a/app/workers/container_registry/migration/enqueuer_worker.rb b/app/workers/container_registry/migration/enqueuer_worker.rb
index a0babb98e82..f3c8dfa63ad 100644
--- a/app/workers/container_registry/migration/enqueuer_worker.rb
+++ b/app/workers/container_registry/migration/enqueuer_worker.rb
@@ -17,19 +17,8 @@ module ContainerRegistry
idempotent!
def perform
- migration.enqueuer_loop? ? perform_with_loop : perform_without_loop
- end
-
- def self.enqueue_a_job
- perform_async
- perform_in(7.seconds) if ::ContainerRegistry::Migration.enqueue_twice?
- end
-
- private
-
- def perform_with_loop
try_obtain_lease do
- while runnable? && Time.zone.now < loop_deadline && migration.enqueuer_loop?
+ while runnable? && Time.zone.now < loop_deadline
repository_handled = handle_aborted_migration || handle_next_migration
# no repository was found: stop the loop
@@ -43,40 +32,29 @@ module ContainerRegistry
end
end
- def perform_without_loop
- re_enqueue = false
- try_obtain_lease do
- break unless runnable?
-
- re_enqueue = handle_aborted_migration || handle_next_migration
- end
- re_enqueue_if_capacity if re_enqueue
+ def self.enqueue_a_job
+ perform_async
end
+ private
+
def handle_aborted_migration
return unless next_aborted_repository
- log_on_done(:import_type, 'retry')
- log_repository(next_aborted_repository)
-
next_aborted_repository.retry_aborted_migration
true
rescue StandardError => e
Gitlab::ErrorTracking.log_exception(e, next_aborted_repository_id: next_aborted_repository&.id)
- migration.enqueuer_loop? ? false : true
+ false
ensure
- log_repository_migration_state(next_aborted_repository)
log_repository_info(next_aborted_repository, import_type: 'retry')
end
def handle_next_migration
return unless next_repository
- log_on_done(:import_type, 'next')
- log_repository(next_repository)
-
# We return true because the repository was successfully processed (migration_state is changed)
return true if tag_count_too_high?
return unless next_repository.start_pre_import
@@ -88,7 +66,6 @@ module ContainerRegistry
false
ensure
- log_repository_migration_state(next_repository)
log_repository_info(next_repository, import_type: 'next')
end
@@ -97,8 +74,6 @@ module ContainerRegistry
return false unless next_repository.tags_count > migration.max_tags_count
next_repository.skip_import(reason: :too_many_tags)
- log_on_done(:tags_count_too_high, true)
- log_on_done(:max_tags_count_setting, migration.max_tags_count)
true
end
@@ -160,7 +135,7 @@ module ContainerRegistry
def next_aborted_repository
strong_memoize(:next_aborted_repository) do
- ContainerRepository.with_migration_state('import_aborted').take # rubocop:disable CodeReuse/ActiveRecord
+ ContainerRepository.with_migration_state('import_aborted').limit(2)[0] # rubocop:disable CodeReuse/ActiveRecord
end
end
@@ -180,29 +155,11 @@ module ContainerRegistry
self.class.enqueue_a_job
end
- def log_repository(repository)
- log_on_done(:container_repository_id, repository&.id)
- log_on_done(:container_repository_path, repository&.path)
- end
-
- def log_repository_migration_state(repository)
- return unless repository
-
- log_on_done(:container_repository_migration_state, repository.migration_state)
- end
-
- def log_on_done(key, value)
- return if migration.enqueuer_loop?
-
- log_extra_metadata_on_done(key, value)
- end
-
def log_info(extras)
logger.info(structured_payload(extras))
end
def log_repository_info(repository, extras = {})
- return unless migration.enqueuer_loop?
return unless repository
repository_info = {
diff --git a/app/workers/container_registry/migration/guard_worker.rb b/app/workers/container_registry/migration/guard_worker.rb
index 1111061a89b..ae29106b502 100644
--- a/app/workers/container_registry/migration/guard_worker.rb
+++ b/app/workers/container_registry/migration/guard_worker.rb
@@ -22,7 +22,7 @@ module ContainerRegistry
repositories = ::ContainerRepository.with_stale_migration(step_before_timestamp)
.limit(max_capacity)
aborts_count = 0
- long_running_migration_ids = []
+ long_running_migrations = []
# the #to_a is safe as the amount of entries is limited.
# In addition, we're calling #each in the next line and we don't want two different SQL queries for these two lines
@@ -32,7 +32,7 @@ module ContainerRegistry
if actively_importing?(repository)
# if a repository is actively importing but not yet long_running, do nothing
if long_running_migration?(repository)
- long_running_migration_ids << repository.id
+ long_running_migrations << repository
cancel_long_running_migration(repository)
aborts_count += 1
end
@@ -44,8 +44,9 @@ module ContainerRegistry
log_extra_metadata_on_done(:aborted_stale_migrations_count, aborts_count)
- if long_running_migration_ids.any?
- log_extra_metadata_on_done(:aborted_long_running_migration_ids, long_running_migration_ids)
+ if long_running_migrations.any?
+ log_extra_metadata_on_done(:aborted_long_running_migration_ids, long_running_migrations.map(&:id))
+ log_extra_metadata_on_done(:aborted_long_running_migration_paths, long_running_migrations.map(&:path))
end
end
@@ -64,14 +65,16 @@ module ContainerRegistry
end
def long_running_migration?(repository)
- timeout = long_running_migration_threshold
-
- if Feature.enabled?(:registry_migration_guard_thresholds)
- timeout = if repository.migration_state == 'pre_importing'
- migration.pre_import_timeout.seconds
- else
- migration.import_timeout.seconds
- end
+ timeout = if repository.migration_state == 'pre_importing'
+ migration.pre_import_timeout.seconds
+ else
+ migration.import_timeout.seconds
+ end
+
+ if repository.migration_state == 'pre_importing' &&
+ Feature.enabled?(:registry_migration_guard_dynamic_pre_import_timeout) &&
+ migration_start_timestamp(repository).before?(timeout.ago)
+ timeout = migration.dynamic_pre_import_timeout_for(repository)
end
migration_start_timestamp(repository).before?(timeout.ago)
@@ -106,10 +109,6 @@ module ContainerRegistry
::ContainerRegistry::Migration
end
- def long_running_migration_threshold
- @threshold ||= 10.minutes
- end
-
def cancel_long_running_migration(repository)
result = repository.migration_cancel
diff --git a/app/workers/database/batched_background_migration/ci_database_worker.rb b/app/workers/database/batched_background_migration/ci_database_worker.rb
index ee9cbba7076..b04db87631a 100644
--- a/app/workers/database/batched_background_migration/ci_database_worker.rb
+++ b/app/workers/database/batched_background_migration/ci_database_worker.rb
@@ -4,12 +4,8 @@ module Database
class CiDatabaseWorker # rubocop:disable Scalability/IdempotentWorker
include SingleDatabaseWorker
- def self.enabled?
- Feature.enabled?(:execute_batched_migrations_on_schedule_ci_database, type: :ops)
- end
-
def self.tracking_database
- @tracking_database ||= Gitlab::Database::CI_DATABASE_NAME
+ @tracking_database ||= Gitlab::Database::CI_DATABASE_NAME.to_sym
end
end
end
diff --git a/app/workers/database/batched_background_migration/single_database_worker.rb b/app/workers/database/batched_background_migration/single_database_worker.rb
index aeadda4b8e1..cfbd44ba397 100644
--- a/app/workers/database/batched_background_migration/single_database_worker.rb
+++ b/app/workers/database/batched_background_migration/single_database_worker.rb
@@ -7,6 +7,7 @@ module Database
include ApplicationWorker
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
+ include Gitlab::Utils::StrongMemoize
LEASE_TIMEOUT_MULTIPLIER = 3
MINIMUM_LEASE_TIMEOUT = 10.minutes.freeze
@@ -23,11 +24,11 @@ module Database
def tracking_database
raise NotImplementedError, "#{self.name} does not implement #{__method__}"
end
+ # :nocov:
def enabled?
- raise NotImplementedError, "#{self.name} does not implement #{__method__}"
+ Feature.enabled?(:execute_batched_migrations_on_schedule, type: :ops)
end
- # :nocov:
def lease_key
name.demodulize.underscore
@@ -44,6 +45,15 @@ module Database
return
end
+ if shares_db_config?
+ Sidekiq.logger.info(
+ class: self.class.name,
+ database: self.class.tracking_database,
+ message: 'skipping migration execution for database that shares database configuration with another database')
+
+ return
+ end
+
Gitlab::Database::SharedModel.using_connection(base_model.connection) do
break unless self.class.enabled? && active_migration
@@ -63,7 +73,7 @@ module Database
private
def active_migration
- @active_migration ||= Gitlab::Database::BackgroundMigration::BatchedMigration.active_migration
+ @active_migration ||= Gitlab::Database::BackgroundMigration::BatchedMigration.active_migration(connection: base_model.connection)
end
def run_active_migration
@@ -71,7 +81,13 @@ module Database
end
def base_model
- @base_model ||= Gitlab::Database.database_base_models[self.class.tracking_database]
+ strong_memoize(:base_model) do
+ Gitlab::Database.database_base_models[self.class.tracking_database]
+ end
+ end
+
+ def shares_db_config?
+ base_model && Gitlab::Database.db_config_share_with(base_model.connection_db_config).present?
end
def with_exclusive_lease(interval)
diff --git a/app/workers/database/batched_background_migration_worker.rb b/app/workers/database/batched_background_migration_worker.rb
index 31208d7a473..29804be832d 100644
--- a/app/workers/database/batched_background_migration_worker.rb
+++ b/app/workers/database/batched_background_migration_worker.rb
@@ -4,10 +4,6 @@ module Database
class BatchedBackgroundMigrationWorker # rubocop:disable Scalability/IdempotentWorker
include BatchedBackgroundMigration::SingleDatabaseWorker
- def self.enabled?
- Feature.enabled?(:execute_batched_migrations_on_schedule, type: :ops)
- end
-
def self.tracking_database
@tracking_database ||= Gitlab::Database::MAIN_DATABASE_NAME.to_sym
end
diff --git a/app/workers/database/ci_namespace_mirrors_consistency_check_worker.rb b/app/workers/database/ci_namespace_mirrors_consistency_check_worker.rb
index b2174be1402..8918dca372d 100644
--- a/app/workers/database/ci_namespace_mirrors_consistency_check_worker.rb
+++ b/app/workers/database/ci_namespace_mirrors_consistency_check_worker.rb
@@ -6,15 +6,13 @@ module Database
include CronjobQueue # rubocop: disable Scalability/CronWorkerContext
sidekiq_options retry: false
- feature_category :sharding
+ feature_category :pods
data_consistency :sticky
idempotent!
version 1
def perform
- return if Feature.disabled?(:ci_namespace_mirrors_consistency_check)
-
results = ConsistencyCheckService.new(
source_model: Namespace,
target_model: Ci::NamespaceMirror,
diff --git a/app/workers/database/ci_project_mirrors_consistency_check_worker.rb b/app/workers/database/ci_project_mirrors_consistency_check_worker.rb
index 84052ab238b..5f10310f8d6 100644
--- a/app/workers/database/ci_project_mirrors_consistency_check_worker.rb
+++ b/app/workers/database/ci_project_mirrors_consistency_check_worker.rb
@@ -6,15 +6,13 @@ module Database
include CronjobQueue # rubocop: disable Scalability/CronWorkerContext
sidekiq_options retry: false
- feature_category :sharding
+ feature_category :pods
data_consistency :sticky
idempotent!
version 1
def perform
- return if Feature.disabled?(:ci_project_mirrors_consistency_check)
-
results = ConsistencyCheckService.new(
source_model: Project,
target_model: Ci::ProjectMirror,
diff --git a/app/workers/delete_container_repository_worker.rb b/app/workers/delete_container_repository_worker.rb
index a4d6adc2195..73e6843fdd0 100644
--- a/app/workers/delete_container_repository_worker.rb
+++ b/app/workers/delete_container_repository_worker.rb
@@ -2,16 +2,17 @@
class DeleteContainerRepositoryWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ include ExclusiveLeaseGuard
data_consistency :always
sidekiq_options retry: 3
- include ExclusiveLeaseGuard
queue_namespace :container_repository
feature_category :container_registry
- LEASE_TIMEOUT = 1.hour
+ LEASE_TIMEOUT = 1.hour.freeze
+ FIXED_DELAY = 10.seconds.freeze
attr_reader :container_repository
@@ -22,6 +23,16 @@ class DeleteContainerRepositoryWorker # rubocop:disable Scalability/IdempotentWo
return unless current_user && container_repository && project
+ if migration.delete_container_repository_worker_support? && migrating?
+ delay = migration_duration
+
+ self.class.perform_in(delay.from_now)
+
+ log_extra_metadata_on_done(:delete_postponed, delay)
+
+ return
+ end
+
# If a user accidentally attempts to delete the same container registry in quick succession,
# this can lead to orphaned tags.
try_obtain_lease do
@@ -29,6 +40,28 @@ class DeleteContainerRepositoryWorker # rubocop:disable Scalability/IdempotentWo
end
end
+ private
+
+ def migrating?
+ !(container_repository.default? ||
+ container_repository.import_done? ||
+ container_repository.import_skipped?)
+ end
+
+ def migration_duration
+ duration = migration.import_timeout.seconds + FIXED_DELAY
+
+ if container_repository.pre_importing?
+ duration += migration.dynamic_pre_import_timeout_for(container_repository)
+ end
+
+ duration
+ end
+
+ def migration
+ ContainerRegistry::Migration
+ end
+
# For ExclusiveLeaseGuard concern
def lease_key
@lease_key ||= "container_repository:delete:#{container_repository.id}"
diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb
deleted file mode 100644
index eaa8810a78e..00000000000
--- a/app/workers/expire_job_cache_worker.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-class ExpireJobCacheWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :delayed
-
- sidekiq_options retry: 3
- include PipelineQueue
-
- queue_namespace :pipeline_cache
- urgency :high
- idempotent!
-
- def perform(job_id)
- job = CommitStatus.find_by_id(job_id)
- return unless job
-
- job.expire_etag_cache!
- ExpirePipelineCacheWorker.perform_async(job.pipeline_id)
- end
-end
diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb
deleted file mode 100644
index 9a0c617da57..00000000000
--- a/app/workers/expire_pipeline_cache_worker.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-# rubocop: disable Scalability/IdempotentWorker
-class ExpirePipelineCacheWorker
- include ApplicationWorker
-
- sidekiq_options retry: 3
- include PipelineQueue
-
- queue_namespace :pipeline_cache
- urgency :high
- worker_resource_boundary :cpu
- data_consistency :delayed
-
- # This worker _should_ be idempotent, but due to us moving this to data_consistency :delayed
- # and an ongoing incompatibility between the two switches, we need to disable this.
- # Uncomment once https://gitlab.com/gitlab-org/gitlab/-/issues/325291 is resolved
- # idempotent!
-
- def perform(pipeline_id)
- pipeline = Ci::Pipeline.find_by_id(pipeline_id)
- return unless pipeline
-
- Ci::ExpirePipelineCacheService.new.execute(pipeline)
- end
-end
-# rubocop:enable Scalability/IdempotentWorker
diff --git a/app/workers/gitlab_service_ping_worker.rb b/app/workers/gitlab_service_ping_worker.rb
index 6cf46458b1e..0f7b3ba56a5 100644
--- a/app/workers/gitlab_service_ping_worker.rb
+++ b/app/workers/gitlab_service_ping_worker.rb
@@ -25,7 +25,25 @@ class GitlabServicePingWorker # rubocop:disable Scalability/IdempotentWorker
# Splay the request over a minute to avoid thundering herd problems.
sleep(rand(0.0..60.0).round(3))
- ServicePing::SubmitService.new.execute
+ ServicePing::SubmitService.new(payload: usage_data).execute
end
end
+
+ def usage_data
+ return unless Feature.enabled?(:prerecord_service_ping_data)
+
+ ServicePing::BuildPayload.new.execute.tap do |payload|
+ record = {
+ recorded_at: payload[:recorded_at],
+ payload: payload,
+ created_at: Time.current,
+ updated_at: Time.current
+ }
+
+ RawUsageData.upsert(record, unique_by: :recorded_at)
+ end
+ rescue StandardError => err
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(err)
+ nil
+ end
end
diff --git a/app/workers/integrations/execute_worker.rb b/app/workers/integrations/execute_worker.rb
new file mode 100644
index 00000000000..443f1d9fe8e
--- /dev/null
+++ b/app/workers/integrations/execute_worker.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Integrations
+ class ExecuteWorker # 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)
+ data = data.with_indifferent_access
+ integration = Integration.find_by_id(hook_id)
+ return unless integration
+
+ begin
+ integration.execute(data)
+ rescue StandardError => e
+ integration.log_exception(e)
+ end
+ end
+ end
+end
diff --git a/app/workers/integrations/irker_worker.rb b/app/workers/integrations/irker_worker.rb
new file mode 100644
index 00000000000..3152d68b372
--- /dev/null
+++ b/app/workers/integrations/irker_worker.rb
@@ -0,0 +1,193 @@
+# frozen_string_literal: true
+
+require 'json'
+require 'socket'
+require 'resolv'
+
+module Integrations
+ 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
+ return false unless start_connection(
+ settings['server_host'],
+ settings['server_port']
+ )
+
+ @project = Project.find(project_id)
+ @colors = colors
+ @channels = channels
+
+ @repo_path = @project.full_path
+ @repo_name = push_data['repository']['name']
+ @committer = push_data['user_name']
+ @branch = push_data['ref'].gsub(%r{refs/[^/]*/}, '')
+
+ if @colors
+ @repo_name = "\x0304#{@repo_name}\x0f"
+ @branch = "\x0305#{@branch}\x0f"
+ end
+
+ # First messages are for branch creation/deletion
+ send_branch_updates(push_data)
+
+ # Next messages are for commits
+ send_commits(push_data)
+
+ close_connection
+ true
+ end
+
+ private
+
+ def start_connection(irker_server, irker_port)
+ ip_address = Resolv.getaddress(irker_server)
+ # handle IP6 addresses
+ domain = Resolv::IPv6::Regex.match?(ip_address) ? "[#{ip_address}]" : ip_address
+
+ begin
+ Gitlab::UrlBlocker.validate!(
+ "irc://#{domain}",
+ allow_localhost: allow_local_requests?,
+ allow_local_network: allow_local_requests?,
+ schemes: ['irc'])
+ @socket = TCPSocket.new ip_address, irker_port
+ rescue Errno::ECONNREFUSED, Gitlab::UrlBlocker::BlockedUrlError => e
+ logger.fatal "Can't connect to Irker daemon: #{e}"
+ return false
+ end
+
+ true
+ end
+
+ def allow_local_requests?
+ Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
+ end
+
+ def send_to_irker(privmsg)
+ to_send = { to: @channels, privmsg: privmsg }
+
+ @socket.puts Gitlab::Json.dump(to_send)
+ end
+
+ def close_connection
+ @socket.close
+ end
+
+ def send_branch_updates(push_data)
+ message =
+ if Gitlab::Git.blank_ref?(push_data['before'])
+ new_branch_message
+ elsif Gitlab::Git.blank_ref?(push_data['after'])
+ delete_branch_message
+ end
+
+ send_to_irker(message)
+ end
+
+ def new_branch_message
+ newbranch = "#{Gitlab.config.gitlab.url}/#{@repo_path}/-/branches"
+ newbranch = "\x0302\x1f#{newbranch}\x0f" if @colors
+
+ "[#{@repo_name}] #{@committer} has created a new branch #{@branch}: #{newbranch}"
+ end
+
+ def delete_branch_message
+ "[#{@repo_name}] #{@committer} has deleted the branch #{@branch}"
+ end
+
+ def send_commits(push_data)
+ return if push_data['total_commits_count'] == 0
+
+ # Next message is for number of commit pushed, if any
+ if Gitlab::Git.blank_ref?(push_data['before'])
+ # Tweak on push_data["before"] in order to have a nice compare URL
+ push_data['before'] = before_on_new_branch(push_data)
+ end
+
+ send_commits_count(push_data)
+
+ # One message per commit, limited by 3 messages (same limit as the
+ # github irc hook)
+ commits = push_data['commits'].first(3)
+ commits.each do |commit_attrs|
+ send_one_commit(commit_attrs)
+ end
+ end
+
+ def before_on_new_branch(push_data)
+ commit = commit_from_id(push_data['commits'][0]['id'])
+ parents = commit.parents
+
+ # Return old value if there's no new one
+ return push_data['before'] if parents.empty?
+
+ # Or return the first parent-commit
+ parents[0].id
+ end
+
+ def send_commits_count(push_data)
+ url = compare_url(push_data['before'], push_data['after'])
+ commits = colorize_commits(push_data['total_commits_count'])
+ new_commits = 'new commit'.pluralize(push_data['total_commits_count'])
+
+ send_to_irker("[#{@repo_name}] #{@committer} pushed #{commits} #{new_commits} " \
+ "to #{@branch}: #{url}")
+ end
+
+ def compare_url(sha_before, sha_after)
+ sha1 = Commit.truncate_sha(sha_before)
+ sha2 = Commit.truncate_sha(sha_after)
+ compare_url = "#{Gitlab.config.gitlab.url}/#{@repo_path}/-/compare" \
+ "/#{sha1}...#{sha2}"
+
+ colorize_url(compare_url)
+ end
+
+ def send_one_commit(commit_attrs)
+ commit = commit_from_id(commit_attrs['id'])
+ sha = colorize_sha(Commit.truncate_sha(commit_attrs['id']))
+ author = commit_attrs['author']['name']
+ files = colorize_nb_files(files_count(commit))
+ title = commit.title
+
+ send_to_irker("#{@repo_name}/#{@branch} #{sha} #{author} (#{files}): #{title}")
+ end
+
+ def commit_from_id(id)
+ @project.commit(id)
+ end
+
+ def files_count(commit)
+ diff_size = commit.raw_deltas.size
+
+ "#{diff_size} file".pluralize(diff_size)
+ end
+
+ def colorize_sha(sha)
+ sha = "\x0314#{sha}\x0f" if @colors
+ sha
+ end
+
+ def colorize_nb_files(nb_files)
+ nb_files = "\x0312#{nb_files}\x0f" if @colors
+ nb_files
+ end
+
+ def colorize_url(url)
+ url = "\x0302\x1f#{url}\x0f" if @colors
+ url
+ end
+
+ def colorize_commits(commits)
+ commits = "\x02#{commits}\x0f" if @colors
+ commits
+ end
+ end
+end
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 4f51bb69b8c..a054021e418 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -1,189 +1,9 @@
# frozen_string_literal: true
-require 'json'
-require 'socket'
-require 'resolv'
-
-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
- return false unless start_connection(settings['server_host'],
- settings['server_port'])
-
- @project = Project.find(project_id)
- @colors = colors
- @channels = channels
-
- @repo_path = @project.full_path
- @repo_name = push_data['repository']['name']
- @committer = push_data['user_name']
- @branch = push_data['ref'].gsub(%r'refs/[^/]*/', '')
-
- if @colors
- @repo_name = "\x0304#{@repo_name}\x0f"
- @branch = "\x0305#{@branch}\x0f"
- end
-
- # First messages are for branch creation/deletion
- send_branch_updates(push_data)
-
- # Next messages are for commits
- send_commits(push_data)
-
- close_connection
- true
- end
-
- private
-
- def start_connection(irker_server, irker_port)
- ip_address = Resolv.getaddress(irker_server)
- # handle IP6 addresses
- domain = Resolv::IPv6::Regex.match?(ip_address) ? "[#{ip_address}]" : ip_address
-
- begin
- Gitlab::UrlBlocker.validate!(
- "irc://#{domain}",
- allow_localhost: allow_local_requests?,
- allow_local_network: allow_local_requests?,
- schemes: ['irc'])
- @socket = TCPSocket.new ip_address, irker_port
- rescue Errno::ECONNREFUSED, Gitlab::UrlBlocker::BlockedUrlError => e
- logger.fatal "Can't connect to Irker daemon: #{e}"
- return false
- end
-
- true
- end
-
- def allow_local_requests?
- Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
- end
-
- def send_to_irker(privmsg)
- to_send = { to: @channels, privmsg: privmsg }
-
- @socket.puts Gitlab::Json.dump(to_send)
- end
-
- def close_connection
- @socket.close
- end
-
- def send_branch_updates(push_data)
- message =
- if Gitlab::Git.blank_ref?(push_data['before'])
- new_branch_message
- elsif Gitlab::Git.blank_ref?(push_data['after'])
- delete_branch_message
- end
-
- send_to_irker(message)
- end
-
- def new_branch_message
- newbranch = "#{Gitlab.config.gitlab.url}/#{@repo_path}/-/branches"
- newbranch = "\x0302\x1f#{newbranch}\x0f" if @colors
-
- "[#{@repo_name}] #{@committer} has created a new branch #{@branch}: #{newbranch}"
- end
-
- def delete_branch_message
- "[#{@repo_name}] #{@committer} has deleted the branch #{@branch}"
- end
-
- def send_commits(push_data)
- return if push_data['total_commits_count'] == 0
-
- # Next message is for number of commit pushed, if any
- if Gitlab::Git.blank_ref?(push_data['before'])
- # Tweak on push_data["before"] in order to have a nice compare URL
- push_data['before'] = before_on_new_branch(push_data)
- end
-
- send_commits_count(push_data)
-
- # One message per commit, limited by 3 messages (same limit as the
- # github irc hook)
- commits = push_data['commits'].first(3)
- commits.each do |commit_attrs|
- send_one_commit(commit_attrs)
- end
- end
-
- def before_on_new_branch(push_data)
- commit = commit_from_id(push_data['commits'][0]['id'])
- parents = commit.parents
-
- # Return old value if there's no new one
- return push_data['before'] if parents.empty?
-
- # Or return the first parent-commit
- parents[0].id
- end
-
- def send_commits_count(push_data)
- url = compare_url(push_data['before'], push_data['after'])
- commits = colorize_commits(push_data['total_commits_count'])
- new_commits = 'new commit'.pluralize(push_data['total_commits_count'])
-
- send_to_irker("[#{@repo_name}] #{@committer} pushed #{commits} #{new_commits} " \
- "to #{@branch}: #{url}")
- end
-
- def compare_url(sha_before, sha_after)
- sha1 = Commit.truncate_sha(sha_before)
- sha2 = Commit.truncate_sha(sha_after)
- compare_url = "#{Gitlab.config.gitlab.url}/#{@repo_path}/-/compare" \
- "/#{sha1}...#{sha2}"
-
- colorize_url(compare_url)
- end
-
- def send_one_commit(commit_attrs)
- commit = commit_from_id(commit_attrs['id'])
- sha = colorize_sha(Commit.truncate_sha(commit_attrs['id']))
- author = commit_attrs['author']['name']
- files = colorize_nb_files(files_count(commit))
- title = commit.title
-
- send_to_irker("#{@repo_name}/#{@branch} #{sha} #{author} (#{files}): #{title}")
- end
-
- def commit_from_id(id)
- @project.commit(id)
- end
-
- def files_count(commit)
- diff_size = commit.raw_deltas.size
-
- "#{diff_size} file".pluralize(diff_size)
- end
-
- def colorize_sha(sha)
- sha = "\x0314#{sha}\x0f" if @colors
- sha
- end
-
- def colorize_nb_files(nb_files)
- nb_files = "\x0312#{nb_files}\x0f" if @colors
- nb_files
- end
-
- def colorize_url(url)
- url = "\x0302\x1f#{url}\x0f" if @colors
- url
- end
-
- def colorize_commits(commits)
- commits = "\x02#{commits}\x0f" if @colors
- commits
- end
+# This worker was renamed in 15.1, we can delete it in 15.2.
+# See: https://gitlab.com/gitlab-org/gitlab/-/issues/364112
+#
+# rubocop: disable Gitlab/NamespacedClass
+# rubocop:disable Scalability/IdempotentWorker
+class IrkerWorker < Integrations::IrkerWorker
end
diff --git a/app/workers/issue_placement_worker.rb b/app/workers/issue_placement_worker.rb
deleted file mode 100644
index 26dec221f45..00000000000
--- a/app/workers/issue_placement_worker.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-# DEPRECATED. Will be removed in 14.7 https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72803
-# Please use Issues::PlacementWorker instead
-#
-# todo: remove this worker and it's queue definition from all_queues after Issues::PlacementWorker is deployed
-# We want to keep it for one release in case some jobs are already scheduled in the old queue so we need the worker
-# to be available to finish those. All new jobs will be queued into the new queue.
-class IssuePlacementWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
-
- idempotent!
- deduplicate :until_executed, including_scheduled: true
- feature_category :team_planning
- urgency :high
- worker_resource_boundary :cpu
- weight 2
-
- # Move at most the most recent 100 issues
- QUERY_LIMIT = 100
-
- # rubocop: disable CodeReuse/ActiveRecord
- def perform(issue_id, project_id = nil)
- issue = find_issue(issue_id, project_id)
- return unless issue
-
- # Temporary disable moving null elements because of performance problems
- # For more information check https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4321
- return if issue.blocked_for_repositioning?
-
- # Move the oldest 100 unpositioned items to the end.
- # This is to deal with out-of-order execution of the worker,
- # while preserving creation order.
- to_place = Issue
- .relative_positioning_query_base(issue)
- .with_null_relative_position
- .order({ created_at: :asc }, { id: :asc })
- .limit(QUERY_LIMIT + 1)
- .to_a
-
- leftover = to_place.pop if to_place.count > QUERY_LIMIT
-
- Issue.move_nulls_to_end(to_place)
- Issues::BaseService.new(project: nil).rebalance_if_needed(to_place.max_by(&:relative_position))
- Issues::PlacementWorker.perform_async(nil, leftover.project_id) if leftover.present?
- rescue RelativePositioning::NoSpaceLeft => e
- Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id, project_id: project_id)
- Issues::RebalancingWorker.perform_async(nil, *root_namespace_id_to_rebalance(issue, project_id))
- end
-
- def find_issue(issue_id, project_id)
- return Issue.id_in(issue_id).take if issue_id
-
- project = Project.id_in(project_id).take
- return unless project
-
- project.issues.take
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- private
-
- def root_namespace_id_to_rebalance(issue, project_id)
- project_id = project_id.presence || issue.project_id
- Project.find(project_id)&.self_or_root_group_ids
- end
-end
diff --git a/app/workers/issue_rebalancing_worker.rb b/app/workers/issue_rebalancing_worker.rb
deleted file mode 100644
index 73edb2eb653..00000000000
--- a/app/workers/issue_rebalancing_worker.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-# DEPRECATED. Will be removed in 14.7 https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72803
-# Please use Issues::RebalancingWorker instead
-#
-# todo: remove this worker and it's queue definition from all_queues after Issue::RebalancingWorker is released.
-# We want to keep it for one release in case some jobs are already scheduled in the old queue so we need the worker
-# to be available to finish those. All new jobs will be queued into the new queue.
-class IssueRebalancingWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
-
- idempotent!
- urgency :low
- feature_category :team_planning
- deduplicate :until_executed, including_scheduled: true
-
- def perform(ignore = nil, project_id = nil, root_namespace_id = nil)
- # we need to have exactly one of the project_id and root_namespace_id params be non-nil
- raise ArgumentError, "Expected only one of the params project_id: #{project_id} and root_namespace_id: #{root_namespace_id}" if project_id && root_namespace_id
- return if project_id.nil? && root_namespace_id.nil?
- return if ::Gitlab::Issues::Rebalancing::State.rebalance_recently_finished?(project_id, root_namespace_id)
-
- # pull the projects collection to be rebalanced either the project if namespace is not a group(i.e. user namesapce)
- # or the root namespace, this also makes the worker backward compatible with previous version where a project_id was
- # passed as the param
- projects_to_rebalance = projects_collection(project_id, root_namespace_id)
-
- # something might have happened with the namespace between scheduling the worker and actually running it,
- # maybe it was removed.
- if projects_to_rebalance.blank?
- Gitlab::ErrorTracking.log_exception(
- ArgumentError.new("Projects to be rebalanced not found for arguments: project_id #{project_id}, root_namespace_id: #{root_namespace_id}"),
- { project_id: project_id, root_namespace_id: root_namespace_id })
-
- return
- end
-
- Issues::RelativePositionRebalancingService.new(projects_to_rebalance).execute
- rescue Issues::RelativePositionRebalancingService::TooManyConcurrentRebalances => e
- Gitlab::ErrorTracking.log_exception(e, root_namespace_id: root_namespace_id, project_id: project_id)
- end
-
- private
-
- def projects_collection(project_id, root_namespace_id)
- # we can have either project_id(older version) or project_id if project is part of a user namespace and not a group
- # or root_namespace_id(newer version) never both.
- return Project.id_in([project_id]) if project_id
-
- Namespace.find_by_id(root_namespace_id)&.all_projects
- end
-end
diff --git a/app/workers/loose_foreign_keys/cleanup_worker.rb b/app/workers/loose_foreign_keys/cleanup_worker.rb
index ecece92ec1b..0d04c503fbf 100644
--- a/app/workers/loose_foreign_keys/cleanup_worker.rb
+++ b/app/workers/loose_foreign_keys/cleanup_worker.rb
@@ -7,7 +7,7 @@ module LooseForeignKeys
include CronjobQueue # rubocop: disable Scalability/CronWorkerContext
sidekiq_options retry: false
- feature_category :sharding
+ feature_category :pods
data_consistency :always
idempotent!
diff --git a/app/workers/merge_requests/create_pipeline_worker.rb b/app/workers/merge_requests/create_pipeline_worker.rb
index ee42a3dee08..b40408cf647 100644
--- a/app/workers/merge_requests/create_pipeline_worker.rb
+++ b/app/workers/merge_requests/create_pipeline_worker.rb
@@ -15,7 +15,7 @@ module MergeRequests
worker_resource_boundary :cpu
idempotent!
- def perform(project_id, user_id, merge_request_id)
+ def perform(project_id, user_id, merge_request_id, params = {})
project = Project.find_by_id(project_id)
return unless project
@@ -25,7 +25,12 @@ module MergeRequests
merge_request = MergeRequest.find_by_id(merge_request_id)
return unless merge_request
- MergeRequests::CreatePipelineService.new(project: project, current_user: user).execute(merge_request)
+ push_options = params.with_indifferent_access[:push_options]
+
+ MergeRequests::CreatePipelineService
+ .new(project: project, current_user: user, params: { push_options: push_options })
+ .execute(merge_request)
+
merge_request.update_head_pipeline
end
end
diff --git a/app/workers/merge_requests/update_head_pipeline_worker.rb b/app/workers/merge_requests/update_head_pipeline_worker.rb
index acebf5fc767..bc3a289c1e1 100644
--- a/app/workers/merge_requests/update_head_pipeline_worker.rb
+++ b/app/workers/merge_requests/update_head_pipeline_worker.rb
@@ -14,7 +14,7 @@ module MergeRequests
def handle_event(event)
Ci::Pipeline.find_by_id(event.data[:pipeline_id]).try do |pipeline|
pipeline.all_merge_requests.opened.each do |merge_request|
- UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
+ merge_request.update_head_pipeline
end
end
end
diff --git a/app/workers/namespaceless_project_destroy_worker.rb b/app/workers/namespaceless_project_destroy_worker.rb
deleted file mode 100644
index c2ed379be48..00000000000
--- a/app/workers/namespaceless_project_destroy_worker.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-# Worker to destroy projects that do not have a namespace
-#
-# It destroys everything it can without having the info about the namespace it
-# used to belong to. Projects in this state should be rare.
-# The worker will reject doing anything for projects that *do* have a
-# namespace. For those use ProjectDestroyWorker instead.
-class NamespacelessProjectDestroyWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
- include ExceptionBacktrace
-
- feature_category :authentication_and_authorization
-
- def perform(project_id)
- begin
- project = Project.unscoped.find(project_id)
- rescue ActiveRecord::RecordNotFound
- return
- end
-
- return if project.namespace # Reject doing anything for projects that *do* have a namespace
-
- project.team.truncate
-
- unlink_fork(project) if project.forked?
-
- project.destroy!
- end
-
- private
-
- def unlink_fork(project)
- merge_requests = project.forked_from_project.merge_requests.opened.from_project(project)
-
- merge_requests.update_all(state_id: MergeRequest.available_states[:closed])
- end
-end
diff --git a/app/workers/namespaces/process_sync_events_worker.rb b/app/workers/namespaces/process_sync_events_worker.rb
index 269710dd804..36c4ab2058d 100644
--- a/app/workers/namespaces/process_sync_events_worker.rb
+++ b/app/workers/namespaces/process_sync_events_worker.rb
@@ -9,11 +9,11 @@ module Namespaces
data_consistency :always
- feature_category :sharding
+ feature_category :pods
urgency :high
idempotent!
- deduplicate :until_executing
+ deduplicate :until_executed
def perform
results = ::Ci::ProcessSyncEventsService.new(
diff --git a/app/workers/pages_transfer_worker.rb b/app/workers/pages_transfer_worker.rb
index 404c79b9e89..6d3918e7ab6 100644
--- a/app/workers/pages_transfer_worker.rb
+++ b/app/workers/pages_transfer_worker.rb
@@ -13,12 +13,8 @@ class PagesTransferWorker # rubocop:disable Scalability/IdempotentWorker
loggable_arguments 0, 1
def perform(method, args)
- return unless Gitlab::PagesTransfer::METHODS.include?(method)
-
- result = Gitlab::PagesTransfer.new.public_send(method, *args) # rubocop:disable GitlabSecurity/PublicSend
-
- # If result isn't truthy, the move failed. Promote this to an
- # exception so that it will be logged and retried appropriately
- raise TransferFailedError unless result
+ # noop
+ # This worker is not necessary anymore and will be removed
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/340616
end
end
diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb
index eb5d0086592..800cf50e732 100644
--- a/app/workers/pipeline_hooks_worker.rb
+++ b/app/workers/pipeline_hooks_worker.rb
@@ -13,6 +13,7 @@ class PipelineHooksWorker # rubocop:disable Scalability/IdempotentWorker
def perform(pipeline_id)
pipeline = Ci::Pipeline.find_by_id(pipeline_id)
return unless pipeline
+ return if pipeline.user&.blocked?
Ci::Pipelines::HookService.new(pipeline).execute
end
diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb
index 640f3494d58..2ed2e2ff1d0 100644
--- a/app/workers/pipeline_notification_worker.rb
+++ b/app/workers/pipeline_notification_worker.rb
@@ -23,6 +23,7 @@ class PipelineNotificationWorker # rubocop:disable Scalability/IdempotentWorker
pipeline = Ci::Pipeline.find_by_id(pipeline_id)
return unless pipeline
+ return if pipeline.user&.blocked?
NotificationService.new.pipeline_finished(pipeline, ref_status: ref_status, recipients: recipients)
end
diff --git a/app/workers/project_daily_statistics_worker.rb b/app/workers/project_daily_statistics_worker.rb
deleted file mode 100644
index 02f8958f82a..00000000000
--- a/app/workers/project_daily_statistics_worker.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-# Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/214585
-class ProjectDailyStatisticsWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
-
- feature_category :source_code_management
-
- def perform(project_id)
- project = Project.find_by_id(project_id)
-
- return unless project&.repository&.exists?
-
- Projects::FetchStatisticsIncrementService.new(project).execute
- end
-end
diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb
index f73958a6ef9..56ac4bc046a 100644
--- a/app/workers/project_service_worker.rb
+++ b/app/workers/project_service_worker.rb
@@ -1,8 +1,11 @@
# frozen_string_literal: true
-class ProjectServiceWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
+# This worker was renamed in 15.1, we can delete it in 15.2.
+# See: https://gitlab.com/gitlab-org/gitlab/-/issues/364112
+#
+# rubocop: disable Gitlab/NamespacedClass
+# rubocop: disable Scalability/IdempotentWorker
+class ProjectServiceWorker < Integrations::ExecuteWorker
data_consistency :always
sidekiq_options retry: 3
sidekiq_options dead: false
@@ -10,16 +13,4 @@ class ProjectServiceWorker # rubocop:disable Scalability/IdempotentWorker
urgency :low
worker_has_external_dependencies!
-
- def perform(hook_id, data)
- data = data.with_indifferent_access
- integration = Integration.find_by_id(hook_id)
- return unless integration
-
- begin
- integration.execute(data)
- rescue StandardError => error
- integration.log_exception(error)
- end
- end
end
diff --git a/app/workers/projects/inactive_projects_deletion_cron_worker.rb b/app/workers/projects/inactive_projects_deletion_cron_worker.rb
index 2c3f4191502..a280c9203d6 100644
--- a/app/workers/projects/inactive_projects_deletion_cron_worker.rb
+++ b/app/workers/projects/inactive_projects_deletion_cron_worker.rb
@@ -9,34 +9,53 @@ module Projects
idempotent!
data_consistency :always
feature_category :compliance_management
+ urgency :low
- INTERVAL = 2.seconds.to_i
+ # This cron worker is executed at an interval of 10 minutes.
+ # Maximum run time is kept as 4 minutes to avoid breaching maximum allowed execution latency of 5 minutes.
+ MAX_RUN_TIME = 4.minutes
+ LAST_PROCESSED_INACTIVE_PROJECT_REDIS_KEY = 'last_processed_inactive_project_id'
+
+ TimeoutError = Class.new(StandardError)
def perform
return unless ::Gitlab::CurrentSettings.delete_inactive_projects?
+ @start_time ||= ::Gitlab::Metrics::System.monotonic_time
admin_user = User.admins.active.first
return unless admin_user
notified_inactive_projects = Gitlab::InactiveProjectsDeletionWarningTracker.notified_projects
- Project.inactive.without_deleted.find_each(batch_size: 100).with_index do |project, index| # rubocop: disable CodeReuse/ActiveRecord
- next unless Feature.enabled?(:inactive_projects_deletion, project.root_namespace)
+ project_id = last_processed_project_id
+
+ Project.where('projects.id > ?', project_id).each_batch(of: 100) do |batch| # rubocop: disable CodeReuse/ActiveRecord
+ inactive_projects = batch.inactive.without_deleted
+
+ inactive_projects.each do |project|
+ if over_time?
+ save_last_processed_project_id(project.id)
+ raise TimeoutError
+ end
- delay = index * INTERVAL
+ next unless Feature.enabled?(:inactive_projects_deletion, project.root_namespace)
- with_context(project: project, user: admin_user) do
- deletion_warning_email_sent_on = notified_inactive_projects["project:#{project.id}"]
+ with_context(project: project, user: admin_user) do
+ deletion_warning_email_sent_on = notified_inactive_projects["project:#{project.id}"]
- if send_deletion_warning_email?(deletion_warning_email_sent_on, project)
- send_notification(delay, project, admin_user)
- elsif deletion_warning_email_sent_on && delete_due_to_inactivity?(deletion_warning_email_sent_on)
- Gitlab::InactiveProjectsDeletionWarningTracker.new(project.id).reset
- delete_project(project, admin_user)
+ if send_deletion_warning_email?(deletion_warning_email_sent_on, project)
+ send_notification(project, admin_user)
+ elsif deletion_warning_email_sent_on && delete_due_to_inactivity?(deletion_warning_email_sent_on)
+ Gitlab::InactiveProjectsDeletionWarningTracker.new(project.id).reset
+ delete_project(project, admin_user)
+ end
end
end
end
+ reset_last_processed_project_id
+ rescue TimeoutError
+ # no-op
end
private
@@ -64,8 +83,30 @@ module Projects
::Projects::DestroyService.new(project, user, {}).async_execute
end
- def send_notification(delay, project, user)
- ::Projects::InactiveProjectsDeletionNotificationWorker.perform_in(delay, project.id, deletion_date)
+ def send_notification(project, user)
+ ::Projects::InactiveProjectsDeletionNotificationWorker.perform_async(project.id, deletion_date)
+ end
+
+ def over_time?
+ (::Gitlab::Metrics::System.monotonic_time - @start_time) > MAX_RUN_TIME
+ end
+
+ def save_last_processed_project_id(project_id)
+ Gitlab::Redis::Cache.with do |redis|
+ redis.set(LAST_PROCESSED_INACTIVE_PROJECT_REDIS_KEY, project_id)
+ end
+ end
+
+ def last_processed_project_id
+ Gitlab::Redis::Cache.with do |redis|
+ redis.get(LAST_PROCESSED_INACTIVE_PROJECT_REDIS_KEY).to_i
+ end
+ end
+
+ def reset_last_processed_project_id
+ Gitlab::Redis::Cache.with do |redis|
+ redis.del(LAST_PROCESSED_INACTIVE_PROJECT_REDIS_KEY)
+ end
end
end
end
diff --git a/app/workers/projects/process_sync_events_worker.rb b/app/workers/projects/process_sync_events_worker.rb
index 1330ae47a68..92322a9ea99 100644
--- a/app/workers/projects/process_sync_events_worker.rb
+++ b/app/workers/projects/process_sync_events_worker.rb
@@ -9,11 +9,11 @@ module Projects
data_consistency :always
- feature_category :sharding
+ feature_category :pods
urgency :high
idempotent!
- deduplicate :until_executing
+ deduplicate :until_executed
def perform
results = ::Ci::ProcessSyncEventsService.new(
diff --git a/app/workers/prometheus/create_default_alerts_worker.rb b/app/workers/prometheus/create_default_alerts_worker.rb
deleted file mode 100644
index 1a0fe7e8d56..00000000000
--- a/app/workers/prometheus/create_default_alerts_worker.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module Prometheus
- class CreateDefaultAlertsWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
-
- feature_category :incident_management
- urgency :high
- idempotent!
-
- def perform(project_id)
- # No-op Will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/360756
- end
- end
-end
diff --git a/app/workers/repository_remove_remote_worker.rb b/app/workers/repository_remove_remote_worker.rb
deleted file mode 100644
index c95393e7d21..00000000000
--- a/app/workers/repository_remove_remote_worker.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-class RepositoryRemoveRemoteWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
- include ExclusiveLeaseGuard
-
- feature_category :source_code_management
- loggable_arguments 1
-
- LEASE_TIMEOUT = 1.hour
-
- attr_reader :project, :remote_name
-
- def perform(project_id, remote_name)
- # 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
- LEASE_TIMEOUT
- end
-
- def lease_key
- "remove_remote_#{project.id}_#{remote_name}"
- end
-end
diff --git a/app/workers/schedule_merge_request_cleanup_refs_worker.rb b/app/workers/schedule_merge_request_cleanup_refs_worker.rb
index b3d0067471a..8099c3d56b6 100644
--- a/app/workers/schedule_merge_request_cleanup_refs_worker.rb
+++ b/app/workers/schedule_merge_request_cleanup_refs_worker.rb
@@ -14,6 +14,7 @@ class ScheduleMergeRequestCleanupRefsWorker
return if Gitlab::Database.read_only?
return unless Feature.enabled?(:merge_request_refs_cleanup)
+ MergeRequest::CleanupSchedule.stuck_retry!
MergeRequestCleanupRefsWorker.perform_with_capacity
end
end
diff --git a/app/workers/terraform/states/destroy_worker.rb b/app/workers/terraform/states/destroy_worker.rb
new file mode 100644
index 00000000000..48abf0f22b8
--- /dev/null
+++ b/app/workers/terraform/states/destroy_worker.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Terraform
+ module States
+ class DestroyWorker
+ include ApplicationWorker
+
+ queue_namespace :terraform
+ feature_category :infrastructure_as_code
+
+ deduplicate :until_executed
+ idempotent!
+ urgency :low
+ data_consistency :always
+
+ def perform(terraform_state_id)
+ if state = Terraform::State.find_by_id(terraform_state_id)
+ Terraform::States::DestroyService.new(state).execute
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index 5c96257cb63..eb69c0eaba6 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -13,13 +13,17 @@ class UpdateMergeRequestsWorker # rubocop:disable Scalability/IdempotentWorker
weight 3
loggable_arguments 2, 3, 4
- def perform(project_id, user_id, oldrev, newrev, ref)
+ def perform(project_id, user_id, oldrev, newrev, ref, params = {})
project = Project.find_by_id(project_id)
return unless project
user = User.find_by_id(user_id)
return unless user
- MergeRequests::RefreshService.new(project: project, current_user: user).execute(oldrev, newrev, ref)
+ push_options = params.with_indifferent_access[:push_options]
+
+ MergeRequests::RefreshService
+ .new(project: project, current_user: user, params: { push_options: push_options })
+ .execute(oldrev, newrev, ref)
end
end
diff --git a/app/workers/web_hooks/destroy_worker.rb b/app/workers/web_hooks/destroy_worker.rb
index 822a1e770d7..8f9b194f88a 100644
--- a/app/workers/web_hooks/destroy_worker.rb
+++ b/app/workers/web_hooks/destroy_worker.rb
@@ -4,6 +4,8 @@ module WebHooks
class DestroyWorker
include ApplicationWorker
+ DestroyError = Class.new(StandardError)
+
data_consistency :always
sidekiq_options retry: 3
feature_category :integrations
@@ -19,12 +21,7 @@ module WebHooks
result = ::WebHooks::DestroyService.new(user).sync_destroy(hook)
- return result if result[:status] == :success
-
- e = ::WebHooks::DestroyService::DestroyError.new(result[:message])
- Gitlab::ErrorTracking.track_exception(e, web_hook_id: hook.id)
-
- raise e
+ result.track_and_raise_exception(as: DestroyError, web_hook_id: hook.id)
end
end
end
diff --git a/app/workers/web_hooks/log_destroy_worker.rb b/app/workers/web_hooks/log_destroy_worker.rb
new file mode 100644
index 00000000000..9ea5c70e416
--- /dev/null
+++ b/app/workers/web_hooks/log_destroy_worker.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module WebHooks
+ class LogDestroyWorker
+ include ApplicationWorker
+
+ DestroyError = Class.new(StandardError)
+
+ data_consistency :always
+ feature_category :integrations
+ urgency :low
+
+ idempotent!
+
+ def perform(params = {})
+ hook_id = params['hook_id']
+ return unless hook_id
+
+ result = ::WebHooks::LogDestroyService.new(hook_id).execute
+
+ result.track_and_raise_exception(as: DestroyError, web_hook_id: hook_id)
+ end
+ end
+end