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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/add_context_commits_modal/store/actions.js2
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue7
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue92
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue2
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/labels_select.vue2
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue104
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue81
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_body.vue48
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/report_details.vue2
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/reported_content.vue4
-rw-r--r--app/assets/javascripts/admin/abuse_report/constants.js2
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/abuse_report.query.graphql (renamed from app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql)1
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/abuse_report_labels.query.graphql (renamed from app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql)0
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/create_abuse_report_label.mutation.graphql (renamed from app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql)0
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql30
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql3
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_notes.query.graphql18
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql18
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/delete_abuse_report_note.fragment.graphql8
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql10
-rw-r--r--app/assets/javascripts/admin/background_migrations/index.js2
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/message_form.vue7
-rw-r--r--app/assets/javascripts/admin/users/components/actions/index.js4
-rw-r--r--app/assets/javascripts/admin/users/components/actions/trust_user.vue62
-rw-r--r--app/assets/javascripts/admin/users/components/actions/untrust_user.vue56
-rw-r--r--app/assets/javascripts/admin/users/components/app.vue63
-rw-r--r--app/assets/javascripts/admin/users/constants.js6
-rw-r--r--app/assets/javascripts/alert.js2
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue2
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/mutations.js4
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/state.js4
-rw-r--r--app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js28
-rw-r--r--app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue45
-rw-r--r--app/assets/javascripts/analytics/shared/components/metric_tile.vue1
-rw-r--r--app/assets/javascripts/analytics/usage_trends/components/usage_trends_count_chart.vue2
-rw-r--r--app/assets/javascripts/analytics/usage_trends/components/users_chart.vue2
-rw-r--r--app/assets/javascripts/api/bulk_imports_api.js15
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue6
-rw-r--r--app/assets/javascripts/batch_comments/components/submit_dropdown.vue247
-rw-r--r--app/assets/javascripts/batch_comments/queries/can_approve.query.graphql11
-rw-r--r--app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js18
-rw-r--r--app/assets/javascripts/behaviors/index.js3
-rw-r--r--app/assets/javascripts/behaviors/load_startup_css.js15
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/keybindings.js7
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js2
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue21
-rw-r--r--app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue34
-rw-r--r--app/assets/javascripts/blob/components/constants.js3
-rw-r--r--app/assets/javascripts/blob/filepath_form/components/template_selector.vue5
-rw-r--r--app/assets/javascripts/blob/pipeline_tour_success_modal.vue6
-rw-r--r--app/assets/javascripts/boards/boards_util.js4
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column_form.vue1
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue1
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue19
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue3
-rw-r--r--app/assets/javascripts/boards/components/new_board_button.vue2
-rw-r--r--app/assets/javascripts/boards/constants.js1
-rw-r--r--app/assets/javascripts/boards/graphql/cache_updates.js7
-rw-r--r--app/assets/javascripts/boards/stores/actions.js8
-rw-r--r--app/assets/javascripts/boards/stores/index.js15
-rw-r--r--app/assets/javascripts/branches/components/branch_more_actions.vue1
-rw-r--r--app/assets/javascripts/branches/components/delete_branch_modal.vue1
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue3
-rw-r--r--app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue6
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue37
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue6
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue11
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_header.vue14
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue11
-rw-r--r--app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue112
-rw-r--r--app/assets/javascripts/ci/catalog/global_catalog.vue10
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql16
-rw-r--r--app/assets/javascripts/ci/catalog/index.js37
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue12
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue511
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue28
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue8
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/constants.js2
-rw-r--r--app/assets/javascripts/ci/common/pipelines_table.vue10
-rw-r--r--app/assets/javascripts/ci/common/private/job_action_component.vue14
-rw-r--r--app/assets/javascripts/ci/common/private/job_links_layer.vue10
-rw-r--r--app/assets/javascripts/ci/common/private/job_name_component.vue2
-rw-r--r--app/assets/javascripts/ci/constants.js1
-rw-r--r--app/assets/javascripts/ci/job_details/components/job_header.vue6
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_header.vue3
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue4
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue15
-rw-r--r--app/assets/javascripts/ci/job_details/job_app.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/store/actions.js2
-rw-r--r--app/assets/javascripts/ci/job_details/store/utils.js45
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue4
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue6
-rw-r--r--app/assets/javascripts/ci/jobs_page/jobs_page_app.vue3
-rw-r--r--app/assets/javascripts/ci/pipeline_details/constants.js1
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue18
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue20
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue6
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue17
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue28
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue39
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue42
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue60
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue36
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue6
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js7
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipelines_index.js4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue97
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue13
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/options.js1
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue13
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue14
-rw-r--r--app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue6
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue3
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue7
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue220
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue33
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue28
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue11
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue4
-rw-r--r--app/assets/javascripts/ci/pipelines_page/constants.js3
-rw-r--r--app/assets/javascripts/ci/pipelines_page/pipelines.vue14
-rw-r--r--app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue4
-rw-r--r--app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue20
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue33
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue41
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue4
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_created_at.vue72
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_details.vue18
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_header.vue29
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_jobs_table.vue6
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_list_header.vue17
-rw-r--r--app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/search_tokens/version_token_config.js12
-rw-r--r--app/assets/javascripts/ci/runner/constants.js7
-rw-r--r--app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql5
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql2
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql10
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql5
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql2
-rw-r--r--app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue4
-rw-r--r--app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue18
-rw-r--r--app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue4
-rw-r--r--app/assets/javascripts/ci/runner/runner_search_utils.js10
-rw-r--r--app/assets/javascripts/ci/runner/sentry_utils.js2
-rw-r--r--app/assets/javascripts/ci/utils.js2
-rw-r--r--app/assets/javascripts/ci_secure_files/components/secure_files_list.vue2
-rw-r--r--app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue8
-rw-r--r--app/assets/javascripts/clusters_list/store/actions.js2
-rw-r--r--app/assets/javascripts/commons/gitlab_ui.js2
-rw-r--r--app/assets/javascripts/content_editor/components/suggestions_dropdown.vue2
-rw-r--r--app/assets/javascripts/content_editor/content_editor.stories.js1
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js9
-rw-r--r--app/assets/javascripts/content_editor/extensions/copy_paste.js40
-rw-r--r--app/assets/javascripts/content_editor/extensions/emoji.js4
-rw-r--r--app/assets/javascripts/content_editor/extensions/html_marks.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/loading.js56
-rw-r--r--app/assets/javascripts/content_editor/extensions/suggestions.js27
-rw-r--r--app/assets/javascripts/content_editor/extensions/word_break.js2
-rw-r--r--app/assets/javascripts/content_editor/services/content_editor.js2
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue10
-rw-r--r--app/assets/javascripts/crm/organizations/components/graphql/update_customer_relations_organization.mutation.graphql (renamed from app/assets/javascripts/crm/organizations/components/graphql/update_organization.mutation.graphql)2
-rw-r--r--app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue4
-rw-r--r--app/assets/javascripts/custom_emoji/components/delete_item.vue2
-rw-r--r--app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue2
-rw-r--r--app/assets/javascripts/deprecated_notes.js2
-rw-r--r--app/assets/javascripts/design_management/components/delete_button.vue2
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue3
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note.vue28
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue2
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue4
-rw-r--r--app/assets/javascripts/design_management/components/design_overlay.vue2
-rw-r--r--app/assets/javascripts/design_management/components/list/item.vue17
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue6
-rw-r--r--app/assets/javascripts/diffs/components/app.vue127
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue7
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue5
-rw-r--r--app/assets/javascripts/diffs/components/graphql/get_mr_codequality_and_security_reports.query.graphql82
-rw-r--r--app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql46
-rw-r--r--app/assets/javascripts/diffs/components/shared/findings_drawer.vue165
-rw-r--r--app/assets/javascripts/diffs/components/shared/findings_drawer_item.vue30
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue140
-rw-r--r--app/assets/javascripts/diffs/components/tree_list_height.vue108
-rw-r--r--app/assets/javascripts/diffs/constants.js1
-rw-r--r--app/assets/javascripts/diffs/index.js4
-rw-r--r--app/assets/javascripts/diffs/store/actions.js13
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js7
-rw-r--r--app/assets/javascripts/diffs/store/utils.js2
-rw-r--r--app/assets/javascripts/diffs/utils/file_reviews.js2
-rw-r--r--app/assets/javascripts/diffs/utils/sort_findings_by_file.js11
-rw-r--r--app/assets/javascripts/editor/constants.js5
-rw-r--r--app/assets/javascripts/editor/schema/ci.json12
-rw-r--r--app/assets/javascripts/emoji/awards_app/store/actions.js2
-rw-r--r--app/assets/javascripts/emoji/components/picker.vue4
-rw-r--r--app/assets/javascripts/emoji/constants.js12
-rw-r--r--app/assets/javascripts/emoji/index.js107
-rw-r--r--app/assets/javascripts/ensure_data.js2
-rw-r--r--app/assets/javascripts/entrypoints/analytics.js6
-rw-r--r--app/assets/javascripts/environments/components/confirm_rollback_modal.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_form.vue1
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue2
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue30
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue8
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_overview.vue2
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_status_bar.vue16
-rw-r--r--app/assets/javascripts/environments/constants.js4
-rw-r--r--app/assets/javascripts/environments/environment_details/index.vue2
-rw-r--r--app/assets/javascripts/environments/graphql/client.js1
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql2
-rw-r--r--app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql15
-rw-r--r--app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql10
-rw-r--r--app/assets/javascripts/environments/graphql/queries/folder.query.graphql2
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/base.js4
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/flux.js116
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/kubernetes.js51
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue6
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags.vue1
-rw-r--r--app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js19
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js23
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js2
-rw-r--r--app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue2
-rw-r--r--app/assets/javascripts/graphql_shared/constants.js1
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json2
-rw-r--r--app/assets/javascripts/graphql_shared/queries/groups_autocomplete.query.graphql10
-rw-r--r--app/assets/javascripts/groups/components/item_actions.vue1
-rw-r--r--app/assets/javascripts/groups/settings/init_access_dropdown.js2
-rw-r--r--app/assets/javascripts/groups_projects/components/transfer_locations.vue6
-rw-r--r--app/assets/javascripts/header.js3
-rw-r--r--app/assets/javascripts/header_search/index.js2
-rw-r--r--app/assets/javascripts/header_search/init.js2
-rw-r--r--app/assets/javascripts/helpers/help_page_helper.js2
-rw-r--r--app/assets/javascripts/helpers/init_simple_app_helper.js29
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue1
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/description.vue2
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue9
-rw-r--r--app/assets/javascripts/ide/lib/alerts/index.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/getters.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/mutations.js2
-rw-r--r--app/assets/javascripts/import/constants.js12
-rw-r--r--app/assets/javascripts/import/details/components/bulk_import_details_app.vue44
-rw-r--r--app/assets/javascripts/import/details/components/import_details_app.vue38
-rw-r--r--app/assets/javascripts/import/details/components/import_details_table.vue96
-rw-r--r--app/assets/javascripts/import_entities/components/import_status.vue57
-rw-r--r--app/assets/javascripts/import_entities/constants.js53
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_status.vue83
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue21
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue1
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue1
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue12
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/actions.js2
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue3
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue2
-rw-r--r--app/assets/javascripts/integrations/overrides/components/integration_overrides.vue2
-rw-r--r--app/assets/javascripts/invite_members/components/invite_groups_modal.vue2
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue80
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue10
-rw-r--r--app/assets/javascripts/invite_members/components/members_token_select.vue12
-rw-r--r--app/assets/javascripts/invite_members/components/project_select.vue1
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js3
-rw-r--r--app/assets/javascripts/invite_members/utils/member_utils.js4
-rw-r--r--app/assets/javascripts/issuable/components/locked_badge.vue9
-rw-r--r--app/assets/javascripts/issuable/components/related_issuable_item.vue11
-rw-r--r--app/assets/javascripts/issuable/components/status_badge.vue10
-rw-r--r--app/assets/javascripts/issuable/popover/components/mr_popover.vue4
-rw-r--r--app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue5
-rw-r--r--app/assets/javascripts/issues/issue.js2
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue12
-rw-r--r--app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue5
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue1
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue259
-rw-r--r--app/assets/javascripts/issues/show/components/issue_header.vue10
-rw-r--r--app/assets/javascripts/issues/show/components/sticky_header.vue14
-rw-r--r--app/assets/javascripts/issues/show/utils/parse_data.js2
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/constants.js6
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue58
-rw-r--r--app/assets/javascripts/lib/graphql.js14
-rw-r--r--app/assets/javascripts/lib/utils/color_utils.js2
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js15
-rw-r--r--app/assets/javascripts/lib/utils/constants.js3
-rw-r--r--app/assets/javascripts/lib/utils/datetime/timeago_utility.js16
-rw-r--r--app/assets/javascripts/lib/utils/forms.js22
-rw-r--r--app/assets/javascripts/lib/utils/keys.js1
-rw-r--r--app/assets/javascripts/main.js2
-rw-r--r--app/assets/javascripts/members/components/avatars/group_avatar.vue48
-rw-r--r--app/assets/javascripts/members/components/icons/private_icon.vue19
-rw-r--r--app/assets/javascripts/members/components/table/member_source.vue20
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue1
-rw-r--r--app/assets/javascripts/members/components/table/role_dropdown.vue70
-rw-r--r--app/assets/javascripts/members/store/actions.js5
-rw-r--r--app/assets/javascripts/members/utils.js34
-rw-r--r--app/assets/javascripts/merge_request_tabs.js7
-rw-r--r--app/assets/javascripts/merge_requests/components/sticky_header.vue37
-rw-r--r--app/assets/javascripts/milestones/components/milestone_combobox.vue206
-rw-r--r--app/assets/javascripts/milestones/stores/mutations.js4
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/constants.js4
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/constants.js7
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue8
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/index.js4
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue61
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue59
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue16
-rw-r--r--app/assets/javascripts/ml/model_registry/components/model_row.vue (renamed from app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue)16
-rw-r--r--app/assets/javascripts/ml/model_registry/components/search_bar.vue71
-rw-r--r--app/assets/javascripts/ml/model_registry/constants.js13
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue49
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/index.js3
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/translations.js16
-rw-r--r--app/assets/javascripts/ml/model_registry/translations.js16
-rw-r--r--app/assets/javascripts/mr_notes/init.js1
-rw-r--r--app/assets/javascripts/notes/components/comment_field_layout.vue8
-rw-r--r--app/assets/javascripts/notes/components/discussion_locked_widget.vue4
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_button.vue6
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_signed_out_widget.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue4
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue5
-rw-r--r--app/assets/javascripts/notes/components/notes_activity_header.vue2
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js5
-rw-r--r--app/assets/javascripts/observability/client.js133
-rw-r--r--app/assets/javascripts/observability/components/loader/constants.js20
-rw-r--r--app/assets/javascripts/observability/components/loader/index.vue (renamed from app/assets/javascripts/observability/components/skeleton/index.vue)80
-rw-r--r--app/assets/javascripts/observability/components/observability_container.vue58
-rw-r--r--app/assets/javascripts/observability/components/observability_empty_state.vue36
-rw-r--r--app/assets/javascripts/observability/components/provisioned_observability_container.vue95
-rw-r--r--app/assets/javascripts/observability/constants.js24
-rw-r--r--app/assets/javascripts/organizations/mock_data.js27
-rw-r--r--app/assets/javascripts/organizations/new/components/app.vue18
-rw-r--r--app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql9
-rw-r--r--app/assets/javascripts/organizations/new/graphql/mutations/organization_create.mutation.graphql9
-rw-r--r--app/assets/javascripts/organizations/new/graphql/typedefs.graphql5
-rw-r--r--app/assets/javascripts/organizations/new/index.js3
-rw-r--r--app/assets/javascripts/organizations/profile/preferences/index.js41
-rw-r--r--app/assets/javascripts/organizations/settings/general/components/app.vue14
-rw-r--r--app/assets/javascripts/organizations/settings/general/components/organization_settings.vue77
-rw-r--r--app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql9
-rw-r--r--app/assets/javascripts/organizations/settings/general/graphql/typedefs.graphql5
-rw-r--r--app/assets/javascripts/organizations/settings/general/index.js38
-rw-r--r--app/assets/javascripts/organizations/shared/components/new_edit_form.vue117
-rw-r--r--app/assets/javascripts/organizations/shared/constants.js3
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/queries/organization.query.graphql9
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/resolvers.js6
-rw-r--r--app/assets/javascripts/organizations/users/components/app.vue51
-rw-r--r--app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql17
-rw-r--r--app/assets/javascripts/organizations/users/index.js29
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue7
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue5
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue5
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue7
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue9
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue13
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/constants.js1
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql2
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/package_path.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/appearances/preview_sign_in/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/service_usage_data/index.js3
-rw-r--r--app/assets/javascripts/pages/explore/catalog/index.js3
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/details/index.js20
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue39
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/index.js3
-rw-r--r--app/assets/javascripts/pages/import/history/components/import_history_app.vue14
-rw-r--r--app/assets/javascripts/pages/organizations/organizations/users/index.js3
-rw-r--r--app/assets/javascripts/pages/organizations/settings/general/index.js3
-rw-r--r--app/assets/javascripts/pages/passwords/new/index.js3
-rw-r--r--app/assets/javascripts/pages/profiles/preferences/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue50
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/page.js3
-rw-r--r--app/assets/javascripts/pages/projects/ml/model_versions/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/ml/models/index/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/ml/models/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/product_analytics/graphs/index.js3
-rw-r--r--app/assets/javascripts/pages/registrations/new/index.js2
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js2
-rw-r--r--app/assets/javascripts/pages/users/index.js11
-rw-r--r--app/assets/javascripts/pages/users/show/index.js5
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js5
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue5
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js2
-rw-r--r--app/assets/javascripts/pipeline_wizard/components/wrapper.vue3
-rw-r--r--app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue3
-rw-r--r--app/assets/javascripts/pipeline_wizard/templates/pages.yml20
-rw-r--r--app/assets/javascripts/profile/edit/components/profile_edit_app.vue7
-rw-r--r--app/assets/javascripts/profile/preferences/components/profile_preferences.vue6
-rw-r--r--app/assets/javascripts/profile/profile.js8
-rw-r--r--app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue7
-rw-r--r--app/assets/javascripts/projects/commit/components/form_modal.vue2
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue12
-rw-r--r--app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js2
-rw-r--r--app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_status.js2
-rw-r--r--app/assets/javascripts/projects/commits/store/actions.js2
-rw-r--r--app/assets/javascripts/projects/default_project_templates.js4
-rw-r--r--app/assets/javascripts/projects/new/components/new_project_url_select.vue6
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue12
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue4
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue1
-rw-r--r--app/assets/javascripts/projects/settings/components/default_branch_selector.vue1
-rw-r--r--app/assets/javascripts/projects/settings/init_access_dropdown.js3
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/app.vue2
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue4
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue57
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue1
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue13
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue59
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue1
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js7
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/index.js2
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue11
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js14
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js2
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.vue2
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit_list.js2
-rw-r--r--app/assets/javascripts/releases/components/app_edit_new.vue25
-rw-r--r--app/assets/javascripts/releases/components/release_block_header.vue2
-rw-r--r--app/assets/javascripts/repository/components/blob_button_group.vue7
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue17
-rw-r--r--app/assets/javascripts/repository/components/commit_info.vue18
-rw-r--r--app/assets/javascripts/repository/components/delete_blob_modal.vue2
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue13
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue6
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue24
-rw-r--r--app/assets/javascripts/search/sidebar/components/archived_filter/data.js11
-rw-r--r--app/assets/javascripts/search/sidebar/components/blobs_filters.vue7
-rw-r--r--app/assets/javascripts/search/sidebar/components/issues_filters.vue5
-rw-r--r--app/assets/javascripts/search/sidebar/components/label_filter/index.vue61
-rw-r--r--app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue7
-rw-r--r--app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue18
-rw-r--r--app/assets/javascripts/search/sidebar/constants/index.js2
-rw-r--r--app/assets/javascripts/search/store/getters.js39
-rw-r--r--app/assets/javascripts/security_configuration/components/training_provider_list.vue2
-rw-r--r--app/assets/javascripts/sentry/init_sentry.js6
-rw-r--r--app/assets/javascripts/sentry/legacy_index.js2
-rw-r--r--app/assets/javascripts/sentry/sentry_browser_wrapper.js16
-rw-r--r--app/assets/javascripts/service_ping_consent.js35
-rw-r--r--app/assets/javascripts/sessions/new/components/update_email.vue4
-rw-r--r--app/assets/javascripts/sessions/new/constants.js1
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue25
-rw-r--r--app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue210
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewers.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/severity/sidebar_severity_widget.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue10
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js3
-rw-r--r--app/assets/javascripts/sidebar/queries/constants.js4
-rw-r--r--app/assets/javascripts/silent_mode_settings/components/app.vue10
-rw-r--r--app/assets/javascripts/single_file_diff.js5
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue16
-rw-r--r--app/assets/javascripts/super_sidebar/components/create_menu.vue1
-rw-r--r--app/assets/javascripts/super_sidebar/components/flyout_menu.vue4
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue11
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue9
-rw-r--r--app/assets/javascripts/super_sidebar/components/menu_section.vue8
-rw-r--r--app/assets/javascripts/super_sidebar/components/nav_item.vue27
-rw-r--r--app/assets/javascripts/super_sidebar/components/pinned_section.vue6
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_menu.vue22
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue1
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu.vue11
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js12
-rw-r--r--app/assets/javascripts/super_sidebar/utils.js2
-rw-r--r--app/assets/javascripts/tags/components/delete_tag_modal.vue1
-rw-r--r--app/assets/javascripts/terraform/components/init_command_modal.vue11
-rw-r--r--app/assets/javascripts/terraform/components/states_table.vue8
-rw-r--r--app/assets/javascripts/time_tracking/components/timelogs_app.vue2
-rw-r--r--app/assets/javascripts/token_access/components/inbound_token_access.vue6
-rw-r--r--app/assets/javascripts/token_access/components/outbound_token_access.vue6
-rw-r--r--app/assets/javascripts/token_access/components/token_projects_table.vue11
-rw-r--r--app/assets/javascripts/token_access/graphql/cache_config.js14
-rw-r--r--app/assets/javascripts/token_access/index.js3
-rw-r--r--app/assets/javascripts/tracking/constants.js3
-rw-r--r--app/assets/javascripts/tracking/dispatch_snowplow_event.js2
-rw-r--r--app/assets/javascripts/users/profile/actions/components/user_actions_app.vue8
-rw-r--r--app/assets/javascripts/users/profile/components/report_abuse_button.vue58
-rw-r--r--app/assets/javascripts/users/profile/index.js23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue24
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js85
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue220
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/unresolved_discussions.vue39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue118
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.vue (renamed from app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.js)51
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue52
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql5
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue4
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/chronic_duration_input.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue157
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue128
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue122
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/constants.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/group_select.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue150
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/project_select.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue36
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue106
-rw-r--r--app/assets/javascripts/vue_shared/components/form/errors_alert.stories.js21
-rw-r--r--app/assets/javascripts/vue_shared/components/form/errors_alert.vue42
-rw-r--r--app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/group_item.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/index.vue193
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/user_item.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue66
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql36
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue122
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/utils.js19
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_labels.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/users_table/constants.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue (renamed from app/assets/javascripts/admin/users/components/user_avatar.vue)21
-rw-r--r--app/assets/javascripts/vue_shared/components/users_table/users_table.vue (renamed from app/assets/javascripts/admin/users/components/users_table.vue)68
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue4
-rw-r--r--app/assets/javascripts/vue_shared/directives/safe_html.js2
-rw-r--r--app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js11
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue6
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue1
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue17
-rw-r--r--app/assets/javascripts/webhooks/components/push_events.vue8
-rw-r--r--app/assets/javascripts/work_items/components/notes/system_note.vue22
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue10
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue7
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue11
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue4
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue2
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue5
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue46
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue28
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_token_input.vue126
-rw-r--r--app/assets/javascripts/work_items/components/work_item_actions.vue104
-rw-r--r--app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue4
-rw-r--r--app/assets/javascripts/work_items/components/work_item_award_emoji.vue17
-rw-r--r--app/assets/javascripts/work_items/components/work_item_created_updated.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description_rendered.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue132
-rw-r--r--app/assets/javascripts/work_items/components/work_item_due_date.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_labels.vue7
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue11
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue9
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue23
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue16
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone.vue134
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue5
-rw-r--r--app/assets/javascripts/work_items/components/work_item_parent.vue58
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue4
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue28
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state_toggle.vue (renamed from app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue)44
-rw-r--r--app/assets/javascripts/work_items/components/work_item_title.vue9
-rw-r--r--app/assets/javascripts/work_items/components/work_item_todos.vue9
-rw-r--r--app/assets/javascripts/work_items/constants.js27
-rw-r--r--app/assets/javascripts/work_items/graphql/award_emoji.query.graphql2
-rw-r--r--app/assets/javascripts/work_items/graphql/group_award_emoji.query.graphql27
-rw-r--r--app/assets/javascripts/work_items/graphql/group_work_items.query.graphql17
-rw-r--r--app/assets/javascripts/work_items/graphql/project_work_items.query.graphql11
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql4
-rw-r--r--app/assets/javascripts/work_items/list/components/work_items_list_app.vue2
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue2
-rw-r--r--app/assets/stylesheets/application.scss4
-rw-r--r--app/assets/stylesheets/application_utilities.scss2
-rw-r--r--app/assets/stylesheets/components/detail_page.scss3
-rw-r--r--app/assets/stylesheets/framework/buttons.scss19
-rw-r--r--app/assets/stylesheets/framework/diffs.scss1
-rw-r--r--app/assets/stylesheets/framework/files.scss1
-rw-r--r--app/assets/stylesheets/framework/header.scss9
-rw-r--r--app/assets/stylesheets/framework/highlight.scss2
-rw-r--r--app/assets/stylesheets/framework/icons.scss87
-rw-r--r--app/assets/stylesheets/framework/layout.scss6
-rw-r--r--app/assets/stylesheets/framework/mixins.scss9
-rw-r--r--app/assets/stylesheets/framework/page_header.scss8
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss18
-rw-r--r--app/assets/stylesheets/page_bundles/_system_note_styles.scss59
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/branches.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/build.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/ci_status.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/issuable.scss53
-rw-r--r--app/assets/stylesheets/page_bundles/merge_request.scss18
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss99
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline.scss86
-rw-r--r--app/assets/stylesheets/page_bundles/pipelines.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/profile.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/projects.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/wiki.scss13
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss25
-rw-r--r--app/assets/stylesheets/pages/commits.scss3
-rw-r--r--app/assets/stylesheets/pages/issues.scss19
-rw-r--r--app/assets/stylesheets/pages/notes.scss42
-rw-r--r--app/assets/stylesheets/print.scss1
-rw-r--r--app/assets/stylesheets/startup/_cloaking.scss15
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss1928
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss1781
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss852
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss12
-rw-r--r--app/assets/stylesheets/themes/theme_blue.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_gray.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_green.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_indigo.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_blue.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_gray.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_green.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_indigo.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_red.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_red.scss2
-rw-r--r--app/assets/stylesheets/tmp_utilities.scss32
-rw-r--r--app/assets/stylesheets/utilities.scss3
-rw-r--r--app/components/projects/ml/models_index_component.rb11
-rw-r--r--app/components/projects/ml/show_ml_model_component.rb15
-rw-r--r--app/components/projects/ml/show_ml_model_version_component.html.haml1
-rw-r--r--app/components/projects/ml/show_ml_model_version_component.rb32
-rw-r--r--app/controllers/acme_challenges_controller.rb4
-rw-r--r--app/controllers/admin/abuse_reports_controller.rb1
-rw-r--r--app/controllers/admin/application_settings_controller.rb21
-rw-r--r--app/controllers/admin/dashboard_controller.rb3
-rw-r--r--app/controllers/admin/spam_logs_controller.rb4
-rw-r--r--app/controllers/admin/users_controller.rb25
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/controllers/autocomplete_controller.rb36
-rw-r--r--app/controllers/base_action_controller.rb31
-rw-r--r--app/controllers/chaos_controller.rb4
-rw-r--r--app/controllers/concerns/creates_commit.rb14
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/concerns/render_access_tokens.rb1
-rw-r--r--app/controllers/concerns/wiki_actions.rb6
-rw-r--r--app/controllers/dashboard_controller.rb1
-rw-r--r--app/controllers/explore/catalog_controller.rb20
-rw-r--r--app/controllers/external_redirect/external_redirect_controller.rb36
-rw-r--r--app/controllers/groups/settings/applications_controller.rb2
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb1
-rw-r--r--app/controllers/groups/work_items_controller.rb7
-rw-r--r--app/controllers/groups_controller.rb5
-rw-r--r--app/controllers/health_controller.rb4
-rw-r--r--app/controllers/import/bulk_imports_controller.rb8
-rw-r--r--app/controllers/jira_connect/subscriptions_controller.rb2
-rw-r--r--app/controllers/jwt_controller.rb8
-rw-r--r--app/controllers/metrics_controller.rb4
-rw-r--r--app/controllers/oauth/jira_dvcs/authorizations_controller.rb86
-rw-r--r--app/controllers/organizations/organizations_controller.rb4
-rw-r--r--app/controllers/profiles/comment_templates_controller.rb2
-rw-r--r--app/controllers/profiles/preferences_controller.rb1
-rw-r--r--app/controllers/profiles_controller.rb3
-rw-r--r--app/controllers/projects/application_controller.rb13
-rw-r--r--app/controllers/projects/artifacts_controller.rb6
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/environments_controller.rb15
-rw-r--r--app/controllers/projects/group_links_controller.rb51
-rw-r--r--app/controllers/projects/incidents_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb6
-rw-r--r--app/controllers/projects/jobs_controller.rb30
-rw-r--r--app/controllers/projects/merge_requests/drafts_controller.rb20
-rw-r--r--app/controllers/projects/merge_requests_controller.rb22
-rw-r--r--app/controllers/projects/ml/model_versions_controller.rb24
-rw-r--r--app/controllers/projects/ml/models_controller.rb31
-rw-r--r--app/controllers/projects/pipelines_controller.rb7
-rw-r--r--app/controllers/projects/raw_controller.rb3
-rw-r--r--app/controllers/projects/repositories_controller.rb2
-rw-r--r--app/controllers/projects/service_desk_controller.rb5
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb1
-rw-r--r--app/controllers/projects/tree_controller.rb2
-rw-r--r--app/controllers/projects/work_items_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb11
-rw-r--r--app/controllers/repositories/git_http_client_controller.rb4
-rw-r--r--app/controllers/repositories/git_http_controller.rb10
-rw-r--r--app/controllers/repositories/lfs_api_controller.rb4
-rw-r--r--app/controllers/search_controller.rb19
-rw-r--r--app/experiments/ios_specific_templates_experiment.rb32
-rw-r--r--app/finders/ci/catalog/resources/versions_finder.rb58
-rw-r--r--app/finders/ci/runners_finder.rb17
-rw-r--r--app/finders/data_transfer/mocked_transfer_finder.rb27
-rw-r--r--app/finders/merge_requests_finder.rb11
-rw-r--r--app/finders/organizations/user_organizations_finder.rb26
-rw-r--r--app/finders/packages/packages_finder.rb1
-rw-r--r--app/finders/packages/pypi/packages_finder.rb11
-rw-r--r--app/finders/projects/ml/model_finder.rb52
-rw-r--r--app/finders/projects_finder.rb14
-rw-r--r--app/finders/user_group_notification_settings_finder.rb17
-rw-r--r--app/graphql/mutations/base_mutation.rb6
-rw-r--r--app/graphql/mutations/ci/catalog/resources/create.rb36
-rw-r--r--app/graphql/mutations/ci/catalog/resources/unpublish.rb30
-rw-r--r--app/graphql/mutations/ci/job/cancel.rb2
-rw-r--r--app/graphql/mutations/ci/pipeline/cancel.rb2
-rw-r--r--app/graphql/mutations/commits/create.rb2
-rw-r--r--app/graphql/mutations/container_registry/protection/rule/create.rb63
-rw-r--r--app/graphql/mutations/merge_requests/accept.rb4
-rw-r--r--app/graphql/mutations/namespace/package_settings/update.rb10
-rw-r--r--app/graphql/mutations/organizations/create.rb35
-rw-r--r--app/graphql/mutations/packages/protection/rule/delete.rb40
-rw-r--r--app/graphql/mutations/saved_replies/base.rb4
-rw-r--r--app/graphql/mutations/saved_replies/create.rb2
-rw-r--r--app/graphql/mutations/saved_replies/destroy.rb2
-rw-r--r--app/graphql/mutations/saved_replies/update.rb2
-rw-r--r--app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb2
-rw-r--r--app/graphql/resolvers/analytics/cycle_analytics/issue_count_resolver.rb2
-rw-r--r--app/graphql/resolvers/ci/catalog/resource_resolver.rb48
-rw-r--r--app/graphql/resolvers/ci/catalog/resources_resolver.rb54
-rw-r--r--app/graphql/resolvers/ci/catalog/versions_resolver.rb24
-rw-r--r--app/graphql/resolvers/ci/runners_resolver.rb14
-rw-r--r--app/graphql/resolvers/concerns/caching_array_resolver.rb2
-rw-r--r--app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb7
-rw-r--r--app/graphql/resolvers/container_repository_tags_resolver.rb56
-rw-r--r--app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb16
-rw-r--r--app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb16
-rw-r--r--app/graphql/resolvers/group_issues_resolver.rb2
-rw-r--r--app/graphql/resolvers/issues/base_parent_resolver.rb9
-rw-r--r--app/graphql/resolvers/issues_resolver.rb7
-rw-r--r--app/graphql/resolvers/namespaces/work_items_resolver.rb2
-rw-r--r--app/graphql/resolvers/packages_base_resolver.rb6
-rw-r--r--app/graphql/resolvers/project_issues_resolver.rb2
-rw-r--r--app/graphql/resolvers/project_members_resolver.rb4
-rw-r--r--app/graphql/resolvers/project_milestones_resolver.rb1
-rw-r--r--app/graphql/resolvers/projects/snippets_resolver.rb4
-rw-r--r--app/graphql/resolvers/projects_resolver.rb42
-rw-r--r--app/graphql/resolvers/saved_reply_resolver.rb2
-rw-r--r--app/graphql/resolvers/snippets_resolver.rb4
-rw-r--r--app/graphql/resolvers/users/frecent_groups_resolver.rb23
-rw-r--r--app/graphql/resolvers/users/frecent_projects_resolver.rb21
-rw-r--r--app/graphql/resolvers/users/organizations_resolver.rb18
-rw-r--r--app/graphql/resolvers/users/snippets_resolver.rb4
-rw-r--r--app/graphql/resolvers/work_items/linked_items_resolver.rb26
-rw-r--r--app/graphql/types/abuse_report_type.rb9
-rw-r--r--app/graphql/types/analytics/cycle_analytics/value_stream_type.rb32
-rw-r--r--app/graphql/types/base_argument.rb22
-rw-r--r--app/graphql/types/base_input_object.rb2
-rw-r--r--app/graphql/types/ci/catalog/resource_scope_enum.rb14
-rw-r--r--app/graphql/types/ci/catalog/resource_sort_enum.rb19
-rw-r--r--app/graphql/types/ci/catalog/resource_type.rb120
-rw-r--r--app/graphql/types/ci/pipeline_status_enum.rb1
-rw-r--r--app/graphql/types/container_registry/protection/rule_access_level_enum.rb17
-rw-r--r--app/graphql/types/container_registry/protection/rule_type.rb41
-rw-r--r--app/graphql/types/container_repository_details_type.rb3
-rw-r--r--app/graphql/types/data_transfer/project_data_transfer_type.rb1
-rw-r--r--app/graphql/types/group_member_type.rb4
-rw-r--r--app/graphql/types/issuable_state_enum.rb5
-rw-r--r--app/graphql/types/merge_request_review_state_enum.rb8
-rw-r--r--app/graphql/types/merge_request_type.rb3
-rw-r--r--app/graphql/types/mutation_type.rb33
-rw-r--r--app/graphql/types/namespace/package_settings_type.rb15
-rw-r--r--app/graphql/types/notes/noteable_interface.rb2
-rw-r--r--app/graphql/types/organizations/organization_type.rb4
-rw-r--r--app/graphql/types/organizations/organization_user_badge_type.rb22
-rw-r--r--app/graphql/types/organizations/organization_user_type.rb4
-rw-r--r--app/graphql/types/packages/package_base_type.rb10
-rw-r--r--app/graphql/types/packages/protection/rule_type.rb5
-rw-r--r--app/graphql/types/packages/pypi/metadatum_type.rb9
-rw-r--r--app/graphql/types/permission_types/abuse_report.rb11
-rw-r--r--app/graphql/types/permission_types/base_permission_type.rb2
-rw-r--r--app/graphql/types/permission_types/ci/job.rb1
-rw-r--r--app/graphql/types/permission_types/ci/pipeline.rb1
-rw-r--r--app/graphql/types/permission_types/package.rb12
-rw-r--r--app/graphql/types/project_type.rb6
-rw-r--r--app/graphql/types/projects/detailed_import_status_type.rb39
-rw-r--r--app/graphql/types/query_type.rb22
-rw-r--r--app/graphql/types/security/codequality_reports_comparer/degradation_type.rb3
-rw-r--r--app/graphql/types/security/codequality_reports_comparer/report_generation_status_enum.rb16
-rw-r--r--app/graphql/types/security/codequality_reports_comparer/report_type.rb2
-rw-r--r--app/graphql/types/security/codequality_reports_comparer/status_enum.rb8
-rw-r--r--app/graphql/types/security/codequality_reports_comparer/summary_type.rb2
-rw-r--r--app/graphql/types/security/codequality_reports_comparer_type.rb7
-rw-r--r--app/graphql/types/user_interface.rb16
-rw-r--r--app/graphql/types/work_items/linked_item_type.rb22
-rw-r--r--app/graphql/types/work_items/widgets/linked_items_type.rb1
-rw-r--r--app/helpers/admin/user_actions_helper.rb15
-rw-r--r--app/helpers/application_helper.rb9
-rw-r--r--app/helpers/application_settings_helper.rb1
-rw-r--r--app/helpers/auth_helper.rb11
-rw-r--r--app/helpers/blob_helper.rb2
-rw-r--r--app/helpers/ci/catalog/resources_helper.rb4
-rw-r--r--app/helpers/ci/pipelines_helper.rb11
-rw-r--r--app/helpers/ci/status_helper.rb112
-rw-r--r--app/helpers/clusters_helper.rb2
-rw-r--r--app/helpers/colors_helper.rb12
-rw-r--r--app/helpers/dropdowns_helper.rb2
-rw-r--r--app/helpers/environment_helper.rb48
-rw-r--r--app/helpers/environments_helper.rb13
-rw-r--r--app/helpers/events_helper.rb44
-rw-r--r--app/helpers/graph_helper.rb13
-rw-r--r--app/helpers/ide_helper.rb10
-rw-r--r--app/helpers/members_helper.rb11
-rw-r--r--app/helpers/merge_requests_helper.rb11
-rw-r--r--app/helpers/nav/new_dropdown_helper.rb11
-rw-r--r--app/helpers/nav_helper.rb26
-rw-r--r--app/helpers/notes_helper.rb24
-rw-r--r--app/helpers/operations_helper.rb2
-rw-r--r--app/helpers/organizations/organization_helper.rb21
-rw-r--r--app/helpers/preferences_helper.rb8
-rw-r--r--app/helpers/projects/pipeline_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb8
-rw-r--r--app/helpers/sidebars_helper.rb4
-rw-r--r--app/helpers/sorting_helper.rb30
-rw-r--r--app/helpers/users/callouts_helper.rb24
-rw-r--r--app/helpers/users_helper.rb33
-rw-r--r--app/helpers/visibility_level_helper.rb10
-rw-r--r--app/helpers/vite_helper.rb16
-rw-r--r--app/helpers/wiki_helper.rb8
-rw-r--r--app/mailers/emails/issues.rb2
-rw-r--r--app/mailers/emails/merge_requests.rb2
-rw-r--r--app/mailers/emails/service_desk.rb2
-rw-r--r--app/models/abuse_report.rb16
-rw-r--r--app/models/active_session.rb36
-rw-r--r--app/models/activity_pub.rb7
-rw-r--r--app/models/activity_pub/releases_subscription.rb22
-rw-r--r--app/models/ai/service_access_token.rb10
-rw-r--r--app/models/analytics/cycle_analytics/value_stream.rb6
-rw-r--r--app/models/application_setting.rb9
-rw-r--r--app/models/application_setting_implementation.rb12
-rw-r--r--app/models/bulk_imports/entity.rb4
-rw-r--r--app/models/bulk_imports/failure.rb4
-rw-r--r--app/models/ci/bridge.rb4
-rw-r--r--app/models/ci/build.rb22
-rw-r--r--app/models/ci/build_trace_chunks/redis_base.rb6
-rw-r--r--app/models/ci/build_trace_metadata.rb2
-rw-r--r--app/models/ci/catalog/components_project.rb7
-rw-r--r--app/models/ci/catalog/listing.rb49
-rw-r--r--app/models/ci/catalog/resource.rb44
-rw-r--r--app/models/ci/catalog/resources/component.rb2
-rw-r--r--app/models/ci/catalog/resources/version.rb96
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/ci/job_token/scope.rb5
-rw-r--r--app/models/ci/pipeline.rb48
-rw-r--r--app/models/ci/ref.rb17
-rw-r--r--app/models/ci/runner.rb4
-rw-r--r--app/models/ci/runner_manager.rb21
-rw-r--r--app/models/ci/sources/pipeline.rb2
-rw-r--r--app/models/ci/stage.rb5
-rw-r--r--app/models/commit.rb8
-rw-r--r--app/models/commit_status.rb32
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage_event_model.rb1
-rw-r--r--app/models/concerns/can_move_repository_storage.rb19
-rw-r--r--app/models/concerns/ci/has_status.rb11
-rw-r--r--app/models/concerns/commit_signature.rb1
-rw-r--r--app/models/concerns/diff_positionable_note.rb1
-rw-r--r--app/models/concerns/enums/package_metadata.rb3
-rw-r--r--app/models/concerns/enums/sbom.rb3
-rw-r--r--app/models/concerns/merge_request_reviewer_state.rb3
-rw-r--r--app/models/concerns/repository_storage_movable.rb27
-rw-r--r--app/models/concerns/restricted_signup.rb1
-rw-r--r--app/models/concerns/token_authenticatable_strategies/base.rb2
-rw-r--r--app/models/concerns/use_sql_function_for_primary_key_lookups.rb39
-rw-r--r--app/models/concerns/users/visitable.rb39
-rw-r--r--app/models/container_repository.rb31
-rw-r--r--app/models/deployment.rb3
-rw-r--r--app/models/environment.rb8
-rw-r--r--app/models/group.rb93
-rw-r--r--app/models/guest.rb9
-rw-r--r--app/models/integration.rb21
-rw-r--r--app/models/integrations/apple_app_store.rb6
-rw-r--r--app/models/integrations/asana.rb6
-rw-r--r--app/models/integrations/assembla.rb4
-rw-r--r--app/models/integrations/bamboo.rb6
-rw-r--r--app/models/integrations/base_chat_notification.rb4
-rw-r--r--app/models/integrations/base_slack_notification.rb2
-rw-r--r--app/models/integrations/bugzilla.rb6
-rw-r--r--app/models/integrations/buildkite.rb12
-rw-r--r--app/models/integrations/campfire.rb6
-rw-r--r--app/models/integrations/clickup.rb6
-rw-r--r--app/models/integrations/confluence.rb4
-rw-r--r--app/models/integrations/custom_issue_tracker.rb6
-rw-r--r--app/models/integrations/datadog.rb6
-rw-r--r--app/models/integrations/discord.rb14
-rw-r--r--app/models/integrations/drone_ci.rb12
-rw-r--r--app/models/integrations/emails_on_push.rb4
-rw-r--r--app/models/integrations/ewm.rb6
-rw-r--r--app/models/integrations/external_wiki.rb14
-rw-r--r--app/models/integrations/gitlab_slack_application.rb4
-rw-r--r--app/models/integrations/google_play.rb6
-rw-r--r--app/models/integrations/hangouts_chat.rb6
-rw-r--r--app/models/integrations/harbor.rb26
-rw-r--r--app/models/integrations/irker.rb38
-rw-r--r--app/models/integrations/jenkins.rb6
-rw-r--r--app/models/integrations/jira.rb24
-rw-r--r--app/models/integrations/mattermost.rb6
-rw-r--r--app/models/integrations/mattermost_slash_commands.rb4
-rw-r--r--app/models/integrations/microsoft_teams.rb6
-rw-r--r--app/models/integrations/mock_ci.rb4
-rw-r--r--app/models/integrations/mock_monitoring.rb4
-rw-r--r--app/models/integrations/packagist.rb4
-rw-r--r--app/models/integrations/pipelines_email.rb4
-rw-r--r--app/models/integrations/pivotaltracker.rb6
-rw-r--r--app/models/integrations/prometheus.rb4
-rw-r--r--app/models/integrations/pumble.rb6
-rw-r--r--app/models/integrations/pushover.rb4
-rw-r--r--app/models/integrations/redmine.rb6
-rw-r--r--app/models/integrations/shimo.rb4
-rw-r--r--app/models/integrations/slack.rb4
-rw-r--r--app/models/integrations/slack_slash_commands.rb4
-rw-r--r--app/models/integrations/squash_tm.rb6
-rw-r--r--app/models/integrations/teamcity.rb6
-rw-r--r--app/models/integrations/telegram.rb6
-rw-r--r--app/models/integrations/unify_circuit.rb6
-rw-r--r--app/models/integrations/webex_teams.rb6
-rw-r--r--app/models/integrations/youtrack.rb6
-rw-r--r--app/models/integrations/zentao.rb8
-rw-r--r--app/models/member.rb11
-rw-r--r--app/models/members/members/members_with_parents.rb105
-rw-r--r--app/models/members/project_member.rb6
-rw-r--r--app/models/merge_request.rb58
-rw-r--r--app/models/merge_request_context_commit_diff_file.rb1
-rw-r--r--app/models/merge_request_diff_commit.rb10
-rw-r--r--app/models/ml/candidate.rb2
-rw-r--r--app/models/ml/model.rb14
-rw-r--r--app/models/ml/model_metadata.rb13
-rw-r--r--app/models/ml/model_version.rb19
-rw-r--r--app/models/namespace.rb31
-rw-r--r--app/models/namespace_setting.rb10
-rw-r--r--app/models/network/graph.rb20
-rw-r--r--app/models/note.rb6
-rw-r--r--app/models/organizations/organization.rb4
-rw-r--r--app/models/packages/npm/metadata_cache.rb6
-rw-r--r--app/models/packages/nuget/symbol.rb3
-rw-r--r--app/models/packages/protection/rule.rb8
-rw-r--r--app/models/packages/pypi/metadatum.rb16
-rw-r--r--app/models/packages/tag.rb7
-rw-r--r--app/models/pages/lookup_path.rb12
-rw-r--r--app/models/pages_deployment.rb17
-rw-r--r--app/models/pages_domain.rb22
-rw-r--r--app/models/personal_access_token.rb7
-rw-r--r--app/models/project.rb96
-rw-r--r--app/models/project_feature_usage.rb13
-rw-r--r--app/models/project_pages_metadatum.rb3
-rw-r--r--app/models/project_snippet.rb2
-rw-r--r--app/models/projects/repository_storage_move.rb5
-rw-r--r--app/models/protected_branch.rb1
-rw-r--r--app/models/repository.rb8
-rw-r--r--app/models/resource_label_event.rb2
-rw-r--r--app/models/service_desk/custom_email_credential.rb11
-rw-r--r--app/models/snippet.rb1
-rw-r--r--app/models/snippet_repository.rb5
-rw-r--r--app/models/system/broadcast_message.rb2
-rw-r--r--app/models/system_note_metadata.rb1
-rw-r--r--app/models/upload.rb2
-rw-r--r--app/models/user.rb24
-rw-r--r--app/models/user_custom_attribute.rb1
-rw-r--r--app/models/user_detail.rb23
-rw-r--r--app/models/user_preference.rb14
-rw-r--r--app/models/users/anonymous.rb11
-rw-r--r--app/models/users/callout.rb7
-rw-r--r--app/models/users/credit_card_validation.rb6
-rw-r--r--app/models/users/group_visit.rb7
-rw-r--r--app/models/users/phone_number_validation.rb4
-rw-r--r--app/models/users/project_visit.rb7
-rw-r--r--app/models/vs_code/settings/vs_code_setting.rb4
-rw-r--r--app/models/wiki_page.rb7
-rw-r--r--app/models/work_item.rb17
-rw-r--r--app/policies/abuse_report_policy.rb1
-rw-r--r--app/policies/analytics/cycle_analytics/value_stream_policy.rb9
-rw-r--r--app/policies/base_policy.rb2
-rw-r--r--app/policies/ci/build_policy.rb2
-rw-r--r--app/policies/ci/deployable_policy.rb5
-rw-r--r--app/policies/ci/pipeline_policy.rb6
-rw-r--r--app/policies/concerns/policy_actor.rb4
-rw-r--r--app/policies/container_registry/protection/rule_policy.rb9
-rw-r--r--app/policies/global_policy.rb4
-rw-r--r--app/policies/group_group_link_policy.rb8
-rw-r--r--app/policies/group_policy.rb1
-rw-r--r--app/policies/issue_policy.rb5
-rw-r--r--app/policies/namespaces/group_project_namespace_shared_policy.rb1
-rw-r--r--app/policies/project_group_link_policy.rb4
-rw-r--r--app/policies/project_import_state_policy.rb5
-rw-r--r--app/policies/project_policy.rb88
-rw-r--r--app/policies/user_policy.rb1
-rw-r--r--app/presenters/clusters/cluster_presenter.rb2
-rw-r--r--app/presenters/commit_status_presenter.rb1
-rw-r--r--app/presenters/member_presenter.rb9
-rw-r--r--app/presenters/ml/model_presenter.rb22
-rw-r--r--app/presenters/ml/model_version_presenter.rb25
-rw-r--r--app/presenters/project_presenter.rb23
-rw-r--r--app/presenters/projects/security/configuration_presenter.rb4
-rw-r--r--app/presenters/user_presenter.rb1
-rw-r--r--app/serializers/build_details_entity.rb2
-rw-r--r--app/serializers/ci/job_entity.rb2
-rw-r--r--app/serializers/ci/pipeline_entity.rb2
-rw-r--r--app/serializers/deployment_entity.rb2
-rw-r--r--app/serializers/group_link/group_group_link_entity.rb2
-rw-r--r--app/serializers/group_link/group_link_entity.rb22
-rw-r--r--app/serializers/group_link/project_group_link_entity.rb2
-rw-r--r--app/serializers/issue_entity.rb6
-rw-r--r--app/serializers/member_entity.rb5
-rw-r--r--app/serializers/merge_request_noteable_entity.rb8
-rw-r--r--app/serializers/merge_request_widget_entity.rb8
-rw-r--r--app/serializers/review_app_setup_entity.rb2
-rw-r--r--app/services/activity_pub/accept_follow_service.rb55
-rw-r--r--app/services/activity_pub/inbox_resolver_service.rb50
-rw-r--r--app/services/activity_pub/third_party_error.rb5
-rw-r--r--app/services/admin/plan_limits/update_service.rb52
-rw-r--r--app/services/auto_merge/base_service.rb25
-rw-r--r--app/services/boards/lists/move_service.rb7
-rw-r--r--app/services/bulk_imports/batched_relation_export_service.rb8
-rw-r--r--app/services/bulk_imports/file_download_service.rb4
-rw-r--r--app/services/bulk_imports/process_service.rb11
-rw-r--r--app/services/bulk_imports/relation_batch_export_service.rb14
-rw-r--r--app/services/bulk_imports/relation_export_service.rb8
-rw-r--r--app/services/ci/build_cancel_service.rb2
-rw-r--r--app/services/ci/cancel_pipeline_service.rb41
-rw-r--r--app/services/ci/catalog/resources/create_service.rb31
-rw-r--r--app/services/ci/catalog/resources/release_service.rb46
-rw-r--r--app/services/ci/catalog/resources/validate_service.rb26
-rw-r--r--app/services/ci/catalog/resources/versions/create_service.rb111
-rw-r--r--app/services/ci/destroy_pipeline_service.rb2
-rw-r--r--app/services/ci/enqueue_job_service.rb11
-rw-r--r--app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb2
-rw-r--r--app/services/ci/pipelines/update_metadata_service.rb31
-rw-r--r--app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb33
-rw-r--r--app/services/ci/retry_job_service.rb4
-rw-r--r--app/services/concerns/update_repository_storage_methods.rb4
-rw-r--r--app/services/container_registry/protection/create_rule_service.rb40
-rw-r--r--app/services/draft_notes/publish_service.rb4
-rw-r--r--app/services/environments/auto_recover_service.rb44
-rw-r--r--app/services/git/branch_hooks_service.rb1
-rw-r--r--app/services/google_cloud/generate_pipeline_service.rb2
-rw-r--r--app/services/groups/ssh_certificates/create_service.rb7
-rw-r--r--app/services/groups/ssh_certificates/destroy_service.rb9
-rw-r--r--app/services/import/validate_remote_git_endpoint_service.rb79
-rw-r--r--app/services/jira_connect_subscriptions/create_service.rb4
-rw-r--r--app/services/members/create_service.rb55
-rw-r--r--app/services/merge_requests/mark_reviewer_reviewed_service.rb21
-rw-r--r--app/services/merge_requests/mergeability/check_base_service.rb5
-rw-r--r--app/services/merge_requests/mergeability/check_ci_status_service.rb2
-rw-r--r--app/services/merge_requests/mergeability/check_discussions_status_service.rb2
-rw-r--r--app/services/merge_requests/mergeability/check_rebase_status_service.rb2
-rw-r--r--app/services/merge_requests/mergeability/detailed_merge_status_service.rb6
-rw-r--r--app/services/merge_requests/mergeability/run_checks_service.rb2
-rw-r--r--app/services/merge_requests/push_options_handler_service.rb11
-rw-r--r--app/services/merge_requests/update_reviewer_state_service.rb34
-rw-r--r--app/services/merge_requests/update_service.rb5
-rw-r--r--app/services/ml/create_candidate_service.rb37
-rw-r--r--app/services/ml/create_model_service.rb51
-rw-r--r--app/services/ml/experiment_tracking/candidate_repository.rb18
-rw-r--r--app/services/ml/find_model_service.rb14
-rw-r--r--app/services/ml/find_or_create_model_service.rb18
-rw-r--r--app/services/ml/find_or_create_model_version_service.rb15
-rw-r--r--app/services/ml/model_versions/get_model_version_service.rb21
-rw-r--r--app/services/ml/update_model_service.rb16
-rw-r--r--app/services/notes/create_service.rb6
-rw-r--r--app/services/notification_service.rb4
-rw-r--r--app/services/organizations/base_service.rb14
-rw-r--r--app/services/organizations/create_service.rb27
-rw-r--r--app/services/packages/ml_model/create_package_file_service.rb3
-rw-r--r--app/services/packages/npm/create_package_service.rb8
-rw-r--r--app/services/packages/nuget/check_duplicates_service.rb30
-rw-r--r--app/services/packages/nuget/extract_metadata_file_service.rb12
-rw-r--r--app/services/packages/nuget/metadata_extraction_service.rb15
-rw-r--r--app/services/packages/nuget/process_package_file_service.rb25
-rw-r--r--app/services/packages/nuget/symbols/create_symbol_files_service.rb20
-rw-r--r--app/services/packages/nuget/symbols/extract_signature_and_checksum_service.rb (renamed from app/services/packages/nuget/symbols/extract_symbol_signature_service.rb)56
-rw-r--r--app/services/packages/nuget/update_package_from_metadata_service.rb19
-rw-r--r--app/services/packages/protection/delete_rule_service.rb43
-rw-r--r--app/services/packages/pypi/create_package_service.rb8
-rw-r--r--app/services/packages/update_tags_service.rb3
-rw-r--r--app/services/pages/delete_service.rb2
-rw-r--r--app/services/personal_access_tokens/rotate_service.rb9
-rw-r--r--app/services/projects/container_repository/gitlab/delete_tags_service.rb2
-rw-r--r--app/services/projects/container_repository/third_party/delete_tags_service.rb2
-rw-r--r--app/services/projects/destroy_service.rb19
-rw-r--r--app/services/projects/fork_service.rb13
-rw-r--r--app/services/projects/group_links/destroy_service.rb33
-rw-r--r--app/services/projects/group_links/update_service.rb8
-rw-r--r--app/services/projects/update_pages_service.rb13
-rw-r--r--app/services/projects/update_repository_storage_service.rb4
-rw-r--r--app/services/projects/update_service.rb15
-rw-r--r--app/services/releases/base_service.rb4
-rw-r--r--app/services/releases/create_service.rb14
-rw-r--r--app/services/releases/destroy_service.rb2
-rw-r--r--app/services/releases/update_service.rb2
-rw-r--r--app/services/resource_access_tokens/create_service.rb10
-rw-r--r--app/services/resource_events/base_synthetic_notes_builder_service.rb5
-rw-r--r--app/services/resource_events/merge_into_notes_service.rb2
-rw-r--r--app/services/security/ci_configuration/sast_parser_service.rb8
-rw-r--r--app/services/service_desk/custom_email_verifications/update_service.rb6
-rw-r--r--app/services/service_desk/custom_emails/create_service.rb4
-rw-r--r--app/services/service_desk_settings/update_service.rb11
-rw-r--r--app/services/spam/spam_action_service.rb19
-rw-r--r--app/services/system_notes/issuables_service.rb2
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb2
-rw-r--r--app/services/users/upsert_credit_card_validation_service.rb63
-rw-r--r--app/services/verify_pages_domain_service.rb2
-rw-r--r--app/services/vs_code/settings/delete_service.rb21
-rw-r--r--app/services/web_hook_service.rb12
-rw-r--r--app/validators/ip_cidr_array_validator.rb25
-rw-r--r--app/validators/ip_cidr_validator.rb45
-rw-r--r--app/validators/json_schemas/activity_pub_follow_payload.json53
-rw-r--r--app/validators/json_schemas/vulnerability_cvss_vectors.json4
-rw-r--r--app/views/admin/abuse_reports/show.html.haml1
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml3
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml6
-rw-r--r--app/views/admin/application_settings/_diagramsnet.html.haml2
-rw-r--r--app/views/admin/application_settings/_email.html.haml2
-rw-r--r--app/views/admin/application_settings/_error_tracking.html.haml4
-rw-r--r--app/views/admin/application_settings/_floc.html.haml2
-rw-r--r--app/views/admin/application_settings/_gitlab_shell_operation_limits.html.haml4
-rw-r--r--app/views/admin/application_settings/_gitpod.html.haml2
-rw-r--r--app/views/admin/application_settings/_import_export_limits.html.haml3
-rw-r--r--app/views/admin/application_settings/_kroki.html.haml2
-rw-r--r--app/views/admin/application_settings/_localization.html.haml4
-rw-r--r--app/views/admin/application_settings/_outbound.html.haml2
-rw-r--r--app/views/admin/application_settings/_performance.html.haml6
-rw-r--r--app/views/admin/application_settings/_plantuml.html.haml2
-rw-r--r--app/views/admin/application_settings/_projects_api_limits.html.haml4
-rw-r--r--app/views/admin/application_settings/_repository_check.html.haml2
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml6
-rw-r--r--app/views/admin/application_settings/_runner_registrars_form.html.haml2
-rw-r--r--app/views/admin/application_settings/_signin.html.haml4
-rw-r--r--app/views/admin/application_settings/_sourcegraph.html.haml2
-rw-r--r--app/views/admin/application_settings/_spam.html.haml4
-rw-r--r--app/views/admin/application_settings/_terms.html.haml2
-rw-r--r--app/views/admin/application_settings/_usage.html.haml24
-rw-r--r--app/views/admin/application_settings/general.html.haml6
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml8
-rw-r--r--app/views/admin/application_settings/network.html.haml26
-rw-r--r--app/views/admin/application_settings/preferences.html.haml10
-rw-r--r--app/views/admin/application_settings/reporting.html.haml2
-rw-r--r--app/views/admin/application_settings/repository.html.haml12
-rw-r--r--app/views/admin/application_settings/service_usage_data.html.haml29
-rw-r--r--app/views/admin/background_migrations/index.html.haml2
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/dev_ops_report/_score.html.haml2
-rw-r--r--app/views/admin/spam_logs/_spam_log.html.haml16
-rw-r--r--app/views/admin/topics/_form.html.haml4
-rw-r--r--app/views/admin/topics/_topic.html.haml2
-rw-r--r--app/views/admin/topics/index.html.haml2
-rw-r--r--app/views/admin/users/_users.html.haml3
-rw-r--r--app/views/admin/users/projects.html.haml4
-rw-r--r--app/views/ci/status/_badge.html.haml13
-rw-r--r--app/views/ci/status/_icon.html.haml11
-rw-r--r--app/views/clusters/clusters/_advanced_settings.html.haml2
-rw-r--r--app/views/clusters/clusters/_deprecation_alert.html.haml2
-rw-r--r--app/views/clusters/clusters/_multiple_clusters_message.html.haml2
-rw-r--r--app/views/clusters/clusters/_namespace.html.haml2
-rw-r--r--app/views/clusters/clusters/_provider_details_form.html.haml32
-rw-r--r--app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml6
-rw-r--r--app/views/clusters/clusters/connect.html.haml2
-rw-r--r--app/views/clusters/clusters/new_cluster_docs.html.haml2
-rw-r--r--app/views/clusters/clusters/show.html.haml8
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml4
-rw-r--r--app/views/dashboard/_projects_head.html.haml5
-rw-r--r--app/views/dashboard/todos/_todo.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml16
-rw-r--r--app/views/devise/shared/_sign_in_link.html.haml2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml75
-rw-r--r--app/views/devise/shared/_signup_box_form.html.haml73
-rw-r--r--app/views/devise/shared/_signup_omniauth_provider_list.haml5
-rw-r--r--app/views/discussions/_notes.html.haml2
-rw-r--r--app/views/events/_event.html.haml4
-rw-r--r--app/views/events/_event_scope.html.haml2
-rw-r--r--app/views/events/event/_common.html.haml4
-rw-r--r--app/views/events/event/_created_project.html.haml2
-rw-r--r--app/views/events/event/_design.html.haml2
-rw-r--r--app/views/events/event/_note.html.haml2
-rw-r--r--app/views/events/event/_private.html.haml6
-rw-r--r--app/views/events/event/_push.html.haml2
-rw-r--r--app/views/events/event/_wiki.html.haml2
-rw-r--r--app/views/explore/catalog/show.html.haml3
-rw-r--r--app/views/external_redirect/external_redirect/index.html.haml12
-rw-r--r--app/views/groups/_home_panel.html.haml2
-rw-r--r--app/views/groups/_import_group_from_another_instance_panel.html.haml6
-rw-r--r--app/views/groups/_import_group_from_file_panel.html.haml2
-rw-r--r--app/views/groups/_invite_members_modal.html.haml6
-rw-r--r--app/views/groups/projects.html.haml15
-rw-r--r--app/views/groups/settings/_export.html.haml4
-rw-r--r--app/views/groups/settings/_git_access_protocols.html.haml2
-rw-r--r--app/views/groups/settings/_permissions.html.haml3
-rw-r--r--app/views/groups/settings/_resource_access_token_creation.html.haml2
-rw-r--r--app/views/groups/settings/ci_cd/_auto_devops_form.html.haml2
-rw-r--r--app/views/import/bitbucket/status.html.haml4
-rw-r--r--app/views/import/bitbucket_server/new.html.haml5
-rw-r--r--app/views/import/bitbucket_server/status.html.haml4
-rw-r--r--app/views/import/bulk_imports/details.html.haml5
-rw-r--r--app/views/import/bulk_imports/history.html.haml2
-rw-r--r--app/views/import/fogbugz/new.html.haml4
-rw-r--r--app/views/import/fogbugz/status.html.haml4
-rw-r--r--app/views/import/gitea/new.html.haml10
-rw-r--r--app/views/import/gitea/status.html.haml5
-rw-r--r--app/views/import/github/new.html.haml13
-rw-r--r--app/views/import/github/status.html.haml4
-rw-r--r--app/views/import/gitlab_projects/new.html.haml6
-rw-r--r--app/views/import/shared/_new_project_form.html.haml4
-rw-r--r--app/views/invites/decline.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml4
-rw-r--r--app/views/layouts/_page.html.haml4
-rw-r--r--app/views/layouts/application.html.haml4
-rw-r--r--app/views/layouts/devise.html.haml6
-rw-r--r--app/views/layouts/devise_empty.html.haml4
-rw-r--r--app/views/layouts/fullscreen.html.haml4
-rw-r--r--app/views/layouts/minimal.html.haml7
-rw-r--r--app/views/layouts/nav/_ask_duo_button.html.haml13
-rw-r--r--app/views/layouts/nav/_top_bar.html.haml2
-rw-r--r--app/views/layouts/signup_onboarding.html.haml4
-rw-r--r--app/views/layouts/terms.html.haml5
-rw-r--r--app/views/notify/github_gists_import_errors_email.html.haml2
-rw-r--r--app/views/notify/pages_domain_auto_ssl_failed_email.html.haml2
-rw-r--r--app/views/notify/pages_domain_auto_ssl_failed_email.text.haml2
-rw-r--r--app/views/notify/pages_domain_disabled_email.html.haml2
-rw-r--r--app/views/notify/pages_domain_disabled_email.text.haml2
-rw-r--r--app/views/notify/pages_domain_enabled_email.html.haml2
-rw-r--r--app/views/notify/pages_domain_enabled_email.text.haml2
-rw-r--r--app/views/notify/pages_domain_verification_failed_email.html.haml2
-rw-r--r--app/views/notify/pages_domain_verification_failed_email.text.haml2
-rw-r--r--app/views/notify/pages_domain_verification_succeeded_email.html.haml2
-rw-r--r--app/views/notify/pages_domain_verification_succeeded_email.text.haml2
-rw-r--r--app/views/organizations/organizations/users.html.haml4
-rw-r--r--app/views/organizations/settings/general.html.haml3
-rw-r--r--app/views/profiles/gpg_keys/index.html.haml2
-rw-r--r--app/views/profiles/keys/_key.html.haml2
-rw-r--r--app/views/profiles/keys/index.html.haml2
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml2
-rw-r--r--app/views/profiles/preferences/show.html.haml9
-rw-r--r--app/views/profiles/show.html.haml6
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml4
-rw-r--r--app/views/projects/_errors.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml8
-rw-r--r--app/views/projects/_import_project_pane.html.haml2
-rw-r--r--app/views/projects/_invite_members_empty_project.html.haml2
-rw-r--r--app/views/projects/_invite_members_modal.html.haml6
-rw-r--r--app/views/projects/_service_desk_settings.html.haml3
-rw-r--r--app/views/projects/artifacts/_tree_file.html.haml2
-rw-r--r--app/views/projects/blob/_editor.html.haml4
-rw-r--r--app/views/projects/blob/_pipeline_tour_success.html.haml2
-rw-r--r--app/views/projects/blob/show.html.haml3
-rw-r--r--app/views/projects/blob/viewers/_route_map.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_route_map_loading.html.haml2
-rw-r--r--app/views/projects/branch_defaults/_branch_names_fields.html.haml2
-rw-r--r--app/views/projects/branch_defaults/_default_branch_fields.html.haml2
-rw-r--r--app/views/projects/branch_defaults/_show.html.haml2
-rw-r--r--app/views/projects/branch_rules/_show.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml6
-rw-r--r--app/views/projects/branches/_panel.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml10
-rw-r--r--app/views/projects/cleanup/_show.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml4
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml6
-rw-r--r--app/views/projects/commits/_commit.html.haml4
-rw-r--r--app/views/projects/diffs/viewers/_collapsed.html.haml3
-rw-r--r--app/views/projects/edit.html.haml215
-rw-r--r--app/views/projects/environments/index.html.haml2
-rw-r--r--app/views/projects/feature_flags/new.html.haml2
-rw-r--r--app/views/projects/feature_flags_user_lists/edit.html.haml2
-rw-r--r--app/views/projects/feature_flags_user_lists/new.html.haml2
-rw-r--r--app/views/projects/find_file/show.html.haml2
-rw-r--r--app/views/projects/forks/index.html.haml2
-rw-r--r--app/views/projects/forks/new.html.haml1
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml2
-rw-r--r--app/views/projects/issues/_new_branch.html.haml2
-rw-r--r--app/views/projects/issues/_related_branches.html.haml4
-rw-r--r--app/views/projects/issues/service_desk/_issue.html.haml2
-rw-r--r--app/views/projects/issues/service_desk/_issue_estimate.html.haml2
-rw-r--r--app/views/projects/jobs/_header.html.haml2
-rw-r--r--app/views/projects/merge_requests/_nav_btns.html.haml9
-rw-r--r--app/views/projects/merge_requests/_page.html.haml2
-rw-r--r--app/views/projects/mirrors/_authentication_method.html.haml2
-rw-r--r--app/views/projects/mirrors/_branch_filter.html.haml2
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml8
-rw-r--r--app/views/projects/mirrors/_mirror_repos_form.html.haml2
-rw-r--r--app/views/projects/mirrors/_mirror_repos_list.html.haml10
-rw-r--r--app/views/projects/mirrors/_mirror_repos_push.html.haml2
-rw-r--r--app/views/projects/mirrors/_ssh_host_keys.html.haml4
-rw-r--r--app/views/projects/ml/model_versions/show.html.haml6
-rw-r--r--app/views/projects/ml/models/index.html.haml2
-rw-r--r--app/views/projects/pages/_access.html.haml2
-rw-r--r--app/views/projects/pages/_waiting.html.haml2
-rw-r--r--app/views/projects/pages/new.html.haml7
-rw-r--r--app/views/projects/pages_domains/_certificate.html.haml2
-rw-r--r--app/views/projects/pages_domains/_dns.html.haml2
-rw-r--r--app/views/projects/pages_domains/_helper_text.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_create_protected_tag.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_dropdown.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_index.html.haml2
-rw-r--r--app/views/projects/readme_templates/default.md.tt5
-rw-r--r--app/views/projects/runners/_group_runners.html.haml2
-rw-r--r--app/views/projects/runners/_runner.html.haml3
-rw-r--r--app/views/projects/settings/access_tokens/_form.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml8
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_merge_commit_template.html.haml2
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_merge_method_settings.html.haml6
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_merge_suggestions_settings.html.haml2
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_pipelines_and_threads_options.html.haml3
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_squash_commit_template.html.haml2
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_squash_options_settings.html.haml2
-rw-r--r--app/views/projects/settings/merge_requests/show.html.haml2
-rw-r--r--app/views/projects/settings/operations/_alert_management.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml3
-rw-r--r--app/views/projects/tree/show.html.haml3
-rw-r--r--app/views/projects/usage_quotas/index.html.haml2
-rw-r--r--app/views/protected_branches/shared/_create_protected_branch.html.haml15
-rw-r--r--app/views/protected_branches/shared/_index.html.haml2
-rw-r--r--app/views/protected_branches/shared/_protected_branch.html.haml12
-rw-r--r--app/views/pwa/manifest.json.erb2
-rw-r--r--app/views/search/show.html.haml5
-rw-r--r--app/views/shared/_auto_devops_callout.html.haml2
-rw-r--r--app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml2
-rw-r--r--app/views/shared/_ci_catalog_badge.html.haml1
-rw-r--r--app/views/shared/_commit_message_container.html.haml2
-rw-r--r--app/views/shared/_custom_attributes.html.haml2
-rw-r--r--app/views/shared/_md_preview.html.haml2
-rw-r--r--app/views/shared/_new_nav_announcement.html.haml33
-rw-r--r--app/views/shared/_new_nav_for_everyone_announcement.html.haml18
-rw-r--r--app/views/shared/_project_limit.html.haml2
-rw-r--r--app/views/shared/_registration_features_discovery_message.html.haml2
-rw-r--r--app/views/shared/_remote_mirror_update_button.html.haml2
-rw-r--r--app/views/shared/_service_ping_consent.html.haml6
-rw-r--r--app/views/shared/access_tokens/_form.html.haml2
-rw-r--r--app/views/shared/deploy_tokens/_form.html.haml18
-rw-r--r--app/views/shared/deploy_tokens/_index.html.haml2
-rw-r--r--app/views/shared/deploy_tokens/_new_deploy_token.html.haml8
-rw-r--r--app/views/shared/deploy_tokens/_table.html.haml2
-rw-r--r--app/views/shared/empty_states/_snippets.html.haml2
-rw-r--r--app/views/shared/integrations/gitlab_slack_application/_help.html.haml2
-rw-r--r--app/views/shared/integrations/gitlab_slack_application/_slack_integration_form.html.haml2
-rw-r--r--app/views/shared/integrations/mattermost_slash_commands/_help.html.haml2
-rw-r--r--app/views/shared/integrations/slack_slash_commands/_help.html.haml12
-rw-r--r--app/views/shared/issuable/_assignees.html.haml2
-rw-r--r--app/views/shared/issuable/_nav.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml5
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/issuable/form/_type_selector.html.haml2
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml5
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml4
-rw-r--r--app/views/shared/projects/_list.html.haml1
-rw-r--r--app/views/shared/projects/_project.html.haml5
-rw-r--r--app/views/shared/runners/_shared_runners_description.html.haml2
-rw-r--r--app/views/shared/web_hooks/_form.html.haml2
-rw-r--r--app/views/shared/wikis/_wiki_directory.html.haml4
-rw-r--r--app/views/shared/wikis/show.html.haml3
-rw-r--r--app/views/users/_cover_controls.html.haml2
-rw-r--r--app/views/users/_overview.html.haml4
-rw-r--r--app/views/users/_profile_basic_info.html.haml4
-rw-r--r--app/views/users/show.html.haml60
-rw-r--r--app/workers/abuse/spam_abuse_events_worker.rb60
-rw-r--r--app/workers/activity_pub/projects/releases_subscription_worker.rb39
-rw-r--r--app/workers/all_queues.yml96
-rw-r--r--app/workers/bulk_import_worker.rb17
-rw-r--r--app/workers/bulk_imports/entity_worker.rb25
-rw-r--r--app/workers/bulk_imports/export_request_worker.rb8
-rw-r--r--app/workers/bulk_imports/finish_batched_pipeline_worker.rb24
-rw-r--r--app/workers/bulk_imports/pipeline_batch_worker.rb84
-rw-r--r--app/workers/bulk_imports/pipeline_worker.rb67
-rw-r--r--app/workers/bulk_imports/relation_batch_export_worker.rb19
-rw-r--r--app/workers/bulk_imports/relation_export_worker.rb28
-rw-r--r--app/workers/bulk_imports/stuck_import_worker.rb17
-rw-r--r--app/workers/ci/cancel_pipeline_worker.rb2
-rw-r--r--app/workers/ci/initial_pipeline_process_worker.rb14
-rw-r--r--app/workers/ci/refs/unlock_previous_pipelines_worker.rb4
-rw-r--r--app/workers/concerns/gitlab/github_import/rescheduling_methods.rb3
-rw-r--r--app/workers/concerns/gitlab/github_import/stage_methods.rb27
-rw-r--r--app/workers/concerns/worker_attributes.rb4
-rw-r--r--app/workers/environments/auto_recover_worker.rb22
-rw-r--r--app/workers/environments/auto_stop_cron_worker.rb1
-rw-r--r--app/workers/gitlab/github_import/stage/import_attachments_worker.rb6
-rw-r--r--app/workers/gitlab/github_import/stage/import_base_data_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_collaborators_worker.rb3
-rw-r--r--app/workers/gitlab/github_import/stage/import_issue_events_worker.rb4
-rw-r--r--app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb4
-rw-r--r--app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb7
-rw-r--r--app/workers/gitlab/github_import/stage/import_notes_worker.rb4
-rw-r--r--app/workers/gitlab/github_import/stage/import_protected_branches_worker.rb4
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb6
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker.rb6
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb6
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb6
-rw-r--r--app/workers/gitlab/import/advance_stage.rb6
-rw-r--r--app/workers/gitlab/jira_import/stage/import_issues_worker.rb9
-rw-r--r--app/workers/hashed_storage/base_worker.rb24
-rw-r--r--app/workers/hashed_storage/migrator_worker.rb18
-rw-r--r--app/workers/hashed_storage/project_migrate_worker.rb18
-rw-r--r--app/workers/hashed_storage/project_rollback_worker.rb18
-rw-r--r--app/workers/hashed_storage/rollbacker_worker.rb18
-rw-r--r--app/workers/merge_request_cleanup_refs_worker.rb2
-rw-r--r--app/workers/merge_requests/set_reviewer_reviewed_worker.rb21
-rw-r--r--app/workers/packages/cleanup_package_registry_worker.rb5
-rw-r--r--app/workers/packages/npm/cleanup_stale_metadata_cache_worker.rb42
-rw-r--r--app/workers/packages/nuget/extraction_worker.rb2
-rw-r--r--app/workers/projects/import_export/after_import_merge_requests_worker.rb21
-rw-r--r--app/workers/remove_expired_group_links_worker.rb2
-rw-r--r--app/workers/repository_fork_worker.rb22
-rw-r--r--app/workers/schedule_merge_request_cleanup_refs_worker.rb1
-rw-r--r--app/workers/tasks_to_be_done/create_worker.rb18
1433 files changed, 14415 insertions, 12343 deletions
diff --git a/app/assets/javascripts/add_context_commits_modal/store/actions.js b/app/assets/javascripts/add_context_commits_modal/store/actions.js
index f085b0d0e5e..890db374160 100644
--- a/app/assets/javascripts/add_context_commits_modal/store/actions.js
+++ b/app/assets/javascripts/add_context_commits_modal/store/actions.js
@@ -1,5 +1,5 @@
import _ from 'lodash';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Api from '~/api';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue b/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
index 3c46de7c2be..f0540ffa71e 100644
--- a/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
@@ -7,6 +7,7 @@ import ReportDetails from './report_details.vue';
import ReportedContent from './reported_content.vue';
import ActivityEventsList from './activity_events_list.vue';
import ActivityHistoryItem from './activity_history_item.vue';
+import AbuseReportNotes from './abuse_report_notes.vue';
const alertDefaults = {
visible: false,
@@ -24,6 +25,7 @@ export default {
ReportedContent,
ActivityEventsList,
ActivityHistoryItem,
+ AbuseReportNotes,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -96,5 +98,10 @@ export default {
/>
</template>
</activity-events-list>
+
+ <abuse-report-notes
+ v-if="glFeatures.abuseReportNotes"
+ :abuse-report-id="abuseReport.report.globalId"
+ />
</section>
</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue b/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue
new file mode 100644
index 00000000000..80af7d7400a
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue
@@ -0,0 +1,92 @@
+<script>
+import { uniqueId } from 'lodash';
+import { __ } from '~/locale';
+import { createAlert } from '~/alert';
+import SkeletonLoadingContainer from '~/vue_shared/components/notes/skeleton_note.vue';
+import { SKELETON_NOTES_COUNT } from '~/admin/abuse_report/constants';
+import abuseReportNotesQuery from '../graphql/notes/abuse_report_notes.query.graphql';
+import AbuseReportDiscussion from './notes/abuse_report_discussion.vue';
+
+export default {
+ name: 'AbuseReportNotes',
+ SKELETON_NOTES_COUNT,
+ i18n: {
+ fetchError: __('An error occurred while fetching comments, please try again.'),
+ },
+ components: {
+ SkeletonLoadingContainer,
+ AbuseReportDiscussion,
+ },
+ props: {
+ abuseReportId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ addNoteKey: uniqueId(`abuse-report-add-note-${this.abuseReportId}`),
+ };
+ },
+ apollo: {
+ abuseReportNotes: {
+ query: abuseReportNotesQuery,
+ variables() {
+ return {
+ id: this.abuseReportId,
+ };
+ },
+ update(data) {
+ return data.abuseReport?.discussions || [];
+ },
+ skip() {
+ return !this.abuseReportId;
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.fetchError });
+ },
+ },
+ },
+ computed: {
+ initialLoading() {
+ return this.$apollo.queries.abuseReportNotes.loading;
+ },
+ notesArray() {
+ return this.abuseReportNotes?.nodes || [];
+ },
+ },
+ methods: {
+ getDiscussionKey(discussion) {
+ const discussionId = discussion.notes.nodes[0].id;
+ return discussionId.split('/')[discussionId.split('/').length - 1];
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="issuable-discussion gl-mb-5 gl-clearfix!">
+ <template v-if="initialLoading">
+ <ul class="notes main-notes-list timeline">
+ <skeleton-loading-container
+ v-for="index in $options.SKELETON_NOTES_COUNT"
+ :key="index"
+ class="note-skeleton"
+ />
+ </ul>
+ </template>
+
+ <template v-else>
+ <ul class="notes main-notes-list timeline">
+ <abuse-report-discussion
+ v-for="discussion in notesArray"
+ :key="getDiscussionKey(discussion)"
+ :discussion="discussion.notes.nodes"
+ :abuse-report-id="abuseReportId"
+ />
+ </ul>
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue b/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue
index 8c4c1da28b8..2206e600543 100644
--- a/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue
@@ -11,7 +11,7 @@ export default {
<!-- The styles `issuable-discussion`, `timeline`, `main-notes-list` and `notes` used below
are declared in app/assets/stylesheets/pages/notes.scss -->
<section class="gl-pt-6 issuable-discussion">
- <h2 class="gl-font-lg gl-mt-0 gl-mb-2">{{ $options.i18n.activity }}</h2>
+ <h2 class="gl-font-size-h1 gl-mt-0 gl-mb-4">{{ $options.i18n.activity }}</h2>
<ul class="timeline main-notes-list notes">
<slot name="history-items"></slot>
</ul>
diff --git a/app/assets/javascripts/admin/abuse_report/components/labels_select.vue b/app/assets/javascripts/admin/abuse_report/components/labels_select.vue
index 747c9a1a947..d2d143f0460 100644
--- a/app/assets/javascripts/admin/abuse_report/components/labels_select.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/labels_select.vue
@@ -11,7 +11,7 @@ import DropdownContentsCreateView from '~/sidebar/components/labels/labels_selec
import DropdownHeader from '~/sidebar/components/labels/labels_select_widget/dropdown_header.vue';
import DropdownFooter from '~/sidebar/components/labels/labels_select_widget/dropdown_footer.vue';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
-import abuseReportLabelsQuery from './graphql/abuse_report_labels.query.graphql';
+import abuseReportLabelsQuery from '../graphql/abuse_report_labels.query.graphql';
export default {
components: {
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue
new file mode 100644
index 00000000000..4d24471fa43
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue
@@ -0,0 +1,104 @@
+<script>
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import DiscussionNotesRepliesWrapper from '~/notes/components/discussion_notes_replies_wrapper.vue';
+import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
+import AbuseReportNote from './abuse_report_note.vue';
+
+export default {
+ name: 'AbuseReportDiscussion',
+ components: {
+ TimelineEntryItem,
+ DiscussionNotesRepliesWrapper,
+ ToggleRepliesWidget,
+ AbuseReportNote,
+ },
+ props: {
+ abuseReportId: {
+ type: String,
+ required: true,
+ },
+ discussion: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isExpanded: true,
+ };
+ },
+ computed: {
+ note() {
+ return this.discussion[0];
+ },
+ noteId() {
+ return getIdFromGraphQLId(this.note.id);
+ },
+ replies() {
+ if (this.discussion?.length > 1) {
+ return this.discussion.slice(1);
+ }
+ return null;
+ },
+ hasReplies() {
+ return Boolean(this.replies?.length);
+ },
+ discussionId() {
+ return this.discussion[0]?.discussion?.id || '';
+ },
+ },
+ methods: {
+ toggleDiscussion() {
+ this.isExpanded = !this.isExpanded;
+ },
+ },
+};
+</script>
+
+<template>
+ <abuse-report-note
+ v-if="!hasReplies"
+ :note="note"
+ :abuse-report-id="abuseReportId"
+ class="gl-mb-4"
+ />
+ <timeline-entry-item v-else :data-note-id="noteId" class="note note-discussion gl-px-0">
+ <div class="timeline-content">
+ <div class="discussion">
+ <div class="discussion-body">
+ <div class="discussion-wrapper">
+ <div class="discussion-notes">
+ <ul class="notes">
+ <abuse-report-note
+ :note="note"
+ :discussion-id="discussionId"
+ :abuse-report-id="abuseReportId"
+ class="gl-mb-4"
+ />
+ <discussion-notes-replies-wrapper>
+ <toggle-replies-widget
+ v-if="hasReplies"
+ :collapsed="!isExpanded"
+ :replies="replies"
+ @toggle="toggleDiscussion({ discussionId })"
+ />
+ <template v-if="isExpanded">
+ <template v-for="reply in replies">
+ <abuse-report-note
+ :key="reply.id"
+ :discussion-id="discussionId"
+ :note="reply"
+ :abuse-report-id="abuseReportId"
+ />
+ </template>
+ </template>
+ </discussion-notes-replies-wrapper>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </timeline-entry-item>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue
new file mode 100644
index 00000000000..6da3017e11e
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue
@@ -0,0 +1,81 @@
+<script>
+import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import NoteHeader from '~/notes/components/note_header.vue';
+import NoteBody from './abuse_report_note_body.vue';
+
+export default {
+ name: 'AbuseReportNote',
+ directives: {
+ SafeHtml,
+ },
+ components: {
+ GlAvatarLink,
+ GlAvatar,
+ TimelineEntryItem,
+ NoteHeader,
+ NoteBody,
+ },
+ props: {
+ abuseReportId: {
+ type: String,
+ required: true,
+ },
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ noteAnchorId() {
+ return `note_${getIdFromGraphQLId(this.note.id)}`;
+ },
+ author() {
+ return this.note.author;
+ },
+ authorId() {
+ return getIdFromGraphQLId(this.author.id);
+ },
+ },
+};
+</script>
+
+<template>
+ <timeline-entry-item :id="noteAnchorId" class="note note-wrapper note-comment">
+ <div :key="note.id" class="timeline-avatar gl-float-left">
+ <gl-avatar-link
+ :href="author.webUrl"
+ :data-user-id="authorId"
+ :data-username="author.username"
+ class="js-user-link"
+ >
+ <gl-avatar
+ :src="author.avatarUrl"
+ :entity-name="author.username"
+ :alt="author.name"
+ :size="32"
+ />
+ </gl-avatar-link>
+ </div>
+ <div class="timeline-content">
+ <div data-testid="note-wrapper">
+ <div class="note-header">
+ <note-header
+ :author="author"
+ :created-at="note.createdAt"
+ :note-id="note.id"
+ :note-url="note.url"
+ >
+ <span v-if="note.createdAt" class="d-none d-sm-inline">&middot;</span>
+ </note-header>
+ </div>
+
+ <div class="timeline-discussion-body">
+ <note-body ref="noteBody" :note="note" />
+ </div>
+ </div>
+ </div>
+ </timeline-entry-item>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_body.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_body.vue
new file mode 100644
index 00000000000..ab3d7f5fa6c
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_body.vue
@@ -0,0 +1,48 @@
+<script>
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+export default {
+ name: 'AbuseReportNoteBody',
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ watch: {
+ 'note.bodyHtml': {
+ immediate: true,
+ async handler(newVal, oldVal) {
+ if (newVal === oldVal) {
+ return;
+ }
+ await this.$nextTick();
+ this.renderGFM();
+ },
+ },
+ },
+ methods: {
+ renderGFM() {
+ renderGFM(this.$refs['note-body']);
+ gl?.lazyLoader?.searchLazyImages();
+ },
+ },
+ safeHtmlConfig: {
+ ADD_TAGS: ['use', 'gl-emoji', 'copy-code'],
+ },
+};
+</script>
+
+<template>
+ <div ref="note-body" class="note-body">
+ <div
+ v-safe-html:[$options.safeHtmlConfig]="note.bodyHtml"
+ class="note-text md"
+ data-testid="abuse-report-note-body"
+ ></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/report_details.vue b/app/assets/javascripts/admin/abuse_report/components/report_details.vue
index 10e1dca7f91..89017e6cbd4 100644
--- a/app/assets/javascripts/admin/abuse_report/components/report_details.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/report_details.vue
@@ -1,8 +1,8 @@
<script>
import { __ } from '~/locale';
import { createAlert } from '~/alert';
+import abuseReportQuery from '../graphql/abuse_report.query.graphql';
import LabelsSelect from './labels_select.vue';
-import abuseReportQuery from './graphql/abuse_report.query.graphql';
export default {
name: 'ReportDetails',
diff --git a/app/assets/javascripts/admin/abuse_report/components/reported_content.vue b/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
index 84d6f25ac05..99c8b3ece10 100644
--- a/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
@@ -67,7 +67,7 @@ export default {
<div
class="gl-pb-3 gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column gl-align-items-center"
>
- <h2 class="gl-font-lg gl-mt-2 gl-mb-2">
+ <h2 class="gl-font-size-h1 gl-mt-2 gl-mb-2">
{{ $options.i18n.reportTypes[reportType] }}
</h2>
@@ -128,7 +128,7 @@ export default {
</gl-link>
<time-ago-tooltip
:time="report.reportedAt"
- class="gl-ml-3 gl-text-secondary gl-xs-w-full"
+ class="gl-ml-3 gl-text-secondary gl-w-full gl-sm-w-auto"
/>
</div>
</div>
diff --git a/app/assets/javascripts/admin/abuse_report/constants.js b/app/assets/javascripts/admin/abuse_report/constants.js
index f028408bed7..c56ea678b1d 100644
--- a/app/assets/javascripts/admin/abuse_report/constants.js
+++ b/app/assets/javascripts/admin/abuse_report/constants.js
@@ -111,3 +111,5 @@ export const HISTORY_ITEMS_I18N = {
reportedByForCategory: s__('AbuseReport|Reported by %{name} for %{category}.'),
deletedReporter: s__('AbuseReport|No user found'),
};
+
+export const SKELETON_NOTES_COUNT = 5;
diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql b/app/assets/javascripts/admin/abuse_report/graphql/abuse_report.query.graphql
index f5b075cb9af..640eec718f8 100644
--- a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql
+++ b/app/assets/javascripts/admin/abuse_report/graphql/abuse_report.query.graphql
@@ -1,5 +1,6 @@
query abuseReportQuery($id: AbuseReportID!) {
abuseReport(id: $id) {
+ id
labels {
nodes {
id
diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql b/app/assets/javascripts/admin/abuse_report/graphql/abuse_report_labels.query.graphql
index 4e724b4db2c..4e724b4db2c 100644
--- a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql
+++ b/app/assets/javascripts/admin/abuse_report/graphql/abuse_report_labels.query.graphql
diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql b/app/assets/javascripts/admin/abuse_report/graphql/create_abuse_report_label.mutation.graphql
index 0781b8e634b..0781b8e634b 100644
--- a/app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql
+++ b/app/assets/javascripts/admin/abuse_report/graphql/create_abuse_report_label.mutation.graphql
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql
new file mode 100644
index 00000000000..84b57b4ed79
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql
@@ -0,0 +1,30 @@
+#import "~/graphql_shared/fragments/author.fragment.graphql"
+#import "./abuse_report_note_permissions.fragment.graphql"
+
+fragment AbuseReportNote on Note {
+ id
+ body
+ bodyHtml
+ createdAt
+ lastEditedAt
+ url
+ resolved
+ author {
+ ...Author
+ }
+ lastEditedBy {
+ ...Author
+ webPath
+ }
+ userPermissions {
+ ...AbuseReportNotePermissions
+ }
+ discussion {
+ id
+ notes {
+ nodes {
+ id
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql
new file mode 100644
index 00000000000..01436436b93
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql
@@ -0,0 +1,3 @@
+fragment AbuseReportNotePermissions on NotePermissions {
+ adminNote
+}
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_notes.query.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_notes.query.graphql
new file mode 100644
index 00000000000..3a13ac1f37a
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_notes.query.graphql
@@ -0,0 +1,18 @@
+#import "./abuse_report_note.fragment.graphql"
+
+query abuseReportNotes($id: AbuseReportID!) {
+ abuseReport(id: $id) {
+ id
+ discussions {
+ nodes {
+ id
+ replyId
+ notes {
+ nodes {
+ ...AbuseReportNote
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql
new file mode 100644
index 00000000000..53ac9468e08
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql
@@ -0,0 +1,18 @@
+#import "./abuse_report_note.fragment.graphql"
+
+mutation createAbuseReportNote($input: CreateNoteInput!) {
+ createNote(input: $input) {
+ note {
+ id
+ discussion {
+ id
+ notes {
+ nodes {
+ ...AbuseReportNote
+ }
+ }
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/delete_abuse_report_note.fragment.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/delete_abuse_report_note.fragment.graphql
new file mode 100644
index 00000000000..e8ff2933159
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/delete_abuse_report_note.fragment.graphql
@@ -0,0 +1,8 @@
+mutation deleteAbuseReportNote($input: DestroyNoteInput!) {
+ destroyNote(input: $input) {
+ errors
+ note {
+ id
+ }
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql
new file mode 100644
index 00000000000..e11165074c9
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql
@@ -0,0 +1,10 @@
+#import "./abuse_report_note.fragment.graphql"
+
+mutation updateAbuseReportNote($input: UpdateNoteInput!) {
+ updateNote(input: $input) {
+ note {
+ ...AbuseReportNote
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/admin/background_migrations/index.js b/app/assets/javascripts/admin/background_migrations/index.js
index 4ddd8f17c9a..890df17080d 100644
--- a/app/assets/javascripts/admin/background_migrations/index.js
+++ b/app/assets/javascripts/admin/background_migrations/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Translate from '~/vue_shared/translate';
import BackgroundMigrationsDatabaseListbox from './components/database_listbox.vue';
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
index 2c555aca3c0..753b1fb1819 100644
--- a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
+++ b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
@@ -107,7 +107,7 @@ export default {
targetSelected: '',
targetPath: this.broadcastMessage.targetPath,
targetAccessLevels: this.broadcastMessage.targetAccessLevels,
- targetAccessLevelOptions: this.targetAccessLevelOptions.map(([text, value]) => ({
+ targetAccessLevelCheckBoxGroupOptions: this.targetAccessLevelOptions.map(([text, value]) => ({
text,
value,
})),
@@ -324,7 +324,10 @@ export default {
:state="!isValidated || targetRolesValid"
data-testid="target-roles-checkboxes"
>
- <gl-form-checkbox-group v-model="targetAccessLevels" :options="targetAccessLevelOptions" />
+ <gl-form-checkbox-group
+ v-model="targetAccessLevels"
+ :options="targetAccessLevelCheckBoxGroupOptions"
+ />
</gl-form-group>
<gl-form-group
diff --git a/app/assets/javascripts/admin/users/components/actions/index.js b/app/assets/javascripts/admin/users/components/actions/index.js
index 4e63a85df89..633bc4d8b15 100644
--- a/app/assets/javascripts/admin/users/components/actions/index.js
+++ b/app/assets/javascripts/admin/users/components/actions/index.js
@@ -9,6 +9,8 @@ import Reject from './reject.vue';
import Unban from './unban.vue';
import Unblock from './unblock.vue';
import Unlock from './unlock.vue';
+import Trust from './trust_user.vue';
+import Untrust from './untrust_user.vue';
export default {
Activate,
@@ -22,4 +24,6 @@ export default {
Unblock,
Unlock,
Reject,
+ Trust,
+ Untrust,
};
diff --git a/app/assets/javascripts/admin/users/components/actions/trust_user.vue b/app/assets/javascripts/admin/users/components/actions/trust_user.vue
new file mode 100644
index 00000000000..41ff8d4120d
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/trust_user.vue
@@ -0,0 +1,62 @@
+<script>
+import { GlDisclosureDropdownItem } from '@gitlab/ui';
+import { sprintf, s__, __ } from '~/locale';
+import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub';
+import { I18N_USER_ACTIONS } from '../../constants';
+
+// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
+const messageHtml = `
+ <p>${s__('AdminUsers|When not being monitored for spam:')}</p>
+ <ul>
+ <li>${s__(
+ 'AdminUsers|The user can create issues, notes, snippets, and merge requests that appear to be spam without being blocked.',
+ )}</li>
+ </ul>
+ <p>${s__('AdminUsers|You can untrust this user in the future.')}</p>
+`;
+
+export default {
+ components: {
+ GlDisclosureDropdownItem,
+ },
+ props: {
+ username: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ onClick() {
+ eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, {
+ path: this.path,
+ method: 'put',
+ modalAttributes: {
+ title: sprintf(s__('AdminUsers|Stop monitoring %{username} for possible spam?'), {
+ username: this.username,
+ }),
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ actionPrimary: {
+ text: I18N_USER_ACTIONS.trust,
+ attributes: { variant: 'confirm' },
+ },
+ messageHtml,
+ },
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown-item @action="onClick">
+ <template #list-item>
+ <slot></slot>
+ </template>
+ </gl-disclosure-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/untrust_user.vue b/app/assets/javascripts/admin/users/components/actions/untrust_user.vue
new file mode 100644
index 00000000000..da59833af07
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/untrust_user.vue
@@ -0,0 +1,56 @@
+<script>
+import { GlDisclosureDropdownItem } from '@gitlab/ui';
+import { sprintf, s__, __ } from '~/locale';
+import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub';
+import { I18N_USER_ACTIONS } from '../../constants';
+
+// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
+const messageHtml = `<p>${s__(
+ 'AdminUsers|You can trust this user in the future if necessary.',
+)}</p>`;
+
+export default {
+ components: {
+ GlDisclosureDropdownItem,
+ },
+ props: {
+ username: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ onClick() {
+ eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, {
+ path: this.path,
+ method: 'put',
+ modalAttributes: {
+ title: sprintf(s__('AdminUsers|Re-enable spam monitoring for %{username}?'), {
+ username: this.username,
+ }),
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ actionPrimary: {
+ text: I18N_USER_ACTIONS.untrust,
+ attributes: { variant: 'confirm' },
+ },
+ messageHtml,
+ },
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown-item @action="onClick">
+ <template #list-item>
+ <slot></slot>
+ </template>
+ </gl-disclosure-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/app.vue b/app/assets/javascripts/admin/users/components/app.vue
index a3abd904a6b..b0caffb6ca6 100644
--- a/app/assets/javascripts/admin/users/components/app.vue
+++ b/app/assets/javascripts/admin/users/components/app.vue
@@ -1,9 +1,15 @@
<script>
-import UsersTable from './users_table.vue';
+import { createAlert } from '~/alert';
+import { s__ } from '~/locale';
+import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils';
+import UsersTable from '~/vue_shared/components/users_table/users_table.vue';
+import getUsersGroupCountsQuery from '../graphql/queries/get_users_group_counts.query.graphql';
+import UserActions from './user_actions.vue';
export default {
components: {
UsersTable,
+ UserActions,
},
props: {
users: {
@@ -16,11 +22,64 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ groupCounts: {},
+ };
+ },
+ apollo: {
+ groupCounts: {
+ query: getUsersGroupCountsQuery,
+ variables() {
+ return {
+ usernames: this.users.map((user) => user.username),
+ };
+ },
+ update(data) {
+ const nodes = data?.users?.nodes || [];
+ const parsedIds = convertNodeIdsFromGraphQLIds(nodes);
+
+ return parsedIds.reduce((acc, { id, groupCount }) => {
+ acc[id] = groupCount || 0;
+ return acc;
+ }, {});
+ },
+ error(error) {
+ createAlert({
+ message: this.$options.i18n.groupCountFetchError,
+ captureError: true,
+ error,
+ });
+ },
+ skip() {
+ return !this.users.length;
+ },
+ },
+ },
+ computed: {
+ groupCountsLoading() {
+ return this.$apollo.queries.groupCounts.loading;
+ },
+ },
+ i18n: {
+ groupCountFetchError: s__(
+ 'AdminUsers|Could not load user group counts. Please refresh the page to try again.',
+ ),
+ },
};
</script>
<template>
<div>
- <users-table :users="users" :paths="paths" />
+ <users-table
+ :users="users"
+ :admin-user-path="paths.adminUser"
+ :group-counts="groupCounts"
+ :group-counts-loading="groupCountsLoading"
+ >
+ <template #user-actions="{ user }">
+ <user-actions :user="user" :paths="paths" :show-button-labels="true" />
+ </template>
+ </users-table>
</div>
</template>
diff --git a/app/assets/javascripts/admin/users/constants.js b/app/assets/javascripts/admin/users/constants.js
index 9cd61d6b1db..73383623aa2 100644
--- a/app/assets/javascripts/admin/users/constants.js
+++ b/app/assets/javascripts/admin/users/constants.js
@@ -1,9 +1,5 @@
import { s__, __ } from '~/locale';
-export const USER_AVATAR_SIZE = 32;
-
-export const LENGTH_OF_USER_NOTE_TOOLTIP = 100;
-
export const I18N_USER_ACTIONS = {
edit: __('Edit'),
userAdministration: s__('AdminUsers|User administration'),
@@ -19,4 +15,6 @@ export const I18N_USER_ACTIONS = {
deleteWithContributions: s__('AdminUsers|Delete user and contributions'),
ban: s__('AdminUsers|Ban user'),
unban: s__('AdminUsers|Unban user'),
+ trust: s__('AdminUsers|Trust user'),
+ untrust: s__('AdminUsers|Untrust user'),
};
diff --git a/app/assets/javascripts/alert.js b/app/assets/javascripts/alert.js
index 4d724b17723..fd20d216385 100644
--- a/app/assets/javascripts/alert.js
+++ b/app/assets/javascripts/alert.js
@@ -1,7 +1,7 @@
-import * as Sentry from '@sentry/browser';
import Vue from 'vue';
import isEmpty from 'lodash/isEmpty';
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __ } from '~/locale';
export const VARIANT_SUCCESS = 'success';
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
index fb872243e5e..29156a624fd 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -13,8 +13,8 @@ import {
GlTabs,
GlTab,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { isEqual, isEmpty, omit } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { PROMO_URL, DOCS_URL_IN_EE_DIR } from 'jh_else_ce/lib/utils/url_utility';
import {
diff --git a/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js b/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
index 4fa88279fe0..d1c8d2c24e7 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { formatMedianValues } from '../utils';
-import { PAGINATION_SORT_FIELD_END_EVENT, PAGINATION_SORT_DIRECTION_DESC } from '../constants';
+import { PAGINATION_SORT_DIRECTION_DESC, PAGINATION_SORT_FIELD_DURATION } from '../constants';
import * as types from './mutation_types';
export default {
@@ -41,7 +41,7 @@ export default {
Vue.set(state, 'pagination', {
page,
hasNextPage,
- sort: sort || PAGINATION_SORT_FIELD_END_EVENT,
+ sort: sort || PAGINATION_SORT_FIELD_DURATION,
direction: direction || PAGINATION_SORT_DIRECTION_DESC,
});
},
diff --git a/app/assets/javascripts/analytics/cycle_analytics/store/state.js b/app/assets/javascripts/analytics/cycle_analytics/store/state.js
index 3d9b56b043d..f387bf65093 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/store/state.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/state.js
@@ -1,5 +1,5 @@
import {
- PAGINATION_SORT_FIELD_END_EVENT,
+ PAGINATION_SORT_FIELD_DURATION,
PAGINATION_SORT_DIRECTION_DESC,
} from '~/analytics/cycle_analytics/constants';
@@ -29,7 +29,7 @@ export default () => ({
pagination: {
page: null,
hasNextPage: false,
- sort: PAGINATION_SORT_FIELD_END_EVENT,
+ sort: PAGINATION_SORT_FIELD_DURATION,
direction: PAGINATION_SORT_DIRECTION_DESC,
},
predefinedDateRange: null,
diff --git a/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js b/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js
deleted file mode 100644
index 91cb48e181b..00000000000
--- a/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import Vue from 'vue';
-import ActivityChart from './components/activity_chart.vue';
-
-export default () => {
- const containers = document.querySelectorAll('.js-project-analytics-chart');
-
- if (!containers) {
- return false;
- }
-
- return containers.forEach((container) => {
- const { chartData } = container.dataset;
- const formattedData = JSON.parse(chartData);
-
- return new Vue({
- el: container,
- components: {
- ActivityChart,
- },
- provide: {
- formattedData,
- },
- render(createElement) {
- return createElement('activity-chart');
- },
- });
- });
-};
diff --git a/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue b/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue
deleted file mode 100644
index 2be9ebda87a..00000000000
--- a/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-<script>
-import { GlColumnChart } from '@gitlab/ui/dist/charts';
-import { s__ } from '~/locale';
-
-export default {
- i18n: {
- noDataMsg: s__(
- 'ProductAnalytics|There is no data for this type of chart currently. Please see the Setup tab if you have not configured the product analytics tool already.',
- ),
- },
- components: {
- GlColumnChart,
- },
- inject: {
- formattedData: {
- default: {},
- },
- },
- computed: {
- barSeriesData() {
- return [
- {
- name: 'full',
- data: this.formattedData.keys.map((val, idx) => [val, this.formattedData.values[idx]]),
- },
- ];
- },
- },
-};
-</script>
-
-<template>
- <div class="gl-xs-w-full">
- <gl-column-chart
- v-if="formattedData.keys"
- :bars="barSeriesData"
- :x-axis-title="__('Value')"
- :y-axis-title="__('Number of events')"
- :x-axis-type="'category'"
- />
- <p v-else data-testid="noActivityChartData">
- {{ $options.i18n.noDataMsg }}
- </p>
- </div>
-</template>
diff --git a/app/assets/javascripts/analytics/shared/components/metric_tile.vue b/app/assets/javascripts/analytics/shared/components/metric_tile.vue
index 54dbe329c7a..9e0262b5175 100644
--- a/app/assets/javascripts/analytics/shared/components/metric_tile.vue
+++ b/app/assets/javascripts/analytics/shared/components/metric_tile.vue
@@ -44,6 +44,7 @@ export default {
:animation-decimal-places="decimalPlaces"
:class="{ 'gl-hover-cursor-pointer': hasLinks }"
tabindex="0"
+ use-delimiters
@click="clickHandler(metric)"
/>
<metric-popover :metric="metric" :target="metric.identifier" />
diff --git a/app/assets/javascripts/analytics/usage_trends/components/usage_trends_count_chart.vue b/app/assets/javascripts/analytics/usage_trends/components/usage_trends_count_chart.vue
index 8d7761694d1..247c147609b 100644
--- a/app/assets/javascripts/analytics/usage_trends/components/usage_trends_count_chart.vue
+++ b/app/assets/javascripts/analytics/usage_trends/components/usage_trends_count_chart.vue
@@ -1,8 +1,8 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { GlLineChart } from '@gitlab/ui/dist/charts';
-import * as Sentry from '@sentry/browser';
import { some, every } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import {
differenceInMonths,
formatDateAsMonth,
diff --git a/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue b/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue
index 06b83c87985..47a34ec8b4d 100644
--- a/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue
+++ b/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue
@@ -1,9 +1,9 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
-import * as Sentry from '@sentry/browser';
import produce from 'immer';
import { sortBy } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { formatDateAsMonth } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
diff --git a/app/assets/javascripts/api/bulk_imports_api.js b/app/assets/javascripts/api/bulk_imports_api.js
index d636cfdff0b..248f5601705 100644
--- a/app/assets/javascripts/api/bulk_imports_api.js
+++ b/app/assets/javascripts/api/bulk_imports_api.js
@@ -2,6 +2,21 @@ import { buildApiUrl } from '~/api/api_utils';
import axios from '~/lib/utils/axios_utils';
const BULK_IMPORT_ENTITIES_PATH = '/api/:version/bulk_imports/entities';
+const BULK_IMPORT_ENTITIES_FAILURES_PATH =
+ '/api/:version/bulk_imports/:id/entities/:entity_id/failures';
export const getBulkImportsHistory = (params) =>
axios.get(buildApiUrl(BULK_IMPORT_ENTITIES_PATH), { params });
+
+export const getBulkImportFailures = (id, entityId, { page, perPage }) => {
+ const failuresPath = buildApiUrl(BULK_IMPORT_ENTITIES_FAILURES_PATH)
+ .replace(':id', encodeURIComponent(id))
+ .replace(':entity_id', encodeURIComponent(entityId));
+
+ return axios.get(failuresPath, {
+ params: {
+ page,
+ per_page: perPage,
+ },
+ });
+};
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index 2be59f00773..19da1253a17 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -277,11 +277,7 @@ export default {
>
{{ saveText }}
</gl-button>
- <gl-button
- :type="cancelButtonType"
- data-qa-selector="cancel_badge_button"
- @click="handleCancel"
- >
+ <gl-button :type="cancelButtonType" @click="handleCancel">
{{ __('Cancel') }}
</gl-button>
</div>
diff --git a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
index fac45f32464..b5cb1862b45 100644
--- a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
@@ -1,39 +1,69 @@
<script>
-import { GlDropdown, GlButton, GlIcon, GlForm, GlFormCheckbox } from '@gitlab/ui';
+import {
+ GlDisclosureDropdown,
+ GlButton,
+ GlIcon,
+ GlForm,
+ GlFormCheckbox,
+ GlFormRadioGroup,
+} from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapActions, mapState } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __ } from '~/locale';
import { createAlert } from '~/alert';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
+import { fetchPolicies } from '~/lib/graphql';
import { CLEAR_AUTOSAVE_ENTRY_EVENT } from '~/vue_shared/constants';
import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub';
import { trackSavedUsingEditor } from '~/vue_shared/components/markdown/tracking';
+import userCanApproveQuery from '../queries/can_approve.query.graphql';
export default {
+ apollo: {
+ userPermissions: {
+ fetchPolicy: fetchPolicies.NETWORK_ONLY,
+ query: userCanApproveQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath.replace(/^\//, ''),
+ iid: `${this.getNoteableData.iid}`,
+ };
+ },
+ update: (data) => data.project?.mergeRequest?.userPermissions,
+ skip() {
+ return !this.dropdownVisible;
+ },
+ },
+ },
components: {
- GlDropdown,
+ GlDisclosureDropdown,
GlButton,
GlIcon,
GlForm,
+ GlFormRadioGroup,
GlFormCheckbox,
MarkdownEditor,
ApprovalPassword: () => import('ee_component/batch_comments/components/approval_password.vue'),
SummarizeMyReview: () =>
import('ee_component/batch_comments/components/summarize_my_review.vue'),
},
+ mixins: [glFeatureFlagsMixin()],
inject: {
canSummarize: { default: false },
},
data() {
return {
isSubmitting: false,
+ dropdownVisible: false,
noteData: {
noteable_type: '',
noteable_id: '',
note: '',
approve: false,
approval_password: '',
+ reviewer_state: 'reviewed',
},
formFieldProps: {
id: 'review-note-body',
@@ -42,17 +72,51 @@ export default {
'aria-label': __('Comment'),
'data-testid': 'comment-textarea',
},
+ userPermissions: {},
};
},
computed: {
...mapGetters(['getNotesData', 'getNoteableData', 'noteableType', 'getCurrentUserLastNote']),
...mapState('batchComments', ['shouldAnimateReviewButton']),
+ ...mapState('diffs', ['projectPath']),
autocompleteDataSources() {
return gl.GfmAutoComplete?.dataSources;
},
autosaveKey() {
return `submit_review_dropdown/${this.getNoteableData.id}`;
},
+ radioGroupOptions() {
+ return [
+ {
+ html: [
+ __('Comment'),
+ `<p class="help-text">
+ ${__('Submit general feedback without explicit approval.')}
+ </p>`,
+ ].join('<br />'),
+ value: 'reviewed',
+ },
+ {
+ html: [
+ __('Approve'),
+ `<p class="help-text">
+ ${__('Submit feedback and approve these changes.')}
+ </p>`,
+ ].join('<br />'),
+ value: 'approved',
+ disabled: !this.userPermissions.canApprove,
+ },
+ {
+ html: [
+ __('Request changes'),
+ `<p class="help-text">
+ ${__('Submit feedback that should be addressed before merging.')}
+ </p>`,
+ ].join('<br />'),
+ value: 'requested_changes',
+ },
+ ];
+ },
},
watch: {
'noteData.approve': function noteDataApproveWatch() {
@@ -60,21 +124,21 @@ export default {
this.repositionDropdown();
});
},
+ dropdownVisible(val) {
+ if (!val) {
+ this.userPermissions = {};
+ }
+ },
+ userPermissions: {
+ handler() {
+ this.repositionDropdown();
+ },
+ deep: true,
+ },
},
mounted() {
this.noteData.noteable_type = this.noteableType;
this.noteData.noteable_id = this.getNoteableData.id;
-
- // We override the Bootstrap Vue click outside behaviour
- // to allow for clicking in the autocomplete dropdowns
- // without this override the submit dropdown will close
- // whenever a item in the autocomplete dropdown is clicked
- const originalClickOutHandler = this.$refs.submitDropdown.$refs.dropdown.clickOutHandler;
- this.$refs.submitDropdown.$refs.dropdown.clickOutHandler = (e) => {
- if (!e.target.closest('.atwho-container')) {
- originalClickOutHandler(e);
- }
- };
},
methods: {
...mapActions('batchComments', ['publishReview']),
@@ -113,86 +177,115 @@ export default {
updateNote(note) {
this.noteData.note = note;
},
+ onBeforeClose({ originalEvent: { target }, preventDefault }) {
+ if (
+ target &&
+ [document.querySelector('.atwho-container'), document.querySelector('.dz-hidden-input')]
+ .filter(Boolean)
+ .some((el) => el.contains(target))
+ ) {
+ preventDefault();
+ }
+ },
+ setDropdownVisible(val) {
+ this.dropdownVisible = val;
+ },
},
restrictedToolbarItems: ['full-screen'],
};
</script>
<template>
- <gl-dropdown
+ <gl-disclosure-dropdown
ref="submitDropdown"
- right
- dropup
+ placement="right"
class="submit-review-dropdown"
:class="{ 'submit-review-dropdown-animated': shouldAnimateReviewButton }"
data-testid="submit-review-dropdown"
- variant="info"
- category="primary"
+ fluid-width
+ @beforeClose="onBeforeClose"
+ @shown="setDropdownVisible(true)"
+ @hidden="setDropdownVisible(false)"
>
- <template #button-content>
- {{ __('Finish review') }}
- <gl-icon class="dropdown-chevron" name="chevron-up" />
+ <template #toggle>
+ <gl-button variant="info" category="primary">
+ {{ __('Finish review') }}
+ <gl-icon class="dropdown-chevron" name="chevron-up" />
+ </gl-button>
</template>
- <gl-form data-testid="submit-gl-form" @submit.prevent="submitReview">
- <div class="gl-display-flex gl-mb-4 gl-align-items-center">
- <label for="review-note-body" class="gl-mb-0">
- {{ __('Summary comment (optional)') }}
- </label>
- <summarize-my-review
- v-if="canSummarize"
- :id="getNoteableData.id"
- class="gl-ml-auto"
- @input="updateNote"
- />
- </div>
- <div class="common-note-form gfm-form">
- <markdown-editor
- ref="markdownEditor"
- v-model="noteData.note"
- class="js-no-autosize"
- :is-submitting="isSubmitting"
- :render-markdown-path="getNoteableData.preview_note_path"
- :markdown-docs-path="getNotesData.markdownDocsPath"
- :form-field-props="formFieldProps"
- enable-autocomplete
- :autocomplete-data-sources="autocompleteDataSources"
- :disabled="isSubmitting"
- :restricted-tool-bar-items="$options.restrictedToolbarItems"
- :force-autosize="false"
- :autosave-key="autosaveKey"
- supports-quick-actions
- @input="$emit('input', $event)"
- @keydown.meta.enter="submitReview"
- @keydown.ctrl.enter="submitReview"
- />
- </div>
- <template v-if="getNoteableData.current_user.can_approve">
- <gl-form-checkbox
- v-model="noteData.approve"
- data-testid="approve_merge_request"
+ <template #default>
+ <gl-form
+ class="submit-review-dropdown-form gl-p-4"
+ data-testid="submit-gl-form"
+ @submit.prevent="submitReview"
+ >
+ <div class="gl-display-flex gl-mb-4 gl-align-items-center">
+ <label for="review-note-body" class="gl-mb-0">
+ {{ __('Summary comment (optional)') }}
+ </label>
+ <summarize-my-review
+ v-if="canSummarize"
+ :id="getNoteableData.id"
+ class="gl-ml-auto"
+ @input="updateNote"
+ />
+ </div>
+ <div class="common-note-form gfm-form">
+ <markdown-editor
+ ref="markdownEditor"
+ v-model="noteData.note"
+ class="js-no-autosize"
+ :is-submitting="isSubmitting"
+ :render-markdown-path="getNoteableData.preview_note_path"
+ :markdown-docs-path="getNotesData.markdownDocsPath"
+ :form-field-props="formFieldProps"
+ enable-autocomplete
+ :autocomplete-data-sources="autocompleteDataSources"
+ :disabled="isSubmitting"
+ :restricted-tool-bar-items="$options.restrictedToolbarItems"
+ :force-autosize="false"
+ :autosave-key="autosaveKey"
+ supports-quick-actions
+ @input="$emit('input', $event)"
+ @keydown.meta.enter="submitReview"
+ @keydown.ctrl.enter="submitReview"
+ />
+ </div>
+ <gl-form-radio-group
+ v-if="glFeatures.mrRequestChanges"
+ v-model="noteData.reviewer_state"
+ :options="radioGroupOptions"
class="gl-mt-4"
- >
- {{ __('Approve merge request') }}
- </gl-form-checkbox>
+ data-testid="reviewer_states"
+ />
+ <template v-else-if="userPermissions.canApprove">
+ <gl-form-checkbox
+ v-model="noteData.approve"
+ data-testid="approve_merge_request"
+ class="gl-mt-4"
+ >
+ {{ __('Approve merge request') }}
+ </gl-form-checkbox>
+ </template>
<approval-password
- v-if="getNoteableData.require_password_to_approve"
- v-show="noteData.approve"
+ v-if="userPermissions.canApprove && getNoteableData.require_password_to_approve"
+ v-show="noteData.approve || noteData.reviewer_state === 'approved'"
v-model="noteData.approval_password"
class="gl-mt-3"
data-testid="approve_password"
/>
- </template>
- <div class="gl-display-flex gl-justify-content-start gl-mt-4">
- <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>
+ <div class="gl-display-flex gl-justify-content-start gl-mt-4">
+ <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>
+ </template>
+ </gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/batch_comments/queries/can_approve.query.graphql b/app/assets/javascripts/batch_comments/queries/can_approve.query.graphql
new file mode 100644
index 00000000000..f0c9ef7b3c8
--- /dev/null
+++ b/app/assets/javascripts/batch_comments/queries/can_approve.query.graphql
@@ -0,0 +1,11 @@
+query userCanApprove($projectPath: ID!, $iid: String!) {
+ project(fullPath: $projectPath) {
+ id
+ mergeRequest(iid: $iid) {
+ id
+ userPermissions {
+ canApprove
+ }
+ }
+ }
+}
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 070ce38c8aa..d97f11a0acd 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
@@ -72,22 +72,20 @@ export const fetchDrafts = ({ commit, getters, state, dispatch }) =>
}),
);
-export const publishSingleDraft = ({ commit, dispatch, getters }, draftId) => {
+export const publishSingleDraft = ({ commit, getters }, draftId) => {
commit(types.REQUEST_PUBLISH_DRAFT, draftId);
service
.publishDraft(getters.getNotesData.draftsPublishPath, draftId)
- .then(() => dispatch('updateDiscussionsAfterPublish'))
.then(() => commit(types.RECEIVE_PUBLISH_DRAFT_SUCCESS, draftId))
.catch(() => commit(types.RECEIVE_PUBLISH_DRAFT_ERROR, draftId));
};
-export const publishReview = ({ commit, dispatch, getters }, noteData = {}) => {
+export const publishReview = ({ commit, getters }, noteData = {}) => {
commit(types.REQUEST_PUBLISH_REVIEW);
return service
.publish(getters.getNotesData.draftsPublishPath, noteData)
- .then(() => dispatch('updateDiscussionsAfterPublish'))
.then(() => commit(types.RECEIVE_PUBLISH_REVIEW_SUCCESS))
.catch((e) => {
commit(types.RECEIVE_PUBLISH_REVIEW_ERROR);
@@ -96,18 +94,6 @@ export const publishReview = ({ commit, dispatch, getters }, noteData = {}) => {
});
};
-export const updateDiscussionsAfterPublish = async ({ dispatch, getters, rootGetters }) => {
- await dispatch(
- 'fetchDiscussions',
- { path: getters.getNotesData.discussionsPath },
- { root: true },
- );
-
- dispatch('diffs/assignDiscussionsToDiff', rootGetters.discussionsStructuredByLineCode, {
- root: true,
- });
-};
-
export const updateDraft = (
{ commit, getters },
{ note, noteText, resolveDiscussion, position, flashContainer, callback, errorCallback },
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index dc9153e61f7..84ff8fa7f33 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -3,7 +3,6 @@ import './autosize';
import initCollapseSidebarOnWindowResize from './collapse_sidebar_on_window_resize';
import initCopyToClipboard from './copy_to_clipboard';
import installGlEmojiElement from './gl_emoji';
-import { loadStartupCSS } from './load_startup_css';
import initCopyAsGFM from './markdown/copy_as_gfm';
import './quick_submit';
import './requires_input';
@@ -13,8 +12,6 @@ import { initGlobalAlerts } from './global_alerts';
import './toggler_behavior';
import './preview_markdown';
-loadStartupCSS();
-
installGlEmojiElement();
initCopyAsGFM();
diff --git a/app/assets/javascripts/behaviors/load_startup_css.js b/app/assets/javascripts/behaviors/load_startup_css.js
deleted file mode 100644
index dbe9ff8b6e7..00000000000
--- a/app/assets/javascripts/behaviors/load_startup_css.js
+++ /dev/null
@@ -1,15 +0,0 @@
-export const loadStartupCSS = () => {
- // We need to fallback to dispatching `load` in case our event listener was added too late
- // or the browser environment doesn't load media=print.
- // Do this on `window.load` so that the default deferred behavior takes precedence.
- // https://gitlab.com/gitlab-org/gitlab/-/issues/239357
- window.addEventListener(
- 'load',
- () => {
- document
- .querySelectorAll('link[media=print]')
- .forEach((x) => x.dispatchEvent(new Event('load')));
- },
- { once: true },
- );
-};
diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
index e8c486f6e74..941662635ea 100644
--- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js
+++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
@@ -381,6 +381,12 @@ export const PROJECT_FILES_GO_TO_PERMALINK = {
defaultKeys: ['y'],
};
+export const PROJECT_FILES_GO_TO_COMPARE = {
+ id: 'projectFiles.goToCompare',
+ description: __('Compare Branches'),
+ defaultKeys: ['shift+c'],
+};
+
export const ISSUABLE_COMMENT_OR_REPLY = {
id: 'issuables.commentReply',
description: __('Comment/Reply (quoting selected text)'),
@@ -606,6 +612,7 @@ const PROJECT_FILES_SHORTCUTS_GROUP = {
PROJECT_FILES_OPEN_SELECTION,
PROJECT_FILES_GO_BACK,
PROJECT_FILES_GO_TO_PERMALINK,
+ PROJECT_FILES_GO_TO_COMPARE,
],
};
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
index d9dc3aae808..4691a4228e6 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
@@ -18,6 +18,7 @@ import {
GO_TO_PROJECT_KUBERNETES,
GO_TO_PROJECT_ENVIRONMENTS,
GO_TO_PROJECT_WEBIDE,
+ PROJECT_FILES_GO_TO_COMPARE,
NEW_ISSUE,
} from './keybindings';
import Shortcuts from './shortcuts';
@@ -43,6 +44,7 @@ export default class ShortcutsNavigation extends Shortcuts {
[GO_TO_PROJECT_SNIPPETS, () => findAndFollowLink('.shortcuts-snippets')],
[GO_TO_PROJECT_KUBERNETES, () => findAndFollowLink('.shortcuts-kubernetes')],
[GO_TO_PROJECT_ENVIRONMENTS, () => findAndFollowLink('.shortcuts-environments')],
+ [PROJECT_FILES_GO_TO_COMPARE, () => findAndFollowLink('.shortcuts-compare')],
[GO_TO_PROJECT_WEBIDE, ShortcutsNavigation.navigateToWebIDE],
[NEW_ISSUE, () => findAndFollowLink('.shortcuts-new-issue')],
]);
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index 699a0491183..5411881a8d2 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -5,7 +5,7 @@ import userInfoQuery from '../queries/user_info.query.graphql';
import applicationInfoQuery from '../queries/application_info.query.graphql';
import BlobFilepath from './blob_header_filepath.vue';
import ViewerSwitcher from './blob_header_viewer_switcher.vue';
-import { SIMPLE_BLOB_VIEWER } from './constants';
+import { SIMPLE_BLOB_VIEWER, BLAME_VIEWER } from './constants';
import TableOfContents from './table_contents.vue';
export default {
@@ -85,6 +85,11 @@ export default {
required: false,
default: '',
},
+ showBlameToggle: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -93,9 +98,6 @@ export default {
};
},
computed: {
- showViewerSwitcher() {
- return !this.hideViewerSwitcher && Boolean(this.blob.simpleViewer && this.blob.richViewer);
- },
showDefaultActions() {
return !this.hideDefaultActions;
},
@@ -114,7 +116,7 @@ export default {
},
watch: {
viewer(newVal, oldVal) {
- if (!this.hideViewerSwitcher && newVal !== oldVal) {
+ if (newVal !== BLAME_VIEWER && newVal !== oldVal) {
this.$emit('viewer-changed', newVal);
}
},
@@ -138,7 +140,14 @@ export default {
</div>
<div class="gl-display-flex gl-flex-wrap file-actions">
- <viewer-switcher v-if="showViewerSwitcher" v-model="viewer" :doc-icon="blobSwitcherDocIcon" />
+ <viewer-switcher
+ v-if="!hideViewerSwitcher"
+ v-model="viewer"
+ :doc-icon="blobSwitcherDocIcon"
+ :show-blame-toggle="showBlameToggle"
+ :show-viewer-toggles="Boolean(blob.simpleViewer && blob.richViewer)"
+ v-on="$listeners"
+ />
<web-ide-link
v-if="showWebIdeLink"
diff --git a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
index 7351df0f93b..9b5b77ebebe 100644
--- a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
+++ b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
@@ -5,6 +5,8 @@ import {
RICH_BLOB_VIEWER_TITLE,
SIMPLE_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER_TITLE,
+ BLAME_VIEWER,
+ BLAME_TITLE,
} from './constants';
export default {
@@ -26,6 +28,16 @@ export default {
default: 'document',
required: false,
},
+ showViewerToggles: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showBlameToggle: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
isSimpleViewer() {
@@ -34,9 +46,16 @@ export default {
isRichViewer() {
return this.value === RICH_BLOB_VIEWER;
},
+ isBlameViewer() {
+ return this.value === BLAME_VIEWER;
+ },
},
methods: {
switchToViewer(viewer) {
+ if (viewer === BLAME_VIEWER) {
+ this.$emit('blame');
+ }
+
if (viewer !== this.value) {
this.$emit('input', viewer);
}
@@ -46,11 +65,14 @@ export default {
RICH_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER_TITLE,
RICH_BLOB_VIEWER_TITLE,
+ BLAME_TITLE,
+ BLAME_VIEWER,
};
</script>
<template>
<gl-button-group class="js-blob-viewer-switcher mx-2">
<gl-button
+ v-if="showViewerToggles"
v-gl-tooltip.hover
:aria-label="$options.SIMPLE_BLOB_VIEWER_TITLE"
:title="$options.SIMPLE_BLOB_VIEWER_TITLE"
@@ -63,6 +85,7 @@ export default {
@click="switchToViewer($options.SIMPLE_BLOB_VIEWER)"
/>
<gl-button
+ v-if="showViewerToggles"
v-gl-tooltip.hover
:aria-label="$options.RICH_BLOB_VIEWER_TITLE"
:title="$options.RICH_BLOB_VIEWER_TITLE"
@@ -74,5 +97,16 @@ export default {
data-viewer="rich"
@click="switchToViewer($options.RICH_BLOB_VIEWER)"
/>
+ <gl-button
+ v-if="showBlameToggle"
+ v-gl-tooltip.hover
+ :title="$options.BLAME_TITLE"
+ :selected="isBlameViewer"
+ category="primary"
+ variant="default"
+ data-test-id="blame-toggle"
+ @click="switchToViewer($options.BLAME_VIEWER)"
+ >{{ __('Blame') }}</gl-button
+ >
</gl-button-group>
</template>
diff --git a/app/assets/javascripts/blob/components/constants.js b/app/assets/javascripts/blob/components/constants.js
index adac4d6408d..bccab09c7a2 100644
--- a/app/assets/javascripts/blob/components/constants.js
+++ b/app/assets/javascripts/blob/components/constants.js
@@ -11,6 +11,9 @@ export const SIMPLE_BLOB_VIEWER_TITLE = __('Display source');
export const RICH_BLOB_VIEWER = 'rich';
export const RICH_BLOB_VIEWER_TITLE = __('Display rendered file');
+export const BLAME_VIEWER = 'blame';
+export const BLAME_TITLE = __('Display blame info');
+
export const BLOB_RENDER_EVENT_LOAD = 'force-content-fetch';
export const BLOB_RENDER_EVENT_SHOW_SOURCE = 'force-switch-viewer';
diff --git a/app/assets/javascripts/blob/filepath_form/components/template_selector.vue b/app/assets/javascripts/blob/filepath_form/components/template_selector.vue
index 51c69590796..379d5e38197 100644
--- a/app/assets/javascripts/blob/filepath_form/components/template_selector.vue
+++ b/app/assets/javascripts/blob/filepath_form/components/template_selector.vue
@@ -2,6 +2,7 @@
import { GlCollapsibleListbox } from '@gitlab/ui';
import SuggestGitlabCiYml from '~/blob/suggest_gitlab_ci_yml/components/popover.vue';
import { __ } from '~/locale';
+import { DEFAULT_CI_CONFIG_PATH, CI_CONFIG_PATH_EXTENSION } from '~/lib/utils/constants';
const templateSelectors = [
{
@@ -12,8 +13,8 @@ const templateSelectors = [
},
{
key: 'gitlab_ci_ymls',
- name: '.gitlab-ci.yml',
- pattern: /(.gitlab-ci.yml)/,
+ name: DEFAULT_CI_CONFIG_PATH,
+ pattern: CI_CONFIG_PATH_EXTENSION,
type: 'gitlab_ci_ymls',
},
{
diff --git a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
index f3c542c467a..0cc75d28e0b 100644
--- a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
+++ b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
@@ -94,14 +94,12 @@ export default {
<gl-modal visible size="sm" modal-id="success-pipeline-modal-id-not-used">
<template #modal-title>
{{ $options.i18n.modalTitle }}
- <gl-emoji class="gl-vertical-align-baseline font-size-inherit gl-mr-1" data-name="tada" />
+ <gl-emoji class="gl-vertical-align-baseline gl-reset-font-size gl-mr-1" data-name="tada" />
</template>
<p>
<gl-sprintf :message="$options.i18n.bodyMessage">
<template #codeQualityLink="{ content }">
- <gl-link :href="codeQualityLink" target="_blank" class="font-size-inherit">{{
- content
- }}</gl-link>
+ <gl-link :href="codeQualityLink" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index bf77aa4996c..fd36eea95eb 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -204,7 +204,8 @@ export function moveItemListHelper(item, fromList, toList) {
export function moveItemVariables({
iid,
- epicId,
+ itemId,
+ epicId = null,
fromListId,
toListId,
moveBeforeId,
@@ -225,6 +226,7 @@ export function moveItemVariables({
};
}
return {
+ itemId,
epicId,
boardId,
moveBeforeId,
diff --git a/app/assets/javascripts/boards/components/board_add_new_column_form.vue b/app/assets/javascripts/boards/components/board_add_new_column_form.vue
index 419d0b41d69..a3c0553d17c 100644
--- a/app/assets/javascripts/boards/components/board_add_new_column_form.vue
+++ b/app/assets/javascripts/boards/components/board_add_new_column_form.vue
@@ -43,7 +43,6 @@ export default {
<div
class="board-add-new-list board gl-display-inline-block gl-h-full gl-vertical-align-top gl-white-space-normal gl-flex-shrink-0 gl-rounded-base gl-px-3"
data-testid="board-add-new-column"
- data-qa-selector="board_add_new_list"
>
<div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base gl-bg-gray-50"
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index c10ff2e08da..a7f46dc9325 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -139,8 +139,11 @@ export default {
}
return false;
},
+ hasChildren() {
+ return this.totalIssuesCount + this.totalEpicsCount > 0;
+ },
shouldRenderEpicCountables() {
- return this.isEpicBoard && this.item.hasIssues;
+ return this.isEpicBoard && this.hasChildren;
},
shouldRenderEpicProgress() {
return this.totalWeight > 0;
@@ -396,7 +399,7 @@ export default {
<issue-due-date
v-if="item.dueDate"
:date="item.dueDate"
- :closed="item.closed || Boolean(item.closedAt)"
+ :closed="Boolean(item.closedAt)"
/>
<issue-time-estimate v-if="item.timeEstimate" :estimate="item.timeEstimate" />
<issue-card-weight
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 554f3bfa416..a6ff1653c17 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -249,7 +249,6 @@ export default {
<transition name="slide" @after-enter="afterFormEnters">
<board-add-new-column
v-if="addColumnFormVisible"
- class="gl-xs-w-full!"
:board-id="boardId"
:list-query-variables="listQueryVariables"
:lists="boardListsById"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 2693a6bb5ea..ca10cbbad5e 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -100,9 +100,6 @@ export default {
filters: this.filterParams,
};
},
- context: {
- isSingleRequest: true,
- },
skip() {
return this.isEpicBoard;
},
@@ -123,9 +120,6 @@ export default {
update(data) {
return data[this.boardType].board.lists.nodes[0];
},
- context: {
- isSingleRequest: true,
- },
error(error) {
setError({
error,
@@ -149,9 +143,6 @@ export default {
update(data) {
return data[this.boardType].board.lists.nodes[0];
},
- context: {
- isSingleRequest: true,
- },
error(error) {
setError({
error,
@@ -400,7 +391,7 @@ export default {
this.updateIssueOrderInProgress = true;
await this.moveBoardItem(
{
- epicId: itemId,
+ itemId,
iid: itemIid,
fromListId: from.dataset.listId,
toListId: to.dataset.listId,
@@ -428,11 +419,11 @@ export default {
return items.some((item) => item.iid === itemIid);
},
async moveBoardItem(variables, newIndex) {
- const { fromListId, toListId, iid } = variables;
+ const { fromListId, toListId, iid, itemId } = variables;
this.toListId = toListId;
await this.$nextTick(); // we need this next tick to retrieve `toList` from Apollo cache
- const itemToMove = this.boardListItems.find((item) => item.iid === iid);
+ const itemToMove = this.boardListItems.find((item) => item.id === itemId);
if (this.shouldCloneCard && this.isItemInTheList(iid)) {
return;
@@ -445,6 +436,7 @@ export default {
...moveItemVariables({
...variables,
isIssue: !this.isEpicBoard,
+ epicId: itemId, // for Epic Boards
boardId: this.boardId,
itemToMove,
}),
@@ -532,7 +524,8 @@ export default {
variables: {
...moveItemVariables({
iid: item.iid,
- epicId: item.id,
+ itemId: item.id,
+ epicId: item.id, // for Epic Boards
fromListId: this.currentList.id,
toListId: this.currentList.id,
isIssue: !this.isEpicBoard,
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 0235edd69ac..bedb3a75a70 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -223,9 +223,6 @@ export default {
variables() {
return this.countQueryVariables;
},
- context: {
- isSingleRequest: true,
- },
error(error) {
setError({
error,
diff --git a/app/assets/javascripts/boards/components/new_board_button.vue b/app/assets/javascripts/boards/components/new_board_button.vue
index f7914c636cc..96cf0fadd6a 100644
--- a/app/assets/javascripts/boards/components/new_board_button.vue
+++ b/app/assets/javascripts/boards/components/new_board_button.vue
@@ -38,7 +38,7 @@ export default {
<template #control> </template>
<template #candidate>
<div v-if="canShowCreateButton" class="gl-ml-1 gl-mr-3 gl-display-flex gl-align-items-center">
- <gl-button data-qa-selector="new_board_button" @click.prevent="showDialog">
+ <gl-button @click.prevent="showDialog">
{{ createButtonText }}
</gl-button>
</div>
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index cb607e5220e..acf01a8c528 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -132,6 +132,7 @@ export const listIssuablesQueries = {
optimisticResponse: {
assignees: { nodes: [], __typename: 'UserCoreConnection' },
confidential: false,
+ closedAt: null,
dueDate: null,
emailsDisabled: false,
hidden: false,
diff --git a/app/assets/javascripts/boards/graphql/cache_updates.js b/app/assets/javascripts/boards/graphql/cache_updates.js
index ea099e02181..bd58f445493 100644
--- a/app/assets/javascripts/boards/graphql/cache_updates.js
+++ b/app/assets/javascripts/boards/graphql/cache_updates.js
@@ -1,5 +1,6 @@
-import * as Sentry from '@sentry/browser';
import produce from 'immer';
+import { toNumber } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { defaultClient } from '~/graphql_shared/issuable_client';
import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
import { listsDeferredQuery } from 'ee_else_ce/boards/constants';
@@ -83,7 +84,9 @@ export function updateIssueCountAndWeight({
boardList: {
...boardList,
issuesCount: boardList.issuesCount + 1,
- ...(issue.weight ? { totalIssueWeight: boardList.totalIssueWeight + issue.weight } : {}),
+ ...(issue.weight
+ ? { totalIssueWeight: toNumber(boardList.totalIssueWeight) + issue.weight }
+ : {}),
},
}),
);
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 3e7d7a7a8d3..97e40c8cc39 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,5 +1,5 @@
-import * as Sentry from '@sentry/browser';
import { sortBy } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import {
ListType,
inactiveId,
@@ -148,9 +148,6 @@ export default {
query: listsQuery[issuableType].query,
variables,
...(resetLists ? { fetchPolicy: fetchPolicies.NO_CACHE } : {}),
- context: {
- isSingleRequest: true,
- },
})
.then(({ data }) => {
const { lists, hideBacklogList } = data[boardType].board;
@@ -439,9 +436,6 @@ export default {
return gqlClient
.query({
query: listsIssuesQuery,
- context: {
- isSingleRequest: true,
- },
variables,
...(!fetchNext ? { fetchPolicy: fetchPolicies.NO_CACHE } : {}),
})
diff --git a/app/assets/javascripts/boards/stores/index.js b/app/assets/javascripts/boards/stores/index.js
index ee0a5e27d9a..fd562df1df7 100644
--- a/app/assets/javascripts/boards/stores/index.js
+++ b/app/assets/javascripts/boards/stores/index.js
@@ -8,12 +8,13 @@ import state from 'ee_else_ce/boards/stores/state';
Vue.use(Vuex);
-export const createStore = () =>
- new Vuex.Store({
- state,
- getters,
- actions,
- mutations,
- });
+export const storeOptions = {
+ state,
+ getters,
+ actions,
+ mutations,
+};
+
+export const createStore = (options = storeOptions) => new Vuex.Store(options);
export default createStore();
diff --git a/app/assets/javascripts/branches/components/branch_more_actions.vue b/app/assets/javascripts/branches/components/branch_more_actions.vue
index c646dab2760..ee47f6af2f8 100644
--- a/app/assets/javascripts/branches/components/branch_more_actions.vue
+++ b/app/assets/javascripts/branches/components/branch_more_actions.vue
@@ -74,7 +74,6 @@ export default {
class: 'js-delete-branch-button gl-text-red-500!',
'aria-label': this.deleteBranchText,
'data-testid': 'delete-branch-button',
- 'data-qa-selector': 'delete_branch_button',
},
});
}
diff --git a/app/assets/javascripts/branches/components/delete_branch_modal.vue b/app/assets/javascripts/branches/components/delete_branch_modal.vue
index d5631337cec..0200a30cbdf 100644
--- a/app/assets/javascripts/branches/components/delete_branch_modal.vue
+++ b/app/assets/javascripts/branches/components/delete_branch_modal.vue
@@ -182,7 +182,6 @@ export default {
ref="deleteBranchButton"
:disabled="deleteButtonDisabled"
variant="danger"
- data-qa-selector="delete_branch_confirmation_button"
data-testid="delete-branch-confirmation-button"
@click="submitForm"
>{{ buttonText }}</gl-button
diff --git a/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue b/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
index 89582e64f3a..55ff647e25f 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
@@ -84,9 +84,6 @@ export default {
update(data) {
return data?.jobs?.count || 0;
},
- context: {
- isSingleRequest: true,
- },
error() {
this.error = this.$options.i18n.jobsCountErrorMsg;
},
diff --git a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
index d8f9eb65236..de37aa431e6 100644
--- a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
+++ b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
@@ -10,7 +10,7 @@ import {
GlFormCheckbox,
GlTooltipDirective,
} from '@gitlab/ui';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { createAlert } from '~/alert';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -68,7 +68,7 @@ export default {
GlPagination,
GlFormCheckbox,
TimeAgo,
- CiBadgeLink,
+ CiIcon,
JobCheckbox,
ArtifactsBulkDelete,
BulkDeleteModal,
@@ -442,7 +442,7 @@ export default {
<template #cell(job)="{ item }">
<div class="gl-display-inline-flex gl-align-items-center gl-mb-3 gl-gap-3">
<span data-testid="job-artifacts-job-status">
- <ci-badge-link :status="item.detailedStatus" size="sm" :show-text="false" />
+ <ci-icon :status="item.detailedStatus" />
</span>
<gl-link :href="item.webPath">
{{ item.name }}
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
index 85dfa12c756..fbc7ddf5c91 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
@@ -1,11 +1,13 @@
<script>
-import { GlLoadingIcon, GlTableLite } from '@gitlab/ui';
+import { GlButton, GlEmptyState, GlLoadingIcon, GlTableLite } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { __, s__ } from '~/locale';
import getCiCatalogResourceComponents from '../../graphql/queries/get_ci_catalog_resource_components.query.graphql';
export default {
components: {
+ GlButton,
+ GlEmptyState,
GlLoadingIcon,
GlTableLite,
},
@@ -37,6 +39,9 @@ export default {
},
},
computed: {
+ isMetadataMissing() {
+ return !this.components || this.components?.length === 0;
+ },
isLoading() {
return this.$apollo.queries.components.loading;
},
@@ -70,6 +75,12 @@ export default {
},
],
i18n: {
+ copyText: __('Copy value'),
+ copyAriaText: __('Copy to clipboard'),
+ emptyStateTitle: s__('CiCatalogComponent|Component details not available'),
+ emptyStateDesc: s__(
+ 'CiCatalogComponent|This tab displays auto-collected information about the components in the repository, but no information was found.',
+ ),
inputTitle: s__('CiCatalogComponent|Inputs'),
fetchError: s__("CiCatalogComponent|There was an error fetching this resource's components"),
},
@@ -79,6 +90,11 @@ export default {
<template>
<div>
<gl-loading-icon v-if="isLoading" size="lg" />
+ <gl-empty-state
+ v-else-if="isMetadataMissing"
+ :title="$options.i18n.emptyStateTitle"
+ :description="$options.i18n.emptyStateDesc"
+ />
<template v-else>
<div
v-for="component in components"
@@ -88,7 +104,24 @@ export default {
>
<h3 class="gl-font-size-h2" data-testid="component-name">{{ component.name }}</h3>
<p class="gl-mt-5">{{ component.description }}</p>
- <pre class="gl-w-85p gl-py-4">{{ generateSnippet(component.path) }}</pre>
+ <div class="gl-display-flex">
+ <pre
+ class="gl-w-85p gl-py-4 gl-display-flex gl-justify-content-space-between gl-m-0 gl-border-r-none"
+ ><span>{{ generateSnippet(component.path) }}</span>
+ </pre>
+ <div class="gl--flex-center gl-bg-gray-10 gl-border gl-border-l-none">
+ <gl-button
+ class="gl-p-4! gl-mr-3!"
+ category="tertiary"
+ icon="copy-to-clipboard"
+ size="small"
+ :title="$options.i18n.copyText"
+ :data-clipboard-text="generateSnippet(component.path)"
+ data-testid="copy-to-clipboard"
+ :aria-label="$options.i18n.copyAriaText"
+ />
+ </div>
+ </div>
<div class="gl-mt-5">
<b class="gl-display-block gl-mb-4"> {{ $options.i18n.inputTitle }}</b>
<gl-table-lite :items="component.inputs.nodes" :fields="$options.fields">
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
index c0feb52c185..026a30988fd 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
@@ -30,12 +30,12 @@ export default {
<template>
<gl-tabs>
- <gl-tab v-if="glFeatures.ciCatalogComponentsTab" :title="$options.i18n.tabs.components" lazy>
- <ci-resource-components :resource-id="resourceId"
- /></gl-tab>
<gl-tab :title="$options.i18n.tabs.readme" lazy>
<ci-resource-readme :resource-id="resourceId" />
</gl-tab>
+ <gl-tab v-if="glFeatures.ciCatalogComponentsTab" :title="$options.i18n.tabs.components" lazy>
+ <ci-resource-components :resource-id="resourceId"
+ /></gl-tab>
</gl-tabs>
</template>
<style></style>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
index 6673785ffd2..29009c14e1b 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
@@ -2,13 +2,13 @@
import { GlAvatar, GlAvatarLink, GlBadge } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isNumeric } from '~/lib/utils/number_utils';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import CiResourceAbout from './ci_resource_about.vue';
import CiResourceHeaderSkeletonLoader from './ci_resource_header_skeleton_loader.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
CiResourceAbout,
CiResourceHeaderSkeletonLoader,
GlAvatar,
@@ -102,12 +102,11 @@ export default {
{{ versionBadgeText }}
</gl-badge>
</span>
- <ci-badge-link
+ <ci-icon
v-if="hasPipelineStatus"
- class="gl-mt-2"
:status="pipelineStatus"
- size="sm"
- show-text
+ show-status-text
+ class="gl-mt-2"
/>
</div>
</div>
diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
index 487215875c0..db84eaa82c2 100644
--- a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
@@ -4,12 +4,22 @@ import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import { CATALOG_FEEDBACK_DISMISSED_KEY } from '../../constants';
+const defaultTitle = __('CI/CD Catalog');
+const defaultDescription = s__(
+ 'CiCatalog|Discover CI configuration resources for a seamless CI/CD experience.',
+);
+
export default {
components: {
GlBanner,
GlLink,
},
- inject: ['pageTitle', 'pageDescription'],
+ inject: {
+ pageTitle: { default: defaultTitle },
+ pageDescription: {
+ default: defaultDescription,
+ },
+ },
data() {
return {
isFeedbackBannerDismissed: localStorage.getItem(CATALOG_FEEDBACK_DISMISSED_KEY) === 'true',
@@ -50,7 +60,7 @@ export default {
</gl-banner>
<h1 class="gl-font-size-h-display">{{ pageTitle }}</h1>
<p>
- <span>{{ pageDescription }}</span>
+ <span data-testid="description">{{ pageDescription }}</span>
<gl-link :href="$options.learnMorePath" target="_blank">{{
$options.i18n.learnMore
}}</gl-link>
diff --git a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
index 63243539575..080955b4322 100644
--- a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
@@ -48,9 +48,6 @@ export default {
starCount() {
return this.resource?.starCount || 0;
},
- forksCount() {
- return this.resource?.forksCount || 0;
- },
hasReleasedVersion() {
return Boolean(this.latestVersion?.releasedAt);
},
@@ -111,14 +108,12 @@ export default {
<gl-icon name="star" :size="14" class="gl-mr-1" />
<span class="gl-mr-3">{{ starCount }}</span>
</span>
- <span class="gl--flex-center" data-testid="stats-forks">
- <gl-icon name="fork" :size="14" class="gl-mr-1" />
- <span>{{ forksCount }}</span>
- </span>
</span>
</div>
</div>
- <div class="gl-display-flex gl-sm-flex-direction-column gl-justify-content-space-between">
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-justify-content-space-between"
+ >
<span class="gl-display-flex gl-flex-basis-two-thirds gl-font-sm">{{
resource.description
}}</span>
diff --git a/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue b/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue
new file mode 100644
index 00000000000..5e8727a3ed0
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue
@@ -0,0 +1,112 @@
+<script>
+import { createAlert } from '~/alert';
+import { s__ } from '~/locale';
+import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue';
+import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue';
+import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue';
+import EmptyState from '~/ci/catalog/components/list/empty_state.vue';
+import { ciCatalogResourcesItemsCount } from '~/ci/catalog/graphql/settings';
+import getCatalogResources from '../../graphql/queries/get_ci_catalog_resources.query.graphql';
+
+export default {
+ components: {
+ CatalogHeader,
+ CatalogListSkeletonLoader,
+ CiResourcesList,
+ EmptyState,
+ },
+ data() {
+ return {
+ catalogResources: [],
+ currentPage: 1,
+ totalCount: 0,
+ pageInfo: {},
+ };
+ },
+ apollo: {
+ catalogResources: {
+ query: getCatalogResources,
+ variables() {
+ return {
+ first: ciCatalogResourcesItemsCount,
+ };
+ },
+ update(data) {
+ return data?.ciCatalogResources?.nodes || [];
+ },
+ result({ data }) {
+ const { pageInfo } = data?.ciCatalogResources || {};
+ this.pageInfo = pageInfo;
+ this.totalCount = data?.ciCatalogResources?.count || 0;
+ },
+ error(e) {
+ createAlert({ message: e.message || this.$options.i18n.fetchError, variant: 'danger' });
+ },
+ },
+ },
+ computed: {
+ hasResources() {
+ return this.catalogResources.length > 0;
+ },
+ isLoading() {
+ return this.$apollo.queries.catalogResources.loading;
+ },
+ },
+ methods: {
+ async handlePrevPage() {
+ try {
+ await this.$apollo.queries.catalogResources.fetchMore({
+ variables: {
+ before: this.pageInfo.startCursor,
+ last: ciCatalogResourcesItemsCount,
+ first: null,
+ },
+ });
+
+ this.currentPage -= 1;
+ } catch (e) {
+ // Ensure that the current query is properly stoped if an error occurs.
+ this.$apollo.queries.catalogResources.stop();
+ createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' });
+ }
+ },
+ async handleNextPage() {
+ try {
+ await this.$apollo.queries.catalogResources.fetchMore({
+ variables: {
+ after: this.pageInfo.endCursor,
+ },
+ });
+
+ this.currentPage += 1;
+ } catch (e) {
+ // Ensure that the current query is properly stoped if an error occurs.
+ this.$apollo.queries.catalogResources.stop();
+
+ createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' });
+ }
+ },
+ },
+ i18n: {
+ fetchError: s__('CiCatalog|There was an error fetching CI/CD Catalog resources.'),
+ },
+};
+</script>
+<template>
+ <div>
+ <catalog-header />
+ <catalog-list-skeleton-loader v-if="isLoading" class="gl-w-full gl-mt-3" />
+ <empty-state v-else-if="!hasResources" />
+ <ci-resources-list
+ v-else
+ :current-page="currentPage"
+ :page-info="pageInfo"
+ :prev-text="__('Prev')"
+ :next-text="__('Next')"
+ :resources="catalogResources"
+ :total-count="totalCount"
+ @onPrevPage="handlePrevPage"
+ @onNextPage="handleNextPage"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/global_catalog.vue b/app/assets/javascripts/ci/catalog/global_catalog.vue
new file mode 100644
index 00000000000..76eac11a122
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/global_catalog.vue
@@ -0,0 +1,10 @@
+<script>
+import CiCatalogHome from './components/ci_catalog_home.vue';
+
+export default {
+ components: { CiCatalogHome },
+};
+</script>
+<template>
+ <ci-catalog-home />
+</template>
diff --git a/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
index f4d1bb0eaaf..a86db4c1b03 100644
--- a/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
@@ -4,7 +4,6 @@ fragment CatalogResourceFields on CiCatalogResource {
name
description
starCount
- forksCount
latestVersion {
id
tagName
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql
new file mode 100644
index 00000000000..aae29edef5e
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql
@@ -0,0 +1,16 @@
+#import "~/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql"
+
+query getCatalogResources($after: String, $before: String, $first: Int = 20, $last: Int) {
+ ciCatalogResources(after: $after, before: $before, first: $first, last: $last) {
+ pageInfo {
+ startCursor
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ }
+ count
+ nodes {
+ ...CatalogResourceFields
+ }
+ }
+}
diff --git a/app/assets/javascripts/ci/catalog/index.js b/app/assets/javascripts/ci/catalog/index.js
new file mode 100644
index 00000000000..5815245506c
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/index.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { cacheConfig, resolvers } from '~/ci/catalog/graphql/settings';
+
+import GlobalCatalog from './global_catalog.vue';
+import CiResourcesPage from './components/pages/ci_resources_page.vue';
+import { createRouter } from './router';
+
+export const initCatalog = (selector = '#js-ci-cd-catalog') => {
+ const el = document.querySelector(selector);
+ if (!el) {
+ return null;
+ }
+
+ const { dataset } = el;
+ const { ciCatalogPath } = dataset;
+
+ Vue.use(VueApollo);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers, cacheConfig),
+ });
+
+ return new Vue({
+ el,
+ name: 'GlobalCatalog',
+ router: createRouter(ciCatalogPath, CiResourcesPage),
+ apolloProvider,
+ provide: {
+ ciCatalogPath,
+ },
+ render(h) {
+ return h(GlobalCatalog);
+ },
+ });
+};
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
index a32c5f476fb..ccfe773b01f 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
@@ -190,6 +190,11 @@ export default {
deep: true,
},
},
+ beforeMount() {
+ // reset to default environments list every time we open the drawer
+ // and re-render the environments scope dropdown
+ this.$emit('search-environment-scope', '');
+ },
mounted() {
if (this.isProtectedByDefault && !this.isEditing) {
this.variable = { ...this.variable, protected: true };
@@ -371,7 +376,6 @@ export default {
:label-text="$options.i18n.key"
class="gl-border-none gl-pb-0! gl-mb-n5"
data-testid="ci-variable-key"
- data-qa-selector="ci_variable_key_field"
/>
<gl-form-group
:label="$options.i18n.value"
@@ -388,7 +392,6 @@ export default {
rows="3"
max-rows="10"
data-testid="ci-variable-value"
- data-qa-selector="ci_variable_value_field"
spellcheck="false"
/>
<p
@@ -419,15 +422,14 @@ export default {
variant="danger"
category="secondary"
class="gl-mr-3"
- data-testid="ci-variable-delete-btn"
+ data-testid="ci-variable-delete-button"
>{{ $options.i18n.deleteVariable }}</gl-button
>
<gl-button
category="primary"
variant="confirm"
:disabled="!canSubmit"
- data-testid="ci-variable-confirm-btn"
- data-qa-selector="ci_variable_save_button"
+ data-testid="ci-variable-confirm-button"
@click="submit"
>{{ modalActionText }}
</gl-button>
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
deleted file mode 100644
index cc664d76267..00000000000
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
+++ /dev/null
@@ -1,511 +0,0 @@
-<script>
-import {
- GlAlert,
- GlButton,
- GlCollapse,
- GlFormCheckbox,
- GlFormCombobox,
- GlFormGroup,
- GlFormSelect,
- GlFormInput,
- GlFormTextarea,
- GlIcon,
- GlLink,
- GlModal,
- GlSprintf,
-} from '@gitlab/ui';
-import { helpPagePath } from '~/helpers/help_page_helper';
-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 {
- allEnvironments,
- AWS_TOKEN_CONSTANTS,
- ADD_CI_VARIABLE_MODAL_ID,
- AWS_TIP_DISMISSED_COOKIE_NAME,
- AWS_TIP_TITLE,
- AWS_TIP_MESSAGE,
- CONTAINS_VARIABLE_REFERENCE_MESSAGE,
- defaultVariableState,
- ENVIRONMENT_SCOPE_LINK_TITLE,
- EVENT_LABEL,
- EVENT_ACTION,
- EXPANDED_VARIABLES_NOTE,
- EDIT_VARIABLE_ACTION,
- FLAG_LINK_TITLE,
- VARIABLE_ACTIONS,
- variableOptions,
-} 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 {
- components: {
- CiEnvironmentsDropdown,
- GlAlert,
- GlButton,
- GlCollapse,
- GlFormCheckbox,
- GlFormCombobox,
- GlFormGroup,
- GlFormSelect,
- GlFormInput,
- GlFormTextarea,
- GlIcon,
- GlLink,
- GlModal,
- GlSprintf,
- },
- mixins: [glFeatureFlagsMixin(), trackingMixin],
- inject: [
- 'containsVariableReferenceLink',
- 'environmentScopeLink',
- 'isProtectedByDefault',
- 'maskedEnvironmentVariablesLink',
- 'maskableRawRegex',
- 'maskableRegex',
- ],
- props: {
- areEnvironmentsLoading: {
- type: Boolean,
- required: true,
- },
- areScopedVariablesAvailable: {
- type: Boolean,
- required: false,
- default: false,
- },
- environments: {
- type: Array,
- required: false,
- default: () => [],
- },
- hideEnvironmentScope: {
- type: Boolean,
- required: false,
- default: false,
- },
- mode: {
- type: String,
- required: true,
- validator(val) {
- return VARIABLE_ACTIONS.includes(val);
- },
- },
- selectedVariable: {
- type: Object,
- required: false,
- default: () => {},
- },
- variables: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- data() {
- return {
- newEnvironments: [],
- isTipDismissed: getCookie(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true',
- validationErrorEventProperty: '',
- variable: { ...defaultVariableState, ...this.selectedVariable },
- };
- },
- computed: {
- canMask() {
- const regex = RegExp(this.useRawMaskableRegexp ? this.maskableRawRegex : this.maskableRegex);
- return regex.test(this.variable.value);
- },
- canSubmit() {
- return this.variableValidationState && this.variable.key !== '';
- },
- containsVariableReference() {
- const regex = /\$/;
- return regex.test(this.variable.value) && this.isExpanded;
- },
- displayMaskedError() {
- return !this.canMask && this.variable.masked;
- },
- isEditing() {
- return this.mode === EDIT_VARIABLE_ACTION;
- },
- isExpanded() {
- return !this.isRaw;
- },
- isRaw() {
- return this.variable.raw;
- },
- isTipVisible() {
- return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
- },
- maskedFeedback() {
- return this.displayMaskedError
- ? __('This variable value does not meet the masking requirements.')
- : '';
- },
- maskedState() {
- if (this.displayMaskedError) {
- return false;
- }
- return true;
- },
- modalActionText() {
- return this.isEditing ? __('Update variable') : __('Add variable');
- },
- 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.value);
- }
-
- return true;
- },
- useRawMaskableRegexp() {
- return this.isRaw;
- },
- variableValidationFeedback() {
- return `${this.tokenValidationFeedback} ${this.maskedFeedback}`;
- },
- variableValidationState() {
- return this.variable.value === '' || (this.tokenValidationState && this.maskedState);
- },
- variableValueHelpText() {
- return this.variable.masked
- ? __('Value must meet regular expression requirements to be masked.')
- : '';
- },
- },
- watch: {
- variable: {
- handler() {
- this.trackVariableValidationErrors();
- },
- deep: true,
- },
- },
- methods: {
- addVariable() {
- this.$emit('add-variable', this.variable);
- },
- deleteVariable() {
- this.$emit('delete-variable', this.variable);
- },
- updateVariable() {
- this.$emit('update-variable', this.variable);
- },
- dismissTip() {
- setCookie(AWS_TIP_DISMISSED_COOKIE_NAME, 'true', { expires: 90 });
- this.isTipDismissed = true;
- },
- deleteVarAndClose() {
- this.deleteVariable();
- this.hideModal();
- },
- hideModal() {
- this.$refs.modal.hide();
- },
- onShow() {
- this.setVariableProtectedByDefault();
- },
- resetModalHandler() {
- this.resetVariableData();
- this.resetValidationErrorEvents();
-
- this.$emit('close-form');
- },
- resetVariableData() {
- this.variable = { ...defaultVariableState };
- },
- setEnvironmentScope(scope) {
- this.variable = { ...this.variable, environmentScope: scope };
- },
- setVariableRaw(expanded) {
- this.variable = { ...this.variable, raw: !expanded };
- },
- setVariableProtected() {
- this.variable = { ...this.variable, protected: true };
- },
- updateOrAddVariable() {
- if (this.isEditing) {
- this.updateVariable();
- } else {
- this.addVariable();
- }
- this.hideModal();
- },
- setVariableProtectedByDefault() {
- if (this.isProtectedByDefault && !this.isEditing) {
- 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.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.value.replace(regex, '');
- }
- if (this.containsVariableReference) {
- property = '$';
- }
- }
-
- return property;
- },
- resetValidationErrorEvents() {
- this.validationErrorEventProperty = '';
- },
- },
- i18n: {
- awsTipTitle: AWS_TIP_TITLE,
- awsTipMessage: AWS_TIP_MESSAGE,
- containsVariableReferenceMessage: CONTAINS_VARIABLE_REFERENCE_MESSAGE,
- defaultScope: allEnvironments.text,
- environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
- expandedVariablesNote: EXPANDED_VARIABLES_NOTE,
- flagsLinkTitle: FLAG_LINK_TITLE,
- },
- flagLink: helpPagePath('ci/variables/index', {
- anchor: 'define-a-cicd-variable-in-the-ui',
- }),
- oidcLink: helpPagePath('ci/cloud_services/index', {
- anchor: 'oidc-authorization-with-your-cloud-provider',
- }),
- modalId: ADD_CI_VARIABLE_MODAL_ID,
- tokens: awsTokens,
- tokenList: awsTokenList,
- variableOptions,
-};
-</script>
-
-<template>
- <gl-modal
- ref="modal"
- :modal-id="$options.modalId"
- :title="modalActionText"
- static
- lazy
- @hidden="resetModalHandler"
- @shown="onShow"
- >
- <gl-collapse :visible="isTipVisible">
- <gl-alert
- :title="$options.i18n.awsTipTitle"
- variant="warning"
- class="gl-mb-5"
- data-testid="aws-guidance-tip"
- @dismiss="dismissTip"
- >
- <gl-sprintf :message="$options.i18n.awsTipMessage">
- <template #link="{ content }">
- <gl-link :href="$options.oidcLink">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
- </gl-collapse>
- <form>
- <gl-form-combobox
- v-model="variable.key"
- :token-list="$options.tokenList"
- :label-text="__('Key')"
- data-testid="pipeline-form-ci-variable-key"
- data-qa-selector="ci_variable_key_field"
- />
-
- <gl-form-group
- :label="__('Value')"
- label-for="ci-variable-value"
- :state="variableValidationState"
- :description="variableValueHelpText"
- :invalid-feedback="variableValidationFeedback"
- >
- <gl-form-textarea
- id="ci-variable-value"
- ref="valueField"
- v-model="variable.value"
- :state="variableValidationState"
- rows="3"
- max-rows="10"
- data-testid="pipeline-form-ci-variable-value"
- data-qa-selector="ci_variable_value_field"
- class="gl-font-monospace!"
- spellcheck="false"
- />
- <p v-if="isRaw" class="gl-mt-2 gl-mb-0 text-secondary" data-testid="raw-variable-tip">
- {{ __('Variable value will be evaluated as raw string.') }}
- </p>
- </gl-form-group>
-
- <div class="gl-display-flex">
- <gl-form-group :label="__('Type')" label-for="ci-variable-type" class="gl-w-half gl-mr-5">
- <gl-form-select
- id="ci-variable-type"
- v-model="variable.variableType"
- :options="$options.variableOptions"
- />
- </gl-form-group>
-
- <template v-if="!hideEnvironmentScope">
- <gl-form-group
- label-for="ci-variable-env"
- class="gl-w-half"
- data-testid="environment-scope"
- >
- <template #label>
- <div class="gl-display-flex gl-align-items-center">
- <span class="gl-mr-2">
- {{ __('Environment scope') }}
- </span>
- <gl-link
- class="gl-display-flex"
- :title="$options.i18n.environmentScopeLinkTitle"
- :href="environmentScopeLink"
- target="_blank"
- data-testid="environment-scope-link"
- >
- <gl-icon name="question-o" :size="14" />
- </gl-link>
- </div>
- </template>
- <ci-environments-dropdown
- v-if="areScopedVariablesAvailable"
- :are-environments-loading="areEnvironmentsLoading"
- :selected-environment-scope="variable.environmentScope"
- :environments="environments"
- @select-environment="setEnvironmentScope"
- @search-environment-scope="$emit('search-environment-scope', $event)"
- />
-
- <gl-form-input v-else :value="$options.i18n.defaultScope" class="gl-w-full" readonly />
- </gl-form-group>
- </template>
- </div>
-
- <gl-form-group>
- <template #label>
- <div class="gl-display-flex gl-align-items-center">
- <span class="gl-mr-2">
- {{ __('Flags') }}
- </span>
- <gl-link
- class="gl-display-flex"
- :title="$options.i18n.flagsLinkTitle"
- :href="$options.flagLink"
- target="_blank"
- >
- <gl-icon name="question-o" :size="14" />
- </gl-link>
- </div>
- </template>
- <gl-form-checkbox
- v-model="variable.protected"
- class="gl-mb-0"
- data-testid="ci-variable-protected-checkbox"
- :data-is-protected-checked="variable.protected"
- >
- {{ __('Protect variable') }}
- <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="variable.masked"
- data-testid="ci-variable-masked-checkbox"
- >
- {{ __('Mask variable') }}
- <p class="gl-mt-2 text-secondary">
- <gl-sprintf
- :message="
- __(
- 'Mask this variable in job logs if it meets %{linkStart}regular expression requirements%{linkEnd}.',
- )
- "
- >
- <template #link="{ content }"
- ><gl-link target="_blank" :href="maskedEnvironmentVariablesLink">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- </gl-form-checkbox>
- <gl-form-checkbox
- ref="expanded-ci-variable"
- :checked="isExpanded"
- data-testid="ci-variable-expanded-checkbox"
- @change="setVariableRaw"
- >
- {{ __('Expand variable reference') }}
- <p class="gl-mt-2 gl-mb-0 gl-text-secondary">
- <gl-sprintf :message="$options.i18n.expandedVariablesNote">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- </gl-sprintf>
- </p>
- </gl-form-checkbox>
- </gl-form-group>
- </form>
-
- <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.i18n.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="isEditing"
- ref="deleteCiVariable"
- variant="danger"
- category="secondary"
- @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/ci_variable_list/components/ci_variable_settings.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
index f2d81b3f271..99270d36df7 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
@@ -3,13 +3,11 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ADD_VARIABLE_ACTION, EDIT_VARIABLE_ACTION, VARIABLE_ACTIONS } from '../constants';
import CiVariableDrawer from './ci_variable_drawer.vue';
import CiVariableTable from './ci_variable_table.vue';
-import CiVariableModal from './ci_variable_modal.vue';
export default {
components: {
CiVariableDrawer,
CiVariableTable,
- CiVariableModal,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -65,15 +63,6 @@ export default {
showForm() {
return VARIABLE_ACTIONS.includes(this.mode);
},
- useDrawerForm() {
- return this.glFeatures?.ciVariableDrawer;
- },
- showDrawer() {
- return this.showForm && this.useDrawerForm;
- },
- showModal() {
- return this.showForm && !this.useDrawerForm;
- },
},
methods: {
addVariable(variable) {
@@ -116,23 +105,8 @@ export default {
@delete-variable="deleteVariable"
@sort-changed="(val) => $emit('sort-changed', val)"
/>
- <ci-variable-modal
- v-if="showModal"
- :are-environments-loading="areEnvironmentsLoading"
- :are-scoped-variables-available="areScopedVariablesAvailable"
- :environments="environments"
- :hide-environment-scope="hideEnvironmentScope"
- :variables="variables"
- :mode="mode"
- :selected-variable="selectedVariable"
- @add-variable="addVariable"
- @delete-variable="deleteVariable"
- @close-form="closeForm"
- @update-variable="updateVariable"
- @search-environment-scope="$emit('search-environment-scope', $event)"
- />
<ci-variable-drawer
- v-if="showDrawer"
+ v-if="showForm"
:are-environments-loading="areEnvironmentsLoading"
:are-scoped-variables-available="areScopedVariablesAvailable"
:environments="environments"
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
index 3d62313815c..86287d586ec 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
@@ -16,7 +16,6 @@ import {
import { __, s__, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
- ADD_CI_VARIABLE_MODAL_ID,
DEFAULT_EXCEEDS_VARIABLE_LIMIT_TEXT,
EXCEEDS_VARIABLE_LIMIT_TEXT,
MAXIMUM_VARIABLE_LIMIT_REACHED,
@@ -25,7 +24,6 @@ import {
import { convertEnvironmentScope } from '../utils';
export default {
- modalId: ADD_CI_VARIABLE_MODAL_ID,
defaultFields: [
{
key: 'key',
@@ -243,10 +241,8 @@ export default {
>{{ valuesButtonText }}</gl-button
>
<gl-button
- v-gl-modal-directive="$options.modalId"
size="small"
:disabled="exceedsVariableLimit"
- data-qa-selector="add_ci_variable_button"
data-testid="add-ci-variable-button"
@click="setSelectedVariable()"
>{{ $options.i18n.addButton }}</gl-button
@@ -375,12 +371,11 @@ export default {
<template v-if="!isInheritedGroupVars" #cell(actions)="{ item }">
<div class="gl-display-flex gl-justify-content-end gl-mt-n2 gl-mb-n2">
<gl-button
- v-gl-modal-directive="$options.modalId"
icon="pencil"
size="small"
class="gl-mr-3"
:aria-label="$options.i18n.editButton"
- data-qa-selector="edit_ci_variable_button"
+ data-testid="edit-ci-variable-button"
@click="setSelectedVariable(item.index)"
/>
<gl-button
@@ -390,7 +385,6 @@ export default {
icon="remove"
size="small"
:aria-label="$options.i18n.deleteButton"
- data-qa-selector="delete_ci_variable_button"
/>
<gl-modal
ref="modal"
diff --git a/app/assets/javascripts/ci/ci_variable_list/constants.js b/app/assets/javascripts/ci/ci_variable_list/constants.js
index fc37b62299d..d85827b8220 100644
--- a/app/assets/javascripts/ci/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci/ci_variable_list/constants.js
@@ -1,6 +1,5 @@
import { __, s__, sprintf } from '~/locale';
-export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable';
export const ENVIRONMENT_QUERY_LIMIT = 30;
export const SORT_DIRECTIONS = {
@@ -45,7 +44,6 @@ export const AWS_TIP_MESSAGE = s__(
'CiVariable|GitLab CI/CD supports OpenID Connect (OIDC) to give your build and deployment jobs access to cloud credentials and services. %{linkStart}How do I configure OIDC for my cloud provider?%{linkEnd}',
);
-export const EVENT_LABEL = 'ci_variable_modal';
export const DRAWER_EVENT_LABEL = 'ci_variable_drawer';
export const EVENT_ACTION = 'validation_error';
diff --git a/app/assets/javascripts/ci/common/pipelines_table.vue b/app/assets/javascripts/ci/common/pipelines_table.vue
index 13b5120654a..d63d2d1713e 100644
--- a/app/assets/javascripts/ci/common/pipelines_table.vue
+++ b/app/assets/javascripts/ci/common/pipelines_table.vue
@@ -13,8 +13,6 @@ import PipelineUrl from '../pipelines_page/components/pipeline_url.vue';
import PipelineStatusBadge from '../pipelines_page/components/pipeline_status_badge.vue';
const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!';
-const DEFAULT_TH_CLASSES =
- 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!';
/**
* Pipelines Table
@@ -77,7 +75,6 @@ export default {
{
key: 'status',
label: s__('Pipeline|Status'),
- thClass: DEFAULT_TH_CLASSES,
columnClass: 'gl-w-15p',
tdClass: this.tdClasses,
thAttr: { 'data-testid': 'status-th' },
@@ -85,7 +82,6 @@ export default {
{
key: 'pipeline',
label: __('Pipeline'),
- thClass: DEFAULT_TH_CLASSES,
tdClass: `${this.tdClasses}`,
columnClass: 'gl-w-30p',
thAttr: { 'data-testid': 'pipeline-th' },
@@ -93,7 +89,6 @@ export default {
{
key: 'triggerer',
label: s__('Pipeline|Created by'),
- thClass: DEFAULT_TH_CLASSES,
tdClass: `${this.tdClasses} ${HIDE_TD_ON_MOBILE}`,
columnClass: 'gl-w-15p',
thAttr: { 'data-testid': 'triggerer-th' },
@@ -101,14 +96,12 @@ export default {
{
key: 'stages',
label: s__('Pipeline|Stages'),
- thClass: DEFAULT_TH_CLASSES,
tdClass: this.tdClasses,
columnClass: 'gl-w-quarter',
thAttr: { 'data-testid': 'stages-th' },
},
{
key: 'actions',
- thClass: DEFAULT_TH_CLASSES,
tdClass: this.tdClasses,
columnClass: 'gl-w-20p',
thAttr: { 'data-testid': 'actions-th' },
@@ -137,8 +130,7 @@ export default {
return cleanLeadingSeparator(item.project.full_path);
},
failedJobsCount(pipeline) {
- // Remove `pipeline?.failed_builds?.length` when we remove `ci_fix_performance_pipelines_json_endpoint`.
- return pipeline?.failed_builds_count || pipeline?.failed_builds?.length || 0;
+ return pipeline?.failed_builds_count || 0;
},
onRefreshPipelinesTable() {
this.$emit('refresh-pipelines-table');
diff --git a/app/assets/javascripts/ci/common/private/job_action_component.vue b/app/assets/javascripts/ci/common/private/job_action_component.vue
index b0fa724d450..c266e061513 100644
--- a/app/assets/javascripts/ci/common/private/job_action_component.vue
+++ b/app/assets/javascripts/ci/common/private/job_action_component.vue
@@ -119,6 +119,7 @@ export default {
ref="button"
:class="cssClass"
:disabled="isDisabled"
+ size="small"
class="js-ci-action gl-ci-action-icon-container ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center"
data-testid="ci-action-button"
@click.stop="onClickAction"
@@ -129,8 +130,17 @@ export default {
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-h-full"
data-testid="ci-action-icon-tooltip-wrapper"
>
- <gl-loading-icon v-if="isLoading" size="sm" class="js-action-icon-loading" />
- <gl-icon v-else :name="actionIcon" class="gl-mr-0!" :aria-label="actionIcon" />
+ <gl-loading-icon
+ v-if="isLoading"
+ size="sm"
+ class="gl-button-icon gl-m-2 js-action-icon-loading"
+ />
+ <gl-icon
+ v-else
+ :name="actionIcon"
+ class="gl-button-icon gl-p-1 gl-mr-0!"
+ :aria-label="actionIcon"
+ />
</div>
</gl-button>
</template>
diff --git a/app/assets/javascripts/ci/common/private/job_links_layer.vue b/app/assets/javascripts/ci/common/private/job_links_layer.vue
index 59260ca3f81..9b3647e9c55 100644
--- a/app/assets/javascripts/ci/common/private/job_links_layer.vue
+++ b/app/assets/javascripts/ci/common/private/job_links_layer.vue
@@ -1,5 +1,6 @@
<script>
import { memoize } from 'lodash';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { reportToSentry } from '~/ci/utils';
import { parseData } from '~/ci/pipeline_details/utils/parsing_utils';
import LinksInner from '~/ci/pipeline_details/graph/components/links_inner.vue';
@@ -16,6 +17,7 @@ export default {
components: {
LinksInner,
},
+ mixins: [glFeatureFlagMixin()],
props: {
containerMeasurements: {
type: Object,
@@ -50,6 +52,9 @@ export default {
showLinkedLayers() {
return this.showLinks && !this.containerZero;
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
},
errorCaptured(err, _vm, info) {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
@@ -68,7 +73,10 @@ export default {
<slot></slot>
</links-inner>
<div v-else>
- <div class="gl-display-flex gl-relative">
+ <div
+ class="gl-display-flex gl-relative"
+ :class="{ 'gl-flex-wrap gl-sm-flex-nowrap': isNewPipelineGraph }"
+ >
<slot></slot>
</div>
</div>
diff --git a/app/assets/javascripts/ci/common/private/job_name_component.vue b/app/assets/javascripts/ci/common/private/job_name_component.vue
index 1c7f5a7476d..b4e831d69d4 100644
--- a/app/assets/javascripts/ci/common/private/job_name_component.vue
+++ b/app/assets/javascripts/ci/common/private/job_name_component.vue
@@ -30,7 +30,7 @@ export default {
</script>
<template>
<span class="mw-100 gl-display-flex gl-align-items-center gl-flex-grow-1">
- <ci-icon :size="iconSize" :status="status" class="gl-line-height-0" />
+ <ci-icon :size="iconSize" :status="status" :show-tooltip="false" class="gl-line-height-0" />
<span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block">
{{ name }}
</span>
diff --git a/app/assets/javascripts/ci/constants.js b/app/assets/javascripts/ci/constants.js
index 5b60528f521..138a44a8dd0 100644
--- a/app/assets/javascripts/ci/constants.js
+++ b/app/assets/javascripts/ci/constants.js
@@ -37,4 +37,5 @@ export const TRACKING_CATEGORIES = {
search: 'pipelines_filtered_search',
failed: 'pipeline_failed_jobs_tab',
tests: 'pipeline_tests_tab',
+ listbox: 'pipeline_id_iid_listbox',
};
diff --git a/app/assets/javascripts/ci/job_details/components/job_header.vue b/app/assets/javascripts/ci/job_details/components/job_header.vue
index 00d15f87064..1aa83a94bc5 100644
--- a/app/assets/javascripts/ci/job_details/components/job_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/job_header.vue
@@ -4,12 +4,12 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { glEmojiTag } from '~/emoji';
import { __, sprintf } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
TimeagoTooltip,
GlButton,
GlAvatarLink,
@@ -113,7 +113,7 @@ export default {
</div>
</div>
<section class="header-main-content gl-display-flex gl-align-items-center gl-mr-3">
- <ci-badge-link class="gl-mr-3" :status="status" />
+ <ci-icon class="gl-mr-3" :status="status" show-status-text />
<template v-if="shouldRenderTriggeredLabel">{{ __('Started') }}</template>
<template v-else>{{ __('Created') }}</template>
diff --git a/app/assets/javascripts/ci/job_details/components/log/line_header.vue b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
index 658a94e6af4..d36701323da 100644
--- a/app/assets/javascripts/ci/job_details/components/log/line_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
@@ -17,7 +17,8 @@ export default {
},
isClosed: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
path: {
type: String,
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue b/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
index 8e87f118fa4..4ec9044a21c 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
@@ -63,11 +63,11 @@ export default {
<gl-icon
v-if="isActive"
name="arrow-right"
+ :show-tooltip="false"
class="icon-arrow-right gl-absolute gl-display-block"
- :size="14"
/>
- <ci-icon :status="job.status" class="gl-mr-3" :size="14" />
+ <ci-icon :status="job.status" :show-tooltip="false" class="gl-mr-3" />
<span class="gl-text-truncate gl-w-full">{{ jobName }}</span>
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue b/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
index 7744395734f..e229abcbe12 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlDisclosureDropdown, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { Mousetrap } from '~/lib/mousetrap';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -14,7 +14,7 @@ export default {
GlDisclosureDropdown,
GlLink,
GlSprintf,
- CiBadgeLink,
+ CiIcon,
},
props: {
pipeline: {
@@ -94,7 +94,10 @@ export default {
</script>
<template>
<div class="dropdown">
- <div class="gl-display-flex gl-flex-wrap gl-gap-2 js-pipeline-info" data-testid="pipeline-info">
+ <div
+ class="gl-display-flex gl-flex-wrap gl-align-items-center gl-gap-2 js-pipeline-info"
+ data-testid="pipeline-info"
+ >
<gl-sprintf :message="pipelineInfo">
<template #bold="{ content }">
<span class="gl-display-flex gl-font-weight-bold">{{ content }}</span>
@@ -108,9 +111,9 @@ export default {
>
</template>
<template #status>
- <ci-badge-link
+ <ci-icon
:status="pipeline.details.status"
- size="sm"
+ show-status-text
data-testid="pipeline-status-link"
/>
</template>
@@ -125,7 +128,7 @@ export default {
<template #ref>
<gl-link
:href="pipeline.ref.path"
- class="link-commit ref-name gl-mt-1"
+ class="link-commit ref-name"
data-testid="source-ref-link"
>{{ pipeline.ref.name }}</gl-link
><clipboard-button
diff --git a/app/assets/javascripts/ci/job_details/job_app.vue b/app/assets/javascripts/ci/job_details/job_app.vue
index 119f8259be7..e0708289b43 100644
--- a/app/assets/javascripts/ci/job_details/job_app.vue
+++ b/app/assets/javascripts/ci/job_details/job_app.vue
@@ -307,7 +307,7 @@ export default {
@scrollJobLogBottom="scrollBottom"
@searchResults="setSearchResults"
/>
- <log :job-log="jobLog" :is-complete="isJobLogComplete" :search-results="searchResults" />
+ <log :search-results="searchResults" />
</div>
<!-- EO job log -->
diff --git a/app/assets/javascripts/ci/job_details/store/actions.js b/app/assets/javascripts/ci/job_details/store/actions.js
index fa23589f7d6..6f538e3b3d4 100644
--- a/app/assets/javascripts/ci/job_details/store/actions.js
+++ b/app/assets/javascripts/ci/job_details/store/actions.js
@@ -175,7 +175,7 @@ export const fetchJobLog = ({ dispatch, state }) =>
}
})
.catch((e) => {
- if (e.response.status === HTTP_STATUS_FORBIDDEN) {
+ if (e.response?.status === HTTP_STATUS_FORBIDDEN) {
dispatch('receiveJobLogUnauthorizedError');
} else {
reportToSentry('job_actions', e);
diff --git a/app/assets/javascripts/ci/job_details/store/utils.js b/app/assets/javascripts/ci/job_details/store/utils.js
index b18a3fa162d..c8b33638821 100644
--- a/app/assets/javascripts/ci/job_details/store/utils.js
+++ b/app/assets/javascripts/ci/job_details/store/utils.js
@@ -117,28 +117,31 @@ export const getNextLineNumber = (acc) => {
* @returns Array parsed log lines
*/
export const logLinesParser = (lines = [], prevLogLines = [], hash = '') =>
- lines.reduce((acc, line) => {
- const lineNumber = getNextLineNumber(acc);
-
- const last = acc[acc.length - 1];
-
- // If the object is an header, we parse it into another structure
- if (line.section_header) {
- acc.push(parseHeaderLine(line, lineNumber, hash));
- } else if (isCollapsibleSection(acc, last, line)) {
- // if the object belongs to a nested section, we append it to the new `lines` array of the
- // previously formatted header
- last.lines.push(parseLine(line, lineNumber));
- } else if (line.section_duration) {
- // if the line has section_duration, we look for the correct header to add it
- addDurationToHeader(acc, line);
- } else {
- // otherwise it's a regular line
- acc.push(parseLine(line, lineNumber));
- }
+ lines.reduce(
+ (acc, line) => {
+ const lineNumber = getNextLineNumber(acc);
+
+ const last = acc[acc.length - 1];
+
+ // If the object is an header, we parse it into another structure
+ if (line.section_header) {
+ acc.push(parseHeaderLine(line, lineNumber, hash));
+ } else if (isCollapsibleSection(acc, last, line)) {
+ // if the object belongs to a nested section, we append it to the new `lines` array of the
+ // previously formatted header
+ last.lines.push(parseLine(line, lineNumber));
+ } else if (line.section_duration) {
+ // if the line has section_duration, we look for the correct header to add it
+ addDurationToHeader(acc, line);
+ } else {
+ // otherwise it's a regular line
+ acc.push(parseLine(line, lineNumber));
+ }
- return acc;
- }, prevLogLines);
+ return acc;
+ },
+ [...prevLogLines],
+ );
/**
* Finds the repeated offset, removes the old one
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
index fbdfc7c9c6a..b97243cf2ca 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
@@ -136,8 +136,8 @@ export default {
v-if="triggered"
variant="info"
:size="$options.badgeSize"
- data-testid="triggered-job-badge"
- >{{ s__('Job|triggered') }}
+ data-testid="trigger-token-job-badge"
+ >{{ s__('Job|trigger token') }}
</gl-badge>
<gl-badge
v-if="showAllowedToFailBadge"
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
index a2b6a430138..efa74d86bd6 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
@@ -1,14 +1,14 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { formatTime } from '~/lib/utils/datetime_utility';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
iconSize: 12,
components: {
- CiBadgeLink,
+ CiIcon,
GlIcon,
TimeAgoTooltip,
},
@@ -38,7 +38,7 @@ export default {
<template>
<div>
- <ci-badge-link :status="job.detailedStatus" />
+ <ci-icon :status="job.detailedStatus" show-status-text />
<div class="gl-font-sm gl-text-secondary gl-mt-2 gl-ml-3">
<div v-if="duration" data-testid="job-duration">
<gl-icon name="timer" :size="$options.iconSize" data-testid="duration-icon" />
diff --git a/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue b/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue
index 03e0f2dadc8..09bbb7afbca 100644
--- a/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue
+++ b/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue
@@ -58,9 +58,6 @@ export default {
},
jobsCount: {
query: GetJobsCount,
- context: {
- isSingleRequest: true,
- },
variables() {
return {
fullPath: this.fullPath,
diff --git a/app/assets/javascripts/ci/pipeline_details/constants.js b/app/assets/javascripts/ci/pipeline_details/constants.js
index 70b758ae6b0..51d0e980e78 100644
--- a/app/assets/javascripts/ci/pipeline_details/constants.js
+++ b/app/assets/javascripts/ci/pipeline_details/constants.js
@@ -2,7 +2,6 @@ import { __, s__ } from '~/locale';
export const CANCEL_REQUEST = 'CANCEL_REQUEST';
export const SUPPORTED_FILTER_PARAMETERS = ['username', 'ref', 'status', 'source'];
-export const SCHEDULE_ORIGIN = 'schedule';
export const NEEDS_PROPERTY = 'needs';
export const EXPLICIT_NEEDS_PROPERTY = 'previousStageJobsOrNeeds';
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue
index f098d790736..3da2f27c1b9 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue
@@ -4,6 +4,7 @@ import {
generateColumnsFromLayersListMemoized,
keepLatestDownstreamPipelines,
} from '~/ci/pipeline_details/utils/parsing_utils';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LinksLayer from '../../../common/private/job_links_layer.vue';
import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH, STAGE_VIEW } from '../constants';
import { validateConfigPaths } from '../utils';
@@ -19,6 +20,7 @@ export default {
LinkedPipelinesColumn,
StageColumnComponent,
},
+ mixins: [glFeatureFlagMixin()],
props: {
configPaths: {
type: Object,
@@ -132,6 +134,9 @@ export default {
upstreamPipelines() {
return this.hasUpstreamPipelines ? this.pipeline.upstream : [];
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
},
errorCaptured(err, _vm, info) {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
@@ -178,10 +183,15 @@ export default {
<div class="js-pipeline-graph">
<div
ref="mainPipelineContainer"
- class="gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap"
+ class="pipeline-graph gl-display-flex gl-position-relative gl-white-space-nowrap gl-rounded-lg"
:class="{
- 'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isLinkedPipeline,
+ 'gl-bg-gray-10': !isNewPipelineGraph,
+ 'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isNewPipelineGraph && !isLinkedPipeline,
+ 'pipeline-graph-container gl-bg-gray-10 gl-pipeline-min-h gl-align-items-flex-start gl-pt-3 gl-pb-8 gl-mt-3 gl-overflow-auto':
+ isNewPipelineGraph && !isLinkedPipeline,
+ 'gl-bg-gray-50 gl-sm-ml-5': isNewPipelineGraph && isLinkedPipeline,
}"
+ data-testid="pipeline-container"
>
<linked-graph-wrapper>
<template #upstream>
@@ -199,7 +209,7 @@ export default {
/>
</template>
<template #main>
- <div :id="containerId" :ref="containerId">
+ <div :id="containerId" :ref="containerId" class="pipeline-links-container">
<links-layer
:pipeline-data="layout"
:pipeline-id="pipeline.id"
@@ -238,7 +248,7 @@ export default {
<template #downstream>
<linked-pipelines-column
v-if="showDownstreamPipelines"
- class="gl-mr-6"
+ :class="{ 'gl-sm-ml-3': isNewPipelineGraph }"
:config-paths="configPaths"
:linked-pipelines="downstreamPipelines"
:column-title="__('Downstream')"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue
index fb7dcb300f1..114b224fbe7 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue
@@ -1,11 +1,11 @@
<script>
import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __, s__ } from '~/locale';
import { STAGE_VIEW, LAYER_VIEW } from '../constants';
export default {
name: 'GraphViewSelector',
-
components: {
GlAlert,
GlButton,
@@ -13,7 +13,7 @@ export default {
GlLoadingIcon,
GlToggle,
},
-
+ mixins: [glFeatureFlagMixin()],
props: {
showLinks: {
type: Boolean,
@@ -77,6 +77,9 @@ export default {
};
});
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
},
watch: {
/*
@@ -138,7 +141,13 @@ export default {
<template>
<div>
- <div class="gl-relative gl-display-flex gl-align-items-center gl-w-max-content gl-my-4">
+ <div
+ class="gl-relative gl-display-flex gl-align-items-center gl-my-4"
+ :class="{
+ 'gl-w-max-content': !isNewPipelineGraph,
+ 'gl-flex-wrap gl-sm-flex-nowrap': isNewPipelineGraph,
+ }"
+ >
<gl-loading-icon
v-if="isSwitcherLoading"
data-testid="switcher-loading-state"
@@ -161,7 +170,10 @@ export default {
<gl-toggle
v-model="showLinksActive"
data-testid="show-links-toggle"
- class="gl-mx-4"
+ :class="{
+ 'gl-mx-4': !isNewPipelineGraph,
+ 'gl-sm-ml-4 gl-mt-4 gl-sm-mt-0': isNewPipelineGraph,
+ }"
:label="$options.i18n.linksLabelText"
:is-loading="isToggleLoading"
label-position="left"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
index bb36ac8b6ab..c6340e6787a 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
@@ -5,7 +5,7 @@ import delayedJobMixin from '~/ci/mixins/delayed_job_mixin';
import { helpPagePath } from '~/helpers/help_page_helper';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import ActionComponent from '../../../common/private/job_action_component.vue';
import JobNameComponent from '../../../common/private/job_name_component.vue';
import { BRIDGE_KIND, RETRY_ACTION_TITLE, SINGLE_JOB, SKIP_RETRY_MODAL_KEY } from '../constants';
@@ -58,7 +58,7 @@ export default {
hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500',
components: {
ActionComponent,
- CiBadgeLink,
+ CiIcon,
GlBadge,
GlForm,
GlFormCheckbox,
@@ -329,7 +329,7 @@ export default {
@mouseout="hideTooltips"
>
<div class="gl-display-flex gl-align-items-center gl-flex-grow-1">
- <ci-badge-link :status="job.status" size="md" :show-text="false" :use-link="false" />
+ <ci-icon :status="job.status" :use-link="false" />
<div class="gl-pl-3 gl-pr-3 gl-display-flex gl-flex-direction-column gl-pipeline-job-width">
<div class="gl-text-truncate gl-pr-9 gl-line-height-normal">{{ job.name }}</div>
<div
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue
index fb2280d971a..0d72373a0f5 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue
@@ -1,5 +1,20 @@
+<script>
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
+export default {
+ mixins: [glFeatureFlagMixin()],
+ computed: {
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
+ },
+};
+</script>
<template>
- <div class="gl-display-flex">
+ <div
+ class="gl-display-flex"
+ :class="{ 'gl-flex-wrap gl-sm-flex-nowrap gl-w-full': isNewPipelineGraph }"
+ >
<slot name="upstream"></slot>
<slot name="main"></slot>
<slot name="downstream"></slot>
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
index 5960eea5b4f..26521f87426 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
@@ -7,13 +7,14 @@ import {
GlTooltip,
GlTooltipDirective,
} from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { TYPENAME_CI_PIPELINE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
import CancelPipelineMutation from '~/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql';
import RetryPipelineMutation from '~/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '~/ci/utils';
import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from '../constants';
@@ -22,13 +23,14 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- CiBadgeLink,
+ CiIcon,
GlBadge,
GlButton,
GlLink,
GlLoadingIcon,
GlTooltip,
},
+ mixins: [glFeatureFlagMixin()],
styles: {
actionSizeClasses: ['gl-h-7 gl-w-7'],
flatLeftBorder: ['gl-rounded-bottom-left-none!', 'gl-rounded-top-left-none!'],
@@ -115,9 +117,6 @@ export default {
downstreamTitle() {
return this.childPipeline ? this.sourceJobName : this.pipeline.project.name;
},
- flexDirection() {
- return this.isUpstream ? 'gl-flex-direction-row-reverse' : 'gl-flex-direction-row';
- },
graphqlPipelineId() {
return convertToGraphQLId(TYPENAME_CI_PIPELINE, this.pipeline.id);
},
@@ -176,6 +175,9 @@ export default {
return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} -
${this.sourceJobInfo}`;
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
},
errorCaptured(err, _vm, info) {
reportToSentry('linked_pipeline', `error: ${err}, info: ${info}`);
@@ -231,9 +233,15 @@ export default {
<template>
<div
ref="linkedPipeline"
- class="gl-h-full gl-display-flex! gl-px-2"
- :class="flexDirection"
+ class="linked-pipeline-container gl-h-full gl-display-flex!"
+ :class="{
+ 'gl-flex-direction-row-reverse': isUpstream,
+ 'gl-flex-direction-row': !isUpstream,
+ 'gl-px-2': !isNewPipelineGraph,
+ 'gl-w-full gl-sm-w-auto': isNewPipelineGraph,
+ }"
data-testid="linked-pipeline-container"
+ :aria-expanded="expanded"
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
>
@@ -242,17 +250,15 @@ export default {
</gl-tooltip>
<div class="gl-bg-white gl-border gl-p-3 gl-rounded-lg gl-w-full" :class="cardClasses">
<div class="gl-display-flex gl-gap-x-3">
- <ci-badge-link
+ <ci-icon
v-if="!pipelineIsLoading"
:status="pipelineStatus"
- size="md"
- :show-text="false"
:use-link="false"
class="gl-align-self-start"
/>
<div v-else class="gl-pr-3"><gl-loading-icon size="sm" inline /></div>
<div
- class="gl-display-flex gl-downstream-pipeline-job-width gl-flex-direction-column gl-line-height-normal"
+ class="gl-display-flex gl-flex-direction-column gl-line-height-normal gl-downstream-pipeline-job-width"
>
<span class="gl-text-truncate" data-testid="downstream-title-content">
{{ downstreamTitle }}
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue
index 2de7e43c9b1..395770826d8 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue
@@ -1,4 +1,5 @@
<script>
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import { reportToSentry } from '~/ci/utils';
import { LOAD_FAILURE } from '../../constants';
@@ -18,6 +19,7 @@ export default {
LinkedPipeline,
PipelineGraph: () => import('./graph_component.vue'),
},
+ mixins: [glFeatureFlagMixin()],
props: {
columnTitle: {
type: String,
@@ -63,23 +65,30 @@ export default {
'gl-pipeline-job-width',
'gl-text-truncate',
'gl-line-height-36',
- 'gl-pl-3',
- 'gl-mb-5',
],
minWidth: `${ONE_COL_WIDTH}px`,
computed: {
columnClass() {
- const positionValues = {
+ const positionValuesOld = {
right: 'gl-ml-6',
left: 'gl-mx-6',
};
+ const positionValues = {
+ right: 'gl-mx-5',
+ left: 'gl-mx-4 gl-flex-basis-full',
+ };
+ const usePositionValues = this.isNewPipelineGraph ? positionValues : positionValuesOld;
- return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
+ return `graph-position-${this.graphPosition} ${usePositionValues[this.graphPosition]}`;
},
computedTitleClasses() {
const positionalClasses = this.isUpstream ? ['gl-w-full', 'gl-linked-pipeline-padding'] : [];
- return [...this.$options.titleClasses, ...positionalClasses];
+ return [
+ ...this.$options.titleClasses,
+ !this.isNewPipelineGraph ?? ['gl-pl-3', 'gl-mb-5'],
+ ...positionalClasses,
+ ];
},
graphPosition() {
return this.isUpstream ? 'left' : 'right';
@@ -93,6 +102,9 @@ export default {
minWidth() {
return this.isUpstream ? 0 : this.$options.minWidth;
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
},
methods: {
getPipelineData(pipeline) {
@@ -197,7 +209,7 @@ export default {
</script>
<template>
- <div class="gl-display-flex">
+ <div class="gl-display-flex" :class="{ 'gl-w-full gl-sm-w-auto': isNewPipelineGraph }">
<div :class="columnClass" class="linked-pipelines-column">
<div data-testid="linked-column-title" :class="computedTitleClasses">
{{ columnTitle }}
@@ -206,8 +218,12 @@ export default {
<li
v-for="pipeline in linkedPipelines"
:key="pipeline.id"
- class="gl-display-flex gl-mb-3"
- :class="{ 'gl-flex-direction-row-reverse': isUpstream }"
+ class="gl-display-flex"
+ :class="{
+ 'gl-mb-3': !isNewPipelineGraph,
+ 'gl-flex-wrap gl-sm-flex-nowrap gl-mb-6': isNewPipelineGraph,
+ 'gl-flex-direction-row-reverse': !isNewPipelineGraph && isUpstream,
+ }"
>
<linked-pipeline
class="gl-display-inline-block"
@@ -224,12 +240,15 @@ export default {
<div
v-if="showContainer(pipeline.id)"
:style="{ minWidth }"
- class="gl-display-inline-block"
+ class="gl-display-inline-block pipeline-show-container"
>
<pipeline-graph
v-if="isExpanded(pipeline.id)"
:type="type"
- class="gl-inline-block gl-mt-n2"
+ class="gl-inline-block"
+ :class="{
+ 'gl-mt-n2': !isNewPipelineGraph,
+ }"
:config-paths="configPaths"
:pipeline="currentPipeline"
:computed-pipeline-info="getPipelineLayers(pipeline.id)"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue
index bcd7705669e..7c07591d0de 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue
@@ -1,5 +1,12 @@
<script>
+import { GlCard } from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
export default {
+ components: {
+ GlCard,
+ },
+ mixins: [glFeatureFlagMixin()],
props: {
stageClasses: {
type: String,
@@ -12,18 +19,37 @@ export default {
default: '',
},
},
+ computed: {
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
+ },
};
</script>
<template>
<div>
- <div class="gl-display-flex gl-align-items-center gl-w-full gl-mb-5" :class="stageClasses">
- <slot name="stages"> </slot>
- </div>
- <div
- class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full"
- :class="jobClasses"
+ <gl-card
+ v-if="isNewPipelineGraph"
+ class="gl-rounded-lg"
+ header-class="gl-rounded-lg gl-px-0 gl-py-0 gl-bg-white gl-border-b-0"
+ body-class="gl-pt-2 gl-pb-0 gl-px-2"
>
- <slot name="jobs"> </slot>
- </div>
+ <template #header>
+ <slot name="stages"></slot>
+ </template>
+
+ <slot name="jobs"></slot>
+ </gl-card>
+ <template v-else>
+ <div class="gl-display-flex gl-align-items-center gl-w-full" :class="stageClasses">
+ <slot name="stages"> </slot>
+ </div>
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full"
+ :class="jobClasses"
+ >
+ <slot name="jobs"> </slot>
+ </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
index 6030adc96ad..01a9c6d030d 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
@@ -68,7 +68,7 @@ export default {
required: true,
},
},
- jobClasses: [
+ legacyJobClasses: [
'gl-p-3',
'gl-border-gray-100',
'gl-border-solid',
@@ -82,18 +82,43 @@ export default {
'gl-hover-border-gray-200',
'gl-focus-border-gray-200',
],
- titleClasses: [
+ jobClasses: [
+ 'gl-p-3',
+ 'gl-border-0',
+ 'gl-bg-transparent',
+ 'gl-rounded-base',
+ 'gl-hover-bg-gray-50',
+ 'gl-focus-bg-gray-50',
+ 'gl-hover-text-gray-900',
+ 'gl-focus-text-gray-900',
+ ],
+ legacyTitleClasses: [
'gl-font-weight-bold',
'gl-pipeline-job-width',
'gl-text-truncate',
'gl-line-height-36',
'gl-pl-3',
],
+ titleClasses: [
+ 'gl-font-weight-bold',
+ 'gl-pipeline-job-width',
+ 'gl-text-truncate',
+ 'gl-line-height-36',
+ 'gl-pl-4',
+ 'gl-mb-n2',
+ ],
computed: {
canUpdatePipeline() {
return this.userPermissions.updatePipeline;
},
columnSpacingClass() {
+ if (this.isNewPipelineGraph) {
+ const baseClasses = 'stage-column gl-relative gl-flex-basis-full';
+ return this.isStageView
+ ? `${baseClasses} is-stage-view gl-m-5`
+ : `${baseClasses} gl-my-5 gl-mx-7`;
+ }
+
return this.isStageView ? 'gl-px-6' : 'gl-px-9';
},
hasAction() {
@@ -102,6 +127,17 @@ export default {
showStageName() {
return !this.isStageView;
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
+ jobClasses() {
+ return this.isNewPipelineGraph ? this.$options.jobClasses : this.$options.legacyJobClasses;
+ },
+ titleClasses() {
+ return this.isNewPipelineGraph
+ ? this.$options.titleClasses
+ : this.$options.legacyTitleClasses;
+ },
},
errorCaptured(err, _vm, info) {
reportToSentry('stage_column_component', `error: ${err}, info: ${info}`);
@@ -135,12 +171,16 @@ export default {
};
</script>
<template>
- <root-graph-layout :class="columnSpacingClass" data-testid="stage-column">
+ <root-graph-layout
+ :class="columnSpacingClass"
+ class="stage-column gl-relative gl-flex-basis-full"
+ data-testid="stage-column"
+ >
<template #stages>
<div
data-testid="stage-column-title"
- class="gl-display-flex gl-justify-content-space-between gl-relative"
- :class="$options.titleClasses"
+ class="stage-column-title gl-display-flex gl-justify-content-space-between gl-relative"
+ :class="titleClasses"
>
<span :title="name" class="gl-text-truncate gl-pr-3 gl-w-85p">
{{ name }}
@@ -161,7 +201,11 @@ export default {
:id="groupId(group)"
:key="getGroupId(group)"
data-testid="stage-column-group"
- class="gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width"
+ class="gl-relative gl-white-space-normal gl-pipeline-job-width"
+ :class="{
+ 'gl-mb-3': !isNewPipelineGraph,
+ 'gl-mb-2': isNewPipelineGraph,
+ }"
@mouseenter="$emit('jobHover', group.name)"
@mouseleave="$emit('jobHover', '')"
>
@@ -174,7 +218,7 @@ export default {
:pipeline-expanded="pipelineExpanded"
:pipeline-id="pipelineId"
:stage-name="showStageName ? group.stageName : ''"
- :css-class-job-name="$options.jobClasses"
+ :css-class-job-name="jobClasses"
:class="[
{ 'gl-opacity-3': isFadedOut(group.name) },
'gl-transition-duration-slow gl-transition-timing-function-ease',
@@ -188,7 +232,7 @@ export default {
:group="group"
:stage-name="showStageName ? group.stageName : ''"
:pipeline-id="pipelineId"
- :css-class-job-name="$options.jobClasses"
+ :css-class-job-name="jobClasses"
/>
</div>
</div>
diff --git a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
index 51a68f6619a..651662d6395 100644
--- a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
+++ b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
@@ -17,7 +17,7 @@ import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility'; // eslint-
import { __, s__, sprintf, formatNumber } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants';
@@ -38,7 +38,7 @@ export default {
pipelineRetry: 'pipelineRetry',
finishedStatuses: ['FAILED', 'SUCCESS', 'CANCELED'],
components: {
- CiBadgeLink,
+ CiIcon,
ClipboardButton,
GlAlert,
GlBadge,
@@ -58,13 +58,17 @@ export default {
i18n: {
scheduleBadgeText: s__('Pipelines|Scheduled'),
scheduleBadgeTooltip: __('This pipeline was created by a schedule'),
+ triggerBadgeText: __('trigger token'),
+ triggerBadgeTooltip: __(
+ 'This pipeline was created by an API call authenticated with a trigger token',
+ ),
childBadgeText: s__('Pipelines|Child pipeline (%{linkStart}parent%{linkEnd})'),
childBadgeTooltip: __('This is a child pipeline within the parent pipeline'),
latestBadgeText: s__('Pipelines|latest'),
latestBadgeTooltip: __('Latest pipeline for the most recent commit on this branch'),
mergeTrainBadgeText: s__('Pipelines|merge train'),
mergeTrainBadgeTooltip: s__(
- 'Pipelines|This pipeline ran on the contents of this merge request combined with the contents of all other merge requests queued for merging into the target branch.',
+ 'Pipelines|This pipeline ran on the contents of the merge request combined with the contents of all other merge requests queued for merging into the target branch.',
),
invalidBadgeText: s__('Pipelines|yaml invalid'),
failedBadgeText: s__('Pipelines|error'),
@@ -74,7 +78,11 @@ export default {
),
detachedBadgeText: s__('Pipelines|merge request'),
detachedBadgeTooltip: s__(
- "Pipelines|This pipeline ran on the contents of this merge request's source branch, not the target branch.",
+ "Pipelines|This pipeline ran on the contents of the merge request's source branch, not the target branch.",
+ ),
+ mergedResultsBadgeText: s__('Pipelines|merged results'),
+ mergedResultsBadgeTooltip: s__(
+ 'Pipelines|This pipeline ran on the contents of the merge request combined with the contents of the target branch.',
),
stuckBadgeText: s__('Pipelines|stuck'),
stuckBadgeTooltip: s__('Pipelines|This pipeline is stuck'),
@@ -403,7 +411,7 @@ export default {
{{ commitTitle }}
</h3>
<div>
- <ci-badge-link :status="detailedStatus" class="gl-display-inline-block gl-mb-3" />
+ <ci-icon :status="detailedStatus" show-status-text :show-link="false" class="gl-mb-3" />
<div class="gl-ml-2 gl-mb-3 gl-display-inline-block gl-h-6">
<gl-link
v-if="user"
@@ -458,6 +466,15 @@ export default {
{{ $options.i18n.scheduleBadgeText }}
</gl-badge>
<gl-badge
+ v-if="badges.trigger"
+ v-gl-tooltip
+ :title="$options.i18n.triggerBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.triggerBadgeText }}
+ </gl-badge>
+ <gl-badge
v-if="badges.child"
v-gl-tooltip
:title="$options.i18n.childBadgeTooltip"
@@ -527,6 +544,15 @@ export default {
{{ $options.i18n.detachedBadgeText }}
</gl-badge>
<gl-badge
+ v-if="badges.mergedResultsPipeline"
+ v-gl-tooltip
+ :title="$options.i18n.mergedResultsBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.mergedResultsBadgeText }}
+ </gl-badge>
+ <gl-badge
v-if="badges.stuck"
v-gl-tooltip
:title="$options.i18n.stuckBadgeTooltip"
diff --git a/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue b/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
index 4752fbb3e96..287f6e045c6 100644
--- a/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
@@ -5,7 +5,7 @@ import { __, s__ } from '~/locale';
import { createAlert } from '~/alert';
import Tracking from '~/tracking';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { TRACKING_CATEGORIES } from '~/ci/constants';
import RetryFailedJobMutation from '../graphql/mutations/retry_failed_job.mutation.graphql';
import { DEFAULT_FIELDS } from '../../constants';
@@ -14,7 +14,7 @@ export default {
fields: DEFAULT_FIELDS,
retry: __('Retry'),
components: {
- CiBadgeLink,
+ CiIcon,
GlButton,
GlLink,
GlTableLite,
@@ -80,7 +80,7 @@ export default {
<div
class="gl-display-flex gl-align-items-center gl-lg-justify-content-start gl-justify-content-end"
>
- <ci-badge-link :status="item.detailedStatus" :show-text="false" class="gl-mr-3" />
+ <ci-icon :status="item.detailedStatus" class="gl-mr-3" />
<div class="gl-text-truncate">
<gl-link
:href="item.detailedStatus.detailsPath"
diff --git a/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
index 067ec3f305e..4966b657887 100644
--- a/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
@@ -23,9 +23,11 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph
failureReason,
triggeredByPath,
schedule,
+ trigger,
child,
latest,
mergeTrainPipeline,
+ mergedResultsPipeline,
invalid,
failed,
autoDevops,
@@ -59,9 +61,11 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph
refText,
badges: {
schedule: parseBoolean(schedule),
+ trigger: parseBoolean(trigger),
child: parseBoolean(child),
latest: parseBoolean(latest),
mergeTrainPipeline: parseBoolean(mergeTrainPipeline),
+ mergedResultsPipeline: parseBoolean(mergedResultsPipeline),
invalid: parseBoolean(invalid),
failed: parseBoolean(failed),
autoDevops: parseBoolean(autoDevops),
diff --git a/app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js b/app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js
index c3be487caae..63a46d81dd5 100644
--- a/app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js
@@ -2,10 +2,5 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
export const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- useGet: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
diff --git a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
index 8a7c3367fc1..ea2875713a9 100644
--- a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
@@ -42,8 +42,6 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
projectId,
defaultBranchName,
params,
- iosRunnersAvailable,
- registrationToken,
fullPath,
visibilityPipelineIdType,
} = el.dataset;
@@ -55,7 +53,6 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
artifactsEndpoint,
artifactsEndpointPlaceholder,
fullPath,
- iosRunnersAvailable: parseBoolean(iosRunnersAvailable),
manualActionsLimit: 50,
pipelineEditorPath,
pipelineSchedulesPath,
@@ -84,7 +81,6 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
newPipelinePath,
params: JSON.parse(params),
projectId,
- registrationToken,
resetCachePath,
store: this.store,
},
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
index 8f4d566e7e6..204eaf20664 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
@@ -80,7 +80,7 @@ export default {
<template>
<div
- class="gl-display-flex gl-p-3 gl-gap-3 gl-border-solid gl-border-gray-100 gl-border-1 gl-sm-flex-direction-column"
+ class="gl-display-flex gl-p-3 gl-gap-3 gl-border-solid gl-border-gray-100 gl-border-1 gl-flex-direction-column gl-md-flex-direction-row"
>
<slot></slot>
<gl-button
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
index 221a45d4d9a..21e21d54758 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
@@ -1,13 +1,5 @@
<script>
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlInfiniteScroll,
- GlLoadingIcon,
- GlSearchBoxByType,
- GlTooltipDirective,
-} from '@gitlab/ui';
+import { GlCollapsibleListbox, GlTooltipDirective } from '@gitlab/ui';
import { produce } from 'immer';
import { historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
@@ -25,17 +17,11 @@ import getLastCommitBranch from '~/ci/pipeline_editor/graphql/queries/client/las
export default {
i18n: {
dropdownHeader: __('Switch branch'),
- title: __('Branches'),
fetchError: __('Unable to fetch branch list for this project.'),
},
inputDebounce: BRANCH_SEARCH_DEBOUNCE,
components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlInfiniteScroll,
- GlLoadingIcon,
- GlSearchBoxByType,
+ GlCollapsibleListbox,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -66,6 +52,7 @@ export default {
pageCounter: 0,
searchTerm: '',
lastCommitBranch: '',
+ infiniteScrollLoading: false,
};
},
apollo: {
@@ -112,6 +99,18 @@ export default {
},
},
computed: {
+ infiniteScrollEnabled() {
+ return this.availableBranches.length > 0;
+ },
+ branchesData() {
+ return this.availableBranches.map((branch) => ({
+ text: branch,
+ extraAttrs: {
+ 'data-qa-selector': 'branch_menu_item_button',
+ },
+ value: branch,
+ }));
+ },
availableBranchesVariables() {
if (this.searchTerm.length > 0) {
return {
@@ -128,7 +127,7 @@ export default {
enableBranchSwitcher() {
return this.availableBranches.length > 0 || this.searchTerm.length > 0;
},
- isBranchesLoading() {
+ areBranchesLoading() {
return this.$apollo.queries.availableBranches.loading;
},
},
@@ -143,7 +142,7 @@ export default {
// if there is no searchPattern, paginate by {paginationLimit} branches
fetchNextBranches() {
if (
- this.isBranchesLoading ||
+ this.areBranchesLoading ||
this.searchTerm.length > 0 ||
this.availableBranches.length >= this.totalBranches
) {
@@ -178,16 +177,14 @@ export default {
this.$emit('refetchContent');
},
selectBranch(newBranch) {
- if (newBranch !== this.currentBranch) {
- // If there are unsaved changes, we want to show the user
- // a modal to confirm what to do with these before changing
- // branches.
- if (this.hasUnsavedChanges) {
- this.branchSelected = newBranch;
- this.$emit('select-branch', newBranch);
- } else {
- this.changeBranch(newBranch);
- }
+ // If there are unsaved changes, we want to show the user
+ // a modal to confirm what to do with these before changing
+ // branches.
+ if (this.hasUnsavedChanges) {
+ this.branchSelected = newBranch;
+ this.$emit('select-branch', newBranch);
+ } else {
+ this.changeBranch(newBranch);
}
},
async setSearchTerm(newSearchTerm) {
@@ -211,41 +208,23 @@ export default {
</script>
<template>
- <gl-dropdown
+ <gl-collapsible-listbox
+ v-model="currentBranch"
v-gl-tooltip.hover
+ data-qa-selector="branch_selector_button"
+ searchable
+ :items="branchesData"
:title="$options.i18n.dropdownHeader"
:header-text="$options.i18n.dropdownHeader"
- :text="currentBranch"
+ :toggle-text="currentBranch"
:disabled="!enableBranchSwitcher"
icon="branch"
data-testid="branch-selector"
- >
- <gl-search-box-by-type :debounce="$options.inputDebounce" @input="setSearchTerm" />
- <gl-dropdown-section-header>
- {{ $options.i18n.title }}
- </gl-dropdown-section-header>
-
- <gl-infinite-scroll
- :fetched-items="availableBranches.length"
- :max-list-height="250"
- @bottomReached="fetchNextBranches"
- >
- <template #items>
- <gl-dropdown-item
- v-for="branch in availableBranches"
- :key="branch"
- :is-checked="currentBranch === branch"
- is-check-item
- @click="selectBranch(branch)"
- >
- {{ branch }}
- </gl-dropdown-item>
- </template>
- <template #default>
- <gl-dropdown-item v-if="isBranchesLoading" key="loading">
- <gl-loading-icon size="lg" />
- </gl-dropdown-item>
- </template>
- </gl-infinite-scroll>
- </gl-dropdown>
+ :no-results-text="$options.i18n.fetchError"
+ :infinite-scroll-loading="areBranchesLoading"
+ :infinite-scroll="infiniteScrollEnabled"
+ @select="selectBranch"
+ @search="setSearchTerm"
+ @bottom-reached="fetchNextBranches"
+ />
</template>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
index 44cf11acfe2..7c4a07e3f83 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
@@ -7,7 +7,7 @@ import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.quer
import getPipelineEtag from '~/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import PipelineMiniGraph from '~/ci/pipeline_mini_graph/pipeline_mini_graph.vue';
import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
@@ -25,7 +25,7 @@ export const i18n = {
export default {
i18n,
components: {
- CiBadgeLink,
+ CiIcon,
GlButton,
GlIcon,
GlLink,
@@ -155,14 +155,7 @@ export default {
</template>
<template v-else>
<div class="gl-text-truncate gl-md-max-w-50p gl-mr-1">
- <a :href="status.detailsPath" class="gl-mr-auto">
- <ci-badge-link
- :status="status"
- size="md"
- :show-text="false"
- data-testid="pipeline-status-icon"
- />
- </a>
+ <ci-icon :status="status" data-testid="pipeline-status-icon" />
<span class="gl-font-weight-bold">
<gl-sprintf :message="$options.i18n.pipelineInfo">
<template #id="{ content }">
diff --git a/app/assets/javascripts/ci/pipeline_editor/options.js b/app/assets/javascripts/ci/pipeline_editor/options.js
index 922c8eee8fc..340cb6ab979 100644
--- a/app/assets/javascripts/ci/pipeline_editor/options.js
+++ b/app/assets/javascripts/ci/pipeline_editor/options.js
@@ -55,7 +55,6 @@ export const createAppOptions = (el) => {
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(resolvers, {
typeDefs,
- useGet: true,
}),
});
const { cache } = apolloProvider.clients.defaultClient;
diff --git a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
index 41e5199e204..09ba6292e13 100644
--- a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
@@ -168,7 +168,7 @@ export default {
@toggle-file-tree="toggleFileTree"
v-on="$listeners"
/>
- <div class="gl-display-flex gl-w-full gl-sm-flex-direction-column">
+ <div class="gl-display-flex gl-w-full gl-flex-direction-column gl-md-flex-direction-row">
<pipeline-editor-file-tree
v-if="showFileTree"
class="gl-flex-shrink-0"
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
index d20d4aec59d..4fded3aec60 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
@@ -132,7 +132,6 @@ export default {
<template>
<div
class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between"
- data-qa-selector="job_item_container"
>
<gl-link
v-if="hasDetails"
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
index 34640d49b80..ed78a335453 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
@@ -13,7 +13,7 @@
*/
import { GlDropdown, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { createAlert } from '~/alert';
import eventHub from '~/ci/event_hub';
import axios from '~/lib/utils/axios_utils';
@@ -33,7 +33,7 @@ export default {
positionFixed: true,
},
components: {
- CiBadgeLink,
+ CiIcon,
GlLoadingIcon,
GlDropdown,
LegacyJobItem,
@@ -126,14 +126,7 @@ export default {
@show="onShowDropdown"
>
<template #button-content>
- <ci-badge-link
- :status="stage.status"
- size="md"
- :show-text="false"
- :show-tooltip="false"
- :use-link="false"
- class="gl-mb-0!"
- />
+ <ci-icon :status="stage.status" :show-tooltip="false" :use-link="false" class="gl-mb-0!" />
</template>
<div v-if="isLoading" class="gl--flex-center gl-p-2" data-testid="pipeline-stage-loading-state">
<gl-loading-icon size="sm" class="gl-mr-3" />
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
index cc703d29e23..f6a375ab94c 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { accessValue } from './accessors/linked_pipelines_accessors';
/**
* Renders the upstream/downstream portions of the pipeline mini graph.
@@ -11,7 +11,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- CiBadgeLink,
+ CiIcon,
},
inject: {
dataMethod: {
@@ -81,11 +81,6 @@ export default {
// detailedStatus is graphQL, details.status is REST
return pipeline?.detailedStatus || pipeline?.details?.status;
},
- triggerButtonClass(pipeline) {
- const { group } = accessValue(pipeline, this.dataMethod, 'detailedStatus');
-
- return `ci-status-icon-${group}`;
- },
},
};
</script>
@@ -99,15 +94,12 @@ export default {
}"
class="linked-pipeline-mini-list gl-display-inline gl-vertical-align-middle"
>
- <ci-badge-link
+ <ci-icon
v-for="pipeline in linkedPipelinesTrimmed"
:key="pipeline.id"
v-gl-tooltip="{ title: pipelineTooltipText(pipeline) }"
:status="pipelineStatus(pipeline)"
- size="md"
- :show-text="false"
:show-tooltip="false"
- :class="triggerButtonClass(pipeline)"
class="linked-pipeline-mini-item gl-mb-0!"
data-testid="linked-pipeline-mini-item"
/>
diff --git a/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
index 2f06b82bac0..722dc29d746 100644
--- a/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
@@ -13,9 +13,9 @@ import {
GlSprintf,
GlLoadingIcon,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { uniqueId } from 'lodash';
import Vue from 'vue';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { fetchPolicies } from '~/lib/graphql';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
index cd1d9a97ef3..5444e66cbdf 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
@@ -371,11 +371,7 @@ export default {
</gl-form-group>
<!--Variable List-->
<gl-form-group class="gl-mb-0" :label="$options.i18n.variables">
- <div
- v-for="(variable, index) in variables"
- :key="`var-${index}`"
- data-qa-selector="ci_variable_row_container"
- >
+ <div v-for="(variable, index) in variables" :key="`var-${index}`">
<div
v-if="!variable.destroy"
class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row gl-mb-3 gl-pb-2"
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
index ed7c2bbeb73..78df7298f4f 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
@@ -61,6 +61,7 @@ export default {
v-if="canPlay"
v-gl-tooltip
:title="$options.i18n.playTooltip"
+ :aria-label="$options.i18n.playTooltip"
icon="play"
data-testid="play-pipeline-schedule-btn"
@click="$emit('playPipelineSchedule', schedule.id)"
@@ -78,6 +79,7 @@ export default {
v-gl-tooltip
:href="editPathWithIdParam"
:title="$options.i18n.editTooltip"
+ :aria-label="$options.i18n.editTooltip"
icon="pencil"
data-testid="edit-pipeline-schedule-btn"
/>
@@ -85,6 +87,7 @@ export default {
v-if="canRemove"
v-gl-tooltip
:title="$options.i18n.deleteTooltip"
+ :aria-label="$options.i18n.deleteTooltip"
icon="remove"
variant="danger"
data-testid="delete-pipeline-schedule-btn"
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
index 92f461c72d7..d979c0efaf2 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
@@ -1,9 +1,9 @@
<script>
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
},
props: {
schedule: {
@@ -24,9 +24,10 @@ export default {
<template>
<div data-testid="last-pipeline-status">
- <ci-badge-link
+ <ci-icon
v-if="hasPipeline"
:status="lastPipelineStatus"
+ show-status-text
class="gl-vertical-align-middle"
/>
<span v-else data-testid="pipeline-schedule-status-text">
diff --git a/app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue
deleted file mode 100644
index 1a2021df9c8..00000000000
--- a/app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue
+++ /dev/null
@@ -1,220 +0,0 @@
-<script>
-import { GlButton, GlCard, GlSprintf, GlLink, GlPopover, GlModalDirective } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { mergeUrlParams, DOCS_URL } from '~/lib/utils/url_utility';
-import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
-import apolloProvider from '~/ci/pipeline_details/graphql/provider';
-import CiTemplates from './ci_templates.vue';
-
-export default {
- components: {
- GlButton,
- GlCard,
- GlSprintf,
- GlLink,
- GlPopover,
- RunnerInstructionsModal,
- CiTemplates,
- },
- directives: {
- GlModalDirective,
- },
- inject: ['pipelineEditorPath', 'iosRunnersAvailable'],
- props: {
- registrationToken: {
- type: String,
- required: false,
- default: null,
- },
- },
- apolloProvider,
- iOSTemplateName: 'iOS-Fastlane',
- modalId: 'runner-instructions-modal',
- runnerDocsLink: `${DOCS_URL}/runner/install/osx`,
- whatElseLink: helpPagePath('ci/index.md'),
- i18n: {
- title: s__('Pipelines|Get started with GitLab CI/CD'),
- subtitle: s__('Pipelines|Building for iOS?'),
- explanation: s__("Pipelines|We'll walk you through how to deploy to iOS in two easy steps."),
- runnerSetupTitle: s__('Pipelines|1. Set up a runner'),
- runnerSetupButton: s__('Pipelines|Set up a runner'),
- runnerSetupBodyUnfinished: s__(
- 'Pipelines|GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline.',
- ),
- runnerSetupBodyFinished: s__(
- 'Pipelines|You have runners available to run your job now. No need to do anything else.',
- ),
- runnerSetupPopoverTitle: s__(
- "Pipelines|Let's get that runner set up! %{emojiStart}tada%{emojiEnd}",
- ),
- runnerSetupPopoverBodyLine1: s__(
- 'Pipelines|Follow these instructions to install GitLab Runner on macOS.',
- ),
- runnerSetupPopoverBodyLine2: s__(
- 'Pipelines|Need more information to set up your runner? %{linkStart}Check out our documentation%{linkEnd}.',
- ),
- configurePipelineTitle: s__('Pipelines|2. Configure deployment pipeline'),
- configurePipelineBody: s__("Pipelines|We'll guide you through a simple pipeline set-up."),
- configurePipelineButton: s__('Pipelines|Configure pipeline'),
- noWalkthroughTitle: s__("Pipelines|Don't need a guide? Jump in right away with a template."),
- noWalkthroughExplanation: s__('Pipelines|Based on your project, we recommend this template:'),
- notBuildingForIos: s__(
- "Pipelines|Not building for iOS or not what you're looking for? %{linkStart}See what else%{linkEnd} GitLab CI/CD has to offer.",
- ),
- },
- data() {
- return {
- isModalShown: false,
- isPopoverShown: false,
- isRunnerSetupFinished: this.iosRunnersAvailable,
- popoverTarget: `${this.$options.modalId}___BV_modal_content_`,
- configurePipelineLink: mergeUrlParams(
- { template: this.$options.iOSTemplateName },
- this.pipelineEditorPath,
- ),
- };
- },
- computed: {
- runnerSetupBodyText() {
- return this.iosRunnersAvailable
- ? this.$options.i18n.runnerSetupBodyFinished
- : this.$options.i18n.runnerSetupBodyUnfinished;
- },
- },
- methods: {
- showModal() {
- this.isModalShown = true;
- },
- hideModal() {
- this.togglePopover();
- this.isRunnerSetupFinished = true;
- },
- togglePopover() {
- this.isPopoverShown = !this.isPopoverShown;
- },
- },
-};
-</script>
-
-<template>
- <div>
- <h2 class="gl-font-size-h2 gl-text-gray-900">{{ $options.i18n.title }}</h2>
- <h3 class="gl-font-lg gl-text-gray-900 gl-mt-1">{{ $options.i18n.subtitle }}</h3>
- <p>{{ $options.i18n.explanation }}</p>
-
- <div class="gl-lg-display-flex">
- <div class="gl-lg-display-flex gl-lg-w-25p gl-lg-pr-4 gl-mb-4">
- <gl-card body-class="gl-display-flex gl-flex-grow-1">
- <div
- class="gl-display-flex gl-flex-grow-1 gl-flex-direction-column gl-justify-content-space-between gl-align-items-flex-start"
- >
- <div>
- <div class="gl-py-5">
- <gl-emoji
- v-show="isRunnerSetupFinished"
- class="gl-font-size-h2-xl"
- data-name="white_check_mark"
- data-testid="runner-setup-marked-completed"
- />
- <gl-emoji
- v-show="!isRunnerSetupFinished"
- class="gl-font-size-h2-xl"
- data-name="tools"
- data-testid="runner-setup-marked-todo"
- />
- </div>
- <span class="gl-text-gray-800 gl-font-weight-bold">
- {{ $options.i18n.runnerSetupTitle }}
- </span>
- <p class="gl-font-sm gl-mt-3">{{ runnerSetupBodyText }}</p>
- </div>
-
- <gl-button
- v-if="!iosRunnersAvailable"
- v-gl-modal-directive="$options.modalId"
- category="primary"
- variant="confirm"
- @click="showModal"
- >
- {{ $options.i18n.runnerSetupButton }}
- </gl-button>
- <runner-instructions-modal
- v-if="isModalShown"
- :modal-id="$options.modalId"
- :registration-token="registrationToken"
- default-platform-name="osx"
- @shown="togglePopover"
- @hide="hideModal"
- />
- <gl-popover
- v-if="isPopoverShown"
- :show="true"
- :show-close-button="true"
- :target="popoverTarget"
- triggers="manual"
- placement="left"
- fallback-placement="clockwise"
- >
- <template #title>
- <gl-sprintf :message="$options.i18n.runnerSetupPopoverTitle">
- <template #emoji="{ content }">
- <gl-emoji class="gl-ml-2" :data-name="content" />
- </template>
- </gl-sprintf>
- </template>
- <div class="gl-mb-5">
- {{ $options.i18n.runnerSetupPopoverBodyLine1 }}
- </div>
- <gl-sprintf :message="$options.i18n.runnerSetupPopoverBodyLine2">
- <template #link="{ content }">
- <gl-link :href="$options.runnerDocsLink" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </gl-popover>
- </div>
- </gl-card>
- </div>
- <div class="gl-lg-display-flex gl-lg-w-25p gl-lg-pr-4 gl-mb-4">
- <gl-card body-class="gl-display-flex gl-flex-grow-1">
- <div
- class="gl-display-flex gl-flex-grow-1 gl-flex-direction-column gl-justify-content-space-between gl-align-items-flex-start"
- >
- <div>
- <div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="tools" /></div>
- <span class="gl-text-gray-800 gl-font-weight-bold">
- {{ $options.i18n.configurePipelineTitle }}
- </span>
- <p class="gl-font-sm gl-mt-3">{{ $options.i18n.configurePipelineBody }}</p>
- </div>
-
- <gl-button
- :disabled="!isRunnerSetupFinished"
- category="primary"
- variant="confirm"
- data-testid="configure-pipeline-link"
- :href="configurePipelineLink"
- >
- {{ $options.i18n.configurePipelineButton }}
- </gl-button>
- </div>
- </gl-card>
- </div>
- </div>
- <h3 class="gl-font-lg gl-text-gray-900 gl-mt-5">{{ $options.i18n.noWalkthroughTitle }}</h3>
- <p>{{ $options.i18n.noWalkthroughExplanation }}</p>
- <ci-templates
- :filter-templates="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
- $options.iOSTemplateName,
- ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
- :disabled="!isRunnerSetupFinished"
- />
- <p>
- <gl-sprintf :message="$options.i18n.notBuildingForIos">
- <template #link="{ content }">
- <gl-link :href="$options.whatElseLink">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
-</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
index 728e8541ae3..aed5f1d235d 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
@@ -1,9 +1,7 @@
<script>
import { GlEmptyState } from '@gitlab/ui';
import { s__ } from '~/locale';
-import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import PipelinesCiTemplates from './pipelines_ci_templates.vue';
-import IosTemplates from './ios_templates.vue';
export default {
i18n: {
@@ -12,9 +10,7 @@ export default {
name: 'PipelinesEmptyState',
components: {
GlEmptyState,
- GitlabExperiment,
PipelinesCiTemplates,
- IosTemplates,
},
props: {
emptyStateSvgPath: {
@@ -25,30 +21,15 @@ export default {
type: Boolean,
required: true,
},
- registrationToken: {
- type: String,
- required: false,
- default: null,
- },
},
};
</script>
<template>
- <div>
- <gitlab-experiment v-if="canSetCi" name="ios_specific_templates">
- <template #control>
- <pipelines-ci-templates />
- </template>
- <template #candidate>
- <ios-templates :registration-token="registrationToken" />
- </template>
- </gitlab-experiment>
- <gl-empty-state
- v-else
- title=""
- :svg-path="emptyStateSvgPath"
- :svg-height="null"
- :description="$options.i18n.noCiDescription"
- />
- </div>
+ <pipelines-ci-templates v-if="canSetCi" />
+ <gl-empty-state
+ v-else
+ :svg-path="emptyStateSvgPath"
+ :svg-height="null"
+ :description="$options.i18n.noCiDescription"
+ />
</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
index 8f45094eb74..31d8f207a63 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { SCHEDULE_ORIGIN } from '~/ci/pipeline_details/constants';
+import { SCHEDULE_ORIGIN, API_ORIGIN, TRIGGER_ORIGIN } from '../constants';
export default {
components: {
@@ -31,6 +31,9 @@ export default {
isScheduled() {
return this.pipeline.source === SCHEDULE_ORIGIN;
},
+ isTriggered() {
+ return this.pipeline.source === TRIGGER_ORIGIN;
+ },
isInFork() {
return Boolean(
this.targetProjectFullPath &&
@@ -50,6 +53,9 @@ export default {
autoDevopsHelpPath() {
return helpPagePath('topics/autodevops/index.md');
},
+ isApi() {
+ return this.pipeline.source === API_ORIGIN;
+ },
},
};
</script>
@@ -64,7 +70,16 @@ export default {
variant="info"
size="sm"
data-testid="pipeline-url-scheduled"
- >{{ __('Scheduled') }}</gl-badge
+ >{{ __('scheduled') }}</gl-badge
+ >
+ <gl-badge
+ v-if="isTriggered"
+ v-gl-tooltip
+ :title="__('This pipeline was created by an API call authenticated with a trigger token')"
+ variant="info"
+ size="sm"
+ data-testid="pipeline-url-triggered"
+ >{{ __('trigger token') }}</gl-badge
>
<gl-badge
v-if="pipeline.flags.latest"
@@ -185,5 +200,14 @@ export default {
data-testid="pipeline-url-fork"
>{{ __('fork') }}</gl-badge
>
+ <gl-badge
+ v-if="isApi"
+ v-gl-tooltip
+ :title="__('This pipeline was triggered using the api')"
+ variant="info"
+ size="sm"
+ data-testid="pipeline-api-badge"
+ >{{ s__('Pipeline|api') }}</gl-badge
+ >
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
index 20e2c7e9dce..380f8ce172f 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
@@ -1,12 +1,12 @@
<script>
import { TRACKING_CATEGORIES } from '~/ci/constants';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Tracking from '~/tracking';
import PipelinesTimeago from './time_ago.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
PipelinesTimeago,
},
mixins: [Tracking.mixin()],
@@ -31,7 +31,12 @@ export default {
<template>
<div>
- <ci-badge-link class="gl-mb-3" :status="pipelineStatus" @ciStatusBadgeClick="trackClick" />
+ <ci-icon
+ class="gl-mb-2"
+ :status="pipelineStatus"
+ show-status-text
+ @ciStatusBadgeClick="trackClick"
+ />
<pipelines-timeago :pipeline="pipeline" />
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue
index 2a73795db0a..a53c7cacae2 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue
@@ -29,9 +29,5 @@ export default {
<gl-avatar-link v-if="user" v-gl-tooltip :href="user.path" :title="user.name" class="gl-ml-3">
<gl-avatar :size="32" :src="user.avatar_url" />
</gl-avatar-link>
-
- <span v-else class="gl-ml-3">
- {{ s__('Pipelines|API') }}
- </span>
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/constants.js b/app/assets/javascripts/ci/pipelines_page/constants.js
index aa6ef8a25ee..438eda44afe 100644
--- a/app/assets/javascripts/ci/pipelines_page/constants.js
+++ b/app/assets/javascripts/ci/pipelines_page/constants.js
@@ -1,2 +1,5 @@
export const ANY_TRIGGER_AUTHOR = 'Any';
export const FILTER_PIPELINES_SEARCH_DELAY = 200;
+export const SCHEDULE_ORIGIN = 'schedule';
+export const API_ORIGIN = 'api';
+export const TRIGGER_ORIGIN = 'trigger';
diff --git a/app/assets/javascripts/ci/pipelines_page/pipelines.vue b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
index faa013079be..98e005a162f 100644
--- a/app/assets/javascripts/ci/pipelines_page/pipelines.vue
+++ b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
@@ -4,7 +4,7 @@ import NO_PIPELINES_SVG from '@gitlab/svgs/dist/illustrations/empty-state/empty-
import ERROR_STATE_SVG from '@gitlab/svgs/dist/illustrations/pipelines_failed.svg?url';
import { GlEmptyState, GlIcon, GlLoadingIcon, GlCollapsibleListbox } from '@gitlab/ui';
import { isEqual } from 'lodash';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert, VARIANT_INFO, VARIANT_WARNING } from '~/alert';
import { getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
@@ -88,11 +88,6 @@ export default {
type: Object,
required: true,
},
- registrationToken: {
- type: String,
- required: false,
- default: null,
- },
defaultVisibilityPipelineIdType: {
type: String,
required: false,
@@ -311,6 +306,12 @@ export default {
},
changeVisibilityPipelineIDType(idType) {
this.visibilityPipelineIdType = idType;
+ if (idType === PIPELINE_IID_KEY) {
+ this.track('pipelines_display_options', {
+ label: TRACKING_CATEGORIES.listbox,
+ property: idType,
+ });
+ }
if (isLoggedIn()) {
this.saveVisibilityPipelineIDType(idType);
@@ -404,7 +405,6 @@ export default {
v-else-if="stateToRender === $options.stateMap.emptyState"
:empty-state-svg-path="$options.noPipelinesSvgPath"
:can-set-ci="canCreatePipeline"
- :registration-token="registrationToken"
/>
<gl-empty-state
diff --git a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
index f0a41a5949e..97163c1f55c 100644
--- a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
@@ -4,7 +4,6 @@ import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
-import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
import { DEFAULT_PLATFORM, PARAM_KEY_PLATFORM, INSTANCE_TYPE } from '../constants';
@@ -14,7 +13,6 @@ export default {
name: 'AdminNewRunnerApp',
components: {
RegistrationCompatibilityAlert,
- RegistrationFeedbackBanner,
RunnerPlatformsRadioGroup,
RunnerCreateForm,
},
@@ -44,8 +42,6 @@ export default {
<template>
<div>
- <registration-feedback-banner />
-
<h1 class="gl-font-size-h2">{{ s__('Runners|New instance runner') }}</h1>
<registration-compatibility-alert :alert-key="$options.INSTANCE_TYPE" />
diff --git a/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
index 0ec94dc865f..1431f156c0e 100644
--- a/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
@@ -14,6 +14,7 @@ import {
import allRunnersQuery from 'ee_else_ce/ci/runner/graphql/list/all_runners.query.graphql';
import allRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/all_runners_count.query.graphql';
+import RunnerListHeader from '../components/runner_list_header.vue';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
@@ -28,6 +29,7 @@ import RunnerJobStatusBadge from '../components/runner_job_status_badge.vue';
import { pausedTokenConfig } from '../components/search_tokens/paused_token_config';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
+import { versionTokenConfig } from '../components/search_tokens/version_token_config';
import {
ADMIN_FILTERED_SEARCH_NAMESPACE,
INSTANCE_TYPE,
@@ -42,6 +44,7 @@ export default {
components: {
GlButton,
GlLink,
+ RunnerListHeader,
RegistrationDropdown,
RunnerFilteredSearchBar,
RunnerList,
@@ -78,9 +81,6 @@ export default {
apollo: {
runners: {
query: allRunnersQuery,
- context: {
- isSingleRequest: true,
- },
fetchPolicy: fetchPolicies.NETWORK_ONLY,
variables() {
return this.variables;
@@ -118,6 +118,7 @@ export default {
return [
pausedTokenConfig,
statusTokenConfig,
+ versionTokenConfig,
{
...tagTokenConfig,
recentSuggestionsStorageKey: `${this.$options.filteredSearchNamespace}-recent-tags`,
@@ -178,11 +179,9 @@ export default {
</script>
<template>
<div>
- <header class="gl-my-5 gl-display-flex gl-justify-content-space-between">
- <h2 class="gl-my-0 header-title">
- {{ s__('Runners|Runners') }}
- </h2>
- <div class="gl-display-flex gl-gap-3">
+ <runner-list-header>
+ <template #title>{{ s__('Runners|Runners') }}</template>
+ <template #actions>
<runner-dashboard-link />
<gl-button :href="newRunnerPath" variant="confirm">
{{ s__('Runners|New instance runner') }}
@@ -192,8 +191,9 @@ export default {
:type="$options.INSTANCE_TYPE"
placement="right"
/>
- </div>
- </header>
+ </template>
+ </runner-list-header>
+
<div
class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
>
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
index a80d6207be8..8a920c85e06 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
@@ -2,9 +2,9 @@
import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { sprintf, __, formatNumber } from '~/locale';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import RunnerCreatedAt from '../runner_created_at.vue';
import RunnerName from '../runner_name.vue';
import RunnerTags from '../runner_tags.vue';
import RunnerTypeBadge from '../runner_type_badge.vue';
@@ -15,8 +15,6 @@ import {
I18N_LOCKED_RUNNER_DESCRIPTION,
I18N_VERSION_LABEL,
I18N_LAST_CONTACT_LABEL,
- I18N_CREATED_AT_LABEL,
- I18N_CREATED_AT_BY_LABEL,
} from '../../constants';
import RunnerSummaryField from './runner_summary_field.vue';
@@ -26,13 +24,13 @@ export default {
GlSprintf,
TimeAgo,
RunnerSummaryField,
+ RunnerCreatedAt,
RunnerName,
RunnerTags,
RunnerTypeBadge,
RunnerManagersBadge,
RunnerUpgradeStatusIcon: () =>
import('ee_component/ci/runner/components/runner_upgrade_status_icon.vue'),
- UserAvatarLink,
TooltipOnTruncate,
},
directives: {
@@ -75,8 +73,6 @@ export default {
I18N_LOCKED_RUNNER_DESCRIPTION,
I18N_VERSION_LABEL,
I18N_LAST_CONTACT_LABEL,
- I18N_CREATED_AT_LABEL,
- I18N_CREATED_AT_BY_LABEL,
},
};
</script>
@@ -143,30 +139,7 @@ export default {
</runner-summary-field>
<runner-summary-field icon="calendar">
- <template v-if="createdBy">
- <gl-sprintf :message="$options.i18n.I18N_CREATED_AT_BY_LABEL">
- <template #timeAgo>
- <time-ago v-if="runner.createdAt" :time="runner.createdAt" />
- </template>
- <template #avatar>
- <user-avatar-link
- :link-href="createdBy.webUrl"
- :img-src="createdBy.avatarUrl"
- img-css-classes="gl-vertical-align-top"
- :img-size="16"
- :img-alt="createdByImgAlt"
- :tooltip-text="createdBy.username"
- />
- </template>
- </gl-sprintf>
- </template>
- <template v-else>
- <gl-sprintf :message="$options.i18n.I18N_CREATED_AT_LABEL">
- <template #timeAgo>
- <time-ago v-if="runner.createdAt" :time="runner.createdAt" />
- </template>
- </gl-sprintf>
- </template>
+ <runner-created-at :runner="runner" />
</runner-summary-field>
</div>
diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue b/app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue
deleted file mode 100644
index 6fd4edf5847..00000000000
--- a/app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<script>
-import ILLUSTRATION_URL from '@gitlab/svgs/dist/illustrations/rocket-launch-md.svg?url';
-import { GlBanner } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
-
-const FEEDBACK_ISSUE_URL = 'https://gitlab.com/gitlab-org/gitlab/-/issues/387993';
-
-export default {
- components: {
- GlBanner,
- UserCalloutDismisser,
- },
- i18n: {
- title: s__("Runners|We've made some changes and want your feedback"),
- body: s__(
- "Runners|We've been making improvements to how you register runners so that it's more secure and efficient. Tell us how we're doing.",
- ),
- button: s__('Runners|Add your feedback to this issue'),
- },
- ILLUSTRATION_URL,
- FEEDBACK_ISSUE_URL,
-};
-</script>
-<template>
- <user-callout-dismisser feature-name="create_runner_workflow_banner">
- <template #default="{ dismiss, shouldShowCallout }">
- <gl-banner
- v-if="shouldShowCallout"
- class="gl-my-6"
- :title="$options.i18n.title"
- :svg-path="$options.ILLUSTRATION_URL"
- :button-text="$options.i18n.button"
- :button-link="$options.FEEDBACK_ISSUE_URL"
- @close="dismiss"
- >
- <p>{{ $options.i18n.body }}</p>
- </gl-banner>
- </template>
- </user-callout-dismisser>
-</template>
diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
index 771ecb1a0d4..a4dec8199a3 100644
--- a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
+++ b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
@@ -100,11 +100,11 @@ export default {
tokenMessage() {
if (this.token) {
return s__(
- 'Runners|The %{boldStart}runner token%{boldEnd} %{token} displays %{boldStart}only for a short time%{boldEnd}, and is stored in the %{codeStart}config.toml%{codeEnd} after you register the runner. It will not be visible once the runner is registered.',
+ 'Runners|The %{boldStart}runner authentication token%{boldEnd} %{token} displays here %{boldStart}for a short time only%{boldEnd}. After you register the runner, this token is stored in the %{codeStart}config.toml%{codeEnd} and cannot be accessed again from the UI.',
);
}
return s__(
- 'Runners|The %{boldStart}runner token%{boldEnd} is no longer visible, it is stored in the %{codeStart}config.toml%{codeEnd} if you have registered the runner.',
+ 'Runners|The %{boldStart}runner authentication token%{boldEnd} is no longer visible, it is stored in the %{codeStart}config.toml%{codeEnd} if you have registered the runner.',
);
},
commandPrompt() {
diff --git a/app/assets/javascripts/ci/runner/components/runner_created_at.vue b/app/assets/javascripts/ci/runner/components/runner_created_at.vue
new file mode 100644
index 00000000000..410142a0eb5
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_created_at.vue
@@ -0,0 +1,72 @@
+<script>
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+
+import {
+ I18N_CREATED_AT_LABEL,
+ I18N_CREATED_BY_LABEL,
+ I18N_CREATED_BY_AT_LABEL,
+} from '../constants';
+
+export default {
+ components: {
+ GlSprintf,
+ GlLink,
+ TimeAgo,
+ },
+ props: {
+ runner: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ createdAt() {
+ return this.runner?.createdAt;
+ },
+ createdBy() {
+ return this.runner?.createdBy;
+ },
+ createdById() {
+ if (this.createdBy?.id) {
+ return getIdFromGraphQLId(this.createdBy.id);
+ }
+ return null;
+ },
+ message() {
+ if (this.createdBy && this.createdAt) {
+ return I18N_CREATED_BY_AT_LABEL;
+ }
+ if (this.createdBy) {
+ return I18N_CREATED_BY_LABEL;
+ }
+ if (this.createdAt) {
+ return I18N_CREATED_AT_LABEL;
+ }
+
+ return null;
+ },
+ },
+};
+</script>
+<template>
+ <span v-if="message">
+ <gl-sprintf :message="message">
+ <template #timeAgo>
+ <time-ago v-if="createdAt" :time="createdAt" />
+ </template>
+ <template #user>
+ <gl-link
+ class="js-user-link gl-reset-color gl-font-weight-bold"
+ :href="createdBy.webUrl"
+ :data-user-id="createdById"
+ :data-username="createdBy.username"
+ :data-name="createdBy.name"
+ :data-avatar-url="createdBy.avatarUrl"
+ >{{ createdBy.name }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </span>
+</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_details.vue b/app/assets/javascripts/ci/runner/components/runner_details.vue
index 0ec2ef30c20..477d28c6c28 100644
--- a/app/assets/javascripts/ci/runner/components/runner_details.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_details.vue
@@ -120,12 +120,9 @@ export default {
}}
</p>
<p class="gl-mb-0">
- <gl-link
- :href="tokenExpirationHelpUrl"
- target="_blank"
- class="gl-reset-font-size"
- >{{ __('Learn more') }}</gl-link
- >
+ <gl-link :href="tokenExpirationHelpUrl" target="_blank">{{
+ __('Learn more')
+ }}</gl-link>
</p>
</help-popover>
</template>
@@ -156,12 +153,9 @@ export default {
"
>
<template #link="{ content }"
- ><gl-link
- :href="$options.RUNNER_MANAGERS_HELP_URL"
- target="_blank"
- class="gl-reset-font-size"
- >{{ content }}</gl-link
- ></template
+ ><gl-link :href="$options.RUNNER_MANAGERS_HELP_URL" target="_blank">{{
+ content
+ }}</gl-link></template
>
</gl-sprintf>
</help-popover>
diff --git a/app/assets/javascripts/ci/runner/components/runner_header.vue b/app/assets/javascripts/ci/runner/components/runner_header.vue
index 0fa06537ed6..f8d0352e532 100644
--- a/app/assets/javascripts/ci/runner/components/runner_header.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_header.vue
@@ -1,16 +1,15 @@
<script>
-import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
-import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { I18N_LOCKED_RUNNER_DESCRIPTION } from '../constants';
import { formatRunnerName } from '../utils';
+import RunnerCreatedAt from './runner_created_at.vue';
import RunnerTypeBadge from './runner_type_badge.vue';
import RunnerStatusBadge from './runner_status_badge.vue';
export default {
components: {
GlIcon,
- GlSprintf,
- TimeAgo,
+ RunnerCreatedAt,
RunnerTypeBadge,
RunnerStatusBadge,
RunnerUpgradeStatusBadge: () =>
@@ -43,21 +42,13 @@ export default {
<runner-status-badge :contacted-at="runner.contactedAt" :status="runner.status" />
<runner-type-badge :type="runner.runnerType" />
<runner-upgrade-status-badge :runner="runner" />
- <span v-if="runner.createdAt">
- <gl-sprintf :message="__('%{locked} created %{timeago}')">
- <template #locked>
- <gl-icon
- v-if="runner.locked"
- v-gl-tooltip="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
- name="lock"
- :aria-label="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
- />
- </template>
- <template #timeago>
- <time-ago :time="runner.createdAt" />
- </template>
- </gl-sprintf>
- </span>
+ <gl-icon
+ v-if="runner.locked"
+ v-gl-tooltip="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
+ name="lock"
+ :aria-label="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
+ />
+ <runner-created-at :runner="runner" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
index 5d8e9dcdee2..653d9b05330 100644
--- a/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
@@ -3,7 +3,7 @@ import { GlTableLite } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { formatTime } from '~/lib/utils/datetime_utility';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import RunnerTags from '~/ci/runner/components/runner_tags.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { tableField } from '../utils';
@@ -11,7 +11,7 @@ import LinkCell from './cells/link_cell.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
GlTableLite,
LinkCell,
RunnerTags,
@@ -80,7 +80,7 @@ export default {
fixed
>
<template #cell(status)="{ item = {} }">
- <ci-badge-link v-if="item.detailedStatus" :status="item.detailedStatus" />
+ <ci-icon v-if="item.detailedStatus" :status="item.detailedStatus" show-status-text />
</template>
<template #cell(job)="{ item = {} }">
diff --git a/app/assets/javascripts/ci/runner/components/runner_list_header.vue b/app/assets/javascripts/ci/runner/components/runner_list_header.vue
new file mode 100644
index 00000000000..e4367db035e
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_list_header.vue
@@ -0,0 +1,17 @@
+<script>
+export default {
+ name: 'RunnerListHeader',
+};
+</script>
+<template>
+ <header
+ class="gl-my-5 gl-display-flex gl-align-items-flex-start gl-flex-wrap gl-justify-content-space-between"
+ >
+ <h1 v-if="$scopedSlots.title" class="gl-my-0 gl-font-size-h1 header-title">
+ <slot name="title"></slot>
+ </h1>
+ <div v-if="$scopedSlots.actions" class="gl-display-flex gl-gap-3">
+ <slot name="actions"></slot>
+ </div>
+ </header>
+</template>
diff --git a/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue b/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue
index dd1cca0a05c..1f61e878eb0 100644
--- a/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue
+++ b/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue
@@ -70,7 +70,7 @@ export default {
@fetch-suggestions="fetchTags"
v-on="$listeners"
>
- <template #view-token="{ viewTokenProps: { listeners, inputValue, activeTokenValue } }">
+ <template #view-token="{ viewTokenProps: { listeners = {}, inputValue, activeTokenValue } }">
<gl-token variant="search-value" :class="$options.RUNNER_TAG_BG_CLASS" v-on="listeners">
{{ activeTokenValue ? activeTokenValue.text : inputValue }}
</gl-token>
diff --git a/app/assets/javascripts/ci/runner/components/search_tokens/version_token_config.js b/app/assets/javascripts/ci/runner/components/search_tokens/version_token_config.js
new file mode 100644
index 00000000000..23f82d06f6d
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/search_tokens/version_token_config.js
@@ -0,0 +1,12 @@
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { PARAM_KEY_VERSION, I18N_VERSION } from '../../constants';
+
+export const versionTokenConfig = {
+ icon: 'doc-versions',
+ title: I18N_VERSION,
+ type: PARAM_KEY_VERSION,
+ token: BaseToken,
+ operators: OPERATORS_IS,
+ suggestionsDisabled: true,
+};
diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js
index b3cc295f8e4..d04d75b6e75 100644
--- a/app/assets/javascripts/ci/runner/constants.js
+++ b/app/assets/javascripts/ci/runner/constants.js
@@ -99,10 +99,14 @@ export const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted');
export const I18N_LOCKED_RUNNER_DESCRIPTION = s__(
'Runners|Runner is locked and available for currently assigned projects only. Only administrators can change the assigned projects.',
);
+export const I18N_VERSION = s__('Runners|Version starts with');
export const I18N_VERSION_LABEL = s__('Runners|Version %{version}');
export const I18N_LAST_CONTACT_LABEL = s__('Runners|Last contact: %{timeAgo}');
+
export const I18N_CREATED_AT_LABEL = s__('Runners|Created %{timeAgo}');
-export const I18N_CREATED_AT_BY_LABEL = s__('Runners|Created %{timeAgo} by %{avatar}');
+export const I18N_CREATED_BY_LABEL = s__('Runners|Created by %{user}');
+export const I18N_CREATED_BY_AT_LABEL = s__('Runners|Created by %{user} %{timeAgo}');
+
export const I18N_SHOW_ONLY_INHERITED = s__('Runners|Show only inherited');
export const I18N_ADMIN = s__('Runners|Administrator');
@@ -154,6 +158,7 @@ export const PARAM_KEY_STATUS = 'status';
export const PARAM_KEY_PAUSED = 'paused';
export const PARAM_KEY_RUNNER_TYPE = 'runner_type';
export const PARAM_KEY_TAG = 'tag';
+export const PARAM_KEY_VERSION = 'version_prefix';
export const PARAM_KEY_SEARCH = 'search';
export const PARAM_KEY_MEMBERSHIP = 'membership';
diff --git a/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql
index 41ec9967d90..5aa96f42b04 100644
--- a/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql
@@ -1,3 +1,5 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
fragment RunnerFieldsShared on CiRunner {
id
shortSha
@@ -10,5 +12,8 @@ fragment RunnerFieldsShared on CiRunner {
maximumTimeout
tagList
createdAt
+ createdBy {
+ ...User
+ }
status
}
diff --git a/app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql b/app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql
index 15401c25c64..628ebfd2029 100644
--- a/app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql
@@ -10,6 +10,7 @@ query getAllRunners(
$type: CiRunnerType
$tagList: [String!]
$search: String
+ $versionPrefix: String
$sort: CiRunnerSort
) {
runners(
@@ -22,6 +23,7 @@ query getAllRunners(
type: $type
tagList: $tagList
search: $search
+ versionPrefix: $versionPrefix
sort: $sort
) {
...AllRunnersConnection
diff --git a/app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql b/app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql
index 82591b88d3e..18f587495b0 100644
--- a/app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql
@@ -4,8 +4,16 @@ query getAllRunnersCount(
$type: CiRunnerType
$tagList: [String!]
$search: String
+ $versionPrefix: String
) {
- runners(paused: $paused, status: $status, type: $type, tagList: $tagList, search: $search) {
+ runners(
+ paused: $paused
+ status: $status
+ type: $type
+ tagList: $tagList
+ search: $search
+ versionPrefix: $versionPrefix
+ ) {
count
}
}
diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql
index e2c890b3834..8f998ab42fa 100644
--- a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql
@@ -1,3 +1,5 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
fragment RunnerDetailsShared on CiRunner {
id
shortSha
@@ -11,6 +13,9 @@ fragment RunnerDetailsShared on CiRunner {
jobCount
tagList
createdAt
+ createdBy {
+ ...User
+ }
status
contactedAt
tokenExpiresAt
diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql
index b6d6996a857..611de43b995 100644
--- a/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql
@@ -8,7 +8,7 @@ query getRunnerJobs($id: CiRunnerID!, $first: Int, $last: Int, $before: String,
nodes {
id
detailedStatus {
- # fields for `<ci-badge-link>`
+ # fields for `<ci-icon>`
id
detailsPath
group
diff --git a/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
index 2e1706ddae9..c907f9c8982 100644
--- a/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
@@ -4,7 +4,6 @@ import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
-import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
import { DEFAULT_PLATFORM, GROUP_TYPE, PARAM_KEY_PLATFORM } from '../constants';
@@ -14,7 +13,6 @@ export default {
name: 'GroupNewRunnerApp',
components: {
RegistrationCompatibilityAlert,
- RegistrationFeedbackBanner,
RunnerPlatformsRadioGroup,
RunnerCreateForm,
},
@@ -50,8 +48,6 @@ export default {
<template>
<div>
- <registration-feedback-banner />
-
<h1 class="gl-font-size-h2">{{ s__('Runners|New group runner') }}</h1>
<registration-compatibility-alert :alert-key="groupId" />
diff --git a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
index dcaf8635f5c..b5042936b1e 100644
--- a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
@@ -14,6 +14,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import groupRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/group_runners_count.query.graphql';
import groupRunnersQuery from 'ee_else_ce/ci/runner/graphql/list/group_runners.query.graphql';
+import RunnerListHeader from '../components/runner_list_header.vue';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
@@ -44,6 +45,7 @@ export default {
components: {
GlButton,
GlLink,
+ RunnerListHeader,
RegistrationDropdown,
RunnerFilteredSearchBar,
RunnerList,
@@ -86,9 +88,6 @@ export default {
apollo: {
runners: {
query: groupRunnersQuery,
- context: {
- isSingleRequest: true,
- },
fetchPolicy: fetchPolicies.NETWORK_ONLY,
variables() {
return this.variables;
@@ -212,11 +211,9 @@ export default {
<template>
<div>
- <header class="gl-my-5 gl-display-flex gl-justify-content-space-between">
- <h2 class="gl-my-0 header-title">
- {{ s__('Runners|Runners') }}
- </h2>
- <div class="gl-display-flex gl-gap-3">
+ <runner-list-header>
+ <template #title>{{ s__('Runners|Runners') }}</template>
+ <template #actions>
<gl-button
v-if="newRunnerPath"
:href="newRunnerPath"
@@ -231,8 +228,9 @@ export default {
:type="$options.GROUP_TYPE"
placement="right"
/>
- </div>
- </header>
+ </template>
+ </runner-list-header>
+
<div
class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
>
diff --git a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
index 51f5a9ce8d9..241479a8c98 100644
--- a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
@@ -4,7 +4,6 @@ import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
-import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
import { DEFAULT_PLATFORM, PARAM_KEY_PLATFORM, PROJECT_TYPE } from '../constants';
@@ -14,7 +13,6 @@ export default {
name: 'ProjectNewRunnerApp',
components: {
RegistrationCompatibilityAlert,
- RegistrationFeedbackBanner,
RunnerPlatformsRadioGroup,
RunnerCreateForm,
},
@@ -50,8 +48,6 @@ export default {
<template>
<div>
- <registration-feedback-banner />
-
<h1 class="gl-font-size-h2">{{ s__('Runners|New project runner') }}</h1>
<registration-compatibility-alert :alert-key="projectId" />
diff --git a/app/assets/javascripts/ci/runner/runner_search_utils.js b/app/assets/javascripts/ci/runner/runner_search_utils.js
index 8915198350f..e3aee15f42c 100644
--- a/app/assets/javascripts/ci/runner/runner_search_utils.js
+++ b/app/assets/javascripts/ci/runner/runner_search_utils.js
@@ -12,6 +12,7 @@ import {
PARAM_KEY_STATUS,
PARAM_KEY_RUNNER_TYPE,
PARAM_KEY_TAG,
+ PARAM_KEY_VERSION,
PARAM_KEY_SEARCH,
PARAM_KEY_MEMBERSHIP,
PARAM_KEY_SORT,
@@ -151,7 +152,12 @@ export const fromUrlQueryToSearch = (query = window.location.search) => {
membership: membership || DEFAULT_MEMBERSHIP,
filters: prepareTokens(
urlQueryToFilter(query, {
- filterNamesAllowList: [PARAM_KEY_PAUSED, PARAM_KEY_STATUS, PARAM_KEY_TAG],
+ filterNamesAllowList: [
+ PARAM_KEY_PAUSED,
+ PARAM_KEY_STATUS,
+ PARAM_KEY_TAG,
+ PARAM_KEY_VERSION,
+ ],
filteredSearchTermKey: PARAM_KEY_SEARCH,
}),
),
@@ -178,6 +184,7 @@ export const fromSearchToUrl = (
[PARAM_KEY_MEMBERSHIP]: [],
[PARAM_KEY_TAG]: [],
[PARAM_KEY_PAUSED]: [],
+ [PARAM_KEY_VERSION]: [],
// Current filters
...filterToQueryObject(processFilters(filters), {
filteredSearchTermKey: PARAM_KEY_SEARCH,
@@ -229,6 +236,7 @@ export const fromSearchToVariables = ({
[filterVariables.status] = queryObj[PARAM_KEY_STATUS] || [];
filterVariables.search = queryObj[PARAM_KEY_SEARCH];
filterVariables.tagList = queryObj[PARAM_KEY_TAG];
+ [filterVariables.versionPrefix] = queryObj[PARAM_KEY_VERSION] || [];
if (queryObj[PARAM_KEY_PAUSED]) {
filterVariables.paused = parseBoolean(queryObj[PARAM_KEY_PAUSED]);
diff --git a/app/assets/javascripts/ci/runner/sentry_utils.js b/app/assets/javascripts/ci/runner/sentry_utils.js
index 25fecdcfa7d..01a20880e0a 100644
--- a/app/assets/javascripts/ci/runner/sentry_utils.js
+++ b/app/assets/javascripts/ci/runner/sentry_utils.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
const COMPONENT_TAG = 'vue_component';
diff --git a/app/assets/javascripts/ci/utils.js b/app/assets/javascripts/ci/utils.js
index 8a4f28404c6..21361aedb9d 100644
--- a/app/assets/javascripts/ci/utils.js
+++ b/app/assets/javascripts/ci/utils.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
export const reportToSentry = (component, failureType) => {
Sentry.captureException(failureType, {
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 509bdabdd9e..fb2e24e15f6 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
@@ -12,7 +12,7 @@ import {
GlTable,
GlTooltipDirective,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Api, { DEFAULT_PER_PAGE } from '~/api';
import { HTTP_STATUS_PAYLOAD_TOO_LARGE } from '~/lib/utils/http_status';
import { __, s__, sprintf } from '~/locale';
diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
index 0871d543d46..e1f6006fedf 100644
--- a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
+++ b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
@@ -180,17 +180,11 @@ export default {
data-confirm-btn-variant="danger"
rel="nofollow"
data-testid="trigger_revoke_button"
- data-qa-selector="trigger_revoke_button"
:href="item.projectTriggerPath"
/>
</template>
</gl-table>
- <div
- v-else
- class="gl-new-card-empty gl-px-5 gl-py-4"
- data-testid="no_triggers_content"
- data-qa-selector="no_triggers_content"
- >
+ <div v-else class="gl-new-card-empty gl-px-5 gl-py-4" data-testid="no_triggers_content">
{{ s__('Pipelines|No triggers have been created yet. Add one using the form above.') }}
</div>
</div>
diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js
index 4537fd51fcf..f474e51622a 100644
--- a/app/assets/javascripts/clusters_list/store/actions.js
+++ b/app/assets/javascripts/clusters_list/store/actions.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js
index 4a9f79460da..fe6142ae145 100644
--- a/app/assets/javascripts/commons/gitlab_ui.js
+++ b/app/assets/javascripts/commons/gitlab_ui.js
@@ -5,6 +5,8 @@ applyGitLabUIConfig({
translations: {
'GlSearchBoxByType.input.placeholder': __('Search'),
'GlSearchBoxByType.clearButtonTitle': __('Clear'),
+ 'GlSorting.sortAscending': __('Sort direction: Ascending'),
+ 'GlSorting.sortDescending': __('Sort direction: Descending'),
'ClearIconButton.title': __('Clear'),
},
});
diff --git a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
index 6535d9eaa5d..b34ebe85eb4 100644
--- a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
+++ b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
@@ -161,6 +161,8 @@ export default {
},
onKeyDown({ event }) {
+ if (!this.items.length) return false;
+
if (event.key === 'ArrowUp') {
this.upHandler();
return true;
diff --git a/app/assets/javascripts/content_editor/content_editor.stories.js b/app/assets/javascripts/content_editor/content_editor.stories.js
index 1aa6568848f..6f7e9653e6e 100644
--- a/app/assets/javascripts/content_editor/content_editor.stories.js
+++ b/app/assets/javascripts/content_editor/content_editor.stories.js
@@ -30,4 +30,5 @@ Default.args = {
serializerConfig: {},
extensions: [],
enableAutocomplete: false,
+ markdownDocsPath: 'fake/path',
};
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 8917417e55e..da5ac7eb158 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -81,4 +81,13 @@ export default CodeBlockLowlight.extend({
addNodeView() {
return new VueNodeViewRenderer(CodeBlockWrapper);
},
+
+ addProseMirrorPlugins() {
+ const parentPlugins = this.parent?.() ?? [];
+ // We don't want TipTap's VSCode paste plugin to be loaded since
+ // it conflicts with our CopyPaste plugin.
+ const i = parentPlugins.findIndex((plugin) => plugin.key.includes('VSCode'));
+ if (i >= 0) parentPlugins.splice(i, 1);
+ return parentPlugins;
+ },
}).configure({ lowlight });
diff --git a/app/assets/javascripts/content_editor/extensions/copy_paste.js b/app/assets/javascripts/content_editor/extensions/copy_paste.js
index ab9e5619600..d29a407c5ca 100644
--- a/app/assets/javascripts/content_editor/extensions/copy_paste.js
+++ b/app/assets/javascripts/content_editor/extensions/copy_paste.js
@@ -11,6 +11,7 @@ import CodeBlockHighlight from './code_block_highlight';
import CodeSuggestion from './code_suggestion';
import Diagram from './diagram';
import Frontmatter from './frontmatter';
+import { loadingPlugin, findLoader } from './loading';
const TEXT_FORMAT = 'text/plain';
const GFM_FORMAT = 'text/x-gfm';
@@ -31,21 +32,6 @@ function parseHTML(schema, html) {
return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body) };
}
-const findLoader = (editor, loaderId) => {
- let position;
-
- editor.view.state.doc.descendants((descendant, pos) => {
- if (descendant.type.name === 'loading' && descendant.attrs.id === loaderId) {
- position = pos;
- return false;
- }
-
- return true;
- });
-
- return position;
-};
-
export default Extension.create({
name: 'copyPaste',
priority: EXTENSION_PRIORITY_HIGHEST,
@@ -74,13 +60,20 @@ export default Extension.create({
Promise.resolve()
.then(() => {
- editor.commands.insertContent({ type: 'loading', attrs: { id: loaderId } });
+ editor
+ .chain()
+ .deleteSelection()
+ .setMeta(loadingPlugin, {
+ add: { loaderId, pos: editor.state.selection.from },
+ })
+ .run();
+
return promise;
})
.then(async ({ document }) => {
if (!document) return;
- const pos = findLoader(editor, loaderId);
+ const pos = findLoader(editor.state, loaderId);
if (!pos) return;
const { firstChild, childCount } = document.content;
@@ -91,7 +84,7 @@ export default Extension.create({
editor
.chain()
- .deleteRange({ from: pos, to: pos + 1 })
+ .setMeta(loadingPlugin, { remove: { loaderId } })
.insertContentAt(pos, toPaste.toJSON(), {
updateSelection: false,
})
@@ -113,7 +106,16 @@ export default Extension.create({
const handleCutAndCopy = (view, event) => {
const slice = view.state.selection.content();
- const gfmContent = this.options.serializer.serialize({ doc: slice.content });
+ let gfmContent = this.options.serializer.serialize({ doc: slice.content });
+ const gfmContentWithoutSingleTableCell = gfmContent.replace(
+ /^<table>[\s\n]*<tr>[\s\n]*<t[hd]>|<\/t[hd]>[\s\n]*<\/tr>[\s\n]*<\/table>[\s\n]*$/gim,
+ '',
+ );
+ const containsSingleTableCell = !/<t[hd]>/.test(gfmContentWithoutSingleTableCell);
+
+ if (containsSingleTableCell) {
+ gfmContent = gfmContentWithoutSingleTableCell;
+ }
const documentFragment = DOMSerializer.fromSchema(view.state.schema).serializeFragment(
slice.content,
);
diff --git a/app/assets/javascripts/content_editor/extensions/emoji.js b/app/assets/javascripts/content_editor/extensions/emoji.js
index 7f8b5da5f46..be6ecb6cafd 100644
--- a/app/assets/javascripts/content_editor/extensions/emoji.js
+++ b/app/assets/javascripts/content_editor/extensions/emoji.js
@@ -1,5 +1,5 @@
import { Node, InputRule } from '@tiptap/core';
-import { initEmojiMap, getAllEmoji } from '~/emoji';
+import { initEmojiMap, getEmojiMap } from '~/emoji';
export default Node.create({
name: 'emoji',
@@ -58,7 +58,7 @@ export default Node.create({
find: emojiInputRegex,
handler: ({ state, range: { from, to }, match }) => {
const [, , name] = match;
- const emojis = getAllEmoji();
+ const emojis = getEmojiMap();
const emoji = emojis[name];
const { tr } = state;
diff --git a/app/assets/javascripts/content_editor/extensions/html_marks.js b/app/assets/javascripts/content_editor/extensions/html_marks.js
index 79fc0eea2c7..58fa2655e25 100644
--- a/app/assets/javascripts/content_editor/extensions/html_marks.js
+++ b/app/assets/javascripts/content_editor/extensions/html_marks.js
@@ -50,7 +50,8 @@ export default marks.map((name) =>
},
parseHTML() {
- return [{ tag: name, priority: PARSE_HTML_PRIORITY_LOWEST }];
+ const tag = name === 'span' ? `${name}:not([data-escaped-char])` : name;
+ return [{ tag, priority: PARSE_HTML_PRIORITY_LOWEST }];
},
renderHTML({ HTMLAttributes }) {
diff --git a/app/assets/javascripts/content_editor/extensions/loading.js b/app/assets/javascripts/content_editor/extensions/loading.js
index 0115fb10d5d..942ac650925 100644
--- a/app/assets/javascripts/content_editor/extensions/loading.js
+++ b/app/assets/javascripts/content_editor/extensions/loading.js
@@ -1,4 +1,52 @@
import { Node } from '@tiptap/core';
+import { Decoration, DecorationSet } from '@tiptap/pm/view';
+import { Plugin } from '@tiptap/pm/state';
+
+const createDotsLoader = () => {
+ const root = document.createElement('span');
+ root.classList.add('gl-display-inline-flex', 'gl-align-items-center');
+ root.innerHTML = '<span class="gl-dots-loader gl-mx-2"><span></span></span>';
+ return root;
+};
+
+export const loadingPlugin = new Plugin({
+ state: {
+ init() {
+ return DecorationSet.empty;
+ },
+ apply(tr, set) {
+ let transformedSet = set.map(tr.mapping, tr.doc);
+ const action = tr.getMeta(this);
+
+ if (action?.add) {
+ const deco = Decoration.widget(action.add.pos, createDotsLoader(), {
+ id: action.add.loaderId,
+ side: -1,
+ });
+ transformedSet = transformedSet.add(tr.doc, [deco]);
+ } else if (action?.remove) {
+ transformedSet = transformedSet.remove(
+ transformedSet.find(null, null, (spec) => spec.id === action.remove.loaderId),
+ );
+ }
+ return transformedSet;
+ },
+ },
+ props: {
+ decorations(state) {
+ return this.getState(state);
+ },
+ },
+});
+
+export const findLoader = (state, loaderId) => {
+ const decos = loadingPlugin.getState(state);
+ const found = decos.find(null, null, (spec) => spec.id === loaderId);
+
+ return found.length ? found[0].from : null;
+};
+
+export const findAllLoaders = (state) => loadingPlugin.getState(state).find();
export default Node.create({
name: 'loading',
@@ -13,11 +61,7 @@ export default Node.create({
};
},
- renderHTML() {
- return [
- 'span',
- { class: 'gl-display-inline-flex gl-align-items-center' },
- ['span', { class: 'gl-dots-loader gl-mx-2' }, ['span']],
- ];
+ addProseMirrorPlugins() {
+ return [loadingPlugin];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/suggestions.js b/app/assets/javascripts/content_editor/extensions/suggestions.js
index f29222a5289..f7ff2fd6647 100644
--- a/app/assets/javascripts/content_editor/extensions/suggestions.js
+++ b/app/assets/javascripts/content_editor/extensions/suggestions.js
@@ -20,6 +20,7 @@ function createSuggestionPlugin({
limit = 15,
nodeType,
nodeProps = {},
+ insertionMap = {},
}) {
const fetchData = memoize(
isFunction(dataSource) ? dataSource : async () => (await axios.get(dataSource)).data,
@@ -36,7 +37,7 @@ function createSuggestionPlugin({
.focus()
.insertContentAt(range, [
{ type: nodeType, attrs: props },
- { type: 'text', text: ' ' },
+ { type: 'text', text: ` ${insertionMap[props.text] || ''}` },
])
.run();
},
@@ -56,6 +57,7 @@ function createSuggestionPlugin({
render: () => {
let component;
let popup;
+ let isHidden = false;
const onUpdate = (props) => {
component?.updateProps({ ...props, loading: false });
@@ -87,6 +89,12 @@ function createSuggestionPlugin({
popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
+ onHide: () => {
+ isHidden = true;
+ },
+ onShow: () => {
+ isHidden = false;
+ },
content: component.element,
showOnCreate: true,
interactive: true,
@@ -99,6 +107,8 @@ function createSuggestionPlugin({
onUpdate,
onKeyDown(props) {
+ if (isHidden) return false;
+
if (props.event.key === 'Escape') {
popup?.[0].hide();
@@ -217,11 +227,24 @@ export default Node.create({
referenceType: 'command',
},
search: (query) => ({ name }) => find(name, query),
+ insertionMap: {
+ '/label': '~',
+ '/unlabel': '~',
+ '/relabel': '~',
+ '/assign': '@',
+ '/unassign': '@',
+ '/reassign': '@',
+ '/cc': '@',
+ '/assign_reviewer': '@',
+ '/unassign_reviewer': '@',
+ '/reassign_reviewer': '@',
+ '/milestone': '%',
+ },
}),
createSuggestionPlugin({
editor: this.editor,
char: ':',
- dataSource: () => Object.values(getAllEmoji()),
+ dataSource: () => getAllEmoji(),
nodeType: 'emoji',
search: (query) => ({ d, name }) => find(d, query) || find(name, query),
limit: 10,
diff --git a/app/assets/javascripts/content_editor/extensions/word_break.js b/app/assets/javascripts/content_editor/extensions/word_break.js
index 457b7c36564..01b19cbbd13 100644
--- a/app/assets/javascripts/content_editor/extensions/word_break.js
+++ b/app/assets/javascripts/content_editor/extensions/word_break.js
@@ -24,7 +24,7 @@ export default Node.create({
},
addInputRules() {
- const inputRegex = /^<wbr>$/;
+ const inputRegex = /<wbr>$/;
return [nodeInputRule({ find: inputRegex, type: this.type })];
},
diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js
index bc1ee696323..d3d2d76e481 100644
--- a/app/assets/javascripts/content_editor/services/content_editor.js
+++ b/app/assets/javascripts/content_editor/services/content_editor.js
@@ -84,9 +84,9 @@ export class ContentEditor {
async setSerializedContent(serializedContent) {
const { _tiptapEditor: editor } = this;
- const { doc, tr } = editor.state;
const { document } = await this.deserialize(serializedContent);
+ const { doc, tr } = editor.state;
if (document) {
this._pristineDoc = document;
diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue
index e3d3360cd0c..3b9f14a218f 100644
--- a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue
+++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue
@@ -47,16 +47,20 @@ export default {
<template>
<li class="gl-mt-5 gl-pb-5 gl-border-b gl-relative">
- <time-ago-tooltip :time="event.created_at" class="gl-float-right gl-text-secondary" />
+ <time-ago-tooltip
+ :time="event.created_at"
+ class="gl-float-right gl-font-sm gl-text-secondary gl-mt-2"
+ />
<gl-avatar-link :href="author.web_url">
<gl-avatar-labeled
:label="author.name"
:sub-label="authorUsername"
+ inline-labels
:src="author.avatar_url"
- :size="32"
+ :size="24"
/>
</gl-avatar-link>
- <div class="gl-pl-8 gl-mt-2" data-testid="event-body">
+ <div class="gl-pl-7" data-testid="event-body">
<div class="gl-text-secondary">
<gl-icon :class="iconClass" :name="iconName" />
<gl-sprintf v-if="message" :message="message">
diff --git a/app/assets/javascripts/crm/organizations/components/graphql/update_organization.mutation.graphql b/app/assets/javascripts/crm/organizations/components/graphql/update_customer_relations_organization.mutation.graphql
index a4c46d1f0fa..5ee3da2dfad 100644
--- a/app/assets/javascripts/crm/organizations/components/graphql/update_organization.mutation.graphql
+++ b/app/assets/javascripts/crm/organizations/components/graphql/update_customer_relations_organization.mutation.graphql
@@ -1,6 +1,6 @@
#import "./crm_organization_fields.fragment.graphql"
-mutation updateOrganization($input: CustomerRelationsOrganizationUpdateInput!) {
+mutation updateCustomerRelationsOrganization($input: CustomerRelationsOrganizationUpdateInput!) {
customerRelationsOrganizationUpdate(input: $input) {
organization {
...OrganizationFragment
diff --git a/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue b/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
index fb056e4fa2c..7dd65205b90 100644
--- a/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
+++ b/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
@@ -5,7 +5,7 @@ import { TYPENAME_CRM_ORGANIZATION, TYPENAME_GROUP } from '~/graphql_shared/cons
import CrmForm from '../../components/crm_form.vue';
import getGroupOrganizationsQuery from './graphql/get_group_organizations.query.graphql';
import createCustomerRelationsOrganizationMutation from './graphql/create_customer_relations_organization.mutation.graphql';
-import updateOrganizationMutation from './graphql/update_organization.mutation.graphql';
+import updateCustomerRelationsOrganizationMutation from './graphql/update_customer_relations_organization.mutation.graphql';
export default {
components: {
@@ -29,7 +29,7 @@ export default {
return convertToGraphQLId(TYPENAME_GROUP, this.groupId);
},
mutation() {
- if (this.isEditMode) return updateOrganizationMutation;
+ if (this.isEditMode) return updateCustomerRelationsOrganizationMutation;
return createCustomerRelationsOrganizationMutation;
},
diff --git a/app/assets/javascripts/custom_emoji/components/delete_item.vue b/app/assets/javascripts/custom_emoji/components/delete_item.vue
index 9d13d40dc47..91bd90c3682 100644
--- a/app/assets/javascripts/custom_emoji/components/delete_item.vue
+++ b/app/assets/javascripts/custom_emoji/components/delete_item.vue
@@ -1,7 +1,7 @@
<script>
-import * as Sentry from '@sentry/browser';
import { uniqueId } from 'lodash';
import { GlButton, GlTooltipDirective, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import deleteCustomEmojiMutation from '../queries/delete_custom_emoji.mutation.graphql';
diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
index 72d1ce9768a..6210e82119f 100644
--- a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
+++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
@@ -142,7 +142,6 @@ export default {
ref="freezeStartCron"
v-model="freezeStartCron"
class="gl-font-monospace!"
- data-qa-selector="deploy_freeze_start_field"
:placeholder="$options.i18n.cronPlaceholder"
:state="freezeStartCronState"
autofocus
@@ -160,7 +159,6 @@ export default {
id="deploy-freeze-end"
v-model="freezeEndCron"
class="gl-font-monospace!"
- data-qa-selector="deploy_freeze_end_field"
:placeholder="$options.i18n.cronPlaceholder"
:state="freezeEndCronState"
trim
diff --git a/app/assets/javascripts/deprecated_notes.js b/app/assets/javascripts/deprecated_notes.js
index 008e12abbcd..9b5b4cef1b9 100644
--- a/app/assets/javascripts/deprecated_notes.js
+++ b/app/assets/javascripts/deprecated_notes.js
@@ -152,7 +152,7 @@ export default class Notes {
// update the file name when an attachment is selected
this.$wrapperEl.on('change', '.js-note-attachment-input', this.updateFormAttachment);
// reply to diff/discussion notes
- this.$wrapperEl.on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
+ this.$wrapperEl.on('focus', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
// add diff note
this.$wrapperEl.on('click', '.js-add-diff-note-button', this.onAddDiffNote);
// add diff note for images
diff --git a/app/assets/javascripts/design_management/components/delete_button.vue b/app/assets/javascripts/design_management/components/delete_button.vue
index dec1038d2e3..88c1b444b31 100644
--- a/app/assets/javascripts/design_management/components/delete_button.vue
+++ b/app/assets/javascripts/design_management/components/delete_button.vue
@@ -63,7 +63,7 @@ export default {
title: s__('DesignManagement|Are you sure you want to archive the selected designs?'),
actionPrimary: {
text: s__('DesignManagement|Archive designs'),
- attributes: { variant: 'confirm', 'data-qa-selector': 'confirm_archiving_button' },
+ attributes: { variant: 'confirm', 'data-testid': 'confirm-archiving-button' },
},
actionCancel: {
text: __('Cancel'),
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
index 45f33967476..2a099b6f22d 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlLink, GlTooltipDirective, GlFormCheckbox } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import { __, s__ } from '~/locale';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
@@ -293,7 +293,6 @@ export default {
<ul
class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none"
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
- data-qa-selector="design_discussion_content"
data-testid="design-discussion-content"
>
<design-note
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
index a5b6d6276f8..b247f17fd97 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
@@ -7,12 +7,13 @@ import {
GlLink,
GlTooltipDirective,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { produce } from 'immer';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { __ } from '~/locale';
+import { setUrlFragment } from '~/lib/utils/url_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import EmojiPicker from '~/emoji/components/picker.vue';
@@ -29,6 +30,7 @@ export default {
editCommentLabel: __('Edit comment'),
moreActionsLabel: __('More actions'),
deleteCommentText: __('Delete comment'),
+ copyCommentLink: __('Copy link'),
},
components: {
DesignNoteAwardsList,
@@ -129,19 +131,27 @@ export default {
this.isEditing = true;
},
extraAttrs: {
- 'data-testid': 'delete-note-button',
- 'data-qa-selector': 'delete_design_note_button',
class: 'gl-sm-display-none!',
},
},
{
+ text: this.$options.i18n.copyCommentLink,
+ action: () => {
+ this.$toast.show(__('Link copied to clipboard.'));
+ },
+ extraAttrs: {
+ 'data-clipboard-text': setUrlFragment(
+ window.location.href,
+ `note_${this.noteAnchorId}`,
+ ),
+ },
+ },
+ {
text: this.$options.i18n.deleteCommentText,
action: () => {
this.$emit('delete-note', this.note);
},
extraAttrs: {
- 'data-testid': 'delete-note-button',
- 'data-qa-selector': 'delete_design_note_button',
class: 'gl-text-red-500!',
},
},
@@ -311,7 +321,6 @@ export default {
v-gl-tooltip.hover
icon="ellipsis_v"
category="tertiary"
- data-qa-selector="design_discussion_actions_ellipsis_dropdown"
text-sr-only
:title="$options.i18n.moreActionsLabel"
:aria-label="$options.i18n.moreActionsLabel"
@@ -322,12 +331,7 @@ export default {
</div>
</div>
<template v-if="!isEditing">
- <div
- v-safe-html="note.bodyHtml"
- class="note-text md"
- data-qa-selector="note_content"
- data-testid="note-text"
- ></div>
+ <div v-safe-html="note.bodyHtml" class="note-text md" data-testid="note-text"></div>
<slot name="resolved-status"></slot>
</template>
<design-note-awards-list
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue b/app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue
index f0812e62bba..de3e71c0e9c 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue
@@ -37,7 +37,7 @@ export default {
</script>
<template>
- <div class="disabled-comment text-center">
+ <div class="disabled-comment gl-text-center gl-text-secondary">
<gl-sprintf :message="signedOutText">
<template #registerLink="{ content }">
<gl-link :href="registerPath">{{ content }}</gl-link>
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
index 764c78ff581..b6a303ddde8 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
@@ -221,7 +221,7 @@ export default {
class="note-textarea js-gfm-input js-autosize markdown-area"
dir="auto"
data-supports-quick-actions="false"
- data-qa-selector="note_textarea"
+ data-testid="note-textarea"
:aria-label="__('Description')"
:placeholder="__('Write a comment…')"
@input="handleInput"
@@ -243,7 +243,7 @@ export default {
variant="confirm"
type="submit"
data-track-action="click_button"
- data-qa-selector="save_comment_button"
+ data-testid="save-comment-button"
@click="submitForm"
>
{{ buttonText }}
diff --git a/app/assets/javascripts/design_management/components/design_overlay.vue b/app/assets/javascripts/design_management/components/design_overlay.vue
index 4ce6395140e..e4361f94026 100644
--- a/app/assets/javascripts/design_management/components/design_overlay.vue
+++ b/app/assets/javascripts/design_management/components/design_overlay.vue
@@ -272,7 +272,7 @@ export default {
role="button"
:aria-label="$options.i18n.newCommentButtonLabel"
class="gl-absolute gl-w-full gl-h-full gl-p-0 gl-top-0 gl-left-0 gl-outline-0! btn-transparent gl-hover-cursor-crosshair"
- data-qa-selector="design_image_button"
+ data-testid="design-image-button"
@mouseup="onAddCommentMouseup"
></button>
diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue
index 7b98557f4f0..6400f939244 100644
--- a/app/assets/javascripts/design_management/components/list/item.vue
+++ b/app/assets/javascripts/design_management/components/list/item.vue
@@ -144,12 +144,17 @@ export default {
:name="icon.name"
:size="16"
:class="icon.classes"
- data-qa-selector="design_status_icon"
+ data-testid="design-status-icon"
:data-qa-status="icon.name"
/>
</span>
</div>
- <gl-intersection-observer class="gl-flex-grow-1" @appear="onAppear">
+ <gl-intersection-observer
+ class="gl-flex-grow-1"
+ data-testid="design-image"
+ :data-qa-filename="filename"
+ @appear="onAppear"
+ >
<gl-loading-icon v-if="showLoadingSpinner" size="lg" />
<gl-icon
v-else-if="showImageErrorIcon"
@@ -162,8 +167,6 @@ export default {
:src="imageLink"
:alt="filename"
class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full gl-w-auto design-img"
- data-qa-selector="design_image"
- :data-qa-filename="filename"
:data-testid="`design-img-${id}`"
@load="onImageLoad"
@error="onImageError"
@@ -171,11 +174,13 @@ export default {
</gl-intersection-observer>
</div>
<div class="card-footer gl-display-flex gl-w-full gl-bg-white gl-py-3 gl-px-4">
- <div class="gl-display-flex gl-flex-direction-column str-truncated-100">
+ <div
+ class="gl-display-flex gl-flex-direction-column str-truncated-100"
+ data-testid="design-file-name"
+ >
<span
v-gl-tooltip
class="gl-font-weight-semibold str-truncated-100"
- data-qa-selector="design_file_name"
:data-testid="`design-img-filename-${id}`"
:title="filename"
>{{ filename }}</span
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index 09f99f0927f..a1fd3520982 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -402,7 +402,7 @@ export default {
button-variant="default"
button-class="gl-mr-3"
button-size="small"
- data-qa-selector="archive_button"
+ data-testid="archive-button"
:loading="loading"
:has-selected-designs="hasSelectedDesigns"
@delete-selected-designs="mutate()"
@@ -490,7 +490,7 @@ export default {
:checked="isDesignSelected(design.filename)"
type="checkbox"
class="design-checkbox gl-absolute gl-top-4 gl-left-6 gl-ml-2"
- data-qa-selector="design_checkbox"
+ data-testid="design-checkbox"
:data-qa-design="design.filename"
@change="changeSelectedDesigns(design.filename)"
/>
@@ -506,7 +506,7 @@ export default {
:class="{ 'design-list-item': !isDesignListEmpty }"
:display-as-card="hasDesigns"
v-bind="$options.dropzoneProps"
- data-qa-selector="design_dropzone_content"
+ data-testid="design-dropzone-content"
@change="onUploadDesign"
@error="onDesignDropzoneError"
>
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 924c515ee2d..54c276c36b1 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -1,6 +1,7 @@
<script>
import { GlLoadingIcon, GlPagination, GlSprintf, GlAlert } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import { debounce } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapGetters, mapActions } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -16,7 +17,8 @@ import {
import { createAlert } from '~/alert';
import { isSingleViewStyle } from '~/helpers/diffs_helper';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { parseBoolean } from '~/lib/utils/common_utils';
+import { parseBoolean, handleLocationHash } from '~/lib/utils/common_utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { Mousetrap } from '~/lib/mousetrap';
import { updateHistory } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -39,8 +41,10 @@ import {
TRACKING_SINGLE_FILE_MODE,
TRACKING_MULTIPLE_FILES_MODE,
EVT_MR_PREPARED,
+ EVT_DISCUSSIONS_ASSIGNED,
} from '../constants';
+import { isCollapsed } from '../utils/diff_file';
import diffsEventHub from '../event_hub';
import { reviewStatuses } from '../utils/file_reviews';
import { diffsApp } from '../utils/performance';
@@ -55,10 +59,16 @@ import HiddenFilesWarning from './hidden_files_warning.vue';
import NoChanges from './no_changes.vue';
import VirtualScrollerScrollSync from './virtual_scroller_scroll_sync';
import DiffsFileTree from './diffs_file_tree.vue';
-import getMRCodequalityReports from './graphql/get_mr_codequality_reports.query.graphql';
+import getMRCodequalityAndSecurityReports from './graphql/get_mr_codequality_and_security_reports.query.graphql';
+
+export const FINDINGS_STATUS_PARSED = 'PARSED';
+export const FINDINGS_STATUS_ERROR = 'ERROR';
+export const FINDINGS_POLL_INTERVAL = 1000;
export default {
name: 'DiffsApp',
+ FINDINGS_STATUS_PARSED,
+ FINDINGS_STATUS_ERROR,
components: {
DiffsFileTree,
FindingsDrawer,
@@ -100,10 +110,10 @@ export default {
required: false,
default: '',
},
- endpointSast: {
- type: String,
+ sastReportAvailable: {
+ type: Boolean,
required: false,
- default: '',
+ default: false,
},
endpointCodequality: {
type: String,
@@ -135,31 +145,53 @@ export default {
diffFilesLength: 0,
virtualScrollCurrentIndex: -1,
subscribedToVirtualScrollingEvents: false,
+ autoScrolled: false,
+ activeProject: undefined,
};
},
apollo: {
- getMRCodequalityReports: {
- query: getMRCodequalityReports,
+ getMRCodequalityAndSecurityReports: {
+ query: getMRCodequalityAndSecurityReports,
+ pollInterval: FINDINGS_POLL_INTERVAL,
variables() {
return { fullPath: this.projectPath, iid: this.iid };
},
skip() {
- return !this.endpointCodequality || !this.sastReportsInInlineDiff;
+ const codeQualityBoolean = Boolean(this.endpointCodequality);
+
+ return !this.sastReportsInInlineDiff || (!codeQualityBoolean && !this.sastReportAvailable);
},
update(data) {
- if (data?.project?.mergeRequest?.codequalityReportsComparer?.report?.newErrors) {
+ const codeQualityBoolean = Boolean(this.endpointCodequality);
+ const { codequalityReportsComparer, sastReport } = data?.project?.mergeRequest || {};
+
+ this.activeProject = data?.project?.mergeRequest?.project;
+ if (
+ (sastReport?.status === FINDINGS_STATUS_PARSED || !this.sastReportAvailable) &&
+ (!codeQualityBoolean || codequalityReportsComparer.status === FINDINGS_STATUS_PARSED)
+ ) {
+ this.getMRCodequalityAndSecurityReportStopPolling(
+ this.$apollo.queries.getMRCodequalityAndSecurityReports,
+ );
+ }
+
+ if (sastReport?.status === FINDINGS_STATUS_ERROR && this.sastReportAvailable) {
+ this.fetchScannerFindingsError();
+ }
+
+ if (codequalityReportsComparer?.report?.newErrors) {
this.$store.commit(
'diffs/SET_CODEQUALITY_DATA',
- sortFindingsByFile(
- data.project.mergeRequest.codequalityReportsComparer.report.newErrors,
- ),
+ sortFindingsByFile(codequalityReportsComparer.report.newErrors),
);
}
+
+ if (sastReport?.report) {
+ this.$store.commit('diffs/SET_SAST_DATA', sastReport.report);
+ }
},
error() {
- createAlert({
- message: __('Something went wrong fetching the CodeQuality Findings. Please try again!'),
- });
+ this.fetchScannerFindingsError();
},
},
},
@@ -304,10 +336,6 @@ export default {
this.setCodequalityEndpoint(this.endpointCodequality);
}
- if (this.endpointSast) {
- this.setSastEndpoint(this.endpointSast);
- }
-
if (this.shouldShow) {
this.fetchData();
}
@@ -355,22 +383,15 @@ export default {
this.adjustView();
this.subscribeToEvents();
- this.unwatchDiscussions = this.$watch(
- () => `${this.flatBlobsList.length}:${this.$store.state.notes.discussions.length}`,
- () => {
- this.setDiscussions();
-
- if (this.$store.state.notes.doneFetchingBatchDiscussions) {
- this.unwatchDiscussions();
- }
- },
- );
-
- this.unwatchRetrievingBatches = this.$watch(
- () => `${this.retrievingBatches}:${this.$store.state.notes.discussions.length}`,
- () => {
- if (!this.retrievingBatches && this.$store.state.notes.discussions.length) {
- this.unwatchRetrievingBatches();
+ this.slowHashHandler = debounce(() => {
+ handleLocationHash();
+ this.autoScrolled = true;
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ this.$watch(
+ () => this.$store.state.notes.discussions.length,
+ (newVal, prevVal) => {
+ if (newVal > prevVal) {
+ this.setDiscussions();
}
},
);
@@ -388,7 +409,6 @@ export default {
...mapActions('diffs', [
'moveToNeighboringCommit',
'setCodequalityEndpoint',
- 'setSastEndpoint',
'fetchDiffFilesMeta',
'fetchDiffFilesBatch',
'fetchFileByFile',
@@ -396,7 +416,6 @@ export default {
'setFileForcedOpen',
'fetchCoverageFiles',
'fetchCodequality',
- 'fetchSast',
'rereadNoteHash',
'startRenderDiffsQueue',
'assignDiscussionsToDiff',
@@ -412,6 +431,11 @@ export default {
closeDrawer() {
this.setDrawer({});
},
+ fetchScannerFindingsError() {
+ createAlert({
+ message: __('Something went wrong fetching the Scanner Findings. Please try again.'),
+ });
+ },
subscribeToEvents() {
notesEventHub.$once('fetchDiffData', this.fetchData);
notesEventHub.$on('refetchDiffData', this.refetchDiffData);
@@ -419,8 +443,13 @@ export default {
diffsEventHub.$on('diffFilesModified', this.setDiscussions);
diffsEventHub.$on('doneLoadingBatches', this.autoScroll);
diffsEventHub.$on(EVT_MR_PREPARED, this.fetchData);
+ diffsEventHub.$on(EVT_DISCUSSIONS_ASSIGNED, this.handleHash);
+ },
+ getMRCodequalityAndSecurityReportStopPolling(query) {
+ query.stopPolling();
},
unsubscribeFromEvents() {
+ diffsEventHub.$off(EVT_DISCUSSIONS_ASSIGNED, this.handleHash);
diffsEventHub.$off(EVT_MR_PREPARED, this.fetchData);
diffsEventHub.$off('doneLoadingBatches', this.autoScroll);
diffsEventHub.$off('diffFilesModified', this.setDiscussions);
@@ -436,15 +465,27 @@ export default {
const idx = this.diffs.findIndex((diffFile) => diffFile.file_hash === sha1InHash);
const file = this.diffs[idx];
+ if (!isCollapsed(file)) return;
+
this.loadCollapsedDiff({ file })
.then(() => {
this.setDiscussions();
- this.scrollVirtualScrollerToIndex(idx);
this.setFileForcedOpen({ filePath: file.new_path });
+
+ this.$nextTick(() => this.scrollVirtualScrollerToIndex(idx));
})
.catch(() => {});
}
},
+ handleHash() {
+ if (this.viewDiffsFileByFile && !this.autoScrolled) {
+ const file = this.diffs[0];
+
+ if (file && !file.isLoadingFullFile) {
+ requestIdleCallback(() => this.slowHashHandler());
+ }
+ }
+ },
navigateToDiffFileNumber(number) {
this.navigateToDiffFileIndex(number - 1);
},
@@ -482,7 +523,7 @@ export default {
})
.catch(() => {
createAlert({
- message: __('Something went wrong on our end. Please try again!'),
+ message: __('Something went wrong on our end. Please try again.'),
});
});
}
@@ -499,7 +540,7 @@ export default {
})
.catch(() => {
createAlert({
- message: __('Something went wrong on our end. Please try again!'),
+ message: __('Something went wrong on our end. Please try again.'),
});
});
}
@@ -512,10 +553,6 @@ export default {
this.fetchCodequality();
}
- if (this.endpointSast) {
- this.fetchSast();
- }
-
if (!this.isNotesFetched) {
notesEventHub.$emit('fetchNotesData');
}
@@ -641,7 +678,7 @@ export default {
<template>
<div v-show="shouldShow">
- <findings-drawer :drawer="activeDrawer" @close="closeDrawer" />
+ <findings-drawer :project="activeProject" :drawer="activeDrawer" @close="closeDrawer" />
<div v-if="isLoading || !isTreeLoaded" class="loading"><gl-loading-icon size="lg" /></div>
<div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane">
<compare-versions :diff-files-count-text="numTotalFiles" />
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 3746ab9427f..7493bd5fdf7 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -135,7 +135,7 @@ export default {
<div
class="commit-detail flex-list gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-grow-1 gl-min-w-0"
>
- <div class="commit-content" data-qa-selector="commit_content">
+ <div class="commit-content" data-testid="commit-content">
<a
v-safe-html:[$options.safeHtmlConfig]="commit.title_html"
:href="commit.commit_url"
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index 8915f32eadf..556f72059c2 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -39,12 +39,6 @@ export default {
},
methods: {
...mapActions(['toggleDiscussion']),
- ...mapActions('diffs', ['removeDiscussionsFromDiff']),
- deleteNoteHandler(discussion) {
- if (discussion.notes.length <= 1) {
- this.removeDiscussionsFromDiff(discussion);
- }
- },
isExpanded(discussion) {
return this.shouldCollapseDiscussions ? discussion.expanded : true;
},
@@ -90,7 +84,6 @@ export default {
:line="line"
:help-page-path="helpPagePath"
:should-scroll-to-note="false"
- @noteDeleted="deleteNoteHandler"
>
<template v-if="renderAvatarBadge" #avatar-badge>
<design-note-pin
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index c74a4b47fcb..8c1cab20ece 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -208,11 +208,6 @@ export default {
this.manageViewedEffects();
},
},
- 'file.viewer.forceOpen': {
- handler: function fileForcedOpenHandler() {
- this.handleToggle();
- },
- },
'file.file_hash': {
handler: function hashChangeWatch(newHash, oldHash) {
if (
diff --git a/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_and_security_reports.query.graphql b/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_and_security_reports.query.graphql
new file mode 100644
index 00000000000..bd8f408f5a1
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_and_security_reports.query.graphql
@@ -0,0 +1,82 @@
+query getMRCodequalityAndSecurityReports($fullPath: ID!, $iid: String!) {
+ project(fullPath: $fullPath) {
+ id
+ mergeRequest(iid: $iid) {
+ id
+ title
+ project {
+ id
+ nameWithNamespace
+ fullPath
+ }
+ hasSecurityReports
+ codequalityReportsComparer {
+ status
+ report {
+ status
+ newErrors {
+ description
+ fingerprint
+ severity
+ filePath
+ line
+ webUrl
+ engineName
+ }
+ resolvedErrors {
+ description
+ fingerprint
+ severity
+ filePath
+ line
+ webUrl
+ engineName
+ }
+ existingErrors {
+ description
+ fingerprint
+ severity
+ filePath
+ line
+ webUrl
+ engineName
+ }
+ summary {
+ errored
+ resolved
+ total
+ }
+ }
+ }
+ sastReport: findingReportsComparer(reportType: SAST) {
+ status
+ report {
+ added {
+ identifiers {
+ externalId
+ externalType
+ name
+ url
+ }
+ uuid
+ title
+ description
+ state
+ severity
+ foundByPipelineIid
+ location {
+ ... on VulnerabilityLocationSast {
+ file
+ startLine
+ endLine
+ vulnerableClass
+ vulnerableMethod
+ blobPath
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql b/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql
deleted file mode 100644
index b6920d0f6ec..00000000000
--- a/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql
+++ /dev/null
@@ -1,46 +0,0 @@
-query getMRCodequalityReports($fullPath: ID!, $iid: String!) {
- project(fullPath: $fullPath) {
- id
- mergeRequest(iid: $iid) {
- id
- title
- codequalityReportsComparer {
- report {
- status
- newErrors {
- description
- fingerprint
- severity
- filePath
- line
- webUrl
- engineName
- }
- resolvedErrors {
- description
- fingerprint
- severity
- filePath
- line
- webUrl
- engineName
- }
- existingErrors {
- description
- fingerprint
- severity
- filePath
- line
- webUrl
- engineName
- }
- summary {
- errored
- resolved
- total
- }
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/diffs/components/shared/findings_drawer.vue b/app/assets/javascripts/diffs/components/shared/findings_drawer.vue
index fddd455b17e..2c1a8305935 100644
--- a/app/assets/javascripts/diffs/components/shared/findings_drawer.vue
+++ b/app/assets/javascripts/diffs/components/shared/findings_drawer.vue
@@ -1,46 +1,56 @@
<script>
-import { GlDrawer, GlIcon, GlLink } from '@gitlab/ui';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import { s__ } from '~/locale';
+import { GlBadge, GlDrawer, GlIcon, GlLink } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
-import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants';
+import { getSeverity } from '~/ci/reports/utils';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
+import DrawerItem from './findings_drawer_item.vue';
export const i18n = {
- severity: s__('FindingsDrawer|Severity:'),
- engine: s__('FindingsDrawer|Engine:'),
- category: s__('FindingsDrawer|Category:'),
- otherLocations: s__('FindingsDrawer|Other locations:'),
+ name: __('Name'),
+ description: __('Description'),
+ status: __('Status'),
+ sast: __('SAST'),
+ engine: __('Engine'),
+ identifiers: __('Identifiers'),
+ project: __('Project'),
+ file: __('File'),
+ tool: __('Tool'),
+ codeQualityFinding: s__('FindingsDrawer|Code Quality Finding'),
+ sastFinding: s__('FindingsDrawer|SAST Finding'),
+ codeQuality: s__('FindingsDrawer|Code Quality'),
+ detected: s__('FindingsDrawer|Detected in pipeline'),
};
+export const codeQuality = 'codeQuality';
export default {
i18n,
- components: { GlDrawer, GlIcon, GlLink },
- directives: {
- SafeHtml,
- },
+ codeQuality,
+ components: { GlBadge, GlDrawer, GlIcon, GlLink, DrawerItem },
props: {
drawer: {
type: Object,
required: true,
},
- },
- safeHtmlConfig: {
- ALLOWED_TAGS: ['a', 'h1', 'h2', 'p'],
- ALLOWED_ATTR: ['href', 'rel'],
+ project: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
},
computed: {
getDrawerHeaderHeight() {
return getContentWrapperHeight();
},
+ isCodeQuality() {
+ return this.drawer.scale === this.$options.codeQuality;
+ },
},
DRAWER_Z_INDEX,
methods: {
- severityClass(severity) {
- return SEVERITY_CLASSES[severity] || SEVERITY_CLASSES.unknown;
- },
- severityIcon(severity) {
- return SEVERITY_ICONS[severity] || SEVERITY_ICONS.unknown;
+ getSeverity,
+ concatIdentifierName(name, index) {
+ return name + (index !== this.drawer.identifiers.length - 1 ? ', ' : '');
},
},
};
@@ -54,57 +64,82 @@ export default {
@close="$emit('close')"
>
<template #title>
- <h2 data-testid="findings-drawer-heading" class="gl-font-size-h2 gl-mt-0 gl-mb-0">
- {{ drawer.description }}
+ <h2 class="drawer-heading gl-font-base gl-mt-0 gl-mb-0">
+ <gl-icon
+ :size="12"
+ :name="getSeverity(drawer).name"
+ :class="getSeverity(drawer).class"
+ class="inline-findings-severity-icon gl-vertical-align-baseline!"
+ />
+ <span class="drawer-heading-severity">{{ drawer.severity }}</span>
+ {{ isCodeQuality ? $options.i18n.codeQualityFinding : $options.i18n.sastFinding }}
</h2>
</template>
<template #default>
<ul class="gl-list-style-none gl-border-b-initial gl-mb-0 gl-pb-0!">
- <li data-testid="findings-drawer-severity" class="gl-mb-4">
- <span class="gl-font-weight-bold">{{ $options.i18n.severity }}</span>
- <gl-icon
- data-testid="findings-drawer-severity-icon"
- :size="12"
- :name="severityIcon(drawer.severity)"
- :class="severityClass(drawer.severity)"
- class="inline-findings-severity-icon"
- />
+ <drawer-item v-if="drawer.title" :description="$options.i18n.name" :value="drawer.title" />
+
+ <drawer-item v-if="drawer.state" :description="$options.i18n.status">
+ <template #value>
+ <gl-badge variant="warning" class="text-capitalize">{{ drawer.state }}</gl-badge>
+ </template>
+ </drawer-item>
+
+ <drawer-item
+ v-if="drawer.description"
+ :description="$options.i18n.description"
+ :value="drawer.description"
+ />
+
+ <drawer-item
+ v-if="project && drawer.scale !== $options.codeQuality"
+ :description="$options.i18n.project"
+ >
+ <template #value>
+ <gl-link :href="`/${project.fullPath}`">{{ project.nameWithNamespace }}</gl-link>
+ </template>
+ </drawer-item>
+
+ <drawer-item v-if="drawer.location || drawer.webUrl" :description="$options.i18n.file">
+ <template #value>
+ <span v-if="drawer.webUrl && drawer.filePath && drawer.line">
+ <gl-link :href="drawer.webUrl">{{ drawer.filePath }}:{{ drawer.line }}</gl-link>
+ </span>
+ <span v-else-if="drawer.location">
+ {{ drawer.location.file }}:{{ drawer.location.startLine }}
+ </span>
+ </template>
+ </drawer-item>
+
+ <drawer-item
+ v-if="drawer.identifiers && drawer.identifiers.length"
+ :description="$options.i18n.identifiers"
+ >
+ <template #value>
+ <span v-for="(identifier, index) in drawer.identifiers" :key="identifier.externalId">
+ <gl-link v-if="identifier.url" :href="identifier.url">
+ {{ concatIdentifierName(identifier.name, index) }}
+ </gl-link>
+ <span v-else>
+ {{ concatIdentifierName(identifier.name, index) }}
+ </span>
+ </span>
+ </template>
+ </drawer-item>
+
+ <drawer-item
+ v-if="drawer.scale"
+ :description="$options.i18n.tool"
+ :value="isCodeQuality ? $options.i18n.codeQuality : $options.i18n.sast"
+ />
- {{ drawer.severity }}
- </li>
- <li data-testid="findings-drawer-engine" class="gl-mb-4">
- <span class="gl-font-weight-bold">{{ $options.i18n.engine }}</span>
- {{ drawer.engineName }}
- </li>
- <li data-testid="findings-drawer-category" class="gl-mb-4">
- <span class="gl-font-weight-bold">{{ $options.i18n.category }}</span>
- {{ drawer.categories ? drawer.categories[0] : '' }}
- </li>
- <li data-testid="findings-drawer-other-locations" class="gl-mb-4">
- <span class="gl-font-weight-bold gl-mb-3 gl-display-block">{{
- $options.i18n.otherLocations
- }}</span>
- <ul class="gl-pl-6">
- <li
- v-for="otherLocation in drawer.otherLocations"
- :key="otherLocation.path"
- class="gl-mb-1"
- >
- <gl-link
- data-testid="findings-drawer-other-locations-link"
- :href="otherLocation.href"
- >{{ otherLocation.path }}</gl-link
- >
- </li>
- </ul>
- </li>
+ <drawer-item
+ v-if="drawer.engineName"
+ :description="$options.i18n.engine"
+ :value="drawer.engineName"
+ />
</ul>
- <span
- v-safe-html:[$options.safeHtmlConfig]="drawer.content ? drawer.content.body : ''"
- data-testid="findings-drawer-body"
- class="drawer-body gl-display-block gl-px-3 gl-py-0!"
- ></span>
</template>
</gl-drawer>
</template>
diff --git a/app/assets/javascripts/diffs/components/shared/findings_drawer_item.vue b/app/assets/javascripts/diffs/components/shared/findings_drawer_item.vue
new file mode 100644
index 00000000000..f488e8e3bb1
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/shared/findings_drawer_item.vue
@@ -0,0 +1,30 @@
+<script>
+export default {
+ props: {
+ description: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+};
+</script>
+<template>
+ <li class="gl-mb-4">
+ <p class="gl-line-height-20">
+ <span
+ data-testid="findings-drawer-item-description"
+ class="gl-font-weight-bold gl-display-block gl-mb-1"
+ >{{ description }}</span
+ >
+ <slot name="value">
+ <span data-testid="findings-drawer-item-value-prop">{{ value }}</span>
+ </slot>
+ </p>
+ </li>
+</template>
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index f4715c591b2..07984beb709 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -3,22 +3,20 @@ import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import micromatch from 'micromatch';
-import { debounce } from 'lodash';
import { getModifierKey } from '~/constants';
import { s__, sprintf } from '~/locale';
import { RecycleScroller } from 'vendor/vue-virtual-scroller';
-import { contentTop } from '~/lib/utils/common_utils';
import DiffFileRow from './diff_file_row.vue';
+import TreeListHeight from './tree_list_height.vue';
const MODIFIER_KEY = getModifierKey();
-const MAX_ITEMS_ON_NARROW_SCREEN = 8;
-const BOTTOM_MARGIN = 16;
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
+ TreeListHeight,
GlIcon,
DiffFileRow,
RecycleScroller,
@@ -32,17 +30,10 @@ export default {
data() {
return {
search: '',
- scrollerHeight: 0,
- rowHeight: 0,
- debouncedHeightCalc: null,
- reviewBarHeight: 0,
- largeBreakpointSize: 0,
};
},
computed: {
...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId', 'viewedDiffFileIds']),
- ...mapState('batchComments', ['reviewBarRendered']),
- ...mapGetters('batchComments', ['draftsCount']),
...mapGetters('diffs', ['allBlobs']),
filteredTreeList() {
let search = this.search.toLowerCase().trim();
@@ -95,76 +86,21 @@ export default {
return result;
},
- reviewBarEnabled() {
- return this.draftsCount > 0;
- },
- },
- watch: {
- reviewBarEnabled() {
- this.debouncedHeightCalc();
- },
- calculateReviewBarHeight() {
- this.debouncedHeightCalc();
- },
- },
- created() {
- this.debouncedHeightCalc = debounce(this.calculateScrollerHeight, 50);
- },
- mounted() {
- const heightProp = getComputedStyle(this.$refs.wrapper).getPropertyValue('--file-row-height');
- const breakpointProp = getComputedStyle(window.document.body).getPropertyValue(
- '--breakpoint-lg',
- );
- this.largeBreakpointSize = parseInt(breakpointProp, 10);
- this.rowHeight = parseInt(heightProp, 10);
- this.calculateScrollerHeight();
- let stop;
- // eslint-disable-next-line prefer-const
- stop = this.$watch(
- () => this.reviewBarRendered,
- (enabled) => {
- if (!enabled) return;
- this.calculateReviewBarHeight();
- stop();
- },
- { immediate: true },
- );
- window.addEventListener('resize', this.debouncedHeightCalc, { passive: true });
- },
- beforeDestroy() {
- window.removeEventListener('resize', this.debouncedHeightCalc, { passive: true });
},
methods: {
...mapActions('diffs', ['toggleTreeOpen', 'goToFile']),
clearSearch() {
this.search = '';
},
- calculateScrollerHeight() {
- if (window.matchMedia(`(max-width: ${this.largeBreakpointSize - 1}px)`).matches) {
- this.calculateMobileScrollerHeight();
- } else {
- let clipping = BOTTOM_MARGIN;
- if (this.reviewBarEnabled) clipping += this.reviewBarHeight;
- this.scrollerHeight = this.$refs.scrollRoot.clientHeight - clipping;
- }
- },
- calculateMobileScrollerHeight() {
- const maxItems = Math.min(MAX_ITEMS_ON_NARROW_SCREEN, this.flatFilteredTreeList.length);
- this.scrollerHeight = Math.min(maxItems * this.rowHeight, window.innerHeight - contentTop());
- },
- calculateReviewBarHeight() {
- this.reviewBarHeight = document.querySelector('.js-review-bar')?.offsetHeight || 0;
- },
},
searchPlaceholder: sprintf(s__('MergeRequest|Search (e.g. *.vue) (%{MODIFIER_KEY}P)'), {
MODIFIER_KEY,
}),
- DiffFileRow,
};
</script>
<template>
- <div ref="wrapper" class="tree-list-holder d-flex flex-column" data-testid="file-tree-container">
+ <div class="tree-list-holder d-flex flex-column" data-testid="file-tree-container">
<div class="gl-pb-3 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex">
<gl-icon name="search" class="gl-absolute gl-top-3 gl-left-3 tree-list-icon" />
@@ -189,41 +125,41 @@ export default {
</button>
</div>
</div>
- <div
- ref="scrollRoot"
- :class="{ 'tree-list-blobs': !renderTreeList || search }"
- class="gl-flex-grow-1 mr-tree-list"
- >
- <recycle-scroller
- v-if="flatFilteredTreeList.length"
- :style="{ height: `${scrollerHeight}px` }"
- :items="flatFilteredTreeList"
- :item-size="rowHeight"
- :buffer="100"
- key-field="key"
- >
- <template #default="{ item }">
- <diff-file-row
- :file="item"
- :level="item.level"
- :viewed-files="viewedDiffFileIds"
- :hide-file-stats="hideFileStats"
- :current-diff-file-id="currentDiffFileId"
- :style="{ '--level': item.level }"
- :class="{ 'tree-list-parent': item.level > 0 }"
- class="gl-relative"
- @toggleTreeOpen="toggleTreeOpen"
- @clickFile="(path) => goToFile({ path })"
- />
- </template>
- <template #after>
- <div class="tree-list-gutter"></div>
- </template>
- </recycle-scroller>
- <p v-else class="prepend-top-20 append-bottom-20 text-center">
- {{ s__('MergeRequest|No files found') }}
- </p>
- </div>
+ <tree-list-height class="gl-flex-grow-1 gl-min-h-0" :items-count="flatFilteredTreeList.length">
+ <template #default="{ scrollerHeight, rowHeight }">
+ <div :class="{ 'tree-list-blobs': !renderTreeList || search }" class="mr-tree-list">
+ <recycle-scroller
+ v-if="flatFilteredTreeList.length"
+ :style="{ height: `${scrollerHeight}px` }"
+ :items="flatFilteredTreeList"
+ :item-size="rowHeight"
+ :buffer="100"
+ key-field="key"
+ >
+ <template #default="{ item }">
+ <diff-file-row
+ :file="item"
+ :level="item.level"
+ :viewed-files="viewedDiffFileIds"
+ :hide-file-stats="hideFileStats"
+ :current-diff-file-id="currentDiffFileId"
+ :style="{ '--level': item.level }"
+ :class="{ 'tree-list-parent': item.level > 0 }"
+ class="gl-relative"
+ @toggleTreeOpen="toggleTreeOpen"
+ @clickFile="(path) => goToFile({ path })"
+ />
+ </template>
+ <template #after>
+ <div class="tree-list-gutter"></div>
+ </template>
+ </recycle-scroller>
+ <p v-else class="prepend-top-20 append-bottom-20 text-center">
+ {{ s__('MergeRequest|No files found') }}
+ </p>
+ </div>
+ </template>
+ </tree-list-height>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/tree_list_height.vue b/app/assets/javascripts/diffs/components/tree_list_height.vue
new file mode 100644
index 00000000000..4da94cacd75
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/tree_list_height.vue
@@ -0,0 +1,108 @@
+<script>
+import { debounce } from 'lodash';
+// eslint-disable-next-line no-restricted-imports
+import { mapState, mapGetters } from 'vuex';
+import { contentTop } from '~/lib/utils/common_utils';
+
+const MAX_ITEMS_ON_NARROW_SCREEN = 8;
+// Should be enough for the very long titles (10+ lines) on the max smallest screen
+const MAX_SCROLL_Y = 600;
+const BOTTOM_OFFSET = 16;
+
+export default {
+ name: 'TreeListHeight',
+ props: {
+ itemsCount: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ scrollerHeight: 0,
+ rowHeight: 0,
+ reviewBarHeight: 0,
+ scrollY: 0,
+ isNarrowScreen: false,
+ mediaQueryMatch: null,
+ };
+ },
+ computed: {
+ ...mapState('batchComments', ['reviewBarRendered']),
+ ...mapGetters('batchComments', ['draftsCount']),
+ reviewBarEnabled() {
+ return this.draftsCount > 0;
+ },
+ debouncedHeightCalc() {
+ return debounce(this.calculateScrollerHeight, 100);
+ },
+ debouncedRecordScroll() {
+ return debounce(this.recordScroll, 50);
+ },
+ },
+ watch: {
+ reviewBarRendered: {
+ handler(rendered) {
+ if (!rendered || this.reviewBarHeight) return;
+ this.reviewBarHeight = document.querySelector('.js-review-bar').offsetHeight;
+ this.debouncedHeightCalc();
+ },
+ immediate: true,
+ },
+ reviewBarEnabled: 'debouncedHeightCalc',
+ scrollY: 'debouncedHeightCalc',
+ isNarrowScreen: 'recordScroll',
+ },
+ mounted() {
+ const computedStyles = getComputedStyle(this.$refs.scrollRoot);
+ this.rowHeight = parseInt(computedStyles.getPropertyValue('--file-row-height'), 10);
+
+ const largeBreakpointSize = parseInt(computedStyles.getPropertyValue('--breakpoint-lg'), 10);
+ this.mediaQueryMatch = window.matchMedia(`(max-width: ${largeBreakpointSize - 1}px)`);
+ this.isNarrowScreen = this.mediaQueryMatch.matches;
+ this.mediaQueryMatch.addEventListener('change', this.handleMediaMatch);
+
+ window.addEventListener('resize', this.debouncedHeightCalc, { passive: true });
+ window.addEventListener('scroll', this.debouncedRecordScroll, { passive: true });
+
+ this.calculateScrollerHeight();
+ },
+ beforeDestroy() {
+ this.mediaQueryMatch.removeEventListener('change', this.handleMediaMatch);
+ this.mediaQueryMatch = null;
+ window.removeEventListener('resize', this.debouncedHeightCalc, { passive: true });
+ window.removeEventListener('scroll', this.debouncedRecordScroll, { passive: true });
+ },
+ methods: {
+ recordScroll() {
+ const { scrollY } = window;
+ if (scrollY > MAX_SCROLL_Y || this.isNarrowScreen) {
+ this.scrollY = MAX_SCROLL_Y;
+ } else {
+ this.scrollY = window.scrollY;
+ }
+ },
+ handleMediaMatch({ matches }) {
+ this.isNarrowScreen = matches;
+ },
+ calculateScrollerHeight() {
+ if (this.isNarrowScreen) {
+ const maxItems = Math.min(MAX_ITEMS_ON_NARROW_SCREEN, this.itemsCount);
+ const maxHeight = maxItems * this.rowHeight;
+ this.scrollerHeight = Math.min(maxHeight, window.innerHeight - contentTop());
+ } else {
+ const { y } = this.$refs.scrollRoot.getBoundingClientRect();
+ const reviewBarOffset = this.reviewBarEnabled ? this.reviewBarHeight : 0;
+ // distance from element's top vertical position in the viewport to the bottom of the viewport minus offsets
+ this.scrollerHeight = window.innerHeight - y - reviewBarOffset - BOTTOM_OFFSET;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div ref="scrollRoot">
+ <slot :scroller-height="scrollerHeight" :row-height="rowHeight"></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index 575cd05ceb8..e48eb10753c 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -82,6 +82,7 @@ export const RENAMED_DIFF_TRANSITIONS = {
// MR Diffs known events
export const EVT_MR_PREPARED = 'mr:asyncPreparationFinished';
export const EVT_EXPAND_ALL_FILES = 'mr:diffs:expandAllFiles';
+export const EVT_DISCUSSIONS_ASSIGNED = 'mr:diffs:discussionsAssigned';
export const EVT_PERF_MARK_FILE_TREE_START = 'mr:diffs:perf:fileTreeStart';
export const EVT_PERF_MARK_FILE_TREE_END = 'mr:diffs:perf:fileTreeEnd';
export const EVT_PERF_MARK_DIFF_FILES_START = 'mr:diffs:perf:filesStart';
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index c0b6c8159dc..034dd4cf6d2 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -36,7 +36,7 @@ export default function initDiffsApp(store = notesStore) {
iid: dataset.iid || '',
endpointCoverage: dataset.endpointCoverage || '',
endpointCodequality: dataset.endpointCodequality || '',
- endpointSast: dataset.endpointSast || '',
+ sastReportAvailable: dataset.endpointSast,
helpPagePath: dataset.helpPagePath,
currentUser: JSON.parse(dataset.currentUserData) || {},
changesEmptyStateIllustration: dataset.changesEmptyStateIllustration,
@@ -86,7 +86,7 @@ export default function initDiffsApp(store = notesStore) {
iid: this.iid,
endpointCoverage: this.endpointCoverage,
endpointCodequality: this.endpointCodequality,
- endpointSast: this.endpointSast,
+ sastReportAvailable: this.sastReportAvailable,
currentUser: this.currentUser,
helpPagePath: this.helpPagePath,
shouldShow: this.activeTab === 'diffs',
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index ed8ae795bda..fcaf8e99b2d 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -49,6 +49,7 @@ import {
TRACKING_MULTIPLE_FILES_MODE,
EVT_MR_PREPARED,
FILE_DIFF_POSITION_TYPE,
+ EVT_DISCUSSIONS_ASSIGNED,
} from '../constants';
import {
DISCUSSION_SINGLE_DIFF_FAILED,
@@ -89,6 +90,7 @@ export const setBaseConfig = ({ commit }, options) => {
viewDiffsFileByFile,
mrReviews,
diffViewType,
+ perPage,
} = options;
commit(types.SET_BASE_CONFIG, {
endpoint,
@@ -104,6 +106,7 @@ export const setBaseConfig = ({ commit }, options) => {
viewDiffsFileByFile,
mrReviews,
diffViewType,
+ perPage,
});
Array.from(new Set(Object.values(mrReviews).flat())).forEach((id) => {
@@ -206,7 +209,7 @@ export const fetchFileByFile = async ({ state, getters, commit }) => {
};
export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
- let perPage = state.viewDiffsFileByFile ? 1 : 5;
+ let perPage = state.viewDiffsFileByFile ? 1 : state.perPage;
let increaseAmount = 1.4;
const startPage = 0;
const id = window?.location?.hash;
@@ -413,12 +416,16 @@ export const assignDiscussionsToDiff = (
}
Vue.nextTick(() => {
- notesEventHub.$emit('scrollToDiscussion');
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
});
};
export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
- const { file_hash: fileHash, line_code: lineCode, id } = removeDiscussion;
+ const {
+ diff_file: { file_hash: fileHash },
+ line_code: lineCode,
+ id,
+ } = removeDiscussion;
commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode, id });
};
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 31369b169f5..08c195469e3 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -39,6 +39,7 @@ export default {
viewDiffsFileByFile,
mrReviews,
diffViewType,
+ perPage,
} = options;
Object.assign(state, {
endpoint,
@@ -54,6 +55,7 @@ export default {
viewDiffsFileByFile,
mrReviews,
diffViewType,
+ perPage,
});
},
@@ -198,9 +200,10 @@ export default {
return {
...line,
discussionsExpanded:
- line.discussions && line.discussions.length
+ line.discussionsExpanded ||
+ (line.discussions && line.discussions.length
? line.discussions.some((disc) => !disc.resolved) || isLineNoteTargeted
- : false,
+ : false),
};
};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 15d2ab71bc8..fb467a606b9 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -338,7 +338,7 @@ function prepareLine(line, file) {
problems.brokenSymlink || problems.fileOnlyMoved || problems.brokenLineCode,
),
rich_text: cleanRichText(line.rich_text),
- discussionsExpanded: true,
+ discussionsExpanded: false,
discussions: [],
hasForm: false,
text: undefined,
diff --git a/app/assets/javascripts/diffs/utils/file_reviews.js b/app/assets/javascripts/diffs/utils/file_reviews.js
index 227be4e4a6c..581d0b6055b 100644
--- a/app/assets/javascripts/diffs/utils/file_reviews.js
+++ b/app/assets/javascripts/diffs/utils/file_reviews.js
@@ -43,7 +43,7 @@ export function reviewable(file) {
}
export function markFileReview(reviews, file, reviewed = true) {
- const usableReviews = { ...(reviews || {}) };
+ const usableReviews = { ...reviews };
const updatedReviews = usableReviews;
let fileReviews;
diff --git a/app/assets/javascripts/diffs/utils/sort_findings_by_file.js b/app/assets/javascripts/diffs/utils/sort_findings_by_file.js
index 3a285e80ace..3cf6dc169e4 100644
--- a/app/assets/javascripts/diffs/utils/sort_findings_by_file.js
+++ b/app/assets/javascripts/diffs/utils/sort_findings_by_file.js
@@ -1,10 +1,17 @@
export function sortFindingsByFile(newErrors = []) {
const files = {};
- newErrors.forEach(({ filePath, line, description, severity }) => {
+ newErrors.forEach(({ line, description, severity, filePath, webUrl, engineName }) => {
if (!files[filePath]) {
files[filePath] = [];
}
- files[filePath].push({ line, description, severity: severity.toLowerCase() });
+ files[filePath].push({
+ line,
+ description,
+ severity: severity.toLowerCase(),
+ filePath,
+ webUrl,
+ engineName,
+ });
});
const sortedFiles = Object.keys(files)
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
index 2be671ec7d8..2d50b7e4319 100644
--- a/app/assets/javascripts/editor/constants.js
+++ b/app/assets/javascripts/editor/constants.js
@@ -53,11 +53,6 @@ export const EDITOR_EXTENSION_STORE_IS_MISSING_ERROR = s__(
export const EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS = 'link-anchor';
export const EXTENSION_BASE_LINE_NUMBERS_CLASS = 'line-numbers';
-// For CI config schemas the filename must match
-// '*.gitlab-ci.yml' regardless of project configuration.
-// https://gitlab.com/gitlab-org/gitlab/-/issues/293641
-export const EXTENSION_CI_SCHEMA_FILE_NAME_MATCH = '.gitlab-ci.yml';
-
export const EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS = 'md';
export const EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS = 'source-editor-preview';
export const EXTENSION_MARKDOWN_PREVIEW_ACTION_ID = 'markdown-preview';
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 0420ffb82f5..308a68544bc 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -2093,9 +2093,15 @@
"description": "A path to a directory that contains the files to be published with Pages",
"type": "string"
},
- "pages_path_prefix": {
- "description": "The path prefix identifier for this version of pages. Allows creation of multiple versions of the same site with different path prefixes",
- "type": "string"
+ "pages": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "path_prefix": {
+ "type": "string",
+ "markdownDescription": "The GitLab Pages URL path prefix used in this version of pages."
+ }
+ }
}
},
"oneOf": [
diff --git a/app/assets/javascripts/emoji/awards_app/store/actions.js b/app/assets/javascripts/emoji/awards_app/store/actions.js
index 677c11277a3..54d98687684 100644
--- a/app/assets/javascripts/emoji/awards_app/store/actions.js
+++ b/app/assets/javascripts/emoji/awards_app/store/actions.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { joinPaths } from '~/lib/utils/url_utility';
diff --git a/app/assets/javascripts/emoji/components/picker.vue b/app/assets/javascripts/emoji/components/picker.vue
index 238f0d81b22..462420ba4e5 100644
--- a/app/assets/javascripts/emoji/components/picker.vue
+++ b/app/assets/javascripts/emoji/components/picker.vue
@@ -3,8 +3,8 @@
import { GlIcon, GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { findLastIndex } from 'lodash';
import VirtualList from 'vue-virtual-scroll-list';
-import { CATEGORY_NAMES, getEmojiCategoryMap, state } from '~/emoji';
-import { CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from '../constants';
+import { getEmojiCategoryMap, state } from '~/emoji';
+import { CATEGORY_NAMES, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from '../constants';
import Category from './category.vue';
import EmojiList from './emoji_list.vue';
import { addToFrequentlyUsed, getEmojiCategories, hasFrequentlyUsedEmojis } from './utils';
diff --git a/app/assets/javascripts/emoji/constants.js b/app/assets/javascripts/emoji/constants.js
index 215ecbfe605..c8bcb79ad15 100644
--- a/app/assets/javascripts/emoji/constants.js
+++ b/app/assets/javascripts/emoji/constants.js
@@ -1,6 +1,18 @@
export const FREQUENTLY_USED_KEY = 'frequently_used';
export const FREQUENTLY_USED_COOKIE_KEY = 'frequently_used_emojis';
+export const CATEGORY_NAMES = [
+ FREQUENTLY_USED_KEY,
+ 'custom',
+ 'people',
+ 'activity',
+ 'nature',
+ 'food',
+ 'travel',
+ 'objects',
+ 'symbols',
+ 'flags',
+];
export const CATEGORY_ICON_MAP = {
[FREQUENTLY_USED_KEY]: 'history',
custom: 'tanuki',
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index 1fa81a000a5..f98369c2fde 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -8,7 +8,7 @@ import { getEmojiScoreWithIntent } from '~/emoji/utils';
import AccessorUtilities from '../lib/utils/accessor';
import axios from '../lib/utils/axios_utils';
import customEmojiQuery from './queries/custom_emoji.query.graphql';
-import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
+import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_NAMES, FREQUENTLY_USED_KEY } from './constants';
let emojiMap = null;
let validEmojiNames = null;
@@ -20,22 +20,27 @@ export const state = Vue.observable({
export const FALLBACK_EMOJI_KEY = 'grey_question';
// Keep the version in sync with `lib/gitlab/emoji.rb`
-export const EMOJI_VERSION = '2';
+export const EMOJI_VERSION = '3';
const isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
async function loadEmoji() {
- if (
- isLocalStorageAvailable &&
- window.localStorage.getItem(CACHE_VERSION_KEY) === EMOJI_VERSION &&
- window.localStorage.getItem(CACHE_KEY)
- ) {
- const emojis = JSON.parse(window.localStorage.getItem(CACHE_KEY));
- // Workaround because the pride flag is broken in EMOJI_VERSION = '1'
- if (emojis.gay_pride_flag) {
- emojis.gay_pride_flag.e = '🏳️‍🌈';
+ try {
+ window.localStorage.removeItem(CACHE_VERSION_KEY);
+ } catch {
+ // Cleanup after us and remove the old EMOJI_VERSION_KEY
+ }
+
+ try {
+ if (isLocalStorageAvailable) {
+ const parsed = JSON.parse(window.localStorage.getItem(CACHE_KEY));
+ if (parsed?.EMOJI_VERSION === EMOJI_VERSION && parsed.data) {
+ return parsed.data;
+ }
}
- return emojis;
+ } catch {
+ // Maybe the stored data was corrupted or the version didn't match.
+ // Let's not error out.
}
// We load the JSON file direct from the server
@@ -44,21 +49,31 @@ async function loadEmoji() {
const { data } = await axios.get(
`${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`,
);
- window.localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
- window.localStorage.setItem(CACHE_KEY, JSON.stringify(data));
+
+ try {
+ window.localStorage.setItem(CACHE_KEY, JSON.stringify({ data, EMOJI_VERSION }));
+ } catch {
+ // Setting data in localstorage may fail when storage quota is exceeded.
+ // We should continue even when this fails.
+ }
+
return data;
}
async function loadEmojiWithNames() {
const emojiRegex = emojiRegexFactory();
- return Object.entries(await loadEmoji()).reduce((acc, [key, value]) => {
- // Filter out entries which aren't emojis
- if (value.e.match(emojiRegex)?.[0] === value.e) {
- acc[key] = { ...value, name: key };
- }
- return acc;
- }, {});
+ return (await loadEmoji()).reduce(
+ (acc, emoji) => {
+ // Filter out entries which aren't emojis
+ if (emoji.e.match(emojiRegex)?.[0] === emoji.e) {
+ acc.emojis[emoji.n] = { ...emoji, name: emoji.n };
+ acc.names.push(emoji.n);
+ }
+ return acc;
+ },
+ { emojis: {}, names: [] },
+ );
}
export async function loadCustomEmojiWithNames() {
@@ -71,31 +86,35 @@ export async function loadCustomEmojiWithNames() {
},
});
- return data?.group?.customEmoji?.nodes?.reduce((acc, e) => {
- // Map the custom emoji into the format of the normal emojis
- acc[e.name] = {
- c: 'custom',
- d: e.name,
- e: undefined,
- name: e.name,
- src: e.url,
- u: 'custom',
- };
+ return data?.group?.customEmoji?.nodes?.reduce(
+ (acc, e) => {
+ // Map the custom emoji into the format of the normal emojis
+ acc.emojis[e.name] = {
+ c: 'custom',
+ d: e.name,
+ e: undefined,
+ name: e.name,
+ src: e.url,
+ u: 'custom',
+ };
+ acc.names.push(e.name);
- return acc;
- }, {});
+ return acc;
+ },
+ { emojis: {}, names: [] },
+ );
}
- return {};
+ return { emojis: {}, names: [] };
}
async function prepareEmojiMap() {
return Promise.all([loadEmojiWithNames(), loadCustomEmojiWithNames()]).then((values) => {
emojiMap = {
- ...values[0],
- ...values[1],
+ ...values[0].emojis,
+ ...values[1].emojis,
};
- validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
+ validEmojiNames = [...values[0].names, ...values[1].names];
state.loading = false;
});
}
@@ -109,10 +128,6 @@ export function normalizeEmojiName(name) {
return Object.prototype.hasOwnProperty.call(emojiAliases, name) ? emojiAliases[name] : name;
}
-export function getValidEmojiNames() {
- return validEmojiNames;
-}
-
export function isEmojiNameValid(name) {
if (!emojiMap) {
// eslint-disable-next-line @gitlab/require-i18n-strings
@@ -122,10 +137,14 @@ export function isEmojiNameValid(name) {
return name in emojiMap || name in emojiAliases;
}
-export function getAllEmoji() {
+export function getEmojiMap() {
return emojiMap;
}
+export function getAllEmoji() {
+ return validEmojiNames.map((n) => emojiMap[n]);
+}
+
export function findCustomEmoji(name) {
return emojiMap[name];
}
@@ -218,8 +237,6 @@ export function searchEmoji(query) {
.sort(sortEmoji);
}
-export const CATEGORY_NAMES = Object.keys(CATEGORY_ICON_MAP);
-
let emojiCategoryMap;
export function getEmojiCategoryMap() {
if (!emojiCategoryMap && emojiMap) {
@@ -229,7 +246,7 @@ export function getEmojiCategoryMap() {
}
return { ...acc, [category]: [] };
}, {});
- Object.keys(emojiMap).forEach((name) => {
+ validEmojiNames.forEach((name) => {
const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.c]) {
emojiCategoryMap[emoji.c].push(name);
diff --git a/app/assets/javascripts/ensure_data.js b/app/assets/javascripts/ensure_data.js
index 4566ab20258..ddb34e59144 100644
--- a/app/assets/javascripts/ensure_data.js
+++ b/app/assets/javascripts/ensure_data.js
@@ -1,6 +1,6 @@
import emptySvg from '@gitlab/svgs/dist/illustrations/security-dashboard-empty-state.svg?raw';
import { GlEmptyState } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __ } from '~/locale';
export const ERROR_FETCHING_DATA_HEADER = __('Could not get the data properly');
diff --git a/app/assets/javascripts/entrypoints/analytics.js b/app/assets/javascripts/entrypoints/analytics.js
index 8eb265cb1e8..e18c4bc8742 100644
--- a/app/assets/javascripts/entrypoints/analytics.js
+++ b/app/assets/javascripts/entrypoints/analytics.js
@@ -14,4 +14,10 @@ if (appId && host) {
errorTracking: false,
},
});
+
+ const userId = window.gl?.snowplowStandardContext?.data?.user_id;
+
+ if (userId) {
+ window.glClient?.identify(userId);
+ }
}
diff --git a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
index 2cf71de7ea2..2bc65e4ad04 100644
--- a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
+++ b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
@@ -3,8 +3,8 @@
* Render modal to confirm rollback/redeploy.
*/
import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { escape } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import csrf from '~/lib/utils/csrf';
import { __, s__, sprintf } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue
index 8ebba0e27bb..c6cf6b7e24b 100644
--- a/app/assets/javascripts/environments/components/environment_form.vue
+++ b/app/assets/javascripts/environments/components/environment_form.vue
@@ -193,6 +193,7 @@ export default {
headers: {
'GitLab-Agent-Id': getIdFromGraphQLId(this.selectedAgentId),
'Content-Type': 'application/json',
+ Accept: 'application/json',
...csrf.headers,
},
credentials: 'include',
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 08a1eacec7a..47edec8dcb0 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -752,7 +752,7 @@ export default {
:title="upcomingDeploymentTooltipText"
data-testid="upcoming-deployment-status-link"
>
- <ci-icon class="gl-mr-2" :status="upcomingDeployment.deployable.status" />
+ <ci-icon :status="upcomingDeployment.deployable.status" class="gl-mr-2" />
</gl-link>
</div>
<span
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 795cbf5327a..4e8b75536a4 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -78,7 +78,7 @@ export default {
newEnvironmentButtonLabel: s__('Environments|New environment'),
reviewAppButtonLabel: s__('Environments|Enable review apps'),
cleanUpEnvsButtonLabel: s__('Environments|Clean up environments'),
- available: __('Available'),
+ active: __('Active'),
stopped: __('Stopped'),
prevPage: __('Go to previous page'),
nextPage: __('Go to next page'),
@@ -97,9 +97,7 @@ export default {
isStopStaleEnvModalVisible: false,
page: parseInt(page, 10),
pageInfo: {},
- scope: Object.values(ENVIRONMENTS_SCOPE).includes(scope)
- ? scope
- : ENVIRONMENTS_SCOPE.AVAILABLE,
+ scope: Object.values(ENVIRONMENTS_SCOPE).includes(scope) ? scope : ENVIRONMENTS_SCOPE.ACTIVE,
environmentToDelete: {},
environmentToRollback: {},
environmentToStop: {},
@@ -112,6 +110,9 @@ export default {
canSetupReviewApp() {
return this.environmentApp?.reviewApp?.canSetupReviewApp;
},
+ hasReviewApp() {
+ return this.environmentApp?.reviewApp?.hasReviewApp;
+ },
canCleanUpEnvs() {
return this.environmentApp?.canStopStaleEnvironments;
},
@@ -130,14 +131,14 @@ export default {
hasSearch() {
return Boolean(this.search);
},
- availableCount() {
- return this.environmentApp?.availableCount;
+ activeCount() {
+ return this.environmentApp?.activeCount ?? 0;
},
stoppedCount() {
- return this.environmentApp?.stoppedCount;
+ return this.environmentApp?.stoppedCount ?? 0;
},
hasAnyEnvironment() {
- return this.availableCount > 0 || this.stoppedCount > 0;
+ return this.activeCount > 0 || this.stoppedCount > 0;
},
showContent() {
return this.hasAnyEnvironment || this.hasSearch;
@@ -157,7 +158,10 @@ export default {
};
},
openReviewAppModal() {
- if (!this.canSetupReviewApp) {
+ // we don't show the Enable review apps button
+ // if a user cannot setup a review app or review
+ // apps are already configured
+ if (!this.canSetupReviewApp || this.hasReviewApp) {
return null;
}
@@ -272,13 +276,13 @@ export default {
@primary="showCleanUpEnvsModal"
>
<gl-tab
- :query-param-value="$options.ENVIRONMENTS_SCOPE.AVAILABLE"
- @click="setScope($options.ENVIRONMENTS_SCOPE.AVAILABLE)"
+ :query-param-value="$options.ENVIRONMENTS_SCOPE.ACTIVE"
+ @click="setScope($options.ENVIRONMENTS_SCOPE.ACTIVE)"
>
<template #title>
- <span>{{ $options.i18n.available }}</span>
+ <span>{{ $options.i18n.active }}</span>
<gl-badge size="sm" class="gl-tab-counter-badge">
- {{ availableCount }}
+ {{ activeCount }}
</gl-badge>
</template>
</gl-tab>
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index b47086a19da..2f49ed847bf 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -147,12 +147,7 @@ export default {
</div>
</div>
<template v-for="(model, i) in sortedEnvironments">
- <environment-item
- :key="`environment-item-${i}`"
- :model="model"
- :table-data="tableData"
- data-qa-selector="environment_item"
- />
+ <environment-item :key="`environment-item-${i}`" :model="model" :table-data="tableData" />
<div
v-if="shouldRenderDeployBoard(model)"
@@ -185,7 +180,6 @@ export default {
:key="`environment-row-${i}-${index}`"
:model="child"
:table-data="tableData"
- data-qa-selector="environment_item"
/>
<div
diff --git a/app/assets/javascripts/environments/components/kubernetes_overview.vue b/app/assets/javascripts/environments/components/kubernetes_overview.vue
index 252ced6391d..36cce29d624 100644
--- a/app/assets/javascripts/environments/components/kubernetes_overview.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_overview.vue
@@ -64,6 +64,7 @@ export default {
headers: {
'GitLab-Agent-Id': this.gitlabAgentId,
'Content-Type': 'application/json',
+ Accept: 'application/json',
...csrf.headers,
},
credentials: 'include',
@@ -110,7 +111,6 @@ export default {
<kubernetes-status-bar
:cluster-health-status="clusterHealthStatus"
:configuration="k8sAccessConfiguration"
- :namespace="namespace"
:environment-name="environmentName"
:flux-resource-path="fluxResourcePath"
class="gl-mb-3" />
diff --git a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
index c603d83db9c..8ecb61711ce 100644
--- a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
@@ -37,11 +37,6 @@ export default {
required: true,
type: String,
},
- namespace: {
- required: false,
- type: String,
- default: '',
- },
fluxResourcePath: {
required: false,
type: String,
@@ -54,14 +49,12 @@ export default {
variables() {
return {
configuration: this.configuration,
- namespace: this.namespace,
- environmentName: this.environmentName.toLowerCase(),
fluxResourcePath: this.fluxResourcePath,
};
},
skip() {
return Boolean(
- !this.namespace || this.fluxResourcePath?.includes(HELM_RELEASES_RESOURCE_TYPE),
+ !this.fluxResourcePath || this.fluxResourcePath?.includes(HELM_RELEASES_RESOURCE_TYPE),
);
},
error(err) {
@@ -73,17 +66,12 @@ export default {
variables() {
return {
configuration: this.configuration,
- namespace: this.namespace,
- environmentName: this.environmentName.toLowerCase(),
fluxResourcePath: this.fluxResourcePath,
};
},
skip() {
return Boolean(
- !this.namespace ||
- this.$apollo.queries.fluxKustomizationStatus.loading ||
- this.hasKustomizations ||
- this.fluxResourcePath?.includes(KUSTOMIZATIONS_RESOURCE_TYPE),
+ !this.fluxResourcePath || this.fluxResourcePath?.includes(KUSTOMIZATIONS_RESOURCE_TYPE),
);
},
error(err) {
diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js
index 7214454c45c..e97720312b0 100644
--- a/app/assets/javascripts/environments/constants.js
+++ b/app/assets/javascripts/environments/constants.js
@@ -42,12 +42,12 @@ export const CANARY_STATUS = {
export const CANARY_UPDATE_MODAL = 'confirm-canary-change';
export const ENVIRONMENTS_SCOPE = {
- AVAILABLE: 'available',
+ ACTIVE: 'active',
STOPPED: 'stopped',
};
export const ENVIRONMENT_COUNT_BY_SCOPE = {
- [ENVIRONMENTS_SCOPE.AVAILABLE]: 'availableCount',
+ [ENVIRONMENTS_SCOPE.ACTIVE]: 'activeCount',
[ENVIRONMENTS_SCOPE.STOPPED]: 'stoppedCount',
};
diff --git a/app/assets/javascripts/environments/environment_details/index.vue b/app/assets/javascripts/environments/environment_details/index.vue
index 6e3ec04ba3b..bc535eb73aa 100644
--- a/app/assets/javascripts/environments/environment_details/index.vue
+++ b/app/assets/javascripts/environments/environment_details/index.vue
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { logError } from '~/lib/logger';
import { toggleQueryPollingByVisibility, etagQueryHeaders } from '~/graphql_shared/utils';
import ConfirmRollbackModal from '~/environments/components/confirm_rollback_modal.vue';
diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js
index 8faed710402..8f57069d89d 100644
--- a/app/assets/javascripts/environments/graphql/client.js
+++ b/app/assets/javascripts/environments/graphql/client.js
@@ -17,7 +17,6 @@ import typeDefs from './typedefs.graphql';
export const apolloProvider = (endpoint) => {
const defaultClient = createDefaultClient(resolvers(endpoint), {
typeDefs,
- useGet: true,
});
const { cache } = defaultClient;
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
index 7a50ded7d6c..ef5a8194dca 100644
--- a/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
@@ -1,6 +1,6 @@
query getEnvironmentApp($page: Int, $scope: String, $search: String) {
environmentApp(page: $page, scope: $scope, search: $search) @client {
- availableCount
+ activeCount
stoppedCount
environments
reviewApp
diff --git a/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
index 544232dafd7..042bdc1992d 100644
--- a/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
@@ -1,15 +1,6 @@
-query getFluxHelmReleaseStatusQuery(
- $configuration: LocalConfiguration
- $namespace: String
- $environmentName: String
- $fluxResourcePath: String
-) {
- fluxHelmReleaseStatus(
- configuration: $configuration
- namespace: $namespace
- environmentName: $environmentName
- fluxResourcePath: $fluxResourcePath
- ) @client {
+query getFluxHelmReleaseStatusQuery($configuration: LocalConfiguration, $fluxResourcePath: String) {
+ fluxHelmReleaseStatus(configuration: $configuration, fluxResourcePath: $fluxResourcePath)
+ @client {
message
status
type
diff --git a/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql
index 2884f95355e..458b8a4d9db 100644
--- a/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql
@@ -1,15 +1,9 @@
query getFluxHelmKustomizationStatusQuery(
$configuration: LocalConfiguration
- $namespace: String
- $environmentName: String
$fluxResourcePath: String
) {
- fluxKustomizationStatus(
- configuration: $configuration
- namespace: $namespace
- environmentName: $environmentName
- fluxResourcePath: $fluxResourcePath
- ) @client {
+ fluxKustomizationStatus(configuration: $configuration, fluxResourcePath: $fluxResourcePath)
+ @client {
message
status
type
diff --git a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
index c662acb8f93..ac6a68e450c 100644
--- a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
@@ -1,6 +1,6 @@
query getEnvironmentFolder($environment: NestedLocalEnvironment, $scope: String, $search: String) {
folder(environment: $environment, scope: $scope, search: $search) @client {
- availableCount
+ activeCount
environments
stoppedCount
}
diff --git a/app/assets/javascripts/environments/graphql/resolvers/base.js b/app/assets/javascripts/environments/graphql/resolvers/base.js
index 9752a3a6634..4427b8ff2ef 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/base.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/base.js
@@ -47,7 +47,7 @@ export const baseQueries = (endpoint) => ({
});
return {
- availableCount: res.data.available_count,
+ activeCount: res.data.active_count,
environments: res.data.environments.map(mapNestedEnvironment),
reviewApp: {
...convertObjectPropsToCamelCase(res.data.review_app),
@@ -61,7 +61,7 @@ export const baseQueries = (endpoint) => ({
},
folder(_, { environment: { folderPath }, scope, search }) {
return axios.get(folderPath, { params: { scope, search, per_page: 3 } }).then((res) => ({
- availableCount: res.data.available_count,
+ activeCount: res.data.active_count,
environments: res.data.environments.map(mapEnvironment),
stoppedCount: res.data.stopped_count,
__typename: 'LocalEnvironmentFolder',
diff --git a/app/assets/javascripts/environments/graphql/resolvers/flux.js b/app/assets/javascripts/environments/graphql/resolvers/flux.js
index d39b1bed7b6..5cb5db5d752 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/flux.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/flux.js
@@ -1,35 +1,83 @@
+import { Configuration, WatchApi, EVENT_DATA } from '@gitlab/cluster-client';
import axios from '~/lib/utils/axios_utils';
import {
HELM_RELEASES_RESOURCE_TYPE,
KUSTOMIZATIONS_RESOURCE_TYPE,
} from '~/environments/constants';
+import fluxKustomizationStatusQuery from '../queries/flux_kustomization_status.query.graphql';
+import fluxHelmReleaseStatusQuery from '../queries/flux_helm_release_status.query.graphql';
const helmReleasesApiVersion = 'helm.toolkit.fluxcd.io/v2beta1';
const kustomizationsApiVersion = 'kustomize.toolkit.fluxcd.io/v1beta1';
+const helmReleaseField = 'fluxHelmReleaseStatus';
+const kustomizationField = 'fluxKustomizationStatus';
+
const handleClusterError = (err) => {
const error = err?.response?.data?.message ? new Error(err.response.data.message) : err;
throw error;
};
-const buildFluxResourceUrl = ({
- basePath,
- namespace,
- apiVersion,
- resourceType,
- environmentName = '',
-}) => {
- return `${basePath}/apis/${apiVersion}/namespaces/${namespace}/${resourceType}/${environmentName}`;
+const buildFluxResourceUrl = ({ basePath, namespace, apiVersion, resourceType }) => {
+ return `${basePath}/apis/${apiVersion}/namespaces/${namespace}/${resourceType}`;
};
-const getFluxResourceStatus = (configuration, url) => {
- const { headers } = configuration;
+const buildFluxResourceWatchPath = ({ namespace, apiVersion, resourceType }) => {
+ return `/apis/${apiVersion}/namespaces/${namespace}/${resourceType}`;
+};
+
+const watchFluxResource = ({ watchPath, resourceName, query, variables, field, client }) => {
+ const config = new Configuration(variables.configuration);
+ const watcherApi = new WatchApi(config);
+ const fieldSelector = `metadata.name=${decodeURIComponent(resourceName)}`;
+
+ watcherApi
+ .subscribeToStream(watchPath, { watch: true, fieldSelector })
+ .then((watcher) => {
+ let result = [];
+
+ watcher.on(EVENT_DATA, (data) => {
+ result = data[0]?.status?.conditions;
+
+ client.writeQuery({
+ query,
+ variables,
+ data: { [field]: result },
+ });
+ });
+ })
+ .catch((err) => {
+ handleClusterError(err);
+ });
+};
+
+const getFluxResourceStatus = ({ query, variables, field, resourceType, client }) => {
+ const { headers } = variables.configuration;
const withCredentials = true;
+ const url = `${variables.configuration.basePath}/apis/${variables.fluxResourcePath}`;
return axios
.get(url, { withCredentials, headers })
.then((res) => {
- return res?.data?.status?.conditions || [];
+ const fluxData = res?.data;
+ const resourceName = fluxData?.metadata?.name;
+ const namespace = fluxData?.metadata?.namespace;
+ const apiVersion = fluxData?.apiVersion;
+
+ if (gon.features?.k8sWatchApi && resourceName) {
+ const watchPath = buildFluxResourceWatchPath({ namespace, apiVersion, resourceType });
+
+ watchFluxResource({
+ watchPath,
+ resourceName,
+ query,
+ variables,
+ field,
+ client,
+ });
+ }
+
+ return fluxData?.status?.conditions || [];
})
.catch((err) => {
handleClusterError(err);
@@ -62,37 +110,23 @@ const getFluxResources = (configuration, url) => {
};
export default {
- fluxKustomizationStatus(_, { configuration, namespace, environmentName, fluxResourcePath = '' }) {
- let url;
-
- if (fluxResourcePath) {
- url = `${configuration.basePath}/apis/${fluxResourcePath}`;
- } else {
- url = buildFluxResourceUrl({
- basePath: configuration.basePath,
- resourceType: KUSTOMIZATIONS_RESOURCE_TYPE,
- apiVersion: kustomizationsApiVersion,
- namespace,
- environmentName,
- });
- }
- return getFluxResourceStatus(configuration, url);
+ fluxKustomizationStatus(_, { configuration, fluxResourcePath }, { client }) {
+ return getFluxResourceStatus({
+ query: fluxKustomizationStatusQuery,
+ variables: { configuration, fluxResourcePath },
+ field: kustomizationField,
+ resourceType: KUSTOMIZATIONS_RESOURCE_TYPE,
+ client,
+ });
},
- fluxHelmReleaseStatus(_, { configuration, namespace, environmentName, fluxResourcePath }) {
- let url;
-
- if (fluxResourcePath) {
- url = `${configuration.basePath}/apis/${fluxResourcePath}`;
- } else {
- url = buildFluxResourceUrl({
- basePath: configuration.basePath,
- resourceType: HELM_RELEASES_RESOURCE_TYPE,
- apiVersion: helmReleasesApiVersion,
- namespace,
- environmentName,
- });
- }
- return getFluxResourceStatus(configuration, url);
+ fluxHelmReleaseStatus(_, { configuration, fluxResourcePath }, { client }) {
+ return getFluxResourceStatus({
+ query: fluxHelmReleaseStatusQuery,
+ variables: { configuration, fluxResourcePath },
+ field: helmReleaseField,
+ resourceType: HELM_RELEASES_RESOURCE_TYPE,
+ client,
+ });
},
fluxKustomizations(_, { configuration, namespace }) {
const url = buildFluxResourceUrl({
diff --git a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
index 67a472dac93..8375b8793d9 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
@@ -1,5 +1,13 @@
-import { CoreV1Api, Configuration, AppsV1Api, BatchV1Api } from '@gitlab/cluster-client';
+import {
+ CoreV1Api,
+ Configuration,
+ AppsV1Api,
+ BatchV1Api,
+ WatchApi,
+ EVENT_DATA,
+} from '@gitlab/cluster-client';
import { humanizeClusterErrors } from '../../helpers/k8s_integration_helper';
+import k8sPodsQuery from '../queries/k8s_pods.query.graphql';
const mapWorkloadItems = (items, kind) => {
return items.map((item) => {
@@ -53,15 +61,50 @@ const handleClusterError = async (err) => {
throw errorData;
};
+const watchPods = ({ configuration, namespace, client }) => {
+ const path = namespace ? `/api/v1/namespaces/${namespace}/pods` : '/api/v1/pods';
+ const config = new Configuration(configuration);
+ const watcherApi = new WatchApi(config);
+
+ watcherApi
+ .subscribeToStream(path, { watch: true })
+ .then((watcher) => {
+ let result = [];
+
+ watcher.on(EVENT_DATA, (data) => {
+ result = data.map((item) => {
+ return { status: { phase: item.status.phase } };
+ });
+
+ client.writeQuery({
+ query: k8sPodsQuery,
+ variables: { configuration, namespace },
+ data: { k8sPods: result },
+ });
+ });
+ })
+ .catch((err) => {
+ handleClusterError(err);
+ });
+};
+
export default {
- k8sPods(_, { configuration, namespace }) {
- const coreV1Api = new CoreV1Api(new Configuration(configuration));
+ k8sPods(_, { configuration, namespace }, { client }) {
+ const config = new Configuration(configuration);
+
+ const coreV1Api = new CoreV1Api(config);
const podsApi = namespace
? coreV1Api.listCoreV1NamespacedPod({ namespace })
: coreV1Api.listCoreV1PodForAllNamespaces();
return podsApi
- .then((res) => res?.items || [])
+ .then((res) => {
+ if (gon.features?.k8sWatchApi) {
+ watchPods({ configuration, namespace, client });
+ }
+
+ return res?.items || [];
+ })
.catch(async (err) => {
try {
await handleClusterError(err);
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index 1821aa073bc..01879a092ed 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -196,7 +196,7 @@ export default {
text: __('Create issue'),
action: this.createIssue,
extraAttrs: {
- 'data-qa-selector': 'create_issue_button',
+ 'data-testid': 'create-issue-button',
},
};
},
@@ -309,7 +309,7 @@ export default {
<div
v-if="!loadingStacktrace && stacktrace"
class="gl-my-auto gl-text-truncate"
- data-qa-selector="reported_text"
+ data-testid="reported-text"
>
<gl-sprintf :message="__('Reported %{timeAgo} by %{reportedBy}')">
<template #reportedBy>
@@ -367,7 +367,7 @@ export default {
category="primary"
variant="confirm"
:loading="issueCreationInProgress"
- data-qa-selector="create_issue_button"
+ data-testid="create-issue-button"
@click="createIssue"
>
{{ __('Create issue') }}
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags.vue b/app/assets/javascripts/feature_flags/components/feature_flags.vue
index daaeb5f8e85..e0a5e92564e 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags.vue
@@ -206,7 +206,6 @@ export default {
v-gl-modal="'configure-feature-flags'"
variant="confirm"
category="secondary"
- data-qa-selector="configure_feature_flags_button"
data-testid="ff-configure-button"
class="gl-mb-0 gl-mr-3"
>
diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
index 4d3647cdf5c..74d91734630 100644
--- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
+++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
@@ -5,9 +5,10 @@ import {
TOKEN_TYPE_APPROVED_BY,
TOKEN_TYPE_REVIEWER,
TOKEN_TYPE_TARGET_BRANCH,
+ TOKEN_TYPE_SOURCE_BRANCH,
} from '~/vue_shared/components/filtered_search_bar/constants';
-export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
+export default (IssuableTokenKeys, disableBranchFilter = false) => {
const reviewerToken = {
formattedKey: TOKEN_TITLE_REVIEWER,
key: TOKEN_TYPE_REVIEWER,
@@ -57,7 +58,7 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
IssuableTokenKeys.tokenKeysWithAlternative.push(draftToken.token);
IssuableTokenKeys.conditions.push(...draftToken.conditions);
- if (!disableTargetBranchFilter) {
+ if (!disableBranchFilter) {
const targetBranchToken = {
formattedKey: __('Target-Branch'),
key: TOKEN_TYPE_TARGET_BRANCH,
@@ -68,8 +69,18 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
tag: 'branch',
};
- IssuableTokenKeys.tokenKeys.push(targetBranchToken);
- IssuableTokenKeys.tokenKeysWithAlternative.push(targetBranchToken);
+ const sourceBranchToken = {
+ formattedKey: __('Source-Branch'),
+ key: TOKEN_TYPE_SOURCE_BRANCH,
+ type: 'string',
+ param: '',
+ symbol: '',
+ icon: 'branch',
+ tag: 'branch',
+ };
+
+ IssuableTokenKeys.tokenKeys.push(targetBranchToken, sourceBranchToken);
+ IssuableTokenKeys.tokenKeysWithAlternative.push(targetBranchToken, sourceBranchToken);
}
const approvedToken = {
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index 892e9130fe8..a1782c549d6 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -11,6 +11,7 @@ import {
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_REVIEWER,
TOKEN_TYPE_TARGET_BRANCH,
+ TOKEN_TYPE_SOURCE_BRANCH,
} from '~/vue_shared/components/filtered_search_bar/constants';
import DropdownEmoji from './dropdown_emoji';
import DropdownHint from './dropdown_hint';
@@ -157,6 +158,15 @@ export default class AvailableDropdownMappings {
},
element: this.container.querySelector('#js-dropdown-target-branch'),
},
+ [TOKEN_TYPE_SOURCE_BRANCH]: {
+ reference: null,
+ gl: DropdownNonUser,
+ extraArguments: {
+ endpoint: this.getMergeRequestSourceBranchesEndpoint(),
+ symbol: '',
+ },
+ element: this.container.querySelector('#js-dropdown-source-branch'),
+ },
environment: {
reference: null,
gl: DropdownNonUser,
@@ -197,10 +207,17 @@ export default class AvailableDropdownMappings {
}
getMergeRequestTargetBranchesEndpoint() {
- const endpoint = `${
- gon.relative_url_root || ''
- }/-/autocomplete/merge_request_target_branches.json`;
+ const targetBranchEndpointPath = '/-/autocomplete/merge_request_target_branches.json';
+ return this.getMergeRequestBranchesEndpoint(targetBranchEndpointPath);
+ }
+
+ getMergeRequestSourceBranchesEndpoint() {
+ const sourceBranchEndpointPath = '/-/autocomplete/merge_request_source_branches.json';
+ return this.getMergeRequestBranchesEndpoint(sourceBranchEndpointPath);
+ }
+ getMergeRequestBranchesEndpoint(endpointPath = '') {
+ const endpoint = `${gon.relative_url_root || ''}${endpointPath}`;
const params = {
group_id: this.getGroupId(),
project_id: this.getProjectId(),
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 99d22b1330b..39a8b1d0a9c 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -979,7 +979,7 @@ GfmAutoComplete.Emoji = {
},
filter(query) {
if (query.length === 0) {
- return Object.values(Emoji.getAllEmoji())
+ return Emoji.getAllEmoji()
.map((emoji) => ({
emoji,
fieldValue: emoji.name,
diff --git a/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue b/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
index f19e047061f..dcf6c90f7fa 100644
--- a/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
+++ b/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import { captureException } from '@sentry/browser';
+import { captureException } from '~/sentry/sentry_browser_wrapper';
import PipelineWizard from '~/pipeline_wizard/pipeline_wizard.vue';
import PagesWizardTemplate from '~/pipeline_wizard/templates/pages.yml?raw';
import { logError } from '~/lib/logger';
diff --git a/app/assets/javascripts/graphql_shared/constants.js b/app/assets/javascripts/graphql_shared/constants.js
index 5ba46697496..2863f52bea9 100644
--- a/app/assets/javascripts/graphql_shared/constants.js
+++ b/app/assets/javascripts/graphql_shared/constants.js
@@ -27,5 +27,6 @@ export const TYPENAME_USER = 'User';
export const TYPENAME_VULNERABILITIES_SCANNER = 'Vulnerabilities::Scanner';
export const TYPENAME_VULNERABILITY = 'Vulnerability';
export const TYPENAME_WORK_ITEM = 'WorkItem';
+export const TYPENAME_ORGANIZATION = 'Organization';
export const TYPE_USERS_SAVED_REPLY = 'Users::SavedReply';
export const TYPE_WORKSPACE = 'RemoteDevelopment::Workspace';
diff --git a/app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql
index 85a28fe1f71..458fdb24e6d 100644
--- a/app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql
@@ -6,6 +6,7 @@ fragment IssueNode on Issue {
iid
title
referencePath: reference(full: true)
+ closedAt
dueDate
timeEstimate
totalTimeSpent
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 4e0b1413f71..1439a3181b0 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -67,9 +67,11 @@
],
"MemberInterface": [
"GroupMember",
+ "PendingGroupMember",
"ProjectMember"
],
"NoteableInterface": [
+ "AbuseReport",
"AlertManagementAlert",
"BoardEpic",
"Design",
diff --git a/app/assets/javascripts/graphql_shared/queries/groups_autocomplete.query.graphql b/app/assets/javascripts/graphql_shared/queries/groups_autocomplete.query.graphql
new file mode 100644
index 00000000000..74da46e5a60
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/queries/groups_autocomplete.query.graphql
@@ -0,0 +1,10 @@
+query groupsAutocomplete($search: String) {
+ groups(search: $search) {
+ nodes {
+ id
+ name
+ fullName
+ avatarUrl
+ }
+ }
+}
diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue
index fc7cfffc22c..43689e6677b 100644
--- a/app/assets/javascripts/groups/components/item_actions.vue
+++ b/app/assets/javascripts/groups/components/item_actions.vue
@@ -57,7 +57,6 @@ export default {
icon="ellipsis_v"
no-caret
:data-testid="`group-${group.id}-dropdown-button`"
- data-qa-selector="group_dropdown_button"
:data-qa-group-id="group.id"
>
<gl-dropdown-item
diff --git a/app/assets/javascripts/groups/settings/init_access_dropdown.js b/app/assets/javascripts/groups/settings/init_access_dropdown.js
index 4da38e0e641..f18f260097b 100644
--- a/app/assets/javascripts/groups/settings/init_access_dropdown.js
+++ b/app/assets/javascripts/groups/settings/init_access_dropdown.js
@@ -1,5 +1,5 @@
-import * as Sentry from '@sentry/browser';
import Vue from 'vue';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import AccessDropdown from './components/access_dropdown.vue';
export const initAccessDropdown = (el) => {
diff --git a/app/assets/javascripts/groups_projects/components/transfer_locations.vue b/app/assets/javascripts/groups_projects/components/transfer_locations.vue
index 997e2bc3138..5774065bff9 100644
--- a/app/assets/javascripts/groups_projects/components/transfer_locations.vue
+++ b/app/assets/javascripts/groups_projects/components/transfer_locations.vue
@@ -260,11 +260,7 @@ export default {
>
<gl-dropdown-divider />
</template>
- <div
- v-if="hasUserTransferLocations"
- data-qa-selector="namespaces_list_users"
- data-testid="user-transfer-locations"
- >
+ <div v-if="hasUserTransferLocations" data-testid="user-transfer-locations">
<gl-dropdown-section-header>{{ $options.i18n.USERS }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="item in userTransferLocations"
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 25a84d17379..095a2dc1324 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,3 +1,6 @@
+// TODO: Remove this with the removal of the old navigation.
+// See https://gitlab.com/groups/gitlab-org/-/epics/11875.
+
import Vue from 'vue';
import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import { highCountTrim } from '~/lib/utils/text_utility';
diff --git a/app/assets/javascripts/header_search/index.js b/app/assets/javascripts/header_search/index.js
index 2bbad5f3f98..7b26dd183ad 100644
--- a/app/assets/javascripts/header_search/index.js
+++ b/app/assets/javascripts/header_search/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Translate from '~/vue_shared/translate';
import HeaderSearchApp from './components/app.vue';
import createStore from './store';
diff --git a/app/assets/javascripts/header_search/init.js b/app/assets/javascripts/header_search/init.js
index 64502d13ee2..1c582ace480 100644
--- a/app/assets/javascripts/header_search/init.js
+++ b/app/assets/javascripts/header_search/init.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { HEADER_INIT_EVENTS } from './constants';
async function eventHandler(callback = () => {}) {
diff --git a/app/assets/javascripts/helpers/help_page_helper.js b/app/assets/javascripts/helpers/help_page_helper.js
index 21d27b5fea9..fab0f17cd3b 100644
--- a/app/assets/javascripts/helpers/help_page_helper.js
+++ b/app/assets/javascripts/helpers/help_page_helper.js
@@ -1,6 +1,6 @@
import { joinPaths, setUrlFragment } from '~/lib/utils/url_utility';
-const HELP_PAGE_URL_ROOT = '/help/';
+const HELP_PAGE_URL_ROOT = '/help';
/**
* Generate link to a GitLab documentation page.
diff --git a/app/assets/javascripts/helpers/init_simple_app_helper.js b/app/assets/javascripts/helpers/init_simple_app_helper.js
index 695fc455f13..f7bef8c563e 100644
--- a/app/assets/javascripts/helpers/init_simple_app_helper.js
+++ b/app/assets/javascripts/helpers/init_simple_app_helper.js
@@ -1,4 +1,26 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+
+/**
+ * @param {boolean|VueApollo} apolloProviderOption
+ * @returns {undefined | VueApollo}
+ */
+const getApolloProvider = (apolloProviderOption) => {
+ if (apolloProviderOption === true) {
+ Vue.use(VueApollo);
+
+ return new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+ }
+
+ if (apolloProviderOption instanceof VueApollo) {
+ return apolloProviderOption;
+ }
+
+ return undefined;
+};
/**
* Initializes a component as a simple vue app, passing the necessary props. If the element
@@ -8,6 +30,8 @@ import Vue from 'vue';
*
* @param {string} selector css selector for where to build
* @param {Vue.component} component The Vue compoment to be built as the root of the app
+ * @param {{withApolloProvider: boolean|VueApollo}} options. extra options to be passed to the vue app
+ * withApolloProvider: if true, instantiates a default apolloProvider. Also accepts and instance of VueApollo
*
* @example
* ```html
@@ -15,13 +39,13 @@ import Vue from 'vue';
* ```
*
* ```javascript
- * initSimpleApp('#mount-here', MyApp)
+ * initSimpleApp('#mount-here', MyApp, { withApolloProvider: true })
* ```
*
* This will mount MyApp as root on '#mount-here'. It will receive {'some': 'object'} as it's
* view model prop.
*/
-export const initSimpleApp = (selector, component) => {
+export const initSimpleApp = (selector, component, { withApolloProvider } = {}) => {
const element = document.querySelector(selector);
if (!element) {
@@ -32,6 +56,7 @@ export const initSimpleApp = (selector, component) => {
return new Vue({
el: element,
+ apolloProvider: getApolloProvider(withApolloProvider),
render(h) {
return h(component, { props });
},
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 76b284b6185..984dc9edaf1 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -88,7 +88,6 @@ export default {
@click="openRightPane($options.rightSidebarViews.pipelines)"
>
<ci-icon
- v-gl-tooltip
:status="latestPipeline.details.status"
:title="latestPipeline.details.status.text"
/>
diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue
index f0c5b29e210..f5840661c17 100644
--- a/app/assets/javascripts/ide/components/jobs/detail/description.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue
@@ -25,9 +25,7 @@ export default {
<template>
<div class="d-flex align-items-center">
<ci-icon
- is-borderless
:status="job.status"
- :size="24"
class="gl-align-items-center gl-border gl-display-inline-flex gl-z-index-1"
/>
<span class="gl-ml-3">
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 6bf51ed06a6..0e07cc34dd8 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -63,7 +63,7 @@ export default {
</div>
<template v-else-if="hasLoadedPipeline">
<header v-if="latestPipeline" class="ide-tree-header ide-pipeline-header">
- <ci-icon :status="latestPipeline.details.status" :size="24" class="d-flex" />
+ <ci-icon :status="latestPipeline.details.status" />
<span class="gl-ml-3">
<strong> {{ __('Pipeline') }} </strong>
<a
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 137df9aa102..3b59fe86764 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -8,7 +8,6 @@ import {
EDITOR_TYPE_CODE,
EDITOR_CODE_INSTANCE_FN,
EDITOR_DIFF_INSTANCE_FN,
- EXTENSION_CI_SCHEMA_FILE_NAME_MATCH,
} from '~/editor/constants';
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import { EditorWebIdeExtension } from '~/editor/extensions/source_editor_webide_ext';
@@ -30,6 +29,7 @@ import { viewerInformationForPath } from '~/vue_shared/components/content_viewer
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import { markRaw } from '~/lib/utils/vue3compat/mark_raw';
import { readFileAsDataURL } from '~/lib/utils/file_utility';
+import { isDefaultCiConfig, hasCiConfigExtension } from '~/lib/utils/common_utils';
import {
leftSidebarViews,
@@ -152,8 +152,9 @@ export default {
},
isCiConfigFile() {
return (
- this.file.path === EXTENSION_CI_SCHEMA_FILE_NAME_MATCH &&
- this.editor?.getEditorType() === EDITOR_TYPE_CODE
+ // For CI config schemas the filename must match '*.gitlab-ci.yml' regardless of project configuration.
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/293641
+ hasCiConfigExtension(this.file.path) && this.editor?.getEditorType() === EDITOR_TYPE_CODE
);
},
},
@@ -162,7 +163,7 @@ export default {
handler() {
this.stopWatchingCiYaml();
- if (this.file.name === '.gitlab-ci.yml') {
+ if (isDefaultCiConfig(this.file.name)) {
this.startWatchingCiYaml();
}
},
diff --git a/app/assets/javascripts/ide/lib/alerts/index.js b/app/assets/javascripts/ide/lib/alerts/index.js
index c9db9779b1f..ac4eeb0386f 100644
--- a/app/assets/javascripts/ide/lib/alerts/index.js
+++ b/app/assets/javascripts/ide/lib/alerts/index.js
@@ -1,3 +1,4 @@
+import { isDefaultCiConfig } from '~/lib/utils/common_utils';
import { leftSidebarViews } from '../../constants';
import EnvironmentsMessage from './environments.vue';
@@ -6,7 +7,7 @@ const alerts = [
key: Symbol('ALERT_ENVIRONMENT'),
show: (state, file) =>
state.currentActivityView === leftSidebarViews.commit.name &&
- file.path === '.gitlab-ci.yml' &&
+ isDefaultCiConfig(file.path) &&
state.environmentsGuidanceAlertDetected &&
!state.environmentsGuidanceAlertDismissed,
props: { variant: 'tip' },
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
index bf0d3ed337c..5681f6cdec5 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
@@ -1,9 +1,10 @@
import { __ } from '~/locale';
+import { DEFAULT_CI_CONFIG_PATH } from '~/lib/utils/constants';
import { leftSidebarViews } from '../../../constants';
export const templateTypes = () => [
{
- name: '.gitlab-ci.yml',
+ name: DEFAULT_CI_CONFIG_PATH,
key: 'gitlab_ci_ymls',
},
{
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/mutations.js b/app/assets/javascripts/ide/stores/modules/terminal/mutations.js
index 37f40af9c2e..8adde8f6b4e 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/mutations.js
@@ -48,7 +48,7 @@ export default {
},
[types.SET_SESSION_STATUS](state, status) {
const session = {
- ...(state.session || {}),
+ ...state.session,
status,
};
diff --git a/app/assets/javascripts/import/constants.js b/app/assets/javascripts/import/constants.js
index ddf69a8fcdf..b02eb3c4307 100644
--- a/app/assets/javascripts/import/constants.js
+++ b/app/assets/javascripts/import/constants.js
@@ -1,6 +1,18 @@
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __, s__ } from '~/locale';
+export const BULK_IMPORT_STATIC_ITEMS = {
+ badges: __('Badge'),
+ boards: s__('IssueBoards|Board'),
+ epics: __('Epic'),
+ issues: __('Issue'),
+ labels: __('Label'),
+ members: __('Member'),
+ merge_requests: __('Merge request'),
+ milestones: __('Milestone'),
+ project: __('Project'),
+};
+
const STATISTIC_ITEMS = {
diff_note: __('Diff notes'),
issue: __('Issues'),
diff --git a/app/assets/javascripts/import/details/components/bulk_import_details_app.vue b/app/assets/javascripts/import/details/components/bulk_import_details_app.vue
new file mode 100644
index 00000000000..5da16454032
--- /dev/null
+++ b/app/assets/javascripts/import/details/components/bulk_import_details_app.vue
@@ -0,0 +1,44 @@
+<script>
+import { __ } from '~/locale';
+import ImportDetailsTable from '~/import/details/components/import_details_table.vue';
+
+export default {
+ name: 'BulkImportDetailsApp',
+ components: { ImportDetailsTable },
+
+ fields: [
+ {
+ key: 'relation',
+ label: __('Type'),
+ tdClass: 'gl-white-space-nowrap',
+ },
+ {
+ key: 'source_title',
+ label: __('Title'),
+ tdClass: 'gl-md-w-30 gl-word-break-word',
+ },
+ {
+ key: 'error',
+ label: __('Error'),
+ },
+ {
+ key: 'correlation_id_value',
+ label: __('Correlation ID'),
+ },
+ ],
+
+ LOCAL_STORAGE_KEY: 'gl-bulk-import-details-page-size',
+};
+</script>
+
+<template>
+ <div>
+ <h1>{{ s__('Import|GitLab Migration details') }}</h1>
+
+ <import-details-table
+ bulk-import
+ :fields="$options.fields"
+ :local-storage-key="$options.LOCAL_STORAGE_KEY"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/import/details/components/import_details_app.vue b/app/assets/javascripts/import/details/components/import_details_app.vue
index 13483fa8ba2..f654dc61e07 100644
--- a/app/assets/javascripts/import/details/components/import_details_app.vue
+++ b/app/assets/javascripts/import/details/components/import_details_app.vue
@@ -1,18 +1,44 @@
<script>
-import { s__ } from '~/locale';
+import { __ } from '~/locale';
import ImportDetailsTable from './import_details_table.vue';
export default {
+ name: 'ImportDetailsApp',
components: { ImportDetailsTable },
- i18n: {
- pageTitle: s__('Import|GitHub import details'),
- },
+
+ fields: [
+ {
+ key: 'type',
+ label: __('Type'),
+ tdClass: 'gl-white-space-nowrap',
+ },
+ {
+ key: 'title',
+ label: __('Title'),
+ tdClass: 'gl-md-w-30 gl-word-break-word',
+ },
+ {
+ key: 'provider_url',
+ label: __('URL'),
+ tdClass: 'gl-white-space-nowrap',
+ },
+ {
+ key: 'details',
+ label: __('Details'),
+ },
+ ],
+
+ LOCAL_STORAGE_KEY: 'gl-import-details-page-size',
};
</script>
<template>
<div>
- <h1>{{ $options.i18n.pageTitle }}</h1>
- <import-details-table />
+ <h1>{{ s__('Import|GitHub import details') }}</h1>
+
+ <import-details-table
+ :fields="$options.fields"
+ :local-storage-key="$options.LOCAL_STORAGE_KEY"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/import/details/components/import_details_table.vue b/app/assets/javascripts/import/details/components/import_details_table.vue
index 813dc1f2645..535ccb525ac 100644
--- a/app/assets/javascripts/import/details/components/import_details_table.vue
+++ b/app/assets/javascripts/import/details/components/import_details_table.vue
@@ -1,12 +1,13 @@
<script>
import { GlEmptyState, GlIcon, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
import { createAlert } from '~/alert';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { getParameterValues } from '~/lib/utils/url_utility';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
-import { STATISTIC_ITEMS } from '../../constants';
+import { getBulkImportFailures } from '~/rest_api';
+import { BULK_IMPORT_STATIC_ITEMS, STATISTIC_ITEMS } from '../../constants';
import { fetchImportFailures } from '../api';
const DEFAULT_PAGE_SIZE = 20;
@@ -21,28 +22,6 @@ export default {
PaginationBar,
},
STATISTIC_ITEMS,
- LOCAL_STORAGE_KEY: 'gl-import-details-page-size',
- fields: [
- {
- key: 'type',
- label: __('Type'),
- tdClass: 'gl-white-space-nowrap',
- },
- {
- key: 'title',
- label: __('Title'),
- tdClass: 'gl-md-w-30 gl-word-break-word',
- },
- {
- key: 'provider_url',
- label: __('URL'),
- tdClass: 'gl-white-space-nowrap',
- },
- {
- key: 'details',
- label: __('Details'),
- },
- ],
i18n: {
fetchErrorMessage: s__('Import|An error occurred while fetching import details.'),
@@ -55,6 +34,25 @@ export default {
},
},
+ props: {
+ bulkImport: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ fields: {
+ type: Array,
+ required: true,
+ },
+
+ localStorageKey: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
data() {
return {
items: [],
@@ -97,18 +95,28 @@ export default {
this.loadImportFailures();
},
+ fetchFn(params) {
+ return this.bulkImport
+ ? getBulkImportFailures(
+ getParameterValues('id')[0],
+ getParameterValues('entity_id')[0],
+ params,
+ )
+ : fetchImportFailures(this.failuresPath, {
+ projectId: getParameterValues('project_id')[0],
+ ...params,
+ });
+ },
+
async loadImportFailures() {
- if (!this.failuresPath) {
+ if (!this.bulkImport && !this.failuresPath) {
return;
}
this.loading = true;
+
try {
- const response = await fetchImportFailures(this.failuresPath, {
- projectId: getParameterValues('project_id')[0],
- page: this.page,
- perPage: this.perPage,
- });
+ const response = await this.fetchFn({ page: this.page, perPage: this.perPage });
const { page, perPage, totalPages, total } = parseIntPagination(
normalizeHeaders(response.headers),
@@ -123,13 +131,17 @@ export default {
}
this.loading = false;
},
+
+ itemTypeText(type) {
+ return (this.bulkImport ? BULK_IMPORT_STATIC_ITEMS[type] : STATISTIC_ITEMS[type]) || type;
+ },
},
};
</script>
<template>
<div>
- <gl-table :fields="$options.fields" :items="items" class="gl-mt-5" :busy="loading" show-empty>
+ <gl-table :fields="fields" :items="items" class="gl-mt-5" :busy="loading" show-empty>
<template #table-busy>
<gl-loading-icon size="lg" class="gl-my-5" />
</template>
@@ -139,7 +151,7 @@ export default {
</template>
<template #cell(type)="{ item: { type } }">
- {{ $options.STATISTIC_ITEMS[type] }}
+ {{ itemTypeText(type) }}
</template>
<template #cell(provider_url)="{ item: { provider_url } }">
<gl-link v-if="provider_url" :href="provider_url" target="_blank">
@@ -147,12 +159,30 @@ export default {
<gl-icon name="external-link" />
</gl-link>
</template>
+
+ <template #cell(relation)="{ item: { relation } }">
+ {{ itemTypeText(relation) }}
+ </template>
+ <template #cell(source_title)="{ item: { source_title, source_url } }">
+ <gl-link v-if="source_url" :href="source_url" target="_blank">
+ {{ source_title }}
+ <gl-icon name="external-link" />
+ </gl-link>
+ <span v-else>
+ {{ source_title }}
+ </span>
+ </template>
+ <template #cell(error)="{ item: { exception_class, exception_message } }">
+ <strong>{{ exception_class }}</strong>
+ <p>{{ exception_message }}</p>
+ </template>
</gl-table>
+
<pagination-bar
v-if="hasItems"
:page-info="pageInfo"
class="gl-mt-5"
- :storage-key="$options.LOCAL_STORAGE_KEY"
+ :storage-key="localStorageKey"
@set-page="setPage"
@set-page-size="setPageSize"
/>
diff --git a/app/assets/javascripts/import_entities/components/import_status.vue b/app/assets/javascripts/import_entities/components/import_status.vue
index 91436457b03..3cde3a8df3c 100644
--- a/app/assets/javascripts/import_entities/components/import_status.vue
+++ b/app/assets/javascripts/import_entities/components/import_status.vue
@@ -1,46 +1,9 @@
<script>
import { GlAccordion, GlAccordionItem, GlBadge, GlIcon, GlLink } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
import { STATISTIC_ITEMS } from '~/import/constants';
-import { STATUSES } from '../constants';
-
-const SCHEDULED_STATUS = {
- icon: 'status-scheduled',
- text: __('Pending'),
- variant: 'muted',
-};
-
-const STATUS_MAP = {
- [STATUSES.NONE]: {
- icon: 'status-waiting',
- text: __('Not started'),
- variant: 'muted',
- },
- [STATUSES.SCHEDULING]: SCHEDULED_STATUS,
- [STATUSES.SCHEDULED]: SCHEDULED_STATUS,
- [STATUSES.CREATED]: SCHEDULED_STATUS,
- [STATUSES.STARTED]: {
- icon: 'status-running',
- text: __('Importing...'),
- variant: 'info',
- },
- [STATUSES.FAILED]: {
- icon: 'status-failed',
- text: __('Failed'),
- variant: 'danger',
- },
- [STATUSES.TIMEOUT]: {
- icon: 'status-failed',
- text: __('Timeout'),
- variant: 'danger',
- },
- [STATUSES.CANCELED]: {
- icon: 'status-stopped',
- text: __('Cancelled'),
- variant: 'neutral',
- },
-};
+import { STATUSES, STATUS_ICON_MAP } from '../constants';
function isIncompleteImport(stats) {
return Object.keys(stats?.fetched ?? []).some(
@@ -96,21 +59,11 @@ export default {
},
mappedStatus() {
- if (this.status === STATUSES.FINISHED) {
- return this.isIncomplete
- ? {
- icon: 'status-alert',
- text: s__('Import|Partially completed'),
- variant: 'warning',
- }
- : {
- icon: 'status-success',
- text: __('Complete'),
- variant: 'success',
- };
+ if (this.isIncomplete) {
+ return STATUS_ICON_MAP[STATUSES.PARTIAL];
}
- return STATUS_MAP[this.status];
+ return STATUS_ICON_MAP[this.status];
},
showDetails() {
diff --git a/app/assets/javascripts/import_entities/constants.js b/app/assets/javascripts/import_entities/constants.js
index 48b7febca4b..23604c7fb44 100644
--- a/app/assets/javascripts/import_entities/constants.js
+++ b/app/assets/javascripts/import_entities/constants.js
@@ -1,18 +1,65 @@
-// The `scheduling` status is only present on the client-side,
-// it is used as the status when we are requesting to start an import.
+import { __, s__ } from '~/locale';
export const STATUSES = {
FINISHED: 'finished',
FAILED: 'failed',
SCHEDULED: 'scheduled',
+ SCHEDULING: 'scheduling', // only present client-side, used when user is requesting to start an import
CREATED: 'created',
STARTED: 'started',
NONE: 'none',
- SCHEDULING: 'scheduling',
CANCELED: 'canceled',
TIMEOUT: 'timeout',
+ PARTIAL: 'partial', // only present client-side, finished but with failures
};
export const PROVIDERS = {
GITHUB: 'github',
};
+
+const SCHEDULED_STATUS_ICON = {
+ icon: 'status-scheduled',
+ text: __('Pending'),
+ variant: 'muted',
+};
+
+export const STATUS_ICON_MAP = {
+ [STATUSES.NONE]: {
+ icon: 'status-waiting',
+ text: __('Not started'),
+ variant: 'muted',
+ },
+ [STATUSES.SCHEDULING]: SCHEDULED_STATUS_ICON,
+ [STATUSES.SCHEDULED]: SCHEDULED_STATUS_ICON,
+ [STATUSES.CREATED]: SCHEDULED_STATUS_ICON,
+ [STATUSES.STARTED]: {
+ icon: 'status-running',
+ text: __('Importing...'),
+ variant: 'info',
+ },
+ [STATUSES.FAILED]: {
+ icon: 'status-failed',
+ text: __('Failed'),
+ variant: 'danger',
+ },
+ [STATUSES.TIMEOUT]: {
+ icon: 'status-failed',
+ text: __('Timeout'),
+ variant: 'danger',
+ },
+ [STATUSES.CANCELED]: {
+ icon: 'status-stopped',
+ text: __('Cancelled'),
+ variant: 'neutral',
+ },
+ [STATUSES.FINISHED]: {
+ icon: 'status-success',
+ text: __('Complete'),
+ variant: 'success',
+ },
+ [STATUSES.PARTIAL]: {
+ icon: 'status-alert',
+ text: s__('Import|Partially completed'),
+ variant: 'warning',
+ },
+};
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_status.vue b/app/assets/javascripts/import_entities/import_groups/components/import_status.vue
new file mode 100644
index 00000000000..cdb38cdf7f1
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_status.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlBadge, GlLink } from '@gitlab/ui';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { STATUSES, STATUS_ICON_MAP } from '~/import_entities/constants';
+
+export default {
+ components: {
+ GlBadge,
+ GlLink,
+ },
+
+ inject: {
+ detailsPath: {
+ default: undefined,
+ },
+ },
+
+ props: {
+ id: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ entityId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ hasFailures: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showDetailsLink: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ status: {
+ type: String,
+ required: true,
+ },
+ },
+
+ computed: {
+ isPartial() {
+ return this.status === STATUSES.FINISHED && this.hasFailures;
+ },
+
+ mappedStatus() {
+ if (this.isPartial) {
+ return STATUS_ICON_MAP[STATUSES.PARTIAL];
+ }
+
+ return STATUS_ICON_MAP[this.status];
+ },
+
+ showDetails() {
+ return this.showDetailsLink && Boolean(this.detailsPathWithId) && this.hasFailures;
+ },
+
+ detailsPathWithId() {
+ if (!this.id || !this.entityId || !this.detailsPath) {
+ return null;
+ }
+
+ return mergeUrlParams({ id: this.id, entity_id: this.entityId }, this.detailsPath);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-badge :icon="mappedStatus.icon" :variant="mappedStatus.variant" size="md" icon-size="sm">
+ {{ mappedStatus.text }}
+ </gl-badge>
+
+ <div v-if="showDetails" class="gl-mt-2">
+ <gl-link :href="detailsPathWithId">{{ s__('Import|See failures') }}</gl-link>
+ </div>
+ </div>
+</template>
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 24197c680eb..df1e50cb433 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
@@ -42,9 +42,6 @@ import ImportTargetCell from './import_target_cell.vue';
const VALIDATION_DEBOUNCE_TIME = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
const PAGE_SIZES = [20, 50, 100];
const DEFAULT_PAGE_SIZE = PAGE_SIZES[0];
-const DEFAULT_TH_CLASSES =
- 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-200! gl-border-b-1! gl-p-5!';
-const DEFAULT_TD_CLASSES = 'gl-vertical-align-top!';
export default {
components: {
@@ -129,36 +126,28 @@ export default {
{
key: 'selected',
label: '',
- // eslint-disable-next-line @gitlab/require-i18n-strings
- thClass: `${DEFAULT_TH_CLASSES} gl-w-3 gl-pr-3!`,
- // eslint-disable-next-line @gitlab/require-i18n-strings
- tdClass: `${DEFAULT_TD_CLASSES} gl-pr-3!`,
+ thClass: 'gl-w-3 gl-pr-3!',
+ tdClass: 'gl-pr-3!',
},
{
key: 'webUrl',
label: s__('BulkImport|Source group'),
- thClass: `${DEFAULT_TH_CLASSES} gl-pl-0! gl-w-half`,
- // eslint-disable-next-line @gitlab/require-i18n-strings
- tdClass: `${DEFAULT_TD_CLASSES} gl-pl-0!`,
+ thClass: 'gl-pl-0! gl-w-half',
+ tdClass: 'gl-pl-0!',
},
{
key: 'importTarget',
label: s__('BulkImport|New group'),
- thClass: `${DEFAULT_TH_CLASSES} gl-w-half`,
- tdClass: DEFAULT_TD_CLASSES,
+ thClass: `gl-w-half`,
},
{
key: 'progress',
label: __('Status'),
- thClass: `${DEFAULT_TH_CLASSES}`,
- tdClass: DEFAULT_TD_CLASSES,
tdAttr: { 'data-qa-selector': 'import_status_indicator' },
},
{
key: 'actions',
label: '',
- thClass: `${DEFAULT_TH_CLASSES}`,
- tdClass: DEFAULT_TD_CLASSES,
},
],
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
index 1aad22f0f3f..c2e35ce8270 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
@@ -60,7 +60,7 @@ export class LocalStorageCache {
updateStatusByJobId(jobId, status) {
this.getCacheKeysByJobId(jobId).forEach((webUrl) =>
this.set(webUrl, {
- ...(this.get(webUrl) ?? {}),
+ ...this.get(webUrl),
progress: {
id: jobId,
status,
diff --git a/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue b/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue
index cf1a4de68ed..d22a52df326 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue
@@ -41,7 +41,7 @@ export default {
:key="name"
:checked="value[name]"
:data-qa-option-name="name"
- data-qa-selector="advanced_settings_checkbox"
+ data-testid="advanced-settings-checkbox"
@change="$emit('input', { ...value, [name]: $event })"
>
{{ label }}
diff --git a/app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue b/app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue
index 5d5965e33da..72c6f45cdc9 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue
@@ -1,6 +1,6 @@
<script>
-import * as Sentry from '@sentry/browser';
import { GlCollapsibleListbox } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import { __, s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue b/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue
index cb3476c48db..5931e0d307a 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue
@@ -78,7 +78,6 @@ export default {
@input="setFilter({ organization_login: $event })"
/>
<gl-search-box-by-click
- data-qa-selector="githubish_import_filter_field"
name="filter"
:disabled="isNameFilterDisabled"
:value="nameFilter"
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 009945f8b9b..d98132382c6 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
@@ -155,7 +155,6 @@ export default {
<slot name="actions"></slot>
<form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent>
<gl-search-box-by-click
- data-qa-selector="githubish_import_filter_field"
name="filter"
:placeholder="__('Filter by name')"
autofocus
diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
index d75ba53d727..9b5aff45375 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
@@ -159,7 +159,7 @@ export default {
<template>
<tr
class="gl-h-11"
- data-qa-selector="project_import_row"
+ data-testid="project-import-row"
:data-qa-source-project="repo.importSource.fullName"
>
<td>
@@ -174,7 +174,7 @@ export default {
:href="repo.importedProject.fullPath"
class="gl-font-sm"
target="_blank"
- data-qa-selector="go_to_project_link"
+ data-testid="go-to-project-link"
>
{{ displayFullPath }}
</gl-link>
@@ -182,7 +182,7 @@ export default {
</gl-sprintf>
</div>
</td>
- <td data-testid="fullPath" data-qa-selector="project_path_content">
+ <td data-testid="fullPath">
<div class="gl-display-flex gl-sm-flex-wrap">
<template v-if="repo.importSource.target">{{ repo.importSource.target }}</template>
<template v-else-if="isImportNotStarted || isSelectedForReimport">
@@ -201,14 +201,14 @@ export default {
ref="newNameInput"
v-model="newNameInput"
class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
- data-qa-selector="project_path_field"
+ data-testid="project-path-field"
/>
</div>
</template>
<template v-else-if="repo.importedProject">{{ displayFullPath }}</template>
</div>
</td>
- <td data-qa-selector="import_status_indicator">
+ <td data-testid="import-status-indicator">
<import-status :project-id="importedProjectId" :status="importStatus" :stats="stats" />
</td>
<td data-testid="actions" class="gl-white-space-nowrap">
@@ -235,7 +235,7 @@ export default {
<gl-button
v-if="isImportNotStarted || isFinished"
type="button"
- data-qa-selector="import_button"
+ data-testid="import-button"
@click="handleImportRepo()"
>
{{ importButtonText }}
diff --git a/app/assets/javascripts/import_entities/import_projects/store/actions.js b/app/assets/javascripts/import_entities/import_projects/store/actions.js
index 4305f8d4db5..e5cbac71ce0 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js
@@ -83,7 +83,7 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit })
.get(
pathWithParams({
path: reposPath,
- ...(filter ?? {}),
+ ...filter,
...paginationParams({ state }),
}),
)
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 0e1afebbe2b..727ab43435d 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -87,12 +87,11 @@ export default {
{
key: 'incidentSla',
label: s__('IncidentManagement|Time to SLA'),
- thClass: `gl-text-right gl-w-10p`,
+ thClass: `${thClass} gl-text-right gl-w-10p`,
tdClass: `${tdClass} gl-text-right`,
thAttr: TH_INCIDENT_SLA_TEST_ID,
actualSortKey: 'SLA_DUE_AT',
sortable: true,
- sortDirection: 'asc',
},
{
key: 'assignees',
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index fa9a59212eb..281666a021d 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -1,9 +1,9 @@
<script>
import { GlAlert, GlForm } from '@gitlab/ui';
import axios from 'axios';
-import * as Sentry from '@sentry/browser';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapActions, mapGetters } from 'vuex';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { s__ } from '~/locale';
import SafeHtml from '~/vue_shared/directives/safe_html';
import {
diff --git a/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue b/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
index a8389e32b40..356557442db 100644
--- a/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
+++ b/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
@@ -1,6 +1,6 @@
<script>
import { GlLink, GlLoadingIcon, GlPagination, GlTable, GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { DEFAULT_PER_PAGE } from '~/api';
import { fetchOverrides } from '~/integrations/overrides/api';
diff --git a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
index 4b492e48095..ceb9200dfad 100644
--- a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
@@ -1,7 +1,7 @@
<script>
-import * as Sentry from '@sentry/browser';
import { GlAlert } from '@gitlab/ui';
import { uniqueId } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Api from '~/api';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_base.vue';
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 509efd31dcd..1a10130e969 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -1,13 +1,17 @@
<script>
-import { GlAlert, GlButton, GlCollapse, GlIcon } from '@gitlab/ui';
+import { GlAlert, GlButton, GlCollapse, GlLink, GlIcon, GlSprintf } from '@gitlab/ui';
import { partition, isString, uniqueId, isEmpty } from 'lodash';
import SafeHtml from '~/vue_shared/directives/safe_html';
import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_base.vue';
import Api from '~/api';
import Tracking from '~/tracking';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
-import { n__, sprintf } from '~/locale';
-import { memberName, triggerExternalAlert } from 'ee_else_ce/invite_members/utils/member_utils';
+import { n__, s__, sprintf } from '~/locale';
+import {
+ memberName,
+ triggerExternalAlert,
+ inviteMembersTrackingOptions,
+} from 'ee_else_ce/invite_members/utils/member_utils';
import { captureException } from '~/ci/runner/sentry_utils';
import {
USERS_FILTER_ALL,
@@ -31,7 +35,9 @@ export default {
GlAlert,
GlButton,
GlCollapse,
+ GlLink,
GlIcon,
+ GlSprintf,
InviteModalBase,
MembersTokenSelect,
ModalConfetti,
@@ -43,6 +49,17 @@ export default {
SafeHtml,
},
mixins: [Tracking.mixin({ category: INVITE_MEMBER_MODAL_TRACKING_CATEGORY })],
+ inject: {
+ isCurrentUserAdmin: {
+ default: false,
+ },
+ isEmailSignupEnabled: {
+ default: true,
+ },
+ newUsersUrl: {
+ default: '',
+ },
+ },
props: {
id: {
type: String,
@@ -122,6 +139,12 @@ export default {
isCelebration() {
return this.mode === 'celebrate';
},
+ baseTrackingDetails() {
+ return { label: this.source, celebrate: this.isCelebration };
+ },
+ isTextForAdmin() {
+ return this.isCurrentUserAdmin && Boolean(this.newUsersUrl);
+ },
modalTitle() {
return this.$options.labels.modal[this.mode].title;
},
@@ -131,6 +154,11 @@ export default {
labelIntroText() {
return this.$options.labels[this.inviteTo][this.mode].introText;
},
+ labelSearchField() {
+ return this.isEmailSignupEnabled
+ ? this.$options.labels.searchField
+ : s__('InviteMembersModal|Username');
+ },
isEmptyInvites() {
return Boolean(this.newUsersToInvite.length);
},
@@ -144,6 +172,14 @@ export default {
this.errorList.length,
);
},
+ signupDisabledText() {
+ return s__(
+ "InviteMembersModal|Administrators can %{linkStart}add new users by email manually%{linkEnd}. After they've been added, you can invite them to this group with their username.",
+ );
+ },
+ signupDisabledTitle() {
+ return s__('InviteMembersModal|Inviting users by email is disabled');
+ },
showUserLimitNotification() {
return !isEmpty(this.usersLimitDataset.alertVariant);
},
@@ -173,8 +209,13 @@ export default {
count: this.errorsExpanded.length,
});
},
+ formGroupDescriptionText() {
+ return this.isEmailSignupEnabled
+ ? this.$options.labels.placeHolder
+ : s__('InviteMembersModal|Select members');
+ },
formGroupDescription() {
- return this.invalidFeedbackMessage ? null : this.$options.labels.placeHolder;
+ return this.invalidFeedbackMessage ? null : this.formGroupDescriptionText;
},
},
watch: {
@@ -218,13 +259,13 @@ export default {
this.source = source;
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
- this.track('render', { label: this.source });
+ this.track('render', inviteMembersTrackingOptions(this.baseTrackingDetails));
},
closeModal() {
this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
showEmptyInvitesAlert() {
- this.invalidFeedbackMessage = this.$options.labels.placeHolder;
+ this.invalidFeedbackMessage = this.formGroupDescriptionText;
this.shouldShowEmptyInvitesAlert = true;
this.$refs.alerts.focus();
},
@@ -287,10 +328,10 @@ export default {
return this.newUsersToInvite.find((member) => memberName(member) === username)?.name;
},
onCancel() {
- this.track('click_cancel', { label: this.source });
+ this.track('click_cancel', inviteMembersTrackingOptions(this.baseTrackingDetails));
},
onClose() {
- this.track('click_x', { label: this.source });
+ this.track('click_x', inviteMembersTrackingOptions(this.baseTrackingDetails));
},
resetFields() {
this.clearValidation();
@@ -299,7 +340,7 @@ export default {
this.newUsersToInvite = [];
},
onInviteSuccess() {
- this.track('invite_successful', { label: this.source });
+ this.track('invite_successful', inviteMembersTrackingOptions(this.baseTrackingDetails));
if (this.reloadPageOnSubmit) {
reloadOnInvitationSuccess();
@@ -345,7 +386,7 @@ export default {
:default-access-level="defaultAccessLevel"
:help-link="helpLink"
:label-intro-text="labelIntroText"
- :label-search-field="$options.labels.searchField"
+ :label-search-field="labelSearchField"
:form-group-description="formGroupDescription"
:invalid-feedback-message="invalidFeedbackMessage"
:is-loading="isLoading"
@@ -429,6 +470,24 @@ export default {
</gl-button>
</template>
</gl-alert>
+ <gl-alert
+ v-if="!isEmailSignupEnabled"
+ id="signup-disabled-alert"
+ :dismissible="false"
+ :title="signupDisabledTitle"
+ class="gl-mb-4"
+ variant="warning"
+ data-testid="email-signup-disabled-alert"
+ >
+ <gl-sprintf :message="signupDisabledText">
+ <template #link="{ content }">
+ <gl-link v-if="isTextForAdmin" :href="newUsersUrl" target="_blank">{{
+ content
+ }}</gl-link>
+ <span v-else>{{ content }}</span>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
<user-limit-notification
v-else-if="showUserLimitNotification"
class="gl-mb-5"
@@ -447,6 +506,7 @@ export default {
v-model="newUsersToInvite"
class="gl-mb-2"
aria-labelledby="empty-invites-alert"
+ :can-use-email-token="isEmailSignupEnabled"
:input-id="inputId"
:exception-state="exceptionState"
:users-filter="usersFilter"
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 18d22395104..a14dcd38aa7 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -297,7 +297,7 @@ export default {
</gl-form-group>
<gl-form-group
- class="gl-w-half gl-xs-w-full"
+ class="gl-sm-w-half gl-w-full"
:label="$options.ACCESS_LEVEL"
:label-for="dropdownId"
>
@@ -317,7 +317,7 @@ export default {
</gl-form-group>
<gl-form-group
- class="gl-w-half gl-xs-w-full"
+ class="gl-sm-w-half gl-w-full"
:label="$options.ACCESS_EXPIRE_DATE"
:label-for="datepickerId"
>
@@ -338,10 +338,10 @@ export default {
<template #modal-footer>
<div
- class="gl-m-0 gl-xs-w-full gl-display-flex gl-xs-flex-direction-column! gl-flex-direction-row-reverse"
+ class="gl-m-0 gl-w-full gl-display-flex gl-xs-flex-direction-column! gl-flex-direction-row-reverse"
>
<gl-button
- class="gl-xs-w-full gl-xs-mb-3! gl-sm-ml-3!"
+ class="gl-w-full gl-sm-w-auto gl-xs-mb-3! gl-sm-ml-3!"
data-testid="invite-modal-submit"
v-bind="actionPrimary.attributes"
@click="onSubmit"
@@ -350,7 +350,7 @@ export default {
</gl-button>
<gl-button
- class="gl-xs-w-full"
+ class="gl-w-full gl-sm-w-auto"
data-testid="invite-modal-cancel"
v-bind="actionCancel.attributes"
@click="onCancel"
diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue
index 8493787f075..0be04b7af35 100644
--- a/app/assets/javascripts/invite_members/components/members_token_select.vue
+++ b/app/assets/javascripts/invite_members/components/members_token_select.vue
@@ -21,6 +21,11 @@ export default {
GlSprintf,
},
props: {
+ canUseEmailToken: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
placeholder: {
type: String,
required: false,
@@ -68,6 +73,10 @@ export default {
},
computed: {
emailIsValid() {
+ if (!this.canUseEmailToken) {
+ return false;
+ }
+
const regex = /^\S+@\S+$/;
return this.originalInput.match(regex) !== null;
@@ -137,9 +146,8 @@ export default {
username: token.username,
avatar_url: token.avatar_url,
}));
- this.loading = false;
})
- .catch(() => {
+ .finally(() => {
this.loading = false;
});
}, SEARCH_DELAY),
diff --git a/app/assets/javascripts/invite_members/components/project_select.vue b/app/assets/javascripts/invite_members/components/project_select.vue
index 640df5cdb88..6c2f53afe3c 100644
--- a/app/assets/javascripts/invite_members/components/project_select.vue
+++ b/app/assets/javascripts/invite_members/components/project_select.vue
@@ -115,7 +115,6 @@ export default {
:search-placeholder="$options.i18n.searchPlaceholder"
:no-results-text="$options.i18n.emptySearchResult"
data-testid="project-select-dropdown"
- data-qa-selector="project_select_dropdown"
class="gl-collapsible-listbox-w-full"
@search="searchTerm = $event"
@select="selectProject"
diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js
index 41ed0179364..8dfe697e2cb 100644
--- a/app/assets/javascripts/invite_members/init_invite_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js
@@ -25,6 +25,9 @@ export default (function initInviteMembersModal() {
name: 'InviteMembersModalRoot',
provide: {
name: el.dataset.name,
+ newUsersUrl: el.dataset.newUsersUrl,
+ isCurrentUserAdmin: parseBoolean(el.dataset.isCurrentUserAdmin),
+ isEmailSignupEnabled: parseBoolean(el.dataset.isSignupEnabled),
},
render: (createElement) =>
createElement(InviteMembersModal, {
diff --git a/app/assets/javascripts/invite_members/utils/member_utils.js b/app/assets/javascripts/invite_members/utils/member_utils.js
index 7998cb69445..52fb5e98f27 100644
--- a/app/assets/javascripts/invite_members/utils/member_utils.js
+++ b/app/assets/javascripts/invite_members/utils/member_utils.js
@@ -6,3 +6,7 @@ export function memberName(member) {
export function triggerExternalAlert() {
return false;
}
+
+export function inviteMembersTrackingOptions(options) {
+ return { label: options.label };
+}
diff --git a/app/assets/javascripts/issuable/components/locked_badge.vue b/app/assets/javascripts/issuable/components/locked_badge.vue
index f97ac888417..652d02e8f9d 100644
--- a/app/assets/javascripts/issuable/components/locked_badge.vue
+++ b/app/assets/javascripts/issuable/components/locked_badge.vue
@@ -20,9 +20,12 @@ export default {
},
computed: {
title() {
- return sprintf(__('This %{issuable} is locked. Only project members can comment.'), {
- issuable: issuableTypeText[this.issuableType],
- });
+ return sprintf(
+ __('The discussion in this %{issuable} is locked. Only project members can comment.'),
+ {
+ issuable: issuableTypeText[this.issuableType],
+ },
+ );
},
},
};
diff --git a/app/assets/javascripts/issuable/components/related_issuable_item.vue b/app/assets/javascripts/issuable/components/related_issuable_item.vue
index 71bd301162e..126a3a84d66 100644
--- a/app/assets/javascripts/issuable/components/related_issuable_item.vue
+++ b/app/assets/javascripts/issuable/components/related_issuable_item.vue
@@ -88,6 +88,9 @@ export default {
workItemIid() {
return String(this.iid);
},
+ pipelinePath() {
+ return this.pipelineStatus?.details_path || this.pipelineStatus?.detailsPath;
+ },
},
methods: {
handleTitleClick(event) {
@@ -191,16 +194,16 @@ export default {
<div
class="item-attributes-area gl-display-flex gl-align-items-center gl-flex-wrap gl-gap-3"
>
- <span v-if="hasPipeline" class="mr-ci-status order-md-last">
- <a :href="pipelineStatus.details_path">
- <ci-icon v-gl-tooltip :status="pipelineStatus" :title="pipelineStatusTooltip" />
+ <span v-if="hasPipeline" class="mr-ci-status order-md-last gl-md-ml-3 gl-mr-n2">
+ <a :href="pipelinePath">
+ <ci-icon :status="pipelineStatus" :title="pipelineStatusTooltip" />
</a>
</span>
<issue-milestone
v-if="hasMilestone"
:milestone="milestone"
- class="item-milestone gl-font-sm gl-display-flex gl-align-items-center order-md-first"
+ class="item-milestone gl-font-sm gl-display-flex gl-align-items-center order-md-first gl-ml-2"
/>
<!-- Flex order for slots is defined in the parent component: e.g. related_issues_block.vue -->
diff --git a/app/assets/javascripts/issuable/components/status_badge.vue b/app/assets/javascripts/issuable/components/status_badge.vue
index 949fb3c1ce5..35f6446d582 100644
--- a/app/assets/javascripts/issuable/components/status_badge.vue
+++ b/app/assets/javascripts/issuable/components/status_badge.vue
@@ -14,29 +14,29 @@ import {
const badgePropertiesMap = {
[TYPE_EPIC]: {
[STATUS_OPEN]: {
- icon: 'epic',
+ icon: 'issue-open-m',
text: __('Open'),
variant: 'success',
},
[STATUS_CLOSED]: {
- icon: 'epic-closed',
+ icon: 'issue-close',
text: __('Closed'),
variant: 'info',
},
},
[TYPE_ISSUE]: {
[STATUS_OPEN]: {
- icon: 'issues',
+ icon: 'issue-open-m',
text: __('Open'),
variant: 'success',
},
[STATUS_CLOSED]: {
- icon: 'issue-closed',
+ icon: 'issue-close',
text: __('Closed'),
variant: 'info',
},
[STATUS_LOCKED]: {
- icon: 'issues',
+ icon: 'issue-open-m',
text: __('Open'),
variant: 'success',
},
diff --git a/app/assets/javascripts/issuable/popover/components/mr_popover.vue b/app/assets/javascripts/issuable/popover/components/mr_popover.vue
index e2c2181684f..80ae8ed8cf6 100644
--- a/app/assets/javascripts/issuable/popover/components/mr_popover.vue
+++ b/app/assets/javascripts/issuable/popover/components/mr_popover.vue
@@ -96,14 +96,14 @@ export default {
</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">
- <gl-badge class="gl-mr-3" :variant="badgeVariant">
+ <gl-badge class="gl-mr-2" :variant="badgeVariant">
{{ stateHumanName }}
</gl-badge>
<span class="gl-text-secondary">
{{ __('Opened') }} <time v-text="formattedTime"></time
></span>
</div>
- <ci-icon v-if="detailedStatus" :status="detailedStatus" />
+ <ci-icon v-if="detailedStatus" :status="detailedStatus" class="gl-ml-2" />
</div>
<h5 v-if="!$apollo.queries.mergeRequest.loading" class="my-2">{{ title }}</h5>
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
index a756229e6ca..b6465cf6c68 100644
--- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
+++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
@@ -5,7 +5,7 @@ import {
GlFilteredSearchToken,
GlTooltipDirective,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import getIssuesQuery from 'ee_else_ce/issues/dashboard/queries/get_issues.query.graphql';
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
@@ -165,9 +165,6 @@ export default {
skip() {
return !this.hasSearch;
},
- context: {
- isSingleRequest: true,
- },
},
},
computed: {
diff --git a/app/assets/javascripts/issues/issue.js b/app/assets/javascripts/issues/issue.js
index 06bbcdc12ea..b83db65caa6 100644
--- a/app/assets/javascripts/issues/issue.js
+++ b/app/assets/javascripts/issues/issue.js
@@ -53,6 +53,8 @@ export default class Issue {
$(document).trigger('issuable:change', isClosed);
+ // TODO: Remove this with the removal of the old navigation.
+ // See https://gitlab.com/groups/gitlab-org/-/epics/11875.
let numProjectIssues = Number(
projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''),
);
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 16e687cff10..72bb88ef1d5 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -9,11 +9,11 @@ import {
GlDrawer,
GlLink,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import produce from 'immer';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { isEmpty } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
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';
@@ -277,9 +277,6 @@ export default {
skip() {
return !this.hasAnyIssues || isEmpty(this.pageParams);
},
- context: {
- isSingleRequest: true,
- },
},
},
computed: {
@@ -910,7 +907,7 @@ export default {
v-if="issuesDrawerEnabled"
:open="isIssuableSelected"
header-height="calc(var(--top-bar-height) + var(--performance-bar-height))"
- class="gl-w-40p gl-xs-w-full"
+ class="gl-w-full gl-sm-w-40p"
@close="activeIssuable = null"
>
<template #title>
@@ -1030,7 +1027,10 @@ export default {
:export-csv-path="exportCsvPathWithQuery"
:issuable-count="currentTabCount"
/>
- <gl-disclosure-dropdown-group :bordered="true" :group="subscribeDropdownOptions" />
+ <gl-disclosure-dropdown-group
+ :bordered="showCsvButtons"
+ :group="subscribeDropdownOptions"
+ />
</gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue b/app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue
index 4b59672428b..eb7bcf70563 100644
--- a/app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue
+++ b/app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue
@@ -1,7 +1,7 @@
<script>
-import * as Sentry from '@sentry/browser';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { isEmpty } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { fetchPolicies } from '~/lib/graphql';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import axios from '~/lib/utils/axios_utils';
@@ -166,9 +166,6 @@ export default {
skip() {
return this.shouldSkipQuery;
},
- context: {
- isSingleRequest: true,
- },
},
},
computed: {
diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue
index 10323b99665..1f159e71da9 100644
--- a/app/assets/javascripts/issues/show/components/fields/description.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description.vue
@@ -79,7 +79,6 @@ export default {
:autocomplete-data-sources="autocompleteDataSources"
supports-quick-actions
autofocus
- data-qa-selector="description_field"
@input="$emit('input', $event)"
@keydown.meta.enter="saveIssuable"
@keydown.ctrl.enter="saveIssuable"
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index dee4c536afa..32df19dfe44 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -1,17 +1,17 @@
<script>
import {
GlButton,
- GlDropdown,
+ GlDisclosureDropdown,
GlDropdownDivider,
- GlDropdownItem,
+ GlDisclosureDropdownItem,
GlLink,
GlModal,
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import { STATUS_CLOSED, TYPE_ISSUE, issuableTypeText } from '~/issues/constants';
@@ -59,9 +59,9 @@ export default {
components: {
DeleteIssueModal,
GlButton,
- GlDropdown,
+ GlDisclosureDropdown,
GlDropdownDivider,
- GlDropdownItem,
+ GlDisclosureDropdownItem,
GlLink,
GlModal,
AbuseCategorySelector,
@@ -184,6 +184,18 @@ export default {
showMovedSidebarOptions() {
return this.isMrSidebarMoved && this.isUserSignedIn;
},
+ newIssueItem() {
+ return {
+ text: this.newIssueTypeText,
+ href: this.newIssuePath,
+ };
+ },
+ submitSpamItem() {
+ return {
+ text: __('Submit as spam'),
+ href: this.submitAsSpamPath,
+ };
+ },
},
created() {
eventHub.$on('toggle.issuable.state', this.toggleIssueState);
@@ -197,6 +209,7 @@ export default {
toggleIssueState() {
if (!this.isClosed && this.getBlockedByIssues?.length) {
this.$refs.blockedByIssuesModal.show();
+ this.closeActionsDropdown();
return;
}
@@ -204,6 +217,7 @@ export default {
},
toggleReportAbuseDrawer(isOpen) {
this.isReportAbuseDrawerOpen = isOpen;
+ this.closeActionsDropdown();
},
invokeUpdateIssueMutation() {
this.toggleStateButtonLoading(true);
@@ -237,6 +251,7 @@ export default {
.catch(() => createAlert({ message: __('Error occurred while updating the issue status') }))
.finally(() => {
this.toggleStateButtonLoading(false);
+ this.closeActionsDropdown();
});
},
promoteToEpic() {
@@ -267,16 +282,24 @@ export default {
.catch(() => createAlert({ message: this.$options.i18n.promoteErrorMessage }))
.finally(() => {
this.toggleStateButtonLoading(false);
+ this.closeActionsDropdown();
});
},
edit() {
issuesEventHub.$emit('open.form');
+ this.closeActionsDropdown();
},
copyReference() {
toast(__('Reference copied'));
+ this.closeActionsDropdown();
},
copyEmailAddress() {
toast(__('Email address copied'));
+ this.closeActionsDropdown();
+ },
+ closeActionsDropdown() {
+ this.$refs.issuableActionsDropdownMobile?.close();
+ this.$refs.issuableActionsDropdownDesktop?.close();
},
},
TYPE_ISSUE,
@@ -285,87 +308,90 @@ export default {
<template>
<div class="detail-page-header-actions gl-display-flex gl-align-self-start gl-sm-gap-3">
- <gl-dropdown
- v-if="hasMobileDropdown"
- class="gl-sm-display-none! w-100"
- block
- :text="dropdownText"
- data-testid="mobile-dropdown"
- :loading="isToggleStateButtonLoading"
- >
- <template v-if="showMovedSidebarOptions">
- <sidebar-subscriptions-widget
- :iid="String(iid)"
- :full-path="fullPath"
- :issuable-type="$options.TYPE_ISSUE"
- data-testid="notification-toggle"
- />
+ <div class="gl-sm-display-none! w-100">
+ <gl-disclosure-dropdown
+ v-if="hasMobileDropdown"
+ ref="issuableActionsDropdownMobile"
+ toggle-class="gl-w-full"
+ block
+ :toggle-text="dropdownText"
+ :auto-close="false"
+ data-testid="mobile-dropdown"
+ :loading="isToggleStateButtonLoading"
+ placement="right"
+ >
+ <template v-if="showMovedSidebarOptions">
+ <sidebar-subscriptions-widget
+ :iid="String(iid)"
+ :full-path="fullPath"
+ :issuable-type="$options.TYPE_ISSUE"
+ data-testid="notification-toggle"
+ />
- <gl-dropdown-divider />
- </template>
+ <gl-dropdown-divider />
+ </template>
- <template v-if="showLockIssueOption">
- <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" />
- </template>
+ <template v-if="showLockIssueOption">
+ <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" />
+ </template>
- <gl-dropdown-item v-if="canUpdateIssue" @click="edit">
- {{ $options.i18n.edit }}
- </gl-dropdown-item>
- <gl-dropdown-item
- v-if="showToggleIssueStateButton"
- :data-testid="`mobile_${qaSelector}`"
- @click="toggleIssueState"
- >
- {{ buttonText }}
- </gl-dropdown-item>
- <gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
- {{ newIssueTypeText }}
- </gl-dropdown-item>
- <gl-dropdown-item v-if="canPromoteToEpic" @click="promoteToEpic">
- {{ __('Promote to epic') }}
- </gl-dropdown-item>
- <template v-if="isMrSidebarMoved">
- <gl-dropdown-item
- :data-clipboard-text="issuableReference"
- button-class="js-copy-reference"
- data-testid="copy-reference"
- @click="copyReference"
- >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item
- >
- <gl-dropdown-item
- v-if="issuableEmailAddress && showMovedSidebarOptions"
- :data-clipboard-text="issuableEmailAddress"
- data-testid="copy-email"
- @click="copyEmailAddress"
- >{{ copyMailAddressText }}</gl-dropdown-item
+ <gl-disclosure-dropdown-item v-if="canUpdateIssue" @action="edit">
+ <template #list-item>{{ $options.i18n.edit }}</template>
+ </gl-disclosure-dropdown-item>
+ <gl-disclosure-dropdown-item
+ v-if="showToggleIssueStateButton"
+ :data-testid="`mobile_${qaSelector}`"
+ @action="toggleIssueState"
>
- </template>
- <gl-dropdown-item
- v-if="canReportSpam"
- :href="submitAsSpamPath"
- data-method="post"
- rel="nofollow"
- >
- {{ __('Submit as spam') }}
- </gl-dropdown-item>
- <template v-if="canDestroyIssue">
- <gl-dropdown-divider />
- <gl-dropdown-item
- v-gl-modal="$options.deleteModalId"
- variant="danger"
- @click="track('click_dropdown')"
+ <template #list-item>{{ buttonText }}</template>
+ </gl-disclosure-dropdown-item>
+ <gl-disclosure-dropdown-item v-if="canCreateIssue" :item="newIssueItem" />
+ <gl-disclosure-dropdown-item v-if="canPromoteToEpic" @action="promoteToEpic">
+ <template #list-item>{{ __('Promote to epic') }}</template>
+ </gl-disclosure-dropdown-item>
+ <template v-if="isMrSidebarMoved">
+ <gl-disclosure-dropdown-item
+ :data-clipboard-text="issuableReference"
+ button-class="js-copy-reference"
+ data-testid="copy-reference"
+ @action="copyReference"
+ ><template #list-item>{{
+ $options.i18n.copyReferenceText
+ }}</template></gl-disclosure-dropdown-item
+ >
+ <gl-disclosure-dropdown-item
+ v-if="issuableEmailAddress && showMovedSidebarOptions"
+ :data-clipboard-text="issuableEmailAddress"
+ data-testid="copy-email"
+ @action="copyEmailAddress"
+ >{{ copyMailAddressText }}</gl-disclosure-dropdown-item
+ >
+ </template>
+ <gl-disclosure-dropdown-item
+ v-if="canReportSpam"
+ :item="submitSpamItem"
+ data-method="post"
+ rel="nofollow"
+ />
+ <template v-if="canDestroyIssue">
+ <gl-dropdown-divider />
+ <gl-disclosure-dropdown-item
+ v-gl-modal="$options.deleteModalId"
+ variant="danger"
+ @action="track('click_dropdown')"
+ >
+ <template #list-item>{{ deleteButtonText }}</template>
+ </gl-disclosure-dropdown-item>
+ </template>
+ <gl-disclosure-dropdown-item
+ v-if="!isIssueAuthor && isUserSignedIn"
+ data-testid="report-abuse-item"
+ @action="toggleReportAbuseDrawer(true)"
>
- {{ deleteButtonText }}
- </gl-dropdown-item>
- </template>
- <gl-dropdown-item
- v-if="!isIssueAuthor && isUserSignedIn"
- data-testid="report-abuse-item"
- @click="toggleReportAbuseDrawer(true)"
- >
- {{ $options.i18n.reportAbuse }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <template #list-item>{{ $options.i18n.reportAbuse }}</template>
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown>
+ </div>
<gl-button
v-if="canUpdateIssue"
@@ -379,20 +405,22 @@ export default {
{{ $options.i18n.edit }}
</gl-button>
- <gl-dropdown
+ <gl-disclosure-dropdown
v-if="hasDesktopDropdown"
id="new-actions-header-dropdown"
+ ref="issuableActionsDropdownDesktop"
v-gl-tooltip.hover
class="gl-display-none gl-sm-display-inline-flex!"
icon="ellipsis_v"
category="tertiary"
- :text="dropdownText"
- :text-sr-only="true"
+ placement="left"
+ :toggle-text="dropdownText"
+ text-sr-only
:title="dropdownText"
:aria-label="dropdownText"
+ :auto-close="false"
data-testid="desktop-dropdown"
no-caret
- right
>
<template v-if="showMovedSidebarOptions && !glFeatures.notificationsTodosButtons">
<sidebar-subscriptions-widget
@@ -401,73 +429,70 @@ export default {
:issuable-type="$options.TYPE_ISSUE"
data-testid="notification-toggle"
/>
-
<gl-dropdown-divider />
</template>
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
v-if="showToggleIssueStateButton"
data-testid="toggle-issue-state-button"
- @click="toggleIssueState"
+ @action="toggleIssueState"
>
- {{ buttonText }}
- </gl-dropdown-item>
- <gl-dropdown-item v-if="canCreateIssue && isUserSignedIn" :href="newIssuePath">
- {{ newIssueTypeText }}
- </gl-dropdown-item>
- <gl-dropdown-item
+ <template #list-item>{{ buttonText }}</template>
+ </gl-disclosure-dropdown-item>
+ <gl-disclosure-dropdown-item v-if="canCreateIssue && isUserSignedIn" :item="newIssueItem" />
+ <gl-disclosure-dropdown-item
v-if="canPromoteToEpic"
:disabled="isToggleStateButtonLoading"
data-testid="promote-button"
- @click="promoteToEpic"
+ @action="promoteToEpic"
>
- {{ __('Promote to epic') }}
- </gl-dropdown-item>
+ <template #list-item>{{ __('Promote to epic') }}</template>
+ </gl-disclosure-dropdown-item>
<template v-if="showLockIssueOption">
<issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" />
</template>
<template v-if="isMrSidebarMoved">
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
:data-clipboard-text="issuableReference"
button-class="js-copy-reference"
data-testid="copy-reference"
- @click="copyReference"
- >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item
+ @action="copyReference"
+ ><template #list-item>{{
+ $options.i18n.copyReferenceText
+ }}</template></gl-disclosure-dropdown-item
>
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
v-if="issuableEmailAddress && showMovedSidebarOptions"
:data-clipboard-text="issuableEmailAddress"
data-testid="copy-email"
- @click="copyEmailAddress"
- >{{ copyMailAddressText }}</gl-dropdown-item
+ @action="copyEmailAddress"
+ ><template #list-item>{{ copyMailAddressText }}</template></gl-disclosure-dropdown-item
>
</template>
<gl-dropdown-divider v-if="canDestroyIssue || canReportSpam || !isIssueAuthor" />
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
v-if="canReportSpam"
- :href="submitAsSpamPath"
+ :item="submitSpamItem"
data-method="post"
rel="nofollow"
- >
- {{ __('Submit as spam') }}
- </gl-dropdown-item>
- <gl-dropdown-item
+ />
+ <gl-disclosure-dropdown-item
v-if="!isIssueAuthor && isUserSignedIn"
data-testid="report-abuse-item"
- @click="toggleReportAbuseDrawer(true)"
+ @action="toggleReportAbuseDrawer(true)"
>
- {{ $options.i18n.reportAbuse }}
- </gl-dropdown-item>
+ <template #list-item>{{ $options.i18n.reportAbuse }}</template>
+ </gl-disclosure-dropdown-item>
<template v-if="canDestroyIssue">
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
v-gl-modal="$options.deleteModalId"
variant="danger"
data-testid="delete-issue-button"
- @click="track('click_dropdown')"
+ @action="track('click_dropdown')"
>
- {{ deleteButtonText }}
- </gl-dropdown-item>
+ <template #list-item>{{ deleteButtonText }}</template>
+ </gl-disclosure-dropdown-item>
</template>
- </gl-dropdown>
+ </gl-disclosure-dropdown>
<gl-modal
ref="blockedByIssuesModal"
diff --git a/app/assets/javascripts/issues/show/components/issue_header.vue b/app/assets/javascripts/issues/show/components/issue_header.vue
index 211f3217ddc..96eb8fbb3c7 100644
--- a/app/assets/javascripts/issues/show/components/issue_header.vue
+++ b/app/assets/javascripts/issues/show/components/issue_header.vue
@@ -82,7 +82,7 @@ export default {
return this.issuableState === STATUS_OPEN || this.issuableState === STATUS_REOPENED;
},
statusIcon() {
- return this.isOpen ? 'issues' : 'issue-closed';
+ return this.isOpen ? 'issue-open-m' : 'issue-close';
},
statusText() {
if (this.isOpen) {
@@ -115,11 +115,9 @@ export default {
<template #status-badge>
<gl-sprintf v-if="closedStatusLink" :message="statusText">
<template #link>
- <gl-link
- class="gl-reset-color! gl-reset-font-size gl-text-decoration-underline"
- :href="closedStatusLink"
- >{{ closedStatusText }}</gl-link
- >
+ <gl-link class="gl-reset-color! gl-text-decoration-underline" :href="closedStatusLink">{{
+ closedStatusText
+ }}</gl-link>
</template>
</gl-sprintf>
<template v-else>{{ statusText }}</template>
diff --git a/app/assets/javascripts/issues/show/components/sticky_header.vue b/app/assets/javascripts/issues/show/components/sticky_header.vue
index 738bb2c2aa0..18e37c4216c 100644
--- a/app/assets/javascripts/issues/show/components/sticky_header.vue
+++ b/app/assets/javascripts/issues/show/components/sticky_header.vue
@@ -2,12 +2,7 @@
import { GlBadge, GlIcon, GlIntersectionObserver, GlLink } from '@gitlab/ui';
import HiddenBadge from '~/issuable/components/hidden_badge.vue';
import LockedBadge from '~/issuable/components/locked_badge.vue';
-import {
- issuableStatusText,
- STATUS_CLOSED,
- TYPE_EPIC,
- WORKSPACE_PROJECT,
-} from '~/issues/constants';
+import { issuableStatusText, STATUS_CLOSED, WORKSPACE_PROJECT } from '~/issues/constants';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
export default {
@@ -60,10 +55,7 @@ export default {
return this.issuableStatus === STATUS_CLOSED;
},
statusIcon() {
- if (this.issuableType === TYPE_EPIC) {
- return this.isClosed ? 'epic-closed' : 'epic';
- }
- return this.isClosed ? 'issue-closed' : 'issues';
+ return this.isClosed ? 'issue-close' : 'issue-open-m';
},
statusText() {
return issuableStatusText[this.issuableStatus];
@@ -84,7 +76,7 @@ export default {
data-testid="issue-sticky-header"
>
<div
- class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-gap-2 gl-mx-auto gl-px-5"
+ class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-gap-2 gl-mx-auto"
>
<gl-badge :variant="statusVariant">
<gl-icon :name="statusIcon" />
diff --git a/app/assets/javascripts/issues/show/utils/parse_data.js b/app/assets/javascripts/issues/show/utils/parse_data.js
index f1e6bd2419a..23d5292da00 100644
--- a/app/assets/javascripts/issues/show/utils/parse_data.js
+++ b/app/assets/javascripts/issues/show/utils/parse_data.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { sanitize } from '~/lib/dompurify';
// We currently load + parse the data from the issue app and related merge request
diff --git a/app/assets/javascripts/jira_connect/subscriptions/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js
index 1a10360ed30..85e250b14a0 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/constants.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js
@@ -37,9 +37,15 @@ export const I18N_OAUTH_FAILED_MESSAGE = s__(
export const INTEGRATIONS_DOC_LINK = helpPagePath('integration/jira/development_panel', {
anchor: 'use-the-integration',
});
+export const PREREQUISITES_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
+ anchor: 'prerequisites',
+});
export const OAUTH_SELF_MANAGED_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
anchor: 'set-up-oauth-authentication',
});
+export const SET_UP_INSTANCE_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
+ anchor: 'set-up-your-instance',
+});
export const FAILED_TO_UPDATE_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
anchor: 'failed-to-update-the-gitlab-instance',
});
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue
index d8d2db18d9f..9f8fae5b476 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue
@@ -1,13 +1,44 @@
<script>
-import { GlButton, GlLink } from '@gitlab/ui';
-import { OAUTH_SELF_MANAGED_DOC_LINK } from '~/jira_connect/subscriptions/constants';
+import { GlButton, GlFormCheckbox, GlLink } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import {
+ PREREQUISITES_DOC_LINK,
+ OAUTH_SELF_MANAGED_DOC_LINK,
+ SET_UP_INSTANCE_DOC_LINK,
+} from '~/jira_connect/subscriptions/constants';
export default {
components: {
GlButton,
+ GlFormCheckbox,
GlLink,
},
- OAUTH_SELF_MANAGED_DOC_LINK,
+ data() {
+ return {
+ requiredSteps: [
+ {
+ name: s__('JiraConnect|Prerequisites'),
+ link: PREREQUISITES_DOC_LINK,
+ checked: false,
+ },
+ {
+ name: s__('JiraConnect|Set up OAuth authentication'),
+ link: OAUTH_SELF_MANAGED_DOC_LINK,
+ checked: false,
+ },
+ {
+ name: s__('JiraConnect|Set up your instance'),
+ link: SET_UP_INSTANCE_DOC_LINK,
+ checked: false,
+ },
+ ],
+ };
+ },
+ computed: {
+ nextDisabled() {
+ return !this.requiredSteps.every((step) => step.checked);
+ },
+ },
};
</script>
@@ -17,20 +48,25 @@ export default {
<p>
{{
s__(
- 'JiraConnect|In order to complete the set up, you’ll need to complete a few steps in GitLab.',
+ 'JiraConnect|In order to complete the set up, you’ll need to complete a few steps in GitLab:',
)
}}
- <gl-link
- class="gl-reset-font-size!"
- :href="$options.OAUTH_SELF_MANAGED_DOC_LINK"
- target="_blank"
- >{{ __('Learn more') }}</gl-link
- >
</p>
+ <div class="gl-mb-5">
+ <div v-for="step in requiredSteps" :key="step.name" class="gl-mb-2">
+ <gl-form-checkbox v-model="step.checked">
+ <gl-link :href="step.link" target="_blank">
+ {{ step.name }}
+ </gl-link>
+ </gl-form-checkbox>
+ </div>
+ </div>
<div class="gl-display-flex gl-justify-content-space-between">
<gl-button @click="$emit('back')">{{ __('Back') }}</gl-button>
- <gl-button variant="confirm" @click="$emit('next')">{{ __('Next') }}</gl-button>
+ <gl-button variant="confirm" :disabled="nextDisabled" @click="$emit('next')"
+ >{{ __('Next') }}
+ </gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 6ab530576fc..5285fa363a5 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -1,5 +1,4 @@
import { ApolloClient, InMemoryCache, ApolloLink, HttpLink } from '@apollo/client/core';
-import { BatchHttpLink } from '@apollo/client/link/batch-http';
import { createUploadLink } from 'apollo-upload-client';
import { persistCache } from 'apollo3-cache-persist';
import ActionCableLink from '~/actioncable_link';
@@ -116,18 +115,14 @@ Object.defineProperty(window, 'pendingApolloRequests', {
function createApolloClient(resolvers = {}, config = {}) {
const {
baseUrl,
- batchMax = 10,
cacheConfig = { typePolicies: {}, possibleTypes: {} },
fetchPolicy = fetchPolicies.CACHE_FIRST,
typeDefs,
httpHeaders = {},
fetchCredentials = 'same-origin',
path = '/api/graphql',
- useGet = false,
} = config;
- const shouldUnbatch = gon.features?.unbatchGraphqlQueries;
-
let ac = null;
let uri = `${gon.relative_url_root || ''}${path}`;
@@ -146,7 +141,6 @@ function createApolloClient(resolvers = {}, config = {}) {
// We set to `same-origin` which is default value in modern browsers.
// See https://github.com/whatwg/fetch/pull/585 for more information.
credentials: fetchCredentials,
- batchMax,
};
/*
@@ -165,14 +159,10 @@ function createApolloClient(resolvers = {}, config = {}) {
return fetch(stripWhitespaceFromQuery(url, uri), options);
};
- const requestLink = ApolloLink.split(
- () => useGet || shouldUnbatch,
- new HttpLink({ ...httpOptions, fetch: fetchIntervention }),
- new BatchHttpLink(httpOptions),
- );
+ const requestLink = new HttpLink({ ...httpOptions, fetch: fetchIntervention });
const uploadsLink = ApolloLink.split(
- (operation) => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
+ (operation) => operation.getContext().hasUpload,
createUploadLink(httpOptions),
);
diff --git a/app/assets/javascripts/lib/utils/color_utils.js b/app/assets/javascripts/lib/utils/color_utils.js
index a9f4257e28b..74c9f7de8c1 100644
--- a/app/assets/javascripts/lib/utils/color_utils.js
+++ b/app/assets/javascripts/lib/utils/color_utils.js
@@ -46,5 +46,5 @@ export function darkModeEnabled() {
if (isWebIde) {
return ideDarkThemes.includes(window.gon?.user_color_scheme);
}
- return document.body.classList.contains('gl-dark');
+ return document.documentElement.classList.contains('gl-dark');
}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 7d16af003e4..27da2ac6ce1 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -7,6 +7,7 @@ import $ from 'jquery';
import { isFunction, defer, escape, partial, toLower } from 'lodash';
import Cookies from '~/lib/utils/cookies';
import { SCOPED_LABEL_DELIMITER } from '~/sidebar/components/labels/labels_select_widget/constants';
+import { DEFAULT_CI_CONFIG_PATH, CI_CONFIG_PATH_EXTENSION } from '~/lib/utils/constants';
import { convertToCamelCase, convertToSnakeCase } from './text_utility';
import { isObject } from './type_utility';
import { getLocationHash } from './url_utility';
@@ -737,3 +738,17 @@ export const isCurrentUser = (userId) => {
export const cloneWithoutReferences = (obj) => {
return JSON.parse(JSON.stringify(obj));
};
+
+/**
+ * Returns true if the given path is the default CI config path.
+ */
+export const isDefaultCiConfig = (path) => {
+ return path === DEFAULT_CI_CONFIG_PATH;
+};
+
+/**
+ * Returns true if the given path has the CI config path extension.
+ */
+export const hasCiConfigExtension = (path) => {
+ return CI_CONFIG_PATH_EXTENSION.test(path);
+};
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index da5fb831ae5..d9ac0abf7b3 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -23,3 +23,6 @@ export const BYTES_FORMAT_BYTES = 'B';
export const BYTES_FORMAT_KIB = 'KiB';
export const BYTES_FORMAT_MIB = 'MiB';
export const BYTES_FORMAT_GIB = 'GiB';
+
+export const DEFAULT_CI_CONFIG_PATH = '.gitlab-ci.yml';
+export const CI_CONFIG_PATH_EXTENSION = /(\.gitlab-ci\.yml)/;
diff --git a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
index a973cd890ba..89170ecc55d 100644
--- a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
@@ -108,15 +108,27 @@ timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining());
timeago.register(`${timeagoLanguageCode}-duration`, memoizedLocaleDuration());
const setupAbsoluteFormatters = () => {
- const cache = {};
+ let cache = {};
// Intl.DateTimeFormat options (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat#using_options)
+ // For hourCycle please check https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle
+ const hourCycle = [undefined, 'h12', 'h23'];
const formats = {
- [DATE_WITH_TIME_FORMAT]: () => ({ dateStyle: 'medium', timeStyle: 'short' }),
+ [DATE_WITH_TIME_FORMAT]: () => ({
+ dateStyle: 'medium',
+ timeStyle: 'short',
+ hourCycle: hourCycle[window.gon?.time_display_format || 0],
+ }),
[DATE_ONLY_FORMAT]: () => ({ dateStyle: 'medium' }),
};
return (formatName = DEFAULT_DATE_TIME_FORMAT) => {
+ if (cache.time_display_format !== window.gon?.time_display_format) {
+ cache = {
+ time_display_format: window.gon?.time_display_format,
+ };
+ }
+
if (cache[formatName]) {
return cache[formatName];
}
diff --git a/app/assets/javascripts/lib/utils/forms.js b/app/assets/javascripts/lib/utils/forms.js
index 652ae337506..6713a18cbf3 100644
--- a/app/assets/javascripts/lib/utils/forms.js
+++ b/app/assets/javascripts/lib/utils/forms.js
@@ -69,18 +69,32 @@ export const isIntegerGreaterThan = (value, greaterThan) =>
isParseableAsInteger(value) && parseInt(value, 10) > greaterThan;
/**
- * Regexp that matches email structure.
+ * Regexp that matches service desk setting email structure.
* Taken from app/models/service_desk_setting.rb custom_email
*/
-export const EMAIL_REGEXP = /^[\w\-._]+@[\w\-.]+\.[a-zA-Z]{2,}$/;
+const SERVICE_DESK_SETTING_EMAIL_REGEXP = /^[\w\-._]+@[\w\-.]+\.[a-zA-Z]{2,}$/;
/**
- * Checks if the input is a valid email address
+ * Checks if the input is a valid service desk setting email address
*
* @param {String} - value
* @returns {Boolean}
*/
-export const isEmail = (value) => EMAIL_REGEXP.test(value);
+export const isServiceDeskSettingEmail = (value) => SERVICE_DESK_SETTING_EMAIL_REGEXP.test(value);
+
+/**
+ * Regexp that matches user email structure.
+ * Taken from DeviseEmailValidator
+ */
+const USER_EMAIL_REGEXP = /^[^@\s]+@[^@\s]+$/;
+
+/**
+ * Checks if the input is a valid user email address
+ *
+ * @param {String} - value
+ * @returns {Boolean}
+ */
+export const isUserEmail = (value) => USER_EMAIL_REGEXP.test(value);
/**
* A form object serializer
diff --git a/app/assets/javascripts/lib/utils/keys.js b/app/assets/javascripts/lib/utils/keys.js
index 7cfcd11ece9..e5022551b97 100644
--- a/app/assets/javascripts/lib/utils/keys.js
+++ b/app/assets/javascripts/lib/utils/keys.js
@@ -1,5 +1,6 @@
export const ESC_KEY = 'Escape';
export const ENTER_KEY = 'Enter';
+export const NUMPAD_ENTER_KEY = 'NumpadEnter';
export const BACKSPACE_KEY = 'Backspace';
export const ARROW_DOWN_KEY = 'ArrowDown';
export const ARROW_UP_KEY = 'ArrowUp';
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 5bfdd174694..29189e3ac2f 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -29,7 +29,6 @@ import initBreadcrumbs from './breadcrumb';
import initPersistentUserCallouts from './persistent_user_callouts';
import { initUserTracking, initDefaultTrackers } from './tracking';
import { initSidebarTracking } from './pages/shared/nav/sidebar_tracking';
-import initServicePingConsent from './service_ping_consent';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
@@ -93,7 +92,6 @@ function deferredInitialisation() {
initBreadcrumbs();
initPrefetchLinks('.js-prefetch-document');
initLogoAnimation();
- initServicePingConsent();
initUserPopovers();
initBroadcastNotifications();
initPersistentUserCallouts();
diff --git a/app/assets/javascripts/members/components/avatars/group_avatar.vue b/app/assets/javascripts/members/components/avatars/group_avatar.vue
index 3b176bf2b43..83b5855492b 100644
--- a/app/assets/javascripts/members/components/avatars/group_avatar.vue
+++ b/app/assets/javascripts/members/components/avatars/group_avatar.vue
@@ -1,11 +1,18 @@
<script>
-import { GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
+import { GlAvatarLink, GlAvatarLabeled, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+import PrivateIcon from '../icons/private_icon.vue';
import { AVATAR_SIZE } from '../../constants';
export default {
name: 'GroupAvatar',
- avatarSize: AVATAR_SIZE,
- components: { GlAvatarLink, GlAvatarLabeled },
+ components: { GlAvatarLink, GlAvatarLabeled, PrivateIcon },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ i18n: {
+ private: __('Private'),
+ },
props: {
member: {
type: Object,
@@ -16,19 +23,36 @@ export default {
group() {
return this.member.sharedWithGroup;
},
+ isPrivate() {
+ return this.member.isSharedWithGroupPrivate;
+ },
+ avatarLabeledProps() {
+ const label = this.isPrivate ? this.$options.i18n.private : this.group.fullName;
+
+ return {
+ label,
+ src: this.group.avatarUrl,
+ alt: label,
+ size: AVATAR_SIZE,
+ entityName: this.isPrivate ? this.$options.i18n.private : this.group.name,
+ entityId: this.group.id,
+ };
+ },
},
};
</script>
<template>
- <gl-avatar-link :href="group.webUrl">
- <gl-avatar-labeled
- :label="group.fullName"
- :src="group.avatarUrl"
- :alt="group.fullName"
- :size="$options.avatarSize"
- :entity-name="group.name"
- :entity-id="group.id"
- />
+ <div v-if="isPrivate">
+ <gl-avatar-labeled v-bind="avatarLabeledProps">
+ <template #meta>
+ <div class="gl-p-1">
+ <private-icon />
+ </div>
+ </template>
+ </gl-avatar-labeled>
+ </div>
+ <gl-avatar-link v-else :href="group.webUrl">
+ <gl-avatar-labeled v-bind="avatarLabeledProps" />
</gl-avatar-link>
</template>
diff --git a/app/assets/javascripts/members/components/icons/private_icon.vue b/app/assets/javascripts/members/components/icons/private_icon.vue
new file mode 100644
index 00000000000..6168ea955f3
--- /dev/null
+++ b/app/assets/javascripts/members/components/icons/private_icon.vue
@@ -0,0 +1,19 @@
+<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ name: 'GroupAvatar',
+ components: { GlIcon },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ i18n: {
+ tooltip: s__('Members|Private group information is only accessible to its members.'),
+ },
+};
+</script>
+
+<template>
+ <gl-icon v-gl-tooltip="$options.i18n.tooltip" name="eye-slash" />
+</template>
diff --git a/app/assets/javascripts/members/components/table/member_source.vue b/app/assets/javascripts/members/components/table/member_source.vue
index ed1971d020b..f1a1c4cecaa 100644
--- a/app/assets/javascripts/members/components/table/member_source.vue
+++ b/app/assets/javascripts/members/components/table/member_source.vue
@@ -1,10 +1,12 @@
<script>
import { GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
+import PrivateIcon from '../icons/private_icon.vue';
export default {
name: 'MemberSource',
i18n: {
+ private: __('Private'),
inherited: __('Inherited'),
directMember: __('Direct member'),
directMemberWithCreatedBy: s__('Members|Direct member by %{createdBy}'),
@@ -13,16 +15,24 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- components: { GlSprintf },
+ components: { GlSprintf, PrivateIcon },
props: {
memberSource: {
type: Object,
- required: true,
+ required: false,
+ default() {
+ return {};
+ },
},
isDirectMember: {
type: Boolean,
required: true,
},
+ isSharedWithGroupPrivate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
createdBy: {
type: Object,
required: false,
@@ -43,7 +53,11 @@ export default {
</script>
<template>
- <span v-if="showCreatedBy">
+ <div v-if="isSharedWithGroupPrivate" class="gl-display-flex gl-column-gap-2">
+ <span>{{ $options.i18n.private }}</span>
+ <private-icon />
+ </div>
+ <span v-else-if="showCreatedBy">
<gl-sprintf :message="messageWithCreatedBy">
<template #group>
<a v-gl-tooltip.hover="$options.i18n.inherited" :href="memberSource.webUrl">{{
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 68f624e9a3d..2b3294c1c79 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -270,6 +270,7 @@ export default {
:is-direct-member="isDirectMember"
:member-source="member.source"
:created-by="member.createdBy"
+ :is-shared-with-group-private="member.isSharedWithGroupPrivate"
/>
</members-table-cell>
</template>
diff --git a/app/assets/javascripts/members/components/table/role_dropdown.vue b/app/assets/javascripts/members/components/table/role_dropdown.vue
index 4b39c000b8f..2b72a3fe6e8 100644
--- a/app/assets/javascripts/members/components/table/role_dropdown.vue
+++ b/app/assets/javascripts/members/components/table/role_dropdown.vue
@@ -3,9 +3,10 @@ import { GlCollapsibleListbox } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
// eslint-disable-next-line no-restricted-imports
import { mapActions } from 'vuex';
-import * as Sentry from '@sentry/browser';
-import { s__ } from '~/locale';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { guestOverageConfirmAction } from 'ee_else_ce/members/guest_overage_confirm_action';
+import { roleDropdownItems, initialSelectedRole } from 'ee_else_ce/members/utils';
+import { s__ } from '~/locale';
export default {
name: 'RoleDropdown',
@@ -29,7 +30,7 @@ export default {
return {
isDesktop: false,
busy: false,
- selectedRoleValue: this.member.accessLevel.integerValue,
+ selectedRole: null,
};
},
computed: {
@@ -37,12 +38,12 @@ export default {
return this.permissions.canOverride && !this.member.isOverridden;
},
dropdownItems() {
- return Object.entries(this.member.validRoles).map(([name, value]) => ({
- value,
- text: name,
- }));
+ return roleDropdownItems(this.member);
},
},
+ created() {
+ this.selectedRole = initialSelectedRole(this.dropdownItems.flatten, this.member);
+ },
mounted() {
this.isDesktop = bp.isDesktop();
},
@@ -52,44 +53,39 @@ export default {
return dispatch(`${this.namespace}/updateMemberRole`, payload);
},
}),
- async handleOverageConfirm(currentRoleValue, newRoleValue, newRoleName) {
- return guestOverageConfirmAction({
- currentRoleValue,
- newRoleValue,
- newRoleName,
- group: this.group,
- memberId: this.member.id,
- memberType: this.namespace,
- });
- },
- async handleSelect(newRoleValue) {
- const currentRoleValue = this.member.accessLevel.integerValue;
- if (newRoleValue === currentRoleValue) {
- return;
- }
-
+ async handleSelect(value) {
this.busy = true;
- const { text: newRoleName } = this.dropdownItems.find((item) => item.value === newRoleValue);
- const confirmed = await this.handleOverageConfirm(
- currentRoleValue,
- newRoleValue,
- newRoleName,
- );
- if (!confirmed) {
- this.selectedRoleValue = currentRoleValue;
- this.busy = false;
- return;
- }
+ const newRole = this.dropdownItems.flatten.find((item) => item.value === value);
+ const previousRole = this.selectedRole;
try {
+ const confirmed = await guestOverageConfirmAction({
+ currentRoleValue: this.member.accessLevel.integerValue,
+ newRoleValue: newRole.accessLevel,
+ newRoleName: newRole.text,
+ newMemberRoleId: newRole.memberRoleId,
+ group: this.group,
+ memberId: this.member.id,
+ memberType: this.namespace,
+ });
+ if (!confirmed) {
+ return;
+ }
+
+ this.selectedRole = value;
+
await this.updateMemberRole({
memberId: this.member.id,
- accessLevel: { integerValue: newRoleValue, stringValue: newRoleName },
+ accessLevel: {
+ integerValue: newRole.accessLevel,
+ memberRoleId: newRole.memberRoleId,
+ },
});
this.$toast.show(s__('Members|Role updated successfully.'));
} catch (error) {
+ this.selectedRole = previousRole;
Sentry.captureException(error);
} finally {
this.busy = false;
@@ -101,14 +97,14 @@ export default {
<template>
<gl-collapsible-listbox
- v-model="selectedRoleValue"
:placement="isDesktop ? 'left' : 'right'"
:toggle-text="member.accessLevel.stringValue"
:header-text="__('Change role')"
:disabled="disabled"
:loading="busy"
data-qa-selector="access_level_dropdown"
- :items="dropdownItems"
+ :items="dropdownItems.formatted"
+ :selected="selectedRole"
@select="handleSelect"
>
<template #list-item="{ item }">
diff --git a/app/assets/javascripts/members/store/actions.js b/app/assets/javascripts/members/store/actions.js
index 712f0d6caa7..d696f618a3c 100644
--- a/app/assets/javascripts/members/store/actions.js
+++ b/app/assets/javascripts/members/store/actions.js
@@ -6,7 +6,10 @@ export const updateMemberRole = async ({ state, commit }, { memberId, accessLeve
try {
await axios.put(
state.memberPath.replace(/:id$/, memberId),
- state.requestFormatter({ accessLevel: accessLevel.integerValue }),
+ state.requestFormatter({
+ accessLevel: accessLevel.integerValue,
+ memberRoleId: accessLevel.memberRoleId,
+ }),
);
commit(types.RECEIVE_MEMBER_ROLE_SUCCESS, { memberId, accessLevel });
diff --git a/app/assets/javascripts/members/utils.js b/app/assets/javascripts/members/utils.js
index 09e4b5e8a6f..1304fb0fee1 100644
--- a/app/assets/javascripts/members/utils.js
+++ b/app/assets/javascripts/members/utils.js
@@ -1,4 +1,4 @@
-import { isUndefined } from 'lodash';
+import { isUndefined, uniqueId } from 'lodash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getParameterByName, setUrlParams } from '~/lib/utils/url_utility';
import {
@@ -35,6 +35,36 @@ export const generateBadges = ({ member, isCurrentUser, canManageMembers }) => [
},
];
+/**
+ * Creates the dropdowns options for static roles
+ *
+ * @param {object} member
+ * @param {Map<string, number>} member.validRoles
+ */
+export const roleDropdownItems = ({ validRoles }) => {
+ const staticRoleDropdownItems = Object.entries(validRoles).map(([name, value]) => ({
+ accessLevel: value,
+ memberRoleId: null, // The value `null` is need to downgrade from custom role to static role. See: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133430#note_1595153555
+ text: name,
+ value: uniqueId('role-static-'),
+ }));
+
+ return { flatten: staticRoleDropdownItems, formatted: staticRoleDropdownItems };
+};
+
+/**
+ * Finds and returns unique value
+ *
+ * @param {Array<{accessLevel: number, memberRoleId: null, text: string, value: string}>} flattenDropdownItems
+ * @param {object} member
+ * @param {{integerValue: number}} member.accessLevel
+ */
+export const initialSelectedRole = (flattenDropdownItems, member) => {
+ return flattenDropdownItems.find(
+ ({ accessLevel }) => accessLevel === member.accessLevel.integerValue,
+ )?.value;
+};
+
export const isGroup = (member) => {
return Boolean(member.sharedWithGroup);
};
@@ -128,6 +158,7 @@ export const parseDataAttributes = (el) => {
export const baseRequestFormatter = (basePropertyName, accessLevelPropertyName) => ({
accessLevel,
+ memberRoleId,
...otherProperties
}) => {
const accessLevelProperty = !isUndefined(accessLevel)
@@ -137,6 +168,7 @@ export const baseRequestFormatter = (basePropertyName, accessLevelPropertyName)
return {
[basePropertyName]: {
...accessLevelProperty,
+ member_role_id: memberRoleId ?? null,
...otherProperties,
},
};
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 2095f24eb84..8ea995b8b4e 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -21,12 +21,7 @@ import syntaxHighlight from './syntax_highlight';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- useGet: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
// MergeRequestTabs
diff --git a/app/assets/javascripts/merge_requests/components/sticky_header.vue b/app/assets/javascripts/merge_requests/components/sticky_header.vue
index e8bdb854334..877e6142bae 100644
--- a/app/assets/javascripts/merge_requests/components/sticky_header.vue
+++ b/app/assets/javascripts/merge_requests/components/sticky_header.vue
@@ -1,5 +1,12 @@
<script>
-import { GlIntersectionObserver, GlLink, GlSprintf, GlBadge } from '@gitlab/ui';
+import {
+ GlIntersectionObserver,
+ GlLink,
+ GlSprintf,
+ GlBadge,
+ GlIcon,
+ GlTooltipDirective,
+} from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapState } from 'vuex';
import SafeHtml from '~/vue_shared/directives/safe_html';
@@ -45,6 +52,7 @@ export default {
GlLink,
GlSprintf,
GlBadge,
+ GlIcon,
DiscussionCounter,
StatusBadge,
TodoWidget,
@@ -53,10 +61,12 @@ export default {
},
directives: {
SafeHtml,
+ GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
inject: {
projectPath: { default: null },
+ sourceProjectPath: { default: null },
title: { default: '' },
tabs: { default: () => [] },
isFluidLayout: { default: false },
@@ -89,6 +99,16 @@ export default {
isNotificationsTodosButtons() {
return this.glFeatures.notificationsTodosButtons && this.glFeatures.movedMrSidebar;
},
+ isForked() {
+ return this.projectPath !== this.sourceProjectPath;
+ },
+ sourceBranch() {
+ if (this.isForked) {
+ return `${this.sourceProjectPath}:${this.getNoteableData.source_branch}`;
+ }
+
+ return this.getNoteableData.source_branch;
+ },
},
watch: {
discussionTabCounter(val) {
@@ -122,8 +142,8 @@ export default {
:class="{ 'gl-visibility-hidden': !isStickyHeaderVisible }"
>
<div
- class="issue-sticky-header-text gl-display-flex gl-flex-direction-column gl-align-items-center gl-mx-auto gl-px-5 gl-w-full"
- :class="{ 'gl-max-w-container-xl': !isFluidLayout }"
+ class="issue-sticky-header-text gl-display-flex gl-flex-direction-column gl-align-items-center gl-mx-auto gl-w-full"
+ :class="{ 'container-limited': !isFluidLayout }"
>
<div class="gl-w-full gl-display-flex gl-align-items-baseline">
<status-badge
@@ -153,8 +173,17 @@ export default {
:title="getNoteableData.source_branch"
:href="getNoteableData.source_branch_path"
class="gl-text-blue-500! gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-text-truncate gl-max-w-26"
+ data-testid="source-branch"
>
- {{ getNoteableData.source_branch }}
+ <span
+ v-if="isForked"
+ v-gl-tooltip
+ class="gl-vertical-align-middle gl-mr-n2"
+ :title="__('The source project is a fork')"
+ >
+ <gl-icon name="fork" :size="12" class="gl-ml-1" />
+ </span>
+ {{ sourceBranch }}
</gl-link>
</template>
<template #target>
diff --git a/app/assets/javascripts/milestones/components/milestone_combobox.vue b/app/assets/javascripts/milestones/components/milestone_combobox.vue
index 2f7fb542d0e..a5e306b5372 100644
--- a/app/assets/javascripts/milestones/components/milestone_combobox.vue
+++ b/app/assets/javascripts/milestones/components/milestone_combobox.vue
@@ -1,19 +1,10 @@
<script>
-import {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlDropdownItem,
- GlLoadingIcon,
- GlSearchBoxByType,
- GlIcon,
-} from '@gitlab/ui';
+import { GlBadge, GlButton, GlCollapsibleListbox } from '@gitlab/ui';
import { debounce, isEqual } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import { s__, __, sprintf } from '~/locale';
import createStore from '../stores';
-import MilestoneResultsSection from './milestone_results_section.vue';
const SEARCH_DEBOUNCE_MS = 250;
@@ -21,14 +12,9 @@ export default {
name: 'MilestoneCombobox',
store: createStore(),
components: {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlDropdownItem,
- GlLoadingIcon,
- GlSearchBoxByType,
- GlIcon,
- MilestoneResultsSection,
+ GlCollapsibleListbox,
+ GlBadge,
+ GlButton,
},
props: {
value: {
@@ -56,27 +42,43 @@ export default {
required: false,
},
},
- data() {
- return {
- searchQuery: '',
- };
- },
translations: {
- milestone: s__('MilestoneCombobox|Milestone'),
selectMilestone: s__('MilestoneCombobox|Select milestone'),
noMilestone: s__('MilestoneCombobox|No milestone'),
- noResultsLabel: s__('MilestoneCombobox|No matching results'),
- searchMilestones: s__('MilestoneCombobox|Search Milestones'),
- searchErrorMessage: s__('MilestoneCombobox|An error occurred while searching for milestones'),
projectMilestones: s__('MilestoneCombobox|Project milestones'),
groupMilestones: s__('MilestoneCombobox|Group milestones'),
+ unselect: __('Unselect'),
},
computed: {
...mapState(['matches', 'selectedMilestones']),
- ...mapGetters(['isLoading', 'groupMilestonesEnabled']),
+ ...mapGetters(['isLoading']),
+ allMilestones() {
+ const { groupMilestones, projectMilestones } = this.matches || {};
+ const milestones = [];
+
+ if (projectMilestones?.totalCount) {
+ milestones.push({
+ id: 'project-milestones',
+ text: this.$options.translations.projectMilestones,
+ options: projectMilestones.list,
+ totalCount: projectMilestones.totalCount,
+ });
+ }
+
+ if (groupMilestones?.totalCount) {
+ milestones.push({
+ id: 'group-milestones',
+ text: this.$options.translations.groupMilestones,
+ options: groupMilestones.list,
+ totalCount: groupMilestones.totalCount,
+ });
+ }
+
+ return milestones;
+ },
selectedMilestonesLabel() {
const { selectedMilestones } = this;
- const firstMilestoneName = selectedMilestones[0];
+ const [firstMilestoneName] = selectedMilestones;
if (selectedMilestones.length === 0) {
return this.$options.translations.noMilestone;
@@ -92,20 +94,6 @@ export default {
numberOfOtherMilestones,
});
},
- showProjectMilestoneSection() {
- return Boolean(
- this.matches.projectMilestones.totalCount > 0 || this.matches.projectMilestones.error,
- );
- },
- showGroupMilestoneSection() {
- return (
- this.groupMilestonesEnabled &&
- Boolean(this.matches.groupMilestones.totalCount > 0 || this.matches.groupMilestones.error)
- );
- },
- showNoResults() {
- return !this.showProjectMilestoneSection && !this.showGroupMilestoneSection;
- },
},
watch: {
// Keep the Vuex store synchronized if the parent
@@ -127,8 +115,8 @@ export default {
// because we need to access the .cancel() method
// lodash attaches to the function, which is
// made inaccessible by Vue.
- this.debouncedSearch = debounce(function search() {
- this.search(this.searchQuery);
+ this.debouncedSearch = debounce(function search(q) {
+ this.search(q);
}, SEARCH_DEBOUNCE_MS);
this.setProjectId(this.projectId);
@@ -143,22 +131,14 @@ export default {
'setGroupMilestonesAvailable',
'setSelectedMilestones',
'clearSelectedMilestones',
- 'toggleMilestones',
'search',
'fetchMilestones',
]),
- focusSearchBox() {
- this.$refs.searchBox.$el.querySelector('input').focus();
- },
- onSearchBoxEnter() {
- this.debouncedSearch.cancel();
- this.search(this.searchQuery);
+ onSearchBoxInput(q) {
+ this.debouncedSearch(q);
},
- onSearchBoxInput() {
- this.debouncedSearch();
- },
- selectMilestone(milestone) {
- this.toggleMilestones(milestone);
+ selectMilestone(milestones) {
+ this.setSelectedMilestones(milestones);
this.$emit('input', this.selectedMilestones);
},
selectNoMilestone() {
@@ -170,84 +150,42 @@ export default {
</script>
<template>
- <gl-dropdown v-bind="$attrs" class="milestone-combobox" @shown="focusSearchBox">
- <template #button-content>
- <span data-testid="milestone-combobox-button-content" class="gl-flex-grow-1 text-muted">{{
- selectedMilestonesLabel
- }}</span>
- <gl-icon name="chevron-down" />
- </template>
-
- <gl-dropdown-section-header>
- <span class="text-center d-block">{{ $options.translations.selectMilestone }}</span>
- </gl-dropdown-section-header>
-
- <gl-dropdown-divider />
-
- <gl-search-box-by-type
- ref="searchBox"
- v-model.trim="searchQuery"
- class="gl-m-3"
- :placeholder="$options.translations.searchMilestones"
- @input="onSearchBoxInput"
- @keydown.enter.prevent="onSearchBoxEnter"
- />
-
- <gl-dropdown-item
- :is-checked="selectedMilestones.length === 0"
- is-check-item
- @click="selectNoMilestone()"
- >
- {{ $options.translations.noMilestone }}
- </gl-dropdown-item>
-
- <gl-dropdown-divider />
-
- <template v-if="isLoading">
- <gl-loading-icon size="sm" />
- <gl-dropdown-divider />
+ <gl-collapsible-listbox
+ :header-text="$options.translations.selectMilestone"
+ :items="allMilestones"
+ :reset-button-label="$options.translations.unselect"
+ :searching="isLoading"
+ :selected="selectedMilestones"
+ :toggle-text="selectedMilestonesLabel"
+ block
+ multiple
+ searchable
+ @reset="selectNoMilestone"
+ @search="onSearchBoxInput"
+ @select="selectMilestone"
+ >
+ <template #group-label="{ group }">
+ <span :data-testid="`${group.id}-section`"
+ >{{ group.text }}<gl-badge size="sm" class="gl-ml-2">{{ group.totalCount }}</gl-badge></span
+ >
</template>
- <template v-else-if="showNoResults">
- <div class="dropdown-item-space">
- <span data-testid="milestone-combobox-no-results" class="gl-pl-6">{{
- $options.translations.noResultsLabel
- }}</span>
+ <template #footer>
+ <div
+ class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-200 gl-display-flex gl-flex-direction-column gl-p-2! gl-pt-0!"
+ >
+ <gl-button
+ v-for="(item, idx) in extraLinks"
+ :key="idx"
+ :href="item.url"
+ is-check-item
+ data-testid="milestone-combobox-extra-links"
+ category="tertiary"
+ block
+ class="gl-justify-content-start! gl-mt-2!"
+ >
+ {{ item.text }}
+ </gl-button>
</div>
- <gl-dropdown-divider />
- </template>
- <template v-else>
- <milestone-results-section
- v-if="showProjectMilestoneSection"
- :section-title="$options.translations.projectMilestones"
- :total-count="matches.projectMilestones.totalCount"
- :items="matches.projectMilestones.list"
- :selected-milestones="selectedMilestones"
- :error="matches.projectMilestones.error"
- :error-message="$options.translations.searchErrorMessage"
- data-testid="project-milestones-section"
- @selected="selectMilestone($event)"
- />
-
- <milestone-results-section
- v-if="showGroupMilestoneSection"
- :section-title="$options.translations.groupMilestones"
- :total-count="matches.groupMilestones.totalCount"
- :items="matches.groupMilestones.list"
- :selected-milestones="selectedMilestones"
- :error="matches.groupMilestones.error"
- :error-message="$options.translations.searchErrorMessage"
- data-testid="group-milestones-section"
- @selected="selectMilestone($event)"
- />
</template>
- <gl-dropdown-item
- v-for="(item, idx) in extraLinks"
- :key="idx"
- :href="item.url"
- is-check-item
- data-testid="milestone-combobox-extra-links"
- >
- {{ item.text }}
- </gl-dropdown-item>
- </gl-dropdown>
+ </gl-collapsible-listbox>
</template>
diff --git a/app/assets/javascripts/milestones/stores/mutations.js b/app/assets/javascripts/milestones/stores/mutations.js
index 1f88c0a1ea6..c0cd58cc5d2 100644
--- a/app/assets/javascripts/milestones/stores/mutations.js
+++ b/app/assets/javascripts/milestones/stores/mutations.js
@@ -37,7 +37,7 @@ export default {
},
[types.RECEIVE_PROJECT_MILESTONES_SUCCESS](state, response) {
state.matches.projectMilestones = {
- list: response.data.map(({ title }) => ({ title })),
+ list: response.data.map(({ title }) => ({ text: title, value: title })),
totalCount: parseInt(response.headers['x-total'], 10) || response.data.length,
error: null,
};
@@ -51,7 +51,7 @@ export default {
},
[types.RECEIVE_GROUP_MILESTONES_SUCCESS](state, response) {
state.matches.groupMilestones = {
- list: response.data.map(({ title }) => ({ title })),
+ list: response.data.map(({ title }) => ({ text: title, value: title })),
totalCount: parseInt(response.headers['x-total'], 10) || response.data.length,
error: null,
};
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/constants.js b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/constants.js
index 3026bce0972..c94e7648d1d 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/constants.js
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/constants.js
@@ -2,9 +2,9 @@ import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
export const CREATE_EXPERIMENT_HELP_PATH = helpPagePath(
- 'user/project/ml/experiment_tracking/index.md',
+ 'user/project/ml/experiment_tracking/index',
{
- anchor: 'tracking-new-experiments-and-trials',
+ anchor: 'track-new-experiments-and-candidates',
},
);
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/constants.js b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/constants.js
index 4d34555ac2f..346c2453715 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/constants.js
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/constants.js
@@ -1,5 +1,4 @@
import { s__ } from '~/locale';
-import { helpPagePath } from '~/helpers/help_page_helper';
export const METRIC_KEY_PREFIX = 'metric.';
export const LIST_KEY_CREATED_AT = 'created_at';
@@ -13,9 +12,3 @@ export const BASE_SORT_FIELDS = Object.freeze([
label: s__('MlExperimentTracking|Created at'),
},
]);
-export const CREATE_CANDIDATE_HELP_PATH = helpPagePath(
- 'user/project/ml/experiment_tracking/index.md',
- {
- anchor: 'tracking-new-experiments-and-trials',
- },
-);
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
index 28a27059b17..afd48df93e4 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
@@ -10,12 +10,8 @@ import KeysetPagination from '~/vue_shared/components/incubation/pagination.vue'
import ModelExperimentsHeader from '~/ml/experiment_tracking/components/model_experiments_header.vue';
import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue';
-import {
- LIST_KEY_CREATED_AT,
- BASE_SORT_FIELDS,
- METRIC_KEY_PREFIX,
- CREATE_CANDIDATE_HELP_PATH,
-} from './constants';
+import { CREATE_EXPERIMENT_HELP_PATH as CREATE_CANDIDATE_HELP_PATH } from '../index/constants';
+import { LIST_KEY_CREATED_AT, BASE_SORT_FIELDS, METRIC_KEY_PREFIX } from './constants';
import * as translations from './translations';
export default {
diff --git a/app/assets/javascripts/ml/model_registry/apps/index.js b/app/assets/javascripts/ml/model_registry/apps/index.js
index f9e5f82e708..92d159f68be 100644
--- a/app/assets/javascripts/ml/model_registry/apps/index.js
+++ b/app/assets/javascripts/ml/model_registry/apps/index.js
@@ -1,3 +1,5 @@
import ShowMlModel from './show_ml_model.vue';
+import ShowMlModelVersion from './show_ml_model_version.vue';
+import IndexMlModels from './index_ml_models.vue';
-export { ShowMlModel };
+export { ShowMlModel, ShowMlModelVersion, IndexMlModels };
diff --git a/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue b/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue
new file mode 100644
index 00000000000..5a55d5669a8
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue
@@ -0,0 +1,61 @@
+<script>
+import { isEmpty } from 'lodash';
+import Pagination from '~/vue_shared/components/incubation/pagination.vue';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import * as i18n from '../translations';
+import { BASE_SORT_FIELDS } from '../constants';
+import SearchBar from '../components/search_bar.vue';
+import ModelRow from '../components/model_row.vue';
+
+export default {
+ name: 'IndexMlModels',
+ components: {
+ Pagination,
+ ModelRow,
+ SearchBar,
+ MetadataItem,
+ TitleArea,
+ },
+ props: {
+ models: {
+ type: Array,
+ required: true,
+ },
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ modelCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ },
+ computed: {
+ hasModels() {
+ return !isEmpty(this.models);
+ },
+ },
+ i18n,
+ sortableFields: BASE_SORT_FIELDS,
+};
+</script>
+
+<template>
+ <div>
+ <title-area :title="$options.i18n.TITLE_LABEL">
+ <template #metadata-models-count>
+ <metadata-item icon="machine-learning" :text="$options.i18n.modelsCountLabel(modelCount)" />
+ </template>
+ </title-area>
+
+ <template v-if="hasModels">
+ <search-bar :sortable-fields="$options.sortableFields" />
+ <model-row v-for="model in models" :key="model.name" :model="model" />
+ <pagination v-bind="pageInfo" />
+ </template>
+
+ <p v-else class="gl-text-secondary">{{ $options.i18n.NO_MODELS_LABEL }}</p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue b/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
index d4f17c840d7..e8ec8f157ef 100644
--- a/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
+++ b/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
@@ -1,16 +1,71 @@
<script>
+import { GlTab, GlTabs, GlBadge } from '@gitlab/ui';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import * as i18n from '../translations';
+
export default {
name: 'ShowMlModelApp',
- components: {},
+ components: {
+ TitleArea,
+ GlTabs,
+ GlTab,
+ GlBadge,
+ MetadataItem,
+ },
props: {
model: {
type: Object,
required: true,
},
},
+ computed: {
+ versionCount() {
+ return this.model.versionCount || 0;
+ },
+ candidateCount() {
+ return this.model.candidateCount || 0;
+ },
+ },
+ i18n,
};
</script>
<template>
- <div>{{ model.name }}</div>
+ <div>
+ <title-area :title="model.name">
+ <template #metadata-versions-count>
+ <metadata-item
+ icon="machine-learning"
+ :text="$options.i18n.versionsCountLabel(model.versionCount)"
+ />
+ </template>
+
+ <template #sub-header>
+ {{ model.description }}
+ </template>
+ </title-area>
+
+ <gl-tabs class="gl-mt-4">
+ <gl-tab :title="$options.i18n.MODEL_DETAILS_TAB_LABEL">
+ <h3 class="gl-font-lg">{{ $options.i18n.LATEST_VERSION_LABEL }}</h3>
+ <template v-if="model.latestVersion">
+ {{ model.latestVersion.version }}
+ </template>
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_VERSIONS_LABEL }}</div>
+ </gl-tab>
+ <gl-tab>
+ <template #title>
+ {{ $options.i18n.MODEL_OTHER_VERSIONS_TAB_LABEL }}
+ <gl-badge size="sm" class="gl-tab-counter-badge">{{ versionCount }}</gl-badge>
+ </template>
+ </gl-tab>
+ <gl-tab>
+ <template #title>
+ {{ $options.i18n.MODEL_CANDIDATES_TAB_LABEL }}
+ <gl-badge size="sm" class="gl-tab-counter-badge">{{ candidateCount }}</gl-badge>
+ </template>
+ </gl-tab>
+ </gl-tabs>
+ </div>
</template>
diff --git a/app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue b/app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue
new file mode 100644
index 00000000000..a9440aff1ce
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue
@@ -0,0 +1,16 @@
+<script>
+export default {
+ name: 'ShowMlModelVersionApp',
+ components: {},
+ props: {
+ modelVersion: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>{{ modelVersion.model.name }} - {{ modelVersion.version }}</div>
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue b/app/assets/javascripts/ml/model_registry/components/model_row.vue
index 4f91f0939a8..ffae7e83099 100644
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue
+++ b/app/assets/javascripts/ml/model_registry/components/model_row.vue
@@ -1,6 +1,6 @@
<script>
import { GlLink } from '@gitlab/ui';
-import { modelVersionCountMessage } from '../translations';
+import { s__, n__ } from '~/locale';
export default {
name: 'MlModelRow',
@@ -17,8 +17,16 @@ export default {
hasVersions() {
return this.model.version != null;
},
+ modelVersionCountMessage() {
+ if (!this.model.versionCount) return s__('MlModelRegistry|No registered versions');
+
+ return n__(
+ 'MlModelRegistry|· No other versions',
+ 'MlModelRegistry|· %d versions',
+ this.model.versionCount,
+ );
+ },
},
- modelVersionCountMessage,
};
</script>
@@ -29,7 +37,9 @@ export default {
</gl-link>
<div class="gl-text-secondary">
- {{ $options.modelVersionCountMessage(model.version, model.versionCount) }}
+ <gl-link v-if="hasVersions" :href="model.versionPath">{{ model.version }}</gl-link>
+
+ {{ modelVersionCountMessage }}
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ml/model_registry/components/search_bar.vue b/app/assets/javascripts/ml/model_registry/components/search_bar.vue
new file mode 100644
index 00000000000..2bcdabc403f
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/components/search_bar.vue
@@ -0,0 +1,71 @@
+<script>
+import { queryToObject, setUrlParams, visitUrl } from '~/lib/utils/url_utility';
+import { LIST_KEY_CREATED_AT } from '~/ml/experiment_tracking/routes/experiments/show/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+
+export default {
+ name: 'SearchBar',
+ components: {
+ RegistrySearch,
+ },
+ props: {
+ sortableFields: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ const query = queryToObject(window.location.search);
+
+ const filter = query.name ? [{ value: { data: query.name }, type: FILTERED_SEARCH_TERM }] : [];
+
+ const orderBy = query.orderBy || LIST_KEY_CREATED_AT;
+
+ return {
+ filters: filter,
+ sorting: {
+ orderBy,
+ sort: (query.sort || 'desc').toLowerCase(),
+ },
+ };
+ },
+ methods: {
+ submitFilters() {
+ return visitUrl(setUrlParams(this.parsedQuery()));
+ },
+ parsedQuery() {
+ const name = this.filters
+ .map((f) => f.value.data)
+ .join(' ')
+ .trim();
+
+ const filterByQuery = name === '' ? {} : { name };
+
+ return { ...filterByQuery, ...this.sorting };
+ },
+ updateFilters(newValue) {
+ this.filters = newValue;
+ },
+ updateSorting(newValue) {
+ this.sorting = { ...this.sorting, ...newValue };
+ },
+ updateSortingAndEmitUpdate(newValue) {
+ this.updateSorting(newValue);
+ this.submitFilters();
+ },
+ },
+};
+</script>
+
+<template>
+ <registry-search
+ :filters="filters"
+ :sorting="sorting"
+ :sortable-fields="sortableFields"
+ @sorting:changed="updateSortingAndEmitUpdate"
+ @filter:changed="updateFilters"
+ @filter:submit="submitFilters"
+ @filter:clear="filters = []"
+ />
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/constants.js b/app/assets/javascripts/ml/model_registry/constants.js
new file mode 100644
index 00000000000..10c21ec4f12
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/constants.js
@@ -0,0 +1,13 @@
+import { s__ } from '~/locale';
+
+export const LIST_KEY_CREATED_AT = 'created_at';
+export const BASE_SORT_FIELDS = Object.freeze([
+ {
+ orderBy: 'name',
+ label: s__('MlExperimentTracking|Name'),
+ },
+ {
+ orderBy: LIST_KEY_CREATED_AT,
+ label: s__('MlExperimentTracking|Created at'),
+ },
+]);
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue b/app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue
deleted file mode 100644
index 3770b4ec3ac..00000000000
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<script>
-import { isEmpty } from 'lodash';
-import * as translations from '~/ml/model_registry/routes/models/index/translations';
-import Pagination from '~/vue_shared/components/incubation/pagination.vue';
-import ModelRow from './model_row.vue';
-
-export default {
- name: 'MlModelRegistryApp',
- components: {
- Pagination,
- ModelRow,
- },
- props: {
- models: {
- type: Array,
- required: true,
- },
- pageInfo: {
- type: Object,
- required: true,
- },
- },
- computed: {
- hasModels() {
- return !isEmpty(this.models);
- },
- },
- i18n: translations,
-};
-</script>
-
-<template>
- <div>
- <div class="detail-page-header gl-flex-wrap">
- <div class="detail-page-header-body">
- <div class="page-title gl-flex-grow-1 gl-display-flex gl-align-items-center">
- <h2 class="gl-font-size-h-display gl-my-0">{{ $options.i18n.TITLE_LABEL }}</h2>
- </div>
- </div>
- </div>
-
- <template v-if="hasModels">
- <model-row v-for="model in models" :key="model.name" :model="model" />
- <pagination v-bind="pageInfo" />
- </template>
-
- <p v-else class="gl-text-secondary">{{ $options.i18n.NO_MODELS_LABEL }}</p>
- </div>
-</template>
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/index.js b/app/assets/javascripts/ml/model_registry/routes/models/index/index.js
deleted file mode 100644
index d303d9716af..00000000000
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import MlModelsIndex from './components/ml_models_index.vue';
-
-export default MlModelsIndex;
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js b/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js
deleted file mode 100644
index 9210d816373..00000000000
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { s__, n__, sprintf } from '~/locale';
-
-export const TITLE_LABEL = s__('MlModelRegistry|Model registry');
-export const NO_MODELS_LABEL = s__('MlModelRegistry|No models registered in this project');
-
-export const modelVersionCountMessage = (version, versionCount) => {
- if (!versionCount) return s__('MlModelRegistry|No registered versions');
-
- const message = n__(
- 'MlModelRegistry|%{version} · No other versions',
- 'MlModelRegistry|%{version} · %{versionCount} versions',
- versionCount,
- );
-
- return sprintf(message, { version, versionCount });
-};
diff --git a/app/assets/javascripts/ml/model_registry/translations.js b/app/assets/javascripts/ml/model_registry/translations.js
new file mode 100644
index 00000000000..89b3f45ed94
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/translations.js
@@ -0,0 +1,16 @@
+import { s__, n__ } from '~/locale';
+
+export const MODEL_DETAILS_TAB_LABEL = s__('MlModelRegistry|Details');
+export const MODEL_OTHER_VERSIONS_TAB_LABEL = s__('MlModelRegistry|Versions');
+export const MODEL_CANDIDATES_TAB_LABEL = s__('MlModelRegistry|Version candidates');
+export const LATEST_VERSION_LABEL = s__('MlModelRegistry|Latest version');
+export const NO_VERSIONS_LABEL = s__('MlModelRegistry|This model has no versions');
+
+export const versionsCountLabel = (versionCount) =>
+ n__('MlModelRegistry|%d version', 'MlModelRegistry|%d versions', versionCount);
+
+export const TITLE_LABEL = s__('MlModelRegistry|Model registry');
+export const NO_MODELS_LABEL = s__('MlModelRegistry|No models registered in this project');
+
+export const modelsCountLabel = (modelCount) =>
+ n__('MlModelRegistry|%d model', 'MlModelRegistry|%d models', modelCount);
diff --git a/app/assets/javascripts/mr_notes/init.js b/app/assets/javascripts/mr_notes/init.js
index 28f294589ae..5594e71641b 100644
--- a/app/assets/javascripts/mr_notes/init.js
+++ b/app/assets/javascripts/mr_notes/init.js
@@ -40,6 +40,7 @@ function setupMrNotesState(store, notesDataset, diffsDataset) {
mrReviews: getReviewsForMergeRequest(mrPath),
diffViewType:
getParameterValues('view')[0] || getCookie(DIFF_VIEW_COOKIE_NAME) || INLINE_DIFF_VIEW_TYPE,
+ perPage: Number(diffsDataset.perPage),
});
}
diff --git a/app/assets/javascripts/notes/components/comment_field_layout.vue b/app/assets/javascripts/notes/components/comment_field_layout.vue
index cefcc1b0c98..7673bd61631 100644
--- a/app/assets/javascripts/notes/components/comment_field_layout.vue
+++ b/app/assets/javascripts/notes/components/comment_field_layout.vue
@@ -1,5 +1,4 @@
<script>
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
import EmailParticipantsWarning from './email_participants_warning.vue';
import AttachmentsWarning from './attachments_warning.vue';
@@ -12,7 +11,6 @@ export default {
EmailParticipantsWarning,
NoteableWarning,
},
- mixins: [glFeatureFlagsMixin()],
props: {
noteableData: {
type: Object,
@@ -56,11 +54,7 @@ export default {
return this.emailParticipants.length && !this.isInternalNote;
},
showAttachmentWarning() {
- return (
- this.glFeatures.serviceDeskNewNoteEmailNativeAttachments &&
- this.showEmailParticipantsWarning &&
- this.containsLink
- );
+ return this.showEmailParticipantsWarning && this.containsLink;
},
},
};
diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
index bcf9b4cf893..a999b633f64 100644
--- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue
+++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
@@ -24,7 +24,9 @@ export default {
},
lockedIssueWarning() {
return sprintf(
- __('This %{issuableDisplayName} is locked. Only project members can comment.'),
+ __(
+ 'The discussion in this %{issuableDisplayName} is locked. Only project members can comment.',
+ ),
{ issuableDisplayName: this.issuableDisplayName },
);
},
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
index b1aee19d5b2..cc4f360a694 100644
--- a/app/assets/javascripts/notes/components/discussion_resolve_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
@@ -21,7 +21,11 @@ export default {
</script>
<template>
- <gl-button :loading="isResolving" class="gl-xs-w-full ml-sm-2" @click="$emit('onClick')">
+ <gl-button
+ :loading="isResolving"
+ class="gl-w-full gl-sm-w-auto ml-sm-2"
+ @click="$emit('onClick')"
+ >
{{ buttonTitle }}
</gl-button>
</template>
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
index 4ccba011014..34cbba8ce43 100644
--- a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
@@ -29,7 +29,7 @@ export default {
:href="url"
:title="$options.i18n.buttonLabel"
:aria-label="$options.i18n.buttonLabel"
- class="new-issue-for-discussion discussion-create-issue-btn gl-xs-w-full"
+ class="new-issue-for-discussion discussion-create-issue-btn gl-w-full gl-sm-w-auto"
icon="issue-new"
/>
</div>
diff --git a/app/assets/javascripts/notes/components/note_signed_out_widget.vue b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
index 30d3bfcb989..738af4f6064 100644
--- a/app/assets/javascripts/notes/components/note_signed_out_widget.vue
+++ b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
@@ -35,5 +35,5 @@ export default {
</script>
<template>
- <div v-safe-html="signedOutText" class="disabled-comment text-center"></div>
+ <div v-safe-html="signedOutText" class="disabled-comment gl-text-center gl-text-secondary"></div>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index e0b1f7a8c6a..493beb8cea9 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -290,9 +290,6 @@ export default {
parent: this.$el,
});
},
- deleteNoteHandler(note) {
- this.$emit('noteDeleted', this.discussion, note);
- },
onStartReplying(discussionId) {
if (this.discussion.id === discussionId) {
this.showReplyForm();
@@ -329,7 +326,6 @@ export default {
:is-overview-tab="isOverviewTab"
:should-scroll-to-note="shouldScrollToNote"
@startReplying="showReplyForm"
- @deleteNote="deleteNoteHandler"
>
<template #avatar-badge>
<slot name="avatar-badge"></slot>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 809b1716b91..c817655b649 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -105,6 +105,11 @@ export default {
required: false,
default: true,
},
+ discussion: {
+ type: Object,
+ required: false,
+ default: null,
+ },
},
data() {
return {
diff --git a/app/assets/javascripts/notes/components/notes_activity_header.vue b/app/assets/javascripts/notes/components/notes_activity_header.vue
index ce642733396..b4eeea8db02 100644
--- a/app/assets/javascripts/notes/components/notes_activity_header.vue
+++ b/app/assets/javascripts/notes/components/notes_activity_header.vue
@@ -40,7 +40,7 @@ export default {
showAiActions() {
return (
this.resourceGlobalId &&
- this.glFeatures.openaiExperimentation &&
+ (this.glFeatures.openaiExperimentation || this.glFeatures.aiGlobalSwitch) &&
this.glFeatures.summarizeNotes
);
},
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 966f4184780..a995b9fa214 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -318,11 +318,6 @@ export default {
const note = noteData;
const selectedDiscussion = state.discussions.find((disc) => disc.id === note.id);
note.expanded = true; // override expand flag to prevent collapse
- if (note.diff_file) {
- Object.assign(note, {
- file_hash: note.diff_file.file_hash,
- });
- }
Object.assign(selectedDiscussion, { ...note });
},
diff --git a/app/assets/javascripts/observability/client.js b/app/assets/javascripts/observability/client.js
index 2e976cd6230..32ff7fff128 100644
--- a/app/assets/javascripts/observability/client.js
+++ b/app/assets/javascripts/observability/client.js
@@ -1,12 +1,15 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
+import { logError } from '~/lib/logger';
+import { DEFAULT_SORTING_OPTION, SORTING_OPTIONS } from './constants';
function reportErrorAndThrow(e) {
+ logError(e);
Sentry.captureException(e);
throw e;
}
// Provisioning API spec: https://gitlab.com/gitlab-org/opstrace/opstrace/-/blob/main/provisioning-api/pkg/provisioningapi/routes.go#L59
-async function enableTraces(provisioningUrl) {
+async function enableObservability(provisioningUrl) {
try {
// Note: axios.put(url, undefined, {withCredentials: true}) does not send cookies properly, so need to use the API below for the correct behaviour
return await axios(provisioningUrl, {
@@ -19,7 +22,7 @@ async function enableTraces(provisioningUrl) {
}
// Provisioning API spec: https://gitlab.com/gitlab-org/opstrace/opstrace/-/blob/main/provisioning-api/pkg/provisioningapi/routes.go#L37
-async function isTracingEnabled(provisioningUrl) {
+async function isObservabilityEnabled(provisioningUrl) {
try {
const { data } = await axios.get(provisioningUrl, { withCredentials: true });
if (data && data.status) {
@@ -42,18 +45,11 @@ async function fetchTrace(tracingUrl, traceId) {
throw new Error('traceId is required.');
}
- const { data } = await axios.get(tracingUrl, {
+ const { data } = await axios.get(`${tracingUrl}/${traceId}`, {
withCredentials: true,
- params: {
- trace_id: traceId,
- },
});
- if (!Array.isArray(data.traces) || data.traces.length === 0) {
- throw new Error('traces are missing/invalid in the response'); // eslint-disable-line @gitlab/require-i18n-strings
- }
-
- return data.traces[0];
+ return data;
} catch (e) {
return reportErrorAndThrow(e);
}
@@ -65,9 +61,10 @@ async function fetchTrace(tracingUrl, traceId) {
const SUPPORTED_FILTERS = {
durationMs: ['>', '<'],
operation: ['=', '!='],
- serviceName: ['=', '!='],
+ service: ['=', '!='],
period: ['='],
traceId: ['=', '!='],
+ attribute: ['='],
// free-text 'search' temporarily ignored https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2309
};
@@ -77,9 +74,10 @@ const SUPPORTED_FILTERS = {
const FILTER_TO_QUERY_PARAM = {
durationMs: 'duration_nano',
operation: 'operation',
- serviceName: 'service_name',
+ service: 'service_name',
period: 'period',
traceId: 'trace_id',
+ attribute: 'attribute',
};
const FILTER_OPERATORS_PREFIX = {
@@ -112,13 +110,32 @@ function getFilterParamName(filterName, operator) {
}
/**
+ * Process `filterValue` and append the proper query params to the `searchParams` arg
+ *
+ * It mutates `searchParams`
+ *
+ * @param {String} filterValue The filter value, in the format `attribute_name=attribute_value`
+ * @param {String} filterOperator The filter operator
+ * @param {URLSearchParams} searchParams The URLSearchParams object where to append the proper query params
+ */
+function handleAttributeFilter(filterValue, filterOperator, searchParams) {
+ const [attrName, attrValue] = filterValue.split('=');
+ if (attrName && attrValue) {
+ if (filterOperator === '=') {
+ searchParams.append('attr_name', attrName);
+ searchParams.append('attr_value', attrValue);
+ }
+ }
+}
+
+/**
* Builds URLSearchParams from a filter object of type { [filterName]: undefined | null | Array<{operator: String, value: any} }
* e.g:
*
* filterObj = {
* durationMs: [{operator: '>', value: '100'}, {operator: '<', value: '1000' }],
* operation: [{operator: '=', value: 'someOp' }],
- * serviceName: [{operator: '!=', value: 'foo' }]
+ * service: [{operator: '!=', value: 'foo' }]
* }
*
* It handles converting the filter to the proper supported query params
@@ -131,20 +148,22 @@ function filterObjToQueryParams(filterObj) {
Object.keys(SUPPORTED_FILTERS).forEach((filterName) => {
const filterValues = filterObj[filterName] || [];
- const supportedFilters = filterValues.filter((f) =>
+ const validFilters = filterValues.filter((f) =>
SUPPORTED_FILTERS[filterName].includes(f.operator),
);
- supportedFilters.forEach(({ operator, value: rawValue }) => {
- const paramName = getFilterParamName(filterName, operator);
-
- let value = rawValue;
- if (filterName === 'durationMs') {
- // converting durationMs to duration_nano
- value *= 1000000;
- }
-
- if (paramName && value) {
- filterParams.append(paramName, value);
+ validFilters.forEach(({ operator, value: rawValue }) => {
+ if (filterName === 'attribute') {
+ handleAttributeFilter(rawValue, operator, filterParams);
+ } else {
+ const paramName = getFilterParamName(filterName, operator);
+ let value = rawValue;
+ if (filterName === 'durationMs') {
+ // converting durationMs to duration_nano
+ value *= 1000000;
+ }
+ if (paramName && value) {
+ filterParams.append(paramName, value);
+ }
}
});
});
@@ -161,12 +180,12 @@ function filterObjToQueryParams(filterObj) {
* {
* durationMs: [ {operator: '>', value: '100'}, {operator: '<', value: '1000'}],
* operation: [ {operator: '=', value: 'someOp}],
- * serviceName: [ {operator: '!=', value: 'foo}]
+ * service: [ {operator: '!=', value: 'foo}]
* }
*
* @returns Array<Trace> : A list of traces
*/
-async function fetchTraces(tracingUrl, { filters = {}, pageToken, pageSize } = {}) {
+async function fetchTraces(tracingUrl, { filters = {}, pageToken, pageSize, sortBy } = {}) {
const params = filterObjToQueryParams(filters);
if (pageToken) {
params.append('page_token', pageToken);
@@ -174,6 +193,10 @@ async function fetchTraces(tracingUrl, { filters = {}, pageToken, pageSize } = {
if (pageSize) {
params.append('page_size', pageSize);
}
+ const sortOrder = Object.values(SORTING_OPTIONS).includes(sortBy)
+ ? sortBy
+ : DEFAULT_SORTING_OPTION;
+ params.append('sort', sortOrder);
try {
const { data } = await axios.get(tracingUrl, {
@@ -228,18 +251,54 @@ async function fetchOperations(operationsUrl, serviceName) {
}
}
-export function buildClient({ provisioningUrl, tracingUrl, servicesUrl, operationsUrl } = {}) {
- if (!provisioningUrl || !tracingUrl || !servicesUrl || !operationsUrl) {
- throw new Error(
- 'missing required params. provisioningUrl, tracingUrl, servicesUrl, operationsUrl are required',
- );
+async function fetchMetrics(metricsUrl) {
+ try {
+ const { data } = await axios.get(metricsUrl, {
+ withCredentials: true,
+ });
+ if (!Array.isArray(data.metrics)) {
+ throw new Error('metrics are missing/invalid in the response'); // eslint-disable-line @gitlab/require-i18n-strings
+ }
+ return data;
+ } catch (e) {
+ return reportErrorAndThrow(e);
+ }
+}
+
+export function buildClient(config) {
+ if (!config) {
+ throw new Error('No options object provided'); // eslint-disable-line @gitlab/require-i18n-strings
+ }
+
+ const { provisioningUrl, tracingUrl, servicesUrl, operationsUrl, metricsUrl } = config;
+
+ if (typeof provisioningUrl !== 'string') {
+ throw new Error('provisioningUrl param must be a string');
}
+
+ if (typeof tracingUrl !== 'string') {
+ throw new Error('tracingUrl param must be a string');
+ }
+
+ if (typeof servicesUrl !== 'string') {
+ throw new Error('servicesUrl param must be a string');
+ }
+
+ if (typeof operationsUrl !== 'string') {
+ throw new Error('operationsUrl param must be a string');
+ }
+
+ if (typeof metricsUrl !== 'string') {
+ throw new Error('metricsUrl param must be a string');
+ }
+
return {
- enableTraces: () => enableTraces(provisioningUrl),
- isTracingEnabled: () => isTracingEnabled(provisioningUrl),
- fetchTraces: (filters) => fetchTraces(tracingUrl, filters),
+ enableObservability: () => enableObservability(provisioningUrl),
+ isObservabilityEnabled: () => isObservabilityEnabled(provisioningUrl),
+ fetchTraces: (options) => fetchTraces(tracingUrl, options),
fetchTrace: (traceId) => fetchTrace(tracingUrl, traceId),
fetchServices: () => fetchServices(servicesUrl),
fetchOperations: (serviceName) => fetchOperations(operationsUrl, serviceName),
+ fetchMetrics: () => fetchMetrics(metricsUrl),
};
}
diff --git a/app/assets/javascripts/observability/components/loader/constants.js b/app/assets/javascripts/observability/components/loader/constants.js
new file mode 100644
index 00000000000..5c2d8ad0d1b
--- /dev/null
+++ b/app/assets/javascripts/observability/components/loader/constants.js
@@ -0,0 +1,20 @@
+import { __ } from '~/locale';
+
+export const CONTENT_STATE = Object.freeze({
+ ERROR: 'error',
+ LOADED: 'loaded',
+});
+
+export const LOADER_STATE = Object.freeze({
+ ERROR: 'error',
+ VISIBLE: 'visible',
+ HIDDEN: 'hidden',
+});
+
+export const DEFAULT_TIMERS = Object.freeze({
+ TIMEOUT_MS: 20000,
+ CONTENT_WAIT_MS: 500,
+});
+
+export const TIMEOUT_ERROR_LABEL = __('Unable to load the page');
+export const TIMEOUT_ERROR_MESSAGE = __('Reload the page to try again.');
diff --git a/app/assets/javascripts/observability/components/skeleton/index.vue b/app/assets/javascripts/observability/components/loader/index.vue
index c3d0a7c90b1..6b92dc428d2 100644
--- a/app/assets/javascripts/observability/components/skeleton/index.vue
+++ b/app/assets/javascripts/observability/components/loader/index.vue
@@ -1,31 +1,30 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
-import { GlSkeletonLoader, GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import {
- SKELETON_STATE,
+ LOADER_STATE,
+ CONTENT_STATE,
DEFAULT_TIMERS,
TIMEOUT_ERROR_LABEL,
TIMEOUT_ERROR_MESSAGE,
- SKELETON_SPINNER_VARIANT,
-} from '../../constants';
+} from './constants';
export default {
components: {
- GlSkeletonLoader,
GlAlert,
GlLoadingIcon,
},
- SKELETON_STATE,
+ LOADER_STATE,
i18n: {
TIMEOUT_ERROR_LABEL,
TIMEOUT_ERROR_MESSAGE,
},
props: {
- variant: {
+ contentState: {
type: String,
required: false,
- default: '',
+ default: null,
},
},
data() {
@@ -35,18 +34,25 @@ export default {
errorTimeout: null,
};
},
+
computed: {
- skeletonVisible() {
- return this.state === SKELETON_STATE.VISIBLE;
+ loaderVisible() {
+ return this.state === LOADER_STATE.VISIBLE;
},
- skeletonHidden() {
- return this.state === SKELETON_STATE.HIDDEN;
+ loaderHidden() {
+ return this.state === LOADER_STATE.HIDDEN;
},
errorVisible() {
- return this.state === SKELETON_STATE.ERROR;
+ return this.state === LOADER_STATE.ERROR;
},
- spinnerVariant() {
- return this.variant === SKELETON_SPINNER_VARIANT;
+ },
+ watch: {
+ contentState(newValue) {
+ if (newValue === CONTENT_STATE.LOADED) {
+ this.onContentLoaded();
+ } else if (newValue === CONTENT_STATE.ERROR) {
+ this.onError();
+ }
},
},
mounted() {
@@ -62,7 +68,7 @@ export default {
clearTimeout(this.errorTimeout);
clearTimeout(this.loadingTimeout);
- this.hideSkeleton();
+ this.hideLoader();
},
onError() {
clearTimeout(this.errorTimeout);
@@ -74,10 +80,10 @@ export default {
this.loadingTimeout = setTimeout(() => {
/**
* If content is not loaded within CONTENT_WAIT_MS,
- * show the skeleton
+ * show the loader
*/
- if (this.state !== SKELETON_STATE.HIDDEN) {
- this.showSkeleton();
+ if (this.state !== LOADER_STATE.HIDDEN) {
+ this.showLoader();
}
}, DEFAULT_TIMERS.CONTENT_WAIT_MS);
},
@@ -87,19 +93,19 @@ export default {
* If content is not loaded within TIMEOUT_MS,
* show the error dialog
*/
- if (this.state !== SKELETON_STATE.HIDDEN) {
+ if (this.state !== LOADER_STATE.HIDDEN) {
this.showError();
}
}, DEFAULT_TIMERS.TIMEOUT_MS);
},
- hideSkeleton() {
- this.state = SKELETON_STATE.HIDDEN;
+ hideLoader() {
+ this.state = LOADER_STATE.HIDDEN;
},
- showSkeleton() {
- this.state = SKELETON_STATE.VISIBLE;
+ showLoader() {
+ this.state = LOADER_STATE.VISIBLE;
},
showError() {
- this.state = SKELETON_STATE.ERROR;
+ this.state = LOADER_STATE.ERROR;
},
},
};
@@ -107,19 +113,12 @@ export default {
<template>
<div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch">
<transition name="fade">
- <div v-if="skeletonVisible" class="gl-px-5 gl-my-5">
- <gl-loading-icon v-if="spinnerVariant" size="lg" />
- <gl-skeleton-loader v-else>
- <rect y="2" width="10" height="8" />
- <rect y="2" x="15" width="15" height="8" />
- <rect y="2" x="35" width="15" height="8" />
- <rect y="15" width="400" height="30" />
- </gl-skeleton-loader>
+ <div v-if="loaderVisible" class="gl-px-5 gl-my-5">
+ <gl-loading-icon size="lg" />
</div>
- <!-- The double condition is only here temporarily for back-compatibility reasons. Will be removed in next iteration https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2275 -->
<div
- v-else-if="spinnerVariant && skeletonHidden"
+ v-else-if="loaderHidden"
data-testid="content-wrapper"
class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch"
>
@@ -136,16 +135,5 @@ export default {
>
{{ $options.i18n.TIMEOUT_ERROR_MESSAGE }}
</gl-alert>
-
- <!-- This is only kept temporarily for back-compatibility reasons. Will be removed in next iteration https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2275 -->
- <transition v-if="!spinnerVariant">
- <div
- v-show="skeletonHidden"
- data-testid="content-wrapper"
- class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch"
- >
- <slot></slot>
- </div>
- </transition>
</div>
</template>
diff --git a/app/assets/javascripts/observability/components/observability_container.vue b/app/assets/javascripts/observability/components/observability_container.vue
index 1518c132560..b89c2624f81 100644
--- a/app/assets/javascripts/observability/components/observability_container.vue
+++ b/app/assets/javascripts/observability/components/observability_container.vue
@@ -1,32 +1,17 @@
<script>
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import { logError } from '~/lib/logger';
import { buildClient } from '../client';
-import { SKELETON_SPINNER_VARIANT } from '../constants';
-import ObservabilitySkeleton from './skeleton/index.vue';
+import ObservabilityLoader from './loader/index.vue';
+import { CONTENT_STATE } from './loader/constants';
export default {
- SKELETON_SPINNER_VARIANT,
components: {
- ObservabilitySkeleton,
+ ObservabilityLoader,
},
props: {
- oauthUrl: {
- type: String,
- required: true,
- },
- provisioningUrl: {
- type: String,
- required: true,
- },
- tracingUrl: {
- type: String,
- required: true,
- },
- servicesUrl: {
- type: String,
- required: true,
- },
- operationsUrl: {
- type: String,
+ apiConfig: {
+ type: Object,
required: true,
},
},
@@ -34,6 +19,7 @@ export default {
return {
observabilityClient: null,
authCompleted: false,
+ loaderContentState: null,
};
},
mounted() {
@@ -53,7 +39,7 @@ export default {
},
methods: {
messageHandler(e) {
- const isExpectedOrigin = e.origin === new URL(this.oauthUrl).origin;
+ const isExpectedOrigin = e.origin === new URL(this.apiConfig.oauthUrl).origin;
if (!isExpectedOrigin) return;
const { data } = e;
@@ -63,17 +49,14 @@ export default {
const { status, message, statusCode } = data;
if (status === 'success') {
- this.observabilityClient = buildClient({
- provisioningUrl: this.provisioningUrl,
- tracingUrl: this.tracingUrl,
- servicesUrl: this.servicesUrl,
- operationsUrl: this.operationsUrl,
- });
- this.$refs.observabilitySkeleton?.onContentLoaded();
+ this.observabilityClient = buildClient(this.apiConfig);
+ this.$emit('observability-client-ready', this.observabilityClient);
+ this.loaderContentState = CONTENT_STATE.LOADED;
} else if (status === 'error') {
- // eslint-disable-next-line @gitlab/require-i18n-strings,no-console
- console.error('GOB auth failed with error:', message, statusCode);
- this.$refs.observabilitySkeleton?.onError();
+ const error = new Error(`GOB auth failed with error: ${message} - status: ${statusCode}`);
+ Sentry.captureException(error);
+ logError(error);
+ this.loaderContentState = CONTENT_STATE.ERROR;
}
this.authCompleted = true;
}
@@ -88,15 +71,12 @@ export default {
v-if="!authCompleted"
sandbox="allow-same-origin allow-forms allow-scripts"
hidden
- :src="oauthUrl"
+ :src="apiConfig.oauthUrl"
data-testid="observability-oauth-iframe"
></iframe>
- <observability-skeleton
- ref="observabilitySkeleton"
- :variant="$options.SKELETON_SPINNER_VARIANT"
- >
+ <observability-loader :content-state="loaderContentState">
<slot v-if="observabilityClient" :observability-client="observabilityClient"></slot>
- </observability-skeleton>
+ </observability-loader>
</div>
</template>
diff --git a/app/assets/javascripts/observability/components/observability_empty_state.vue b/app/assets/javascripts/observability/components/observability_empty_state.vue
new file mode 100644
index 00000000000..d4d8b887934
--- /dev/null
+++ b/app/assets/javascripts/observability/components/observability_empty_state.vue
@@ -0,0 +1,36 @@
+<script>
+import EMPTY_TRACING_SVG from '@gitlab/svgs/dist/illustrations/monitoring/tracing.svg?url';
+import { GlEmptyState, GlButton } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ EMPTY_TRACING_SVG,
+ i18n: {
+ title: s__('Observability|Get started with GitLab Observability'),
+ description: s__('Observability|Monitor your applications with GitLab Observability.'),
+ enableButtonText: s__('Observability|Enable'),
+ },
+ components: {
+ GlEmptyState,
+ GlButton,
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="$options.i18n.title"
+ :svg-path="$options.EMPTY_TRACING_SVG"
+ :svg-height="null"
+ >
+ <template #description>
+ <span>{{ $options.i18n.description }}</span>
+ </template>
+
+ <template #actions>
+ <gl-button variant="confirm" class="gl-mx-2 gl-mb-3" @click="$emit('enable-observability')">
+ {{ $options.i18n.enableButtonText }}
+ </gl-button>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/observability/components/provisioned_observability_container.vue b/app/assets/javascripts/observability/components/provisioned_observability_container.vue
new file mode 100644
index 00000000000..95ffd54fd1d
--- /dev/null
+++ b/app/assets/javascripts/observability/components/provisioned_observability_container.vue
@@ -0,0 +1,95 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import ObservabilityContainer from '~/observability/components/observability_container.vue';
+import { s__ } from '~/locale';
+import { createAlert } from '~/alert';
+import ObservabilityEmptyState from './observability_empty_state.vue';
+
+export default {
+ components: {
+ ObservabilityContainer,
+ ObservabilityEmptyState,
+ GlLoadingIcon,
+ },
+ props: {
+ apiConfig: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ /**
+ * observabilityEnabled: boolean | null.
+ * null identifies a state where we don't know if observability is enabled or not (e.g. when fetching the status from the API fails)
+ */
+ observabilityEnabled: null,
+ observabilityClient: null,
+ };
+ },
+ computed: {
+ isObservabilityStatusKnown() {
+ return this.observabilityEnabled !== null;
+ },
+ isObservabilityDisabled() {
+ return this.observabilityEnabled === false;
+ },
+ isObservabilityEnabled() {
+ return this.observabilityEnabled;
+ },
+ },
+ methods: {
+ onObservabilityClientReady(client) {
+ this.observabilityClient = client;
+ this.checkEnabled();
+ },
+ async checkEnabled() {
+ this.loading = true;
+ try {
+ this.observabilityEnabled = await this.observabilityClient.isObservabilityEnabled();
+ } catch (e) {
+ createAlert({
+ message: s__('Observability|Error: Failed to load page. Try reloading the page.'),
+ });
+ } finally {
+ this.loading = false;
+ }
+ },
+ async onEnableObservability() {
+ this.loading = true;
+ try {
+ await this.observabilityClient.enableObservability();
+ this.observabilityEnabled = true;
+ } catch (e) {
+ createAlert({
+ message: s__(
+ 'Observability|Error: Failed to enable GitLab Observability. Please retry later.',
+ ),
+ });
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <observability-container
+ :api-config="apiConfig"
+ @observability-client-ready="onObservabilityClientReady"
+ >
+ <div v-if="loading" class="gl-py-5">
+ <gl-loading-icon size="lg" />
+ </div>
+
+ <template v-else-if="isObservabilityStatusKnown">
+ <observability-empty-state
+ v-if="isObservabilityDisabled"
+ @enable-observability="onEnableObservability"
+ />
+ <slot v-if="isObservabilityEnabled" :observability-client="observabilityClient"></slot>
+ </template>
+ </observability-container>
+</template>
diff --git a/app/assets/javascripts/observability/constants.js b/app/assets/javascripts/observability/constants.js
index 83eaea185e5..34c43a10fc0 100644
--- a/app/assets/javascripts/observability/constants.js
+++ b/app/assets/javascripts/observability/constants.js
@@ -1,17 +1,7 @@
-import { __ } from '~/locale';
-
-export const SKELETON_SPINNER_VARIANT = 'spinner';
-
-export const SKELETON_STATE = Object.freeze({
- ERROR: 'error',
- VISIBLE: 'visible',
- HIDDEN: 'hidden',
-});
-
-export const DEFAULT_TIMERS = Object.freeze({
- TIMEOUT_MS: 20000,
- CONTENT_WAIT_MS: 500,
-});
-
-export const TIMEOUT_ERROR_LABEL = __('Unable to load the page');
-export const TIMEOUT_ERROR_MESSAGE = __('Reload the page to try again.');
+export const SORTING_OPTIONS = {
+ TIMESTAMP_DESC: 'timestamp_desc',
+ TIMESTAMP_ASC: 'timestamp_asc',
+ DURATION_DESC: 'duration_desc',
+ DURATION_ASC: 'duration_asc',
+};
+export const DEFAULT_SORTING_OPTION = SORTING_OPTIONS.TIMESTAMP_DESC;
diff --git a/app/assets/javascripts/organizations/mock_data.js b/app/assets/javascripts/organizations/mock_data.js
index d281a0d8a1c..725b6ac1ad8 100644
--- a/app/assets/javascripts/organizations/mock_data.js
+++ b/app/assets/javascripts/organizations/mock_data.js
@@ -281,10 +281,31 @@ export const organizationGroups = {
],
};
-export const createOrganizationResponse = {
+export const organizationCreateResponse = {
+ data: {
+ organizationCreate: {
+ organization: {
+ id: 'gid://gitlab/Organizations::Organization/1',
+ webUrl: 'http://127.0.0.1:3000/-/organizations/default',
+ },
+ errors: [],
+ },
+ },
+};
+
+export const organizationCreateResponseWithErrors = {
+ data: {
+ organizationCreate: {
+ organization: null,
+ errors: ['Path is too short (minimum is 2 characters)'],
+ },
+ },
+};
+
+export const updateOrganizationResponse = {
organization: {
- name: 'Default',
- path: '/-/organizations/default',
+ id: 'gid://gitlab/Organizations/1',
+ name: 'Default updated',
},
errors: [],
};
diff --git a/app/assets/javascripts/organizations/new/components/app.vue b/app/assets/javascripts/organizations/new/components/app.vue
index 8f71fdfe68b..f7f7b79d52b 100644
--- a/app/assets/javascripts/organizations/new/components/app.vue
+++ b/app/assets/javascripts/organizations/new/components/app.vue
@@ -4,12 +4,13 @@ import { s__ } from '~/locale';
import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
import { createAlert } from '~/alert';
import { helpPagePath } from '~/helpers/help_page_helper';
-import createOrganizationMutation from '../graphql/mutations/create_organization.mutation.graphql';
+import FormErrorsAlert from '~/vue_shared/components/form/errors_alert.vue';
+import organizationCreateMutation from '../graphql/mutations/organization_create.mutation.graphql';
import NewEditForm from '../../shared/components/new_edit_form.vue';
export default {
name: 'OrganizationNewApp',
- components: { NewEditForm, GlSprintf, GlLink },
+ components: { NewEditForm, GlSprintf, GlLink, FormErrorsAlert },
i18n: {
pageTitle: s__('Organization|New organization'),
pageDescription: s__(
@@ -22,6 +23,7 @@ export default {
data() {
return {
loading: false,
+ errors: [],
};
},
computed: {
@@ -35,21 +37,22 @@ export default {
try {
const {
data: {
- createOrganization: { organization, errors },
+ organizationCreate: { organization, errors },
},
} = await this.$apollo.mutate({
- mutation: createOrganizationMutation,
+ mutation: organizationCreateMutation,
variables: {
- ...formValues,
+ input: { name: formValues.name, path: formValues.path },
},
});
if (errors.length) {
- // TODO: handle errors when using real API after https://gitlab.com/gitlab-org/gitlab/-/issues/417891 is complete.
+ this.errors = errors;
+
return;
}
- visitUrlWithAlerts(organization.path, [
+ visitUrlWithAlerts(organization.webUrl, [
{
id: 'organization-successfully-created',
title: this.$options.i18n.successAlertTitle,
@@ -69,6 +72,7 @@ export default {
<template>
<div class="gl-py-6">
+ <form-errors-alert v-model="errors" />
<h1 class="gl-mt-0 gl-font-size-h-display">{{ $options.i18n.pageTitle }}</h1>
<p>
<gl-sprintf :message="$options.i18n.pageDescription">
diff --git a/app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql b/app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql
deleted file mode 100644
index 766c7e96d14..00000000000
--- a/app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-mutation createOrganization($input: LocalCreateOrganizationInput!) {
- createOrganization(input: $input) @client {
- organization {
- name
- path
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/organizations/new/graphql/mutations/organization_create.mutation.graphql b/app/assets/javascripts/organizations/new/graphql/mutations/organization_create.mutation.graphql
new file mode 100644
index 00000000000..81fbfddd1e4
--- /dev/null
+++ b/app/assets/javascripts/organizations/new/graphql/mutations/organization_create.mutation.graphql
@@ -0,0 +1,9 @@
+mutation organizationCreate($input: OrganizationCreateInput!) {
+ organizationCreate(input: $input) {
+ organization {
+ id
+ webUrl
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/organizations/new/graphql/typedefs.graphql b/app/assets/javascripts/organizations/new/graphql/typedefs.graphql
deleted file mode 100644
index f708c4ad162..00000000000
--- a/app/assets/javascripts/organizations/new/graphql/typedefs.graphql
+++ /dev/null
@@ -1,5 +0,0 @@
-# TODO: Use real input type when https://gitlab.com/gitlab-org/gitlab/-/issues/417891 is complete.
-input LocalCreateOrganizationInput {
- name: String
- path: String
-}
diff --git a/app/assets/javascripts/organizations/new/index.js b/app/assets/javascripts/organizations/new/index.js
index a65603227f6..9c7e5344800 100644
--- a/app/assets/javascripts/organizations/new/index.js
+++ b/app/assets/javascripts/organizations/new/index.js
@@ -3,7 +3,6 @@ import VueApollo from 'vue-apollo';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
-import resolvers from '../shared/graphql/resolvers';
import App from './components/app.vue';
export const initOrganizationsNew = () => {
@@ -17,7 +16,7 @@ export const initOrganizationsNew = () => {
const { organizationsPath, rootUrl } = convertObjectPropsToCamelCase(JSON.parse(appData));
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(resolvers),
+ defaultClient: createDefaultClient(),
});
return new Vue({
diff --git a/app/assets/javascripts/organizations/profile/preferences/index.js b/app/assets/javascripts/organizations/profile/preferences/index.js
new file mode 100644
index 00000000000..0b0dd313cd8
--- /dev/null
+++ b/app/assets/javascripts/organizations/profile/preferences/index.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { s__ } from '~/locale';
+import OrganizationSelect from '~/vue_shared/components/entity_select/organization_select.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import resolvers from '../../shared/graphql/resolvers';
+
+export const initHomeOrganizationSetting = () => {
+ const el = document.getElementById('js-home-organization-setting');
+
+ if (!el) return false;
+
+ const {
+ dataset: { appData },
+ } = el;
+ const { initialSelection } = convertObjectPropsToCamelCase(JSON.parse(appData));
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
+
+ return new Vue({
+ el,
+ name: 'HomeOrganizationSetting',
+ apolloProvider,
+ render(createElement) {
+ return createElement(OrganizationSelect, {
+ props: {
+ block: true,
+ label: s__('Organization|Home organization'),
+ description: s__('Organization|Choose what organization you want to see by default.'),
+ inputName: 'home_organization',
+ inputId: 'home_organization',
+ initialSelection,
+ toggleClass: 'gl-form-input-xl',
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/organizations/settings/general/components/app.vue b/app/assets/javascripts/organizations/settings/general/components/app.vue
new file mode 100644
index 00000000000..134fcc17b54
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/components/app.vue
@@ -0,0 +1,14 @@
+<script>
+import OrganizationSettings from './organization_settings.vue';
+
+export default {
+ name: 'OrganizationSettingsGeneralApp',
+ components: { OrganizationSettings },
+};
+</script>
+
+<template>
+ <div>
+ <organization-settings />
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue b/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue
new file mode 100644
index 00000000000..14826825cd6
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue
@@ -0,0 +1,77 @@
+<script>
+import { s__, __ } from '~/locale';
+import { createAlert, VARIANT_INFO } from '~/alert';
+import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
+import { FORM_FIELD_NAME, FORM_FIELD_ID } from '~/organizations/shared/constants';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+import updateOrganizationMutation from '../graphql/mutations/update_organization.mutation.graphql';
+
+export default {
+ name: 'OrganizationSettings',
+ components: { NewEditForm, SettingsBlock },
+ inject: ['organization'],
+ i18n: {
+ submitButtonText: __('Save changes'),
+ settingsBlock: {
+ title: s__('Organization|Organization settings'),
+ description: s__('Organization|Update your organization name, description, and avatar.'),
+ },
+ errorMessage: s__(
+ 'Organization|An error occurred updating your organization. Please try again.',
+ ),
+ successMessage: s__('Organization|Organization was successfully updated.'),
+ },
+ fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID],
+ data() {
+ return {
+ loading: false,
+ };
+ },
+ methods: {
+ async onSubmit(formValues) {
+ this.loading = true;
+ try {
+ const {
+ data: {
+ updateOrganization: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: updateOrganizationMutation,
+ variables: {
+ id: this.organization.id,
+ name: formValues.name,
+ },
+ });
+
+ if (errors.length) {
+ // TODO: handle errors when using real API after https://gitlab.com/gitlab-org/gitlab/-/issues/419608 is complete.
+ return;
+ }
+
+ createAlert({ message: this.$options.i18n.successMessage, variant: VARIANT_INFO });
+ } catch (error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <settings-block default-expanded slide-animated>
+ <template #title>{{ $options.i18n.settingsBlock.title }}</template>
+ <template #description>{{ $options.i18n.settingsBlock.description }}</template>
+ <template #default>
+ <new-edit-form
+ :loading="loading"
+ :initial-form-values="organization"
+ :fields-to-render="$options.fieldsToRender"
+ :submit-button-text="$options.i18n.submitButtonText"
+ :show-cancel-button="false"
+ @submit="onSubmit"
+ />
+ </template>
+ </settings-block>
+</template>
diff --git a/app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql b/app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql
new file mode 100644
index 00000000000..b571a523260
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql
@@ -0,0 +1,9 @@
+mutation updateOrganization($input: LocalUpdateOrganizationInput!) {
+ updateOrganization(input: $input) @client {
+ organization {
+ id
+ name
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/organizations/settings/general/graphql/typedefs.graphql b/app/assets/javascripts/organizations/settings/general/graphql/typedefs.graphql
new file mode 100644
index 00000000000..eb81a7b0321
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/graphql/typedefs.graphql
@@ -0,0 +1,5 @@
+# TODO: Use real input type when https://gitlab.com/gitlab-org/gitlab/-/issues/419608 is complete.
+input LocalUpdateOrganizationInput {
+ id: ID!
+ name: String
+}
diff --git a/app/assets/javascripts/organizations/settings/general/index.js b/app/assets/javascripts/organizations/settings/general/index.js
new file mode 100644
index 00000000000..36303c32b94
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/index.js
@@ -0,0 +1,38 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import createDefaultClient from '~/lib/graphql';
+import resolvers from '../../shared/graphql/resolvers';
+import App from './components/app.vue';
+
+export const initOrganizationsSettingsGeneral = () => {
+ const el = document.getElementById('js-organizations-settings-general');
+
+ if (!el) return false;
+
+ const {
+ dataset: { appData },
+ } = el;
+ const { organization, organizationsPath, rootUrl } = convertObjectPropsToCamelCase(
+ JSON.parse(appData),
+ );
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
+
+ return new Vue({
+ el,
+ name: 'OrganizationSettingsGeneralRoot',
+ apolloProvider,
+ provide: {
+ organization,
+ organizationsPath,
+ rootUrl,
+ },
+ render(createElement) {
+ return createElement(App);
+ },
+ });
+};
diff --git a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
index db33f240966..8aaa680036f 100644
--- a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
+++ b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
@@ -12,6 +12,7 @@ import { formValidators } from '@gitlab/ui/dist/utils';
import { s__, __ } from '~/locale';
import { slugify } from '~/lib/utils/text_utility';
import { joinPaths } from '~/lib/utils/url_utility';
+import { FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_PATH } from '../constants';
export default {
name: 'NewEditForm',
@@ -25,43 +26,47 @@ export default {
GlTruncate,
},
i18n: {
- createOrganization: s__('Organization|Create organization'),
cancel: __('Cancel'),
pathPlaceholder: s__('Organization|my-organization'),
},
formId: 'new-organization-form',
- fields: {
- name: {
- label: s__('Organization|Organization name'),
- validators: [formValidators.required(s__('Organization|Organization name is required.'))],
- groupAttrs: {
- description: s__(
- 'Organization|Must start with a letter, digit, emoji, or underscore. Can also contain periods, dashes, spaces, and parentheses.',
- ),
- },
- inputAttrs: {
- class: 'gl-md-form-input-lg',
- placeholder: s__('Organization|My organization'),
- },
- },
- path: {
- label: s__('Organization|Organization URL'),
- validators: [formValidators.required(s__('Organization|Organization URL is required.'))],
- },
- },
inject: ['organizationsPath', 'rootUrl'],
props: {
loading: {
type: Boolean,
required: true,
},
+ initialFormValues: {
+ type: Object,
+ required: false,
+ default() {
+ return {
+ [FORM_FIELD_NAME]: '',
+ [FORM_FIELD_PATH]: '',
+ };
+ },
+ },
+ fieldsToRender: {
+ type: Array,
+ required: false,
+ default() {
+ return [FORM_FIELD_NAME, FORM_FIELD_PATH];
+ },
+ },
+ submitButtonText: {
+ type: String,
+ required: false,
+ default: s__('Organization|Create organization'),
+ },
+ showCancelButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
- formValues: {
- name: '',
- path: '',
- },
+ formValues: this.initialFormValues,
hasPathBeenManuallySet: false,
};
},
@@ -69,10 +74,63 @@ export default {
baseUrl() {
return joinPaths(this.rootUrl, this.organizationsPath, '/');
},
+ fields() {
+ const fields = {
+ [FORM_FIELD_NAME]: {
+ label: s__('Organization|Organization name'),
+ validators: [formValidators.required(s__('Organization|Organization name is required.'))],
+ groupAttrs: {
+ class: this.fieldsToRender.includes(FORM_FIELD_ID)
+ ? 'gl-flex-grow-1 gl-md-form-input-lg'
+ : 'gl-flex-grow-1',
+ description: s__(
+ 'Organization|Must start with a letter, digit, emoji, or underscore. Can also contain periods, dashes, spaces, and parentheses.',
+ ),
+ },
+ inputAttrs: {
+ class: !this.fieldsToRender.includes(FORM_FIELD_ID) ? 'gl-md-form-input-lg' : null,
+ placeholder: s__('Organization|My organization'),
+ },
+ },
+ [FORM_FIELD_ID]: {
+ label: s__('Organization|Organization ID'),
+ groupAttrs: {
+ class: 'gl-md-form-input-lg gl-flex-grow-1',
+ },
+ inputAttrs: {
+ disabled: true,
+ },
+ },
+ [FORM_FIELD_PATH]: {
+ label: s__('Organization|Organization URL'),
+ validators: [
+ formValidators.required(s__('Organization|Organization URL is required.')),
+ formValidators.factory(
+ s__('Organization|Organization URL must be a minimum of two characters.'),
+ (val) => val.length >= 2,
+ ),
+ ],
+ groupAttrs: {
+ class: 'gl-w-full',
+ },
+ },
+ };
+
+ return Object.entries(fields).reduce((accumulator, [fieldKey, fieldDefinition]) => {
+ if (!this.fieldsToRender.includes(fieldKey)) {
+ return accumulator;
+ }
+
+ return {
+ ...accumulator,
+ [fieldKey]: fieldDefinition,
+ };
+ }, {});
+ },
},
watch: {
'formValues.name': function watchName(value) {
- if (this.hasPathBeenManuallySet) {
+ if (this.hasPathBeenManuallySet || !this.fieldsToRender.includes(FORM_FIELD_PATH)) {
return;
}
@@ -93,7 +151,8 @@ export default {
<gl-form-fields
v-model="formValues"
:form-id="$options.formId"
- :fields="$options.fields"
+ :fields="fields"
+ class="gl-display-flex gl-column-gap-5 gl-flex-wrap"
@submit="$emit('submit', formValues)"
>
<template #input(path)="{ id, value, validation, input, blur }">
@@ -117,9 +176,11 @@ export default {
</gl-form-fields>
<div class="gl-display-flex gl-gap-3">
<gl-button type="submit" variant="confirm" class="js-no-auto-disable" :loading="loading">{{
- $options.i18n.createOrganization
+ submitButtonText
+ }}</gl-button>
+ <gl-button v-if="showCancelButton" :href="organizationsPath">{{
+ $options.i18n.cancel
}}</gl-button>
- <gl-button :href="organizationsPath">{{ $options.i18n.cancel }}</gl-button>
</div>
</gl-form>
</template>
diff --git a/app/assets/javascripts/organizations/shared/constants.js b/app/assets/javascripts/organizations/shared/constants.js
new file mode 100644
index 00000000000..010613bc9fd
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/constants.js
@@ -0,0 +1,3 @@
+export const FORM_FIELD_NAME = 'name';
+export const FORM_FIELD_ID = 'id';
+export const FORM_FIELD_PATH = 'path';
diff --git a/app/assets/javascripts/organizations/shared/graphql/queries/organization.query.graphql b/app/assets/javascripts/organizations/shared/graphql/queries/organization.query.graphql
new file mode 100644
index 00000000000..1d95786fcb0
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/graphql/queries/organization.query.graphql
@@ -0,0 +1,9 @@
+query getOrganization($id: ID!) {
+ organization(id: $id) @client {
+ id
+ name
+ descriptionHtml
+ avatarUrl
+ webUrl
+ }
+}
diff --git a/app/assets/javascripts/organizations/shared/graphql/resolvers.js b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
index 9f7e9b22e1d..9ed1be62352 100644
--- a/app/assets/javascripts/organizations/shared/graphql/resolvers.js
+++ b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
@@ -2,7 +2,7 @@ import {
organizations,
organizationProjects,
organizationGroups,
- createOrganizationResponse,
+ updateOrganizationResponse,
} from '../../mock_data';
const simulateLoading = () => {
@@ -34,11 +34,11 @@ export default {
},
},
Mutation: {
- createOrganization: async () => {
+ updateOrganization: async () => {
// Simulate API loading
await simulateLoading();
- return createOrganizationResponse;
+ return updateOrganizationResponse;
},
},
};
diff --git a/app/assets/javascripts/organizations/users/components/app.vue b/app/assets/javascripts/organizations/users/components/app.vue
new file mode 100644
index 00000000000..ae22bedd69a
--- /dev/null
+++ b/app/assets/javascripts/organizations/users/components/app.vue
@@ -0,0 +1,51 @@
+<script>
+import { __, s__ } from '~/locale';
+import { createAlert } from '~/alert';
+import organizationUsersQuery from '../graphql/organization_users.query.graphql';
+
+export default {
+ name: 'OrganizationsUsersApp',
+ i18n: {
+ users: __('Users'),
+ loadingPlaceholder: __('Loading'),
+ errorMessage: s__(
+ 'Organization|An error occurred loading the organization users. Please refresh the page to try again.',
+ ),
+ },
+ inject: ['organizationGid'],
+ data() {
+ return {
+ users: [],
+ };
+ },
+ apollo: {
+ users: {
+ query: organizationUsersQuery,
+ variables() {
+ return { id: this.organizationGid };
+ },
+ update(data) {
+ return data.organization.organizationUsers.nodes;
+ },
+ error(error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ },
+ },
+ },
+ computed: {
+ loading() {
+ return this.$apollo.queries.users.loading;
+ },
+ },
+};
+</script>
+
+<template>
+ <section>
+ <h1 class="gl-my-4 gl-font-size-h-display">{{ $options.i18n.users }}</h1>
+ <template v-if="loading">
+ {{ $options.i18n.loadingPlaceholder }}
+ </template>
+ <div data-testid="organization-users">{{ users }}</div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql b/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql
new file mode 100644
index 00000000000..a0b2a639401
--- /dev/null
+++ b/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql
@@ -0,0 +1,17 @@
+query getOrganizationUsers($id: OrganizationsOrganizationID!) {
+ organization(id: $id) {
+ id
+ organizationUsers {
+ nodes {
+ badges {
+ text
+ variant
+ }
+ id
+ user {
+ id
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/organizations/users/index.js b/app/assets/javascripts/organizations/users/index.js
new file mode 100644
index 00000000000..76656243075
--- /dev/null
+++ b/app/assets/javascripts/organizations/users/index.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import OrganizationsUsersApp from './components/app.vue';
+
+export const initOrganizationsUsers = () => {
+ const el = document.getElementById('js-organizations-users');
+
+ if (!el) return false;
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ const { organizationGid } = convertObjectPropsToCamelCase(el.dataset);
+
+ return new Vue({
+ el,
+ name: 'OrganizationsUsersRoot',
+ apolloProvider,
+ provide: {
+ organizationGid,
+ },
+ render(createElement) {
+ return createElement(OrganizationsUsersApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
index 7c594a6c091..934bb206cc4 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
@@ -219,7 +219,6 @@ export default {
<template>
<div>
<persisted-search
- class="gl-mb-5"
:sortable-fields="$options.sortableFields"
:default-order="$options.sortableFields[0].orderBy"
default-sort="asc"
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
index a1c4d7ea1f2..89a8c4c2a2f 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
@@ -293,7 +293,6 @@ export default {
</template>
</registry-header>
<persisted-search
- class="gl-mb-5"
:sortable-fields="$options.searchConfig"
:default-order="$options.searchConfig[0].orderBy"
default-sort="desc"
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue
index bfe0c250dd9..cf1ee44b82e 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue
@@ -44,12 +44,7 @@ export default {
<template>
<list-item v-bind="$attrs">
<template #left-primary>
- <router-link
- class="gl-text-body gl-font-weight-bold"
- data-testid="details-link"
- data-qa-selector="registry_image_content"
- :to="linkTo"
- >
+ <router-link class="gl-text-body gl-font-weight-bold" data-testid="details-link" :to="linkTo">
{{ item.name }}
</router-link>
<clipboard-button
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue
index bff32a124bc..bf0cdd5db10 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue
@@ -135,7 +135,6 @@ export default {
<div class="gl-my-3">
<details-header :images-detail="imagesDetail" />
<persisted-search
- class="gl-mb-5"
:sortable-fields="$options.searchConfig.nameSortFields"
:default-order="$options.searchConfig.nameSortFields[0].orderBy"
default-sort="asc"
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
index b49c448c478..a821a2483cd 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
@@ -167,7 +167,7 @@ export default {
<gl-tabs>
<gl-tab :title="__('Detail')">
- <div data-qa-selector="package_information_content">
+ <div>
<package-history :package-entity="packageEntity" :project-name="projectName" />
<terraform-installation />
</div>
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue
index cd5f9f5a676..9d70391a8dd 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue
@@ -34,7 +34,7 @@ export default {
</script>
<template>
- <title-area :title="packageEntity.name" data-qa-selector="package_title">
+ <title-area :title="packageEntity.name">
<template #sub-header>
<gl-icon name="eye" class="gl-mr-3" />
<gl-sprintf :message="$options.i18n.packageInfo">
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 a3bbd569f41..937553e25cc 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,6 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
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';
@@ -41,9 +41,6 @@ export default {
apollo: {
packageMetadata: {
query: getPackageMetadataQuery,
- context: {
- isSingleRequest: true,
- },
variables() {
return {
id: this.packageId,
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 c8924e6548b..7b3acaf2ab6 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
@@ -12,7 +12,7 @@ import {
GlSprintf,
GlKeysetPagination,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/alert';
import { NEXT, PREV } from '~/vue_shared/components/pagination/constants';
import { numberToHumanSize } from '~/lib/utils/number_utils';
@@ -92,9 +92,6 @@ export default {
apollo: {
packageFiles: {
query: getPackageFilesQuery,
- context: {
- isSingleRequest: true,
- },
variables() {
return this.queryVariables;
},
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 663c361819e..32f94b82fa3 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,7 +1,7 @@
<script>
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { first } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__, n__ } from '~/locale';
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
index cdf03d64b27..db5e007b81f 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
@@ -77,7 +77,6 @@ export default {
v-gl-resize-observer="checkBreakpoints"
:title="packageEntity.name"
:avatar="packageIcon"
- data-qa-selector="package_title"
>
<template #sub-header>
<div data-testid="sub-header" class="gl-display-flex gl-flex-wrap gl-gap-3">
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
index 482249bc252..0c0001ba6d6 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { n__ } from '~/locale';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
index a545ad1d09c..674683aa02f 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
@@ -183,7 +183,12 @@ export default {
<span data-testid="right-secondary">
<gl-sprintf :message="publishedMessage">
<template v-if="isGroupPage" #projectName>
- <gl-link data-testid="root-link" :href="projectLink">{{ projectName }}</gl-link>
+ <gl-link
+ data-testid="root-link"
+ class="gl-text-decoration-underline"
+ :href="projectLink"
+ >{{ projectName }}</gl-link
+ >
</template>
<template #date>
<timeago-tooltip :time="packageEntity.createdAt" />
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue
index 8ecf433f3ab..2f74de9a615 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue
@@ -39,9 +39,12 @@ export default {
<span data-testid="pipeline-ref" class="gl-mr-2">{{ pipeline.ref }}</span>
<gl-icon name="commit" class="gl-mr-2" />
- <gl-link data-testid="pipeline-sha" :href="pipeline.commitPath" class="gl-mr-2">{{
- packageShaShort
- }}</gl-link>
+ <gl-link
+ data-testid="pipeline-sha"
+ :href="pipeline.commitPath"
+ class="gl-mr-2 gl-text-decoration-underline"
+ >{{ packageShaShort }}</gl-link
+ >
<clipboard-button
:text="pipeline.sha"
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
index a187c7a70d2..294c6baad1b 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
@@ -171,7 +171,7 @@ export default {
/>
</template>
</package-title>
- <package-search class="gl-mb-5" @update="handleSearchUpdate" />
+ <package-search @update="handleSearchUpdate" />
<delete-packages
:refetch-queries="refetchQueriesData"
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
index de087a8fcc5..e15f204dc6e 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
@@ -3,6 +3,7 @@ import { GlTableLite, GlToggle } from '@gitlab/ui';
import {
GENERIC_PACKAGE_FORMAT,
MAVEN_PACKAGE_FORMAT,
+ NUGET_PACKAGE_FORMAT,
PACKAGE_FORMATS_TABLE_HEADER,
PACKAGE_SETTINGS_HEADER,
PACKAGE_SETTINGS_DESCRIPTION,
@@ -91,6 +92,18 @@ export default {
},
testid: 'generic-settings',
},
+ {
+ id: 'nuget-duplicated-settings-regex-input',
+ format: NUGET_PACKAGE_FORMAT,
+ duplicatesAllowed: this.packageSettings.nugetDuplicatesAllowed,
+ duplicateExceptionRegex: this.packageSettings.nugetDuplicateExceptionRegex,
+ duplicateExceptionRegexError: this.errors.nugetDuplicateExceptionRegex,
+ modelNames: {
+ allowed: 'nugetDuplicatesAllowed',
+ exception: 'nugetDuplicateExceptionRegex',
+ },
+ testid: 'nuget-settings',
+ },
];
},
},
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
index bfb57e3ac1c..54b337a4296 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
@@ -10,6 +10,7 @@ export const MAVEN_PACKAGE_FORMAT = s__('PackageRegistry|Maven');
export const NPM_PACKAGE_FORMAT = s__('PackageRegistry|npm');
export const PYPI_PACKAGE_FORMAT = s__('PackageRegistry|PyPI');
export const GENERIC_PACKAGE_FORMAT = s__('PackageRegistry|Generic');
+export const NUGET_PACKAGE_FORMAT = s__('PackageRegistry|NuGet');
export const DUPLICATES_TOGGLE_LABEL = s__('PackageRegistry|Allow duplicates');
export const DUPLICATES_SETTING_EXCEPTION_TITLE = __('Exceptions');
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql
index 267e40263f2..0e36f48e9a6 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql
@@ -3,6 +3,8 @@ fragment PackageSettingsFields on PackageSettings {
mavenDuplicateExceptionRegex
genericDuplicatesAllowed
genericDuplicateExceptionRegex
+ nugetDuplicatesAllowed
+ nugetDuplicateExceptionRegex
mavenPackageRequestsForwarding
lockMavenPackageRequestsForwarding
mavenPackageRequestsForwardingLocked
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue b/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
index 0a94f67ea5e..18e95ee313e 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
@@ -47,7 +47,7 @@ export default {
</script>
<template>
- <div data-qa-selector="package_path" class="gl-display-flex gl-align-items-center">
+ <div class="gl-display-flex gl-align-items-center">
<gl-icon data-testid="base-icon" name="project" class="gl-mx-3 gl-min-w-0" />
<gl-link
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue b/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue
index f67bee77eb6..ac83f5fc1ad 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue
@@ -92,7 +92,7 @@ export default {
<div>
<div
v-if="!hiddenDelete"
- class="gl-display-flex gl-justify-content-space-between gl-mb-3 gl-align-items-center"
+ class="gl-display-flex gl-justify-content-space-between gl-mb-3 gl-mt-5 gl-align-items-center"
>
<div class="gl-display-flex gl-align-items-center">
<gl-form-checkbox
diff --git a/app/assets/javascripts/pages/admin/application_settings/appearances/preview_sign_in/index.js b/app/assets/javascripts/pages/admin/application_settings/appearances/preview_sign_in/index.js
new file mode 100644
index 00000000000..e3524bfbee3
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/appearances/preview_sign_in/index.js
@@ -0,0 +1,3 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+renderGFM(document.body);
diff --git a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
index a5305777dd5..6ca9f39842a 100644
--- a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
@@ -1,3 +1,5 @@
import setup from '~/admin/application_settings/setup_metrics_and_profiling';
+import initServiceUsageData from '~/admin/application_settings/setup_service_usage_data';
setup();
+initServiceUsageData();
diff --git a/app/assets/javascripts/pages/admin/application_settings/service_usage_data/index.js b/app/assets/javascripts/pages/admin/application_settings/service_usage_data/index.js
deleted file mode 100644
index 8a12e753847..00000000000
--- a/app/assets/javascripts/pages/admin/application_settings/service_usage_data/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initServiceUsageData from '~/admin/application_settings/setup_service_usage_data';
-
-initServiceUsageData();
diff --git a/app/assets/javascripts/pages/explore/catalog/index.js b/app/assets/javascripts/pages/explore/catalog/index.js
new file mode 100644
index 00000000000..fec738a93a6
--- /dev/null
+++ b/app/assets/javascripts/pages/explore/catalog/index.js
@@ -0,0 +1,3 @@
+import { initCatalog } from '~/ci/catalog/';
+
+initCatalog();
diff --git a/app/assets/javascripts/pages/import/bulk_imports/details/index.js b/app/assets/javascripts/pages/import/bulk_imports/details/index.js
new file mode 100644
index 00000000000..5c2571af60f
--- /dev/null
+++ b/app/assets/javascripts/pages/import/bulk_imports/details/index.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import BulkImportDetailsApp from '~/import/details/components/bulk_import_details_app.vue';
+
+export const initBulkImportDetails = () => {
+ const el = document.querySelector('.js-bulk-import-details');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ name: 'BulkImportDetailsRoot',
+ render(createElement) {
+ return createElement(BulkImportDetailsApp);
+ },
+ });
+};
+
+initBulkImportDetails();
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 459546a5562..e912bfa4f92 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
@@ -14,13 +14,14 @@ import { createAlert } from '~/alert';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { getBulkImportsHistory } from '~/rest_api';
-import ImportStatus from '~/import_entities/components/import_status.vue';
+import ImportStatus from '~/import_entities/import_groups/components/import_status.vue';
import { StatusPoller } from '~/import_entities/import_groups/services/status_poller';
import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { isImporting } from '../utils';
import { DEFAULT_ERROR } from '../utils/error_messages';
@@ -57,6 +58,8 @@ export default {
GlTooltip,
},
+ mixins: [glFeatureFlagMixin()],
+
inject: ['realtimeChangesPath'],
data() {
@@ -103,6 +106,10 @@ export default {
.filter((item) => isImporting(item.status))
.map((item) => item.bulk_import_id);
},
+
+ showDetailsLink() {
+ return this.glFeatures.bulkImportDetailsPage;
+ },
},
watch: {
@@ -225,12 +232,7 @@ export default {
:description="s__('BulkImport|Your imported groups and projects will appear here.')"
/>
<template v-else>
- <gl-table-lite
- :fields="$options.fields"
- :items="historyItems"
- data-qa-selector="import_history_table"
- class="gl-w-full"
- >
+ <gl-table-lite :fields="$options.fields" :items="historyItems" class="gl-w-full">
<template #cell(destination_name)="{ item }">
<gl-icon
v-gl-tooltip
@@ -252,14 +254,23 @@ export default {
<time-ago :time="value" />
</template>
<template #cell(status)="{ value, item, toggleDetails, detailsShowing }">
- <import-status :status="value" class="gl-display-inline-block gl-w-13" />
- <gl-button
- v-if="item.failures.length"
- class="gl-ml-3"
- :selected="detailsShowing"
- @click="toggleDetails"
- >{{ __('Details') }}</gl-button
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-align-items-flex-start gl-justify-content-space-between gl-gap-3"
>
+ <import-status
+ :id="item.bulk_import_id"
+ :entity-id="item.id"
+ :has-failures="item.has_failures"
+ :show-details-link="showDetailsLink"
+ :status="value"
+ />
+ <gl-button
+ v-if="!showDetailsLink && item.failures.length"
+ :selected="detailsShowing"
+ @click="toggleDetails"
+ >{{ __('Details') }}</gl-button
+ >
+ </div>
</template>
<template #row-details="{ item }">
<pre><code>{{ item.failures }}</code></pre>
diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/index.js b/app/assets/javascripts/pages/import/bulk_imports/history/index.js
index cc12723572d..ac975db3667 100644
--- a/app/assets/javascripts/pages/import/bulk_imports/history/index.js
+++ b/app/assets/javascripts/pages/import/bulk_imports/history/index.js
@@ -4,13 +4,14 @@ import BulkImportHistoryApp from './components/bulk_imports_history_app.vue';
function mountImportHistoryApp(mountElement) {
if (!mountElement) return undefined;
- const { realtimeChangesPath } = mountElement.dataset;
+ const { realtimeChangesPath, detailsPath } = mountElement.dataset;
return new Vue({
el: mountElement,
name: 'BulkImportHistoryRoot',
provide: {
realtimeChangesPath,
+ detailsPath,
},
render(createElement) {
return createElement(BulkImportHistoryApp);
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 938c2be89c5..9c0f937fe0e 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
@@ -11,11 +11,8 @@ import { DEFAULT_ERROR } from '../utils/error_messages';
import ImportErrorDetails from './import_error_details.vue';
const DEFAULT_PER_PAGE = 20;
-const DEFAULT_TH_CLASSES =
- 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-200! gl-border-b-1! gl-p-5!';
const tableCell = (config) => ({
- thClass: DEFAULT_TH_CLASSES,
tdClass: (value, key, item) => {
return {
// eslint-disable-next-line no-underscore-dangle
@@ -57,12 +54,12 @@ export default {
tableCell({
key: 'source',
label: s__('BulkImport|Source'),
- thClass: `${DEFAULT_TH_CLASSES} gl-w-30p`,
+ thClass: 'gl-w-30p',
}),
tableCell({
key: 'destination',
label: s__('BulkImport|Destination'),
- thClass: `${DEFAULT_TH_CLASSES} gl-w-40p`,
+ thClass: 'gl-w-40p',
}),
tableCell({
key: 'created_at',
@@ -144,12 +141,7 @@ export default {
:description="s__('BulkImport|Your imported projects will appear here.')"
/>
<template v-else>
- <gl-table
- :fields="$options.fields"
- :items="historyItems"
- data-qa-selector="import_history_table"
- class="gl-w-full"
- >
+ <gl-table :fields="$options.fields" :items="historyItems" class="gl-w-full">
<template #cell(source)="{ item }">
<template v-if="item.import_url">
<gl-link
diff --git a/app/assets/javascripts/pages/organizations/organizations/users/index.js b/app/assets/javascripts/pages/organizations/organizations/users/index.js
new file mode 100644
index 00000000000..12d53207b22
--- /dev/null
+++ b/app/assets/javascripts/pages/organizations/organizations/users/index.js
@@ -0,0 +1,3 @@
+import { initOrganizationsUsers } from '~/organizations/users';
+
+initOrganizationsUsers();
diff --git a/app/assets/javascripts/pages/organizations/settings/general/index.js b/app/assets/javascripts/pages/organizations/settings/general/index.js
new file mode 100644
index 00000000000..5b74af6206e
--- /dev/null
+++ b/app/assets/javascripts/pages/organizations/settings/general/index.js
@@ -0,0 +1,3 @@
+import { initOrganizationsSettingsGeneral } from '~/organizations/settings/general';
+
+initOrganizationsSettingsGeneral();
diff --git a/app/assets/javascripts/pages/passwords/new/index.js b/app/assets/javascripts/pages/passwords/new/index.js
new file mode 100644
index 00000000000..e3524bfbee3
--- /dev/null
+++ b/app/assets/javascripts/pages/passwords/new/index.js
@@ -0,0 +1,3 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+renderGFM(document.body);
diff --git a/app/assets/javascripts/pages/profiles/preferences/show/index.js b/app/assets/javascripts/pages/profiles/preferences/show/index.js
index 76939434680..3668811bec7 100644
--- a/app/assets/javascripts/pages/profiles/preferences/show/index.js
+++ b/app/assets/javascripts/pages/profiles/preferences/show/index.js
@@ -1,5 +1,7 @@
import initProfilePreferences from '~/profile/preferences/profile_preferences_bundle';
import initProfilePreferencesDiffsColors from '~/profile/preferences/profile_preferences_diffs_colors';
+import { initHomeOrganizationSetting } from '~/organizations/profile/preferences';
initProfilePreferences();
initProfilePreferencesDiffsColors();
+initHomeOrganizationSetting();
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index 07662e4411e..d42fb10063e 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -153,9 +153,10 @@ const initForkInfo = () => {
initForkInfo();
const CommitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status');
-const statusLink = document.querySelector('.commit-actions .ci-status-link');
-if (statusLink) {
- statusLink.remove();
+const legacyStatusBadge = document.querySelector('.js-ci-status-badge-legacy');
+
+if (legacyStatusBadge) {
+ legacyStatusBadge.remove();
// eslint-disable-next-line no-new
new Vue({
el: CommitPipelineStatusEl,
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
index 9659c927fbf..e3d50e900ca 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
@@ -7,6 +7,7 @@ import {
GlFormGroup,
GlFormTextarea,
GlButton,
+ GlSprintf,
GlFormRadio,
GlFormRadioGroup,
} from '@gitlab/ui';
@@ -56,6 +57,7 @@ export default {
GlIcon,
GlLink,
GlButton,
+ GlSprintf,
GlFormInput,
GlFormTextarea,
GlFormGroup,
@@ -91,6 +93,9 @@ export default {
projectDescription: {
default: '',
},
+ projectDefaultBranch: {
+ default: '',
+ },
projectVisibility: {
default: '',
},
@@ -116,6 +121,7 @@ export default {
required: false,
skipValidation: true,
}),
+ branches: initFormField({ value: '', required: true, skipValidation: true }),
visibility: initFormField({ value: null }),
},
};
@@ -168,6 +174,18 @@ export default {
return allowedLevels;
},
+ branchesOptions() {
+ return [
+ {
+ text: s__('ForkProject|All branches'),
+ value: '',
+ },
+ {
+ text: s__(`ForkProject|Only the default branch %{defaultBranch}`),
+ value: this.projectDefaultBranch,
+ },
+ ];
+ },
visibilityLevels() {
return [
{
@@ -245,7 +263,7 @@ export default {
this.form.showValidation = false;
const { projectId } = this;
- const { name, slug, description, visibility, namespace } = this.form.fields;
+ const { name, slug, description, branches, visibility, namespace } = this.form.fields;
const postParams = {
id: projectId,
@@ -253,6 +271,7 @@ export default {
namespace_id: namespace.value.id,
path: slug.value,
description: description.value,
+ branches: branches.value,
visibility: visibility.value,
};
@@ -263,6 +282,7 @@ export default {
const { data } = await axios.post(url, postParams);
redirectTo(data.web_url); // eslint-disable-line import/no-deprecated
} catch (error) {
+ this.isSaving = false;
createAlert({
message: s__(
'ForkProject|An error occurred while forking the project. Please try again.',
@@ -348,6 +368,34 @@ export default {
/>
</gl-form-group>
+ <gl-form-group>
+ <label>
+ {{ s__('ForkProject|Branches to include') }}
+ </label>
+ <gl-form-radio-group
+ v-model="form.fields.branches.value"
+ data-testid="fork-branches-radio-group"
+ name="branches"
+ :aria-label="__('branches')"
+ required
+ >
+ <gl-form-radio
+ v-for="{ text, value } in branchesOptions"
+ :key="value"
+ :value="value"
+ :data-testid="`radio-${value}`"
+ >
+ <div>
+ <gl-sprintf :message="text">
+ <template #defaultBranch>
+ <code class="gl-ml-2">{{ projectDefaultBranch }}</code>
+ </template>
+ </gl-sprintf>
+ </div>
+ </gl-form-radio>
+ </gl-form-radio-group>
+ </gl-form-group>
+
<gl-form-group
v-validation:[form.showValidation]
:invalid-feedback="s__('ForkProject|Please select a visibility level')"
diff --git a/app/assets/javascripts/pages/projects/forks/new/index.js b/app/assets/javascripts/pages/projects/forks/new/index.js
index a31b8b1a1f4..694914e9154 100644
--- a/app/assets/javascripts/pages/projects/forks/new/index.js
+++ b/app/assets/javascripts/pages/projects/forks/new/index.js
@@ -15,6 +15,7 @@ const {
projectId,
projectName,
projectPath,
+ projectDefaultBranch,
projectDescription,
projectVisibility,
restrictedVisibilityLevels,
@@ -38,6 +39,7 @@ new Vue({
projectName,
projectPath,
projectDescription,
+ projectDefaultBranch,
projectVisibility,
restrictedVisibilityLevels: JSON.parse(restrictedVisibilityLevels),
},
diff --git a/app/assets/javascripts/pages/projects/merge_requests/page.js b/app/assets/javascripts/pages/projects/merge_requests/page.js
index fb243d01dc6..a9d281fc899 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/page.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/page.js
@@ -28,7 +28,7 @@ requestIdleCallback(() => {
if (el) {
const { data } = el.dataset;
- const { iid, projectPath, title, tabs, isFluidLayout } = JSON.parse(data);
+ const { iid, projectPath, title, tabs, isFluidLayout, sourceProjectPath } = JSON.parse(data);
// eslint-disable-next-line no-new
new Vue({
@@ -42,6 +42,7 @@ requestIdleCallback(() => {
title,
tabs,
isFluidLayout: parseBoolean(isFluidLayout),
+ sourceProjectPath,
},
render(h) {
return h(StickyHeader);
diff --git a/app/assets/javascripts/pages/projects/ml/model_versions/show/index.js b/app/assets/javascripts/pages/projects/ml/model_versions/show/index.js
new file mode 100644
index 00000000000..1a2b85d7e16
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/ml/model_versions/show/index.js
@@ -0,0 +1,4 @@
+import { initSimpleApp } from '~/helpers/init_simple_app_helper';
+import { ShowMlModelVersion } from '~/ml/model_registry/apps';
+
+initSimpleApp('#js-mount-show-ml-model-version', ShowMlModelVersion);
diff --git a/app/assets/javascripts/pages/projects/ml/models/index/index.js b/app/assets/javascripts/pages/projects/ml/models/index/index.js
index 62d326f43a5..3f8ef4910a7 100644
--- a/app/assets/javascripts/pages/projects/ml/models/index/index.js
+++ b/app/assets/javascripts/pages/projects/ml/models/index/index.js
@@ -1,4 +1,4 @@
import { initSimpleApp } from '~/helpers/init_simple_app_helper';
-import MlModelsIndex from '~/ml/model_registry/routes/models/index';
+import { IndexMlModels } from '~/ml/model_registry/apps';
-initSimpleApp('#js-index-ml-models', MlModelsIndex);
+initSimpleApp('#js-index-ml-models', IndexMlModels);
diff --git a/app/assets/javascripts/pages/projects/ml/models/show/index.js b/app/assets/javascripts/pages/projects/ml/models/show/index.js
index 87ee5c851f6..c8e25e0f0e8 100644
--- a/app/assets/javascripts/pages/projects/ml/models/show/index.js
+++ b/app/assets/javascripts/pages/projects/ml/models/show/index.js
@@ -1,4 +1,4 @@
import { initSimpleApp } from '~/helpers/init_simple_app_helper';
import { ShowMlModel } from '~/ml/model_registry/apps';
-initSimpleApp('#js-mount-show-ml-model', ShowMlModel);
+initSimpleApp('#js-mount-show-ml-model', ShowMlModel, { withApolloProvider: true });
diff --git a/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js b/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js
deleted file mode 100644
index ba03fccdb03..00000000000
--- a/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initActivityCharts from '~/analytics/product_analytics/activity_charts_bundle';
-
-initActivityCharts();
diff --git a/app/assets/javascripts/pages/registrations/new/index.js b/app/assets/javascripts/pages/registrations/new/index.js
index 90a9c9e7279..54974e878c3 100644
--- a/app/assets/javascripts/pages/registrations/new/index.js
+++ b/app/assets/javascripts/pages/registrations/new/index.js
@@ -5,6 +5,7 @@ import EmailFormatValidator from '~/pages/sessions/new/email_format_validator';
import { initLanguageSwitcher } from '~/language_switcher';
import { initPasswordInput } from '~/authentication/password';
import Tracking from '~/tracking';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
new UsernameValidator(); // eslint-disable-line no-new
new LengthValidator(); // eslint-disable-line no-new
@@ -17,3 +18,4 @@ Tracking.enableFormTracking({
initLanguageSwitcher();
initPasswordInput();
+renderGFM(document.body);
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index 1d5d885753c..32df2911a48 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -4,6 +4,7 @@ import NoEmojiValidator from '~/emoji/no_emoji_validator';
import { initLanguageSwitcher } from '~/language_switcher';
import LengthValidator from '~/validators/length_validator';
import mountEmailVerificationApplication from '~/sessions/new';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import OAuthRememberMe from './oauth_remember_me';
import preserveUrlFragment from './preserve_url_fragment';
import SigninTabsMemoizer from './signin_tabs_memoizer';
@@ -24,3 +25,4 @@ preserveUrlFragment(window.location.hash);
initVueAlerts();
initLanguageSwitcher();
mountEmailVerificationApplication();
+renderGFM(document.body);
diff --git a/app/assets/javascripts/pages/users/index.js b/app/assets/javascripts/pages/users/index.js
index af55a5dc01a..d2c31314bba 100644
--- a/app/assets/javascripts/pages/users/index.js
+++ b/app/assets/javascripts/pages/users/index.js
@@ -1,11 +1,15 @@
import $ from 'jquery';
import { setCookie } from '~/lib/utils/common_utils';
import UserCallout from '~/user_callout';
-import { initReportAbuse } from '~/users/profile';
-import { initProfileTabs } from '~/profile';
+import { initProfileTabs, initUserAchievements } from '~/profile';
+import { initUserActionsApp } from '~/users/profile/actions';
import UserTabs from './user_tabs';
function initUserProfile(action) {
+ // TODO: Remove both Vue and legacy JS tabs code/feature flag uses with the
+ // removal of the old navigation.
+ // See https://gitlab.com/groups/gitlab-org/-/epics/11875.
+
if (gon.features?.profileTabsVue) {
initProfileTabs();
} else {
@@ -24,5 +28,6 @@ function initUserProfile(action) {
const page = $('body').attr('data-page');
const action = page.split(':')[1];
initUserProfile(action);
+initUserAchievements();
+initUserActionsApp();
new UserCallout(); // eslint-disable-line no-new
-initReportAbuse();
diff --git a/app/assets/javascripts/pages/users/show/index.js b/app/assets/javascripts/pages/users/show/index.js
deleted file mode 100644
index 7d612d6cc4e..00000000000
--- a/app/assets/javascripts/pages/users/show/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { initUserAchievements } from '~/profile';
-import { initUserActionsApp } from '~/users/profile/actions';
-
-initUserAchievements();
-initUserActionsApp();
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index 430022f9a9b..79eb3902116 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -1,3 +1,6 @@
+// TODO: Remove this with the removal of the old navigation.
+// See https://gitlab.com/groups/gitlab-org/-/epics/11875.
+
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
import Activities from '~/activities';
@@ -194,7 +197,7 @@ export default class UserTabs {
this.loadActivityCalendar();
UserTabs.renderMostRecentBlocks('#js-overview .activities-block', {
- requestParams: { limit: 10 },
+ requestParams: { limit: 15 },
});
UserTabs.renderMostRecentBlocks('#js-overview .projects-block', {
requestParams: { limit: 10, skip_pagination: true, skip_namespace: true, compact_mode: true },
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 720c1e0d7f2..c5f8fd1904f 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -61,11 +61,6 @@ export default {
keys: ['feature', 'request'],
},
{
- metric: 'rugged',
- header: s__('PerformanceBar|Rugged calls'),
- keys: ['feature', 'args'],
- },
- {
metric: 'redis',
header: s__('PerformanceBar|Redis calls'),
keys: ['cmd', 'instance'],
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index cea01852630..bba8e1f7ba5 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -23,7 +23,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-geo-migrate-hashed-storage-callout',
'.js-unlimited-members-during-trial-alert',
'.js-branch-rules-info-callout',
- '.js-new-navigation-callout',
+ '.js-new-nav-for-everyone-callout',
'.js-namespace-over-storage-users-combined-alert',
];
diff --git a/app/assets/javascripts/pipeline_wizard/components/wrapper.vue b/app/assets/javascripts/pipeline_wizard/components/wrapper.vue
index ab837d04d9a..43e70046cfb 100644
--- a/app/assets/javascripts/pipeline_wizard/components/wrapper.vue
+++ b/app/assets/javascripts/pipeline_wizard/components/wrapper.vue
@@ -14,8 +14,7 @@ import CommitStep from './commit.vue';
export const i18n = {
stepNofN: __('Step %{currentStep} of %{stepCount}'),
draft: __('Draft: %{filename}'),
- overlayMessage: __(`Start inputting changes and we will generate a
- YAML-file for you to add to your repository`),
+ overlayMessage: __(`Enter values to populate the .gitlab-ci.yml configuration file.`),
};
const trackingMixin = Tracking.mixin();
diff --git a/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue b/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue
index 5a93de3b1be..3676ba96254 100644
--- a/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue
+++ b/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue
@@ -1,5 +1,6 @@
<script>
import { parseDocument } from 'yaml';
+import { DEFAULT_CI_CONFIG_PATH } from '~/lib/utils/constants';
import WizardWrapper from './components/wrapper.vue';
export default {
@@ -23,7 +24,7 @@ export default {
defaultFilename: {
type: String,
required: false,
- default: '.gitlab-ci.yml',
+ default: DEFAULT_CI_CONFIG_PATH,
},
},
computed: {
diff --git a/app/assets/javascripts/pipeline_wizard/templates/pages.yml b/app/assets/javascripts/pipeline_wizard/templates/pages.yml
index 9d7936f2f5a..8eecd51fe27 100644
--- a/app/assets/javascripts/pipeline_wizard/templates/pages.yml
+++ b/app/assets/javascripts/pipeline_wizard/templates/pages.yml
@@ -1,12 +1,10 @@
id: gitlab/pages
-title: Get started with Pages
-description: "GitLab Pages lets you deploy static websites in minutes. All you
- need is a .gitlab-ci.yml file. Follow the below steps to
- create one for your app now."
+title: Get started with GitLab Pages
+description: "Use GitLab Pages to deploy your static website. Follow these steps to create the configuration file, .gitlab-ci.yml, and start a pipeline to deploy the site."
steps:
- inputs:
- label: Select your build image
- description: A Docker image that we can use to build your image
+ description: A Docker image, used to create an instance where your job runs.
placeholder: node:lts
widget: text
target: $BUILD_IMAGE
@@ -14,18 +12,15 @@ steps:
pattern: "(?:[a-z]+/)?([a-z]+)(?::[0-9]+)?"
invalid-feedback: Please enter a valid docker image
- widget: checklist
- title: "Before we begin, please check:"
items:
- - text: The app's built output files are in a folder named "public"
- help: GitLab Pages will only publish files in that folder.
- You may need to adjust your build engine's config.
+ - text: The application files are in the `public` folder
+ help: GitLab Pages publishes files in the public folder only. If needed, change your jobs to send output to this folder.
template:
# The Docker image that will be used to build your app
image: $BUILD_IMAGE
- inputs:
- label: Installation Steps
- description: "Enter the steps that need to run to set up a local build
- environment, for example installing dependencies."
+ description: "Enter steps to set up a local build environment, like installing dependencies."
placeholder: npm ci
widget: list
target: $INSTALLATION_STEPS
@@ -34,8 +29,7 @@ steps:
before_script: $INSTALLATION_STEPS
- inputs:
- label: Build Steps
- description: "Enter the steps necessary to build a production version of
- your application."
+ description: "Enter steps to build a production version of your application."
widget: list
target: $BUILD_STEPS
template:
diff --git a/app/assets/javascripts/profile/edit/components/profile_edit_app.vue b/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
index 6b39f137880..815b8742500 100644
--- a/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
+++ b/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
@@ -109,8 +109,11 @@ export default {
async syncHeaderAvatars() {
const dataURL = await readFileAsDataURL(this.avatarBlob);
- // TODO: implement sync for super sidebar
- ['.header-user-avatar', '.js-sidebar-user-avatar'].forEach((selector) => {
+ const elements = gon?.use_new_navigation
+ ? ['[data-testid="user-dropdown"] .gl-avatar']
+ : ['.header-user-avatar', '.js-sidebar-user-avatar'];
+
+ elements.forEach((selector) => {
const node = document.querySelector(selector);
if (!node) return;
diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
index aa30192b74b..2fc1f99c183 100644
--- a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
+++ b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
@@ -5,9 +5,9 @@ import { INTEGRATION_VIEW_CONFIGS, i18n } from '../constants';
import IntegrationView from './integration_view.vue';
function updateClasses(bodyClasses = '', applicationTheme, layout) {
- // Remove body class for any previous theme, re-add current one
- document.body.classList.remove(...bodyClasses.split(' '));
- document.body.classList.add(applicationTheme);
+ // Remove documentElement class for any previous theme, re-add current one
+ document.documentElement.classList.remove(...bodyClasses.split(' '));
+ document.documentElement.classList.add(applicationTheme);
// Toggle container-fluid class
if (layout === 'fluid') {
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 947bf7acd5c..2ccb360c7c1 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -89,8 +89,12 @@ export default class Profile {
}
updateHeaderAvatar() {
- $('.header-user-avatar').attr('src', this.avatarGlCrop.dataURL);
- $('.js-sidebar-user-avatar').attr('src', this.avatarGlCrop.dataURL);
+ if (gon?.use_new_navigation) {
+ $('[data-testid="user-dropdown"] .gl-avatar').attr('src', this.avatarGlCrop.dataURL);
+ } else {
+ $('.header-user-avatar').attr('src', this.avatarGlCrop.dataURL);
+ $('.js-sidebar-user-avatar').attr('src', this.avatarGlCrop.dataURL);
+ }
}
setRepoRadio() {
diff --git a/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue b/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue
index 7c00ce45b3a..377310b087e 100644
--- a/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue
+++ b/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue
@@ -51,7 +51,6 @@ export default {
text: s__('ChangeTypeAction|Cherry-pick'),
extraAttrs: {
'data-testid': 'cherry-pick-link',
- 'data-qa-selector': 'cherry_pick_button',
},
action: () => this.showModal(OPEN_CHERRY_PICK_MODAL),
};
@@ -62,7 +61,6 @@ export default {
text: s__('ChangeTypeAction|Revert'),
extraAttrs: {
'data-testid': 'revert-link',
- 'data-qa-selector': 'revert_button',
},
action: () => this.showModal(OPEN_REVERT_MODAL),
};
@@ -85,7 +83,6 @@ export default {
download: '',
rel: 'nofollow',
'data-testid': 'plain-diff-link',
- 'data-qa-selector': 'plain_diff',
},
};
},
@@ -97,7 +94,6 @@ export default {
download: '',
rel: 'nofollow',
'data-testid': 'email-patches-link',
- 'data-qa-selector': 'email_patches',
},
};
},
@@ -148,8 +144,7 @@ export default {
:toggle-text="__('Options')"
right
data-testid="commit-options-dropdown"
- data-qa-selector="options_button"
- class="gl-xs-w-full gl-line-height-20"
+ class="gl-line-height-20"
>
<gl-disclosure-dropdown-group :group="optionsGroup" @action="closeDropdown" />
diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue
index 44b8ccb57ca..d1e78084b9f 100644
--- a/app/assets/javascripts/projects/commit/components/form_modal.vue
+++ b/app/assets/javascripts/projects/commit/components/form_modal.vue
@@ -57,7 +57,6 @@ export default {
variant: 'confirm',
category: 'primary',
'data-testid': 'submit-commit',
- 'data-qa-selector': 'submit_commit_button',
},
},
actionCancel: {
@@ -74,7 +73,6 @@ export default {
'branchCollaboration',
'modalTitle',
'existingBranch',
- 'prependedText',
'targetProjectId',
'targetProjectName',
'branchesEndpoint',
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
index 0feaf8db82b..a4851b4fe4b 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { createAlert } from '~/alert';
import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
import getLatestPipelineStatusQuery from '../graphql/queries/get_latest_pipeline_status.query.graphql';
@@ -9,7 +9,7 @@ import { COMMIT_BOX_POLL_INTERVAL, PIPELINE_STATUS_FETCH_ERROR } from '../consta
export default {
PIPELINE_STATUS_FETCH_ERROR,
components: {
- CiBadgeLink,
+ CiIcon,
GlLoadingIcon,
},
inject: {
@@ -63,12 +63,6 @@ export default {
<template>
<div class="gl-display-inline-block gl-vertical-align-middle gl-mr-2">
<gl-loading-icon v-if="loading" />
- <ci-badge-link
- v-else
- :status="pipelineStatus"
- :details-path="pipelineStatus.detailsPath"
- size="md"
- :show-text="false"
- />
+ <ci-icon v-else :status="pipelineStatus" />
</div>
</template>
diff --git a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
index c206e648561..24b7130e765 100644
--- a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
+++ b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
@@ -5,7 +5,7 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { useGet: true }),
+ defaultClient: createDefaultClient(),
});
export const initCommitPipelineMiniGraph = async (selector = '.js-commit-pipeline-mini-graph') => {
diff --git a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_status.js b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_status.js
index d5e62531283..079f74dc8a2 100644
--- a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_status.js
+++ b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_status.js
@@ -6,7 +6,7 @@ import CommitBoxPipelineStatus from './components/commit_box_pipeline_status.vue
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { useGet: true }),
+ defaultClient: createDefaultClient(),
});
export default (selector = '.js-commit-pipeline-status') => {
diff --git a/app/assets/javascripts/projects/commits/store/actions.js b/app/assets/javascripts/projects/commits/store/actions.js
index 5175f7f9151..7d04e9a15a3 100644
--- a/app/assets/javascripts/projects/commits/store/actions.js
+++ b/app/assets/javascripts/projects/commits/store/actions.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
diff --git a/app/assets/javascripts/projects/default_project_templates.js b/app/assets/javascripts/projects/default_project_templates.js
index 5bbc881952f..e3599c87616 100644
--- a/app/assets/javascripts/projects/default_project_templates.js
+++ b/app/assets/javascripts/projects/default_project_templates.js
@@ -121,4 +121,8 @@ export default {
text: s__('ProjectTemplates|Laravel Framework'),
icon: '.template-option .icon-laravel',
},
+ astro_tailwind: {
+ text: s__('ProjectTemplates|Astro Tailwind'),
+ icon: '.template-option .icon-gitlab_logo',
+ },
};
diff --git a/app/assets/javascripts/projects/new/components/new_project_url_select.vue b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
index ef2a2aa5526..84a2ddfce07 100644
--- a/app/assets/javascripts/projects/new/components/new_project_url_select.vue
+++ b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
@@ -67,7 +67,7 @@ export default {
}
: this.$options.emptyNameSpace,
shouldSkipQuery: true,
- userNamespaceId: this.userNamespaceId,
+ userNamespaceUniqueId: this.userNamespaceId,
};
},
computed: {
@@ -186,7 +186,7 @@ export default {
{{ group.fullPath }}
</gl-dropdown-item>
</template>
- <template v-if="hasNamespaceMatches && userNamespaceId">
+ <template v-if="hasNamespaceMatches && userNamespaceUniqueId">
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
<gl-dropdown-item @click="handleDropdownItemClick(userNamespace)">
{{ userNamespace.fullPath }}
@@ -202,7 +202,7 @@ export default {
:id="inputId"
type="hidden"
:name="inputName"
- :value="selectedNamespace.id || userNamespaceId"
+ :value="selectedNamespace.id || userNamespaceUniqueId"
/>
</gl-button-group>
</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue b/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue
index 5383a6cdddf..f921b2dfdd6 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink } from '@gitlab/ui';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
-import { s__, n__ } from '~/locale';
+import { s__, n__, formatNumber } from '~/locale';
const defaultPrecision = 2;
@@ -22,25 +22,25 @@ export default {
},
computed: {
statistics() {
- const formatter = getFormatter(SUPPORTED_FORMATS.percentHundred);
+ const formatPercent = getFormatter(SUPPORTED_FORMATS.percentHundred);
return [
{
title: s__('PipelineCharts|Total:'),
- value: n__('1 pipeline', '%d pipelines', this.counts.total),
+ value: n__('1 pipeline', '%d pipelines', formatNumber(this.counts.total)),
},
{
title: s__('PipelineCharts|Successful:'),
- value: n__('1 pipeline', '%d pipelines', this.counts.success),
+ value: n__('1 pipeline', '%d pipelines', formatNumber(this.counts.success)),
},
{
title: s__('PipelineCharts|Failed:'),
- value: n__('1 pipeline', '%d pipelines', this.counts.failed),
+ value: n__('1 pipeline', '%d pipelines', formatNumber(this.counts.failed)),
link: this.failedPipelinesLink,
},
{
title: s__('PipelineCharts|Success ratio:'),
- value: formatter(this.counts.successRatio, defaultPrecision),
+ value: formatPercent(this.counts.successRatio, defaultPrecision),
},
];
},
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
index dbcb77b67f3..becd373c5f1 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
@@ -187,7 +187,7 @@ export default {
:roles="pushAccessLevels.roles"
:users="pushAccessLevels.users"
:groups="pushAccessLevels.groups"
- data-qa-selector="allowed_to_push_content"
+ data-testid="allowed-to-push-content"
/>
<!-- Allowed to merge -->
@@ -198,7 +198,7 @@ export default {
:roles="mergeAccessLevels.roles"
:users="mergeAccessLevels.users"
:groups="mergeAccessLevels.groups"
- data-qa-selector="allowed_to_merge_content"
+ data-testid="allowed-to-merge-content"
/>
<!-- Force push -->
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
index 3a5b3409596..366c69556f2 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
@@ -105,7 +105,6 @@ export default {
v-for="(item, index) in accessLevels"
:key="index"
data-testid="access-level"
- data-qa-selector="access_level_content"
:data-qa-role="item.accessLevelDescription"
>
<span v-if="commaSeparateList && index > 0" data-testid="comma-separator">,</span>
diff --git a/app/assets/javascripts/projects/settings/components/default_branch_selector.vue b/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
index fee2f591216..f5fb72e84bc 100644
--- a/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
+++ b/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
@@ -33,6 +33,5 @@ export default {
:translations="$options.i18n"
name="project[default_branch]"
data-testid="default-branch-dropdown"
- data-qa-selector="default_branch_dropdown"
/>
</template>
diff --git a/app/assets/javascripts/projects/settings/init_access_dropdown.js b/app/assets/javascripts/projects/settings/init_access_dropdown.js
index 67afbee3854..b02a33675ee 100644
--- a/app/assets/javascripts/projects/settings/init_access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/init_access_dropdown.js
@@ -1,5 +1,5 @@
-import * as Sentry from '@sentry/browser';
import Vue from 'vue';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import AccessDropdown from './components/access_dropdown.vue';
export const initAccessDropdown = (el, options) => {
@@ -22,6 +22,7 @@ export const initAccessDropdown = (el, options) => {
data() {
return { preselected };
},
+ disabled,
methods: {
setPreselectedItems(items) {
this.preselected = items;
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
index 7753b850744..7d9ad83a1c6 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
@@ -76,7 +76,7 @@ export default {
v-gl-modal="$options.modalId"
size="small"
class="gl-ml-3"
- data-qa-selector="add_branch_rule_button"
+ data-testid="add-branch-rule-button"
>{{ $options.i18n.addBranchRule }}</gl-button
>
</template>
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
index f45a5b12db6..0a5fa288828 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
@@ -156,7 +156,7 @@ export default {
<li>
<div
class="gl-display-flex gl-justify-content-space-between"
- data-qa-selector="branch_content"
+ data-testid="branch-content"
:data-qa-branch-name="name"
>
<div>
@@ -178,7 +178,7 @@ export default {
class="gl-align-self-start"
category="tertiary"
size="small"
- data-qa-selector="details_button"
+ data-testid="details-button"
:href="detailsPath"
>
{{ $options.i18n.detailsButtonLabel }}</gl-button
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
index 09bc275cbd4..6f22af4bd26 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
@@ -6,10 +6,16 @@ import {
GlFormInputGroup,
GlFormInput,
GlLink,
+ GlFormSelect,
GlSprintf,
} from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { isEmptyValue, hasMinimumLength, isIntegerGreaterThan, isEmail } from '~/lib/utils/forms';
+import {
+ isEmptyValue,
+ hasMinimumLength,
+ isIntegerGreaterThan,
+ isServiceDeskSettingEmail,
+} from '~/lib/utils/forms';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
I18N_FORM_INTRODUCTION_PARAGRAPH,
@@ -23,6 +29,11 @@ import {
I18N_FORM_SMTP_USERNAME_LABEL,
I18N_FORM_SMTP_PASSWORD_LABEL,
I18N_FORM_SMTP_PASSWORD_DESCRIPTION,
+ I18N_FORM_SMTP_AUTHENTICATION_LABEL,
+ I18N_FORM_SMTP_AUTHENTICATION_NONE,
+ I18N_FORM_SMTP_AUTHENTICATION_PLAIN,
+ I18N_FORM_SMTP_AUTHENTICATION_LOGIN,
+ I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5,
I18N_FORM_SUBMIT_LABEL,
I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL,
I18N_FORM_INVALID_FEEDBACK_SMTP_ADDRESS,
@@ -42,6 +53,7 @@ export default {
GlFormGroup,
GlFormInputGroup,
GlFormInput,
+ GlFormSelect,
GlLink,
GlSprintf,
},
@@ -56,6 +68,11 @@ export default {
I18N_FORM_SMTP_USERNAME_LABEL,
I18N_FORM_SMTP_PASSWORD_LABEL,
I18N_FORM_SMTP_PASSWORD_DESCRIPTION,
+ I18N_FORM_SMTP_AUTHENTICATION_LABEL,
+ I18N_FORM_SMTP_AUTHENTICATION_NONE,
+ I18N_FORM_SMTP_AUTHENTICATION_PLAIN,
+ I18N_FORM_SMTP_AUTHENTICATION_LOGIN,
+ I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5,
I18N_FORM_SUBMIT_LABEL,
I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL,
I18N_FORM_INVALID_FEEDBACK_SMTP_ADDRESS,
@@ -82,6 +99,7 @@ export default {
smtpPort: '587',
smtpUsername: '',
smtpPassword: '',
+ smtpAuthentication: null,
validationState: {
customEmail: null,
smtpAddress: null,
@@ -113,6 +131,7 @@ export default {
smtp_port: this.smtpPort,
smtp_username: this.smtpUsername,
smtp_password: this.smtpPassword,
+ smtp_authentication: this.smtpAuthentication,
};
},
onCustomEmailChange() {
@@ -124,7 +143,7 @@ export default {
}
},
validateCustomEmail() {
- this.validationState.customEmail = isEmail(this.customEmail);
+ this.validationState.customEmail = isServiceDeskSettingEmail(this.customEmail);
},
validateSmtpAddress() {
this.validationState.smtpAddress = !isEmptyValue(this.smtpAddress);
@@ -145,6 +164,26 @@ export default {
this.validateSmtpUsername();
this.validateSmtpPassword();
},
+ getSmtpAuthenticationOptions() {
+ return [
+ {
+ text: this.$options.I18N_FORM_SMTP_AUTHENTICATION_NONE,
+ value: null,
+ },
+ {
+ text: this.$options.I18N_FORM_SMTP_AUTHENTICATION_PLAIN,
+ value: 'plain',
+ },
+ {
+ text: this.$options.I18N_FORM_SMTP_AUTHENTICATION_LOGIN,
+ value: 'login',
+ },
+ {
+ text: this.$options.I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5,
+ value: 'cram_md5',
+ },
+ ];
+ },
},
};
</script>
@@ -298,6 +337,20 @@ export default {
/>
</gl-form-group>
+ <gl-form-group
+ :label="$options.I18N_FORM_SMTP_AUTHENTICATION_LABEL"
+ label-for="custom-email-form-smtp-password"
+ class="gl-mt-3"
+ >
+ <gl-form-select
+ id="custom-email-form-smtp-authentication"
+ v-model.trim="smtpAuthentication"
+ :options="getSmtpAuthenticationOptions()"
+ :aria-label="$options.I18N_FORM_SMTP_AUTHENTICATION_LABEL"
+ :disabled="isSubmitting"
+ />
+ </gl-form-group>
+
<gl-button
type="submit"
variant="confirm"
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
index 03ba99bcf71..f72aa19bdf2 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
@@ -234,7 +234,6 @@ export default {
:href="$options.FEEDBACK_ISSUE_URL"
target="_blank"
data-testid="feedback-link"
- class="gl-text-blue-600 font-size-inherit"
>{{ content }}
</gl-link>
</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
index 2b2722ab329..6674937be67 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
@@ -55,6 +55,9 @@ export default {
projectKey: {
default: '',
},
+ addExternalParticipantsFromCc: {
+ default: false,
+ },
templates: {
default: [],
},
@@ -109,13 +112,20 @@ export default {
});
},
- onSaveTemplate({ selectedTemplate, fileTemplateProjectId, outgoingName, projectKey }) {
+ onSaveTemplate({
+ selectedTemplate,
+ fileTemplateProjectId,
+ outgoingName,
+ projectKey,
+ addExternalParticipantsFromCc,
+ }) {
this.isTemplateSaving = true;
const body = {
issue_template_key: selectedTemplate,
outgoing_name: outgoingName,
project_key: projectKey,
+ add_external_participants_from_cc: addExternalParticipantsFromCc,
service_desk_enabled: this.isEnabled,
file_template_project_id: fileTemplateProjectId,
};
@@ -187,6 +197,7 @@ export default {
:initial-selected-file-template-project-id="selectedFileTemplateProjectId"
:initial-outgoing-name="outgoingName"
:initial-project-key="projectKey"
+ :initial-add-external-participants-from-cc="addExternalParticipantsFromCc"
:templates="templates"
:is-template-saving="isTemplateSaving"
@save="onSaveTemplate"
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
index 5078cbbdf59..5febb6ff0aa 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
@@ -4,6 +4,7 @@ import {
GlToggle,
GlLoadingIcon,
GlSprintf,
+ GlFormCheckbox,
GlFormInputGroup,
GlFormGroup,
GlFormInput,
@@ -11,7 +12,8 @@ import {
GlAlert,
} from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ServiceDeskTemplateDropdown from './service_desk_template_dropdown.vue';
@@ -21,12 +23,22 @@ export default {
issueTrackerEnableMessage: __(
'To use Service Desk in this project, you must %{linkStart}activate the issue tracker%{linkEnd}.',
),
+ addExternalParticipantsFromCc: {
+ label: s__('ServiceDesk|Add external participants from the %{codeStart}Cc%{codeEnd} header'),
+ help: s__(
+ 'ServiceDesk|Add email addresses in the %{codeStart}Cc%{codeEnd} header of Service Desk emails to the issue.',
+ ),
+ helpNotificationExtra: s__(
+ 'ServiceDesk|Like the author, external participants receive Service Desk emails and can participate in the discussion.',
+ ),
+ },
},
components: {
ClipboardButton,
GlButton,
GlToggle,
GlLoadingIcon,
+ GlFormCheckbox,
GlSprintf,
GlFormInput,
GlFormGroup,
@@ -35,6 +47,7 @@ export default {
GlAlert,
ServiceDeskTemplateDropdown,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
isEnabled: {
type: Boolean,
@@ -78,6 +91,11 @@ export default {
required: false,
default: '',
},
+ initialAddExternalParticipantsFromCc: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
templates: {
type: Array,
required: false,
@@ -95,11 +113,15 @@ export default {
selectedFileTemplateProjectId: this.initialSelectedFileTemplateProjectId,
outgoingName: this.initialOutgoingName || __('GitLab Support Bot'),
projectKey: this.initialProjectKey,
+ addExternalParticipantsFromCc: this.initialAddExternalParticipantsFromCc,
searchTerm: '',
projectKeyError: null,
};
},
computed: {
+ showAddExternalParticipantsFromCC() {
+ return this.glFeatures.issueEmailParticipants;
+ },
hasProjectKeySupport() {
return Boolean(this.serviceDeskEmailEnabled);
},
@@ -134,6 +156,7 @@ export default {
selectedTemplate: this.selectedTemplate,
outgoingName: this.outgoingName,
projectKey: this.projectKey,
+ addExternalParticipantsFromCc: this.addExternalParticipantsFromCc,
fileTemplateProjectId: this.selectedFileTemplateProjectId,
});
},
@@ -240,12 +263,7 @@ export default {
"
>
<template #link="{ content }">
- <gl-link
- :href="emailSuffixHelpUrl"
- target="_blank"
- class="gl-text-blue-600 font-size-inherit"
- >{{ content }}
- </gl-link>
+ <gl-link :href="emailSuffixHelpUrl" target="_blank">{{ content }} </gl-link>
</template>
</gl-sprintf>
</template>
@@ -259,10 +277,7 @@ export default {
"
>
<template #link="{ content }">
- <gl-link
- :href="serviceDeskEmailAddressHelpUrl"
- target="_blank"
- class="gl-text-blue-600 font-size-inherit"
+ <gl-link :href="serviceDeskEmailAddressHelpUrl" target="_blank"
>{{ content }}
</gl-link>
</template>
@@ -307,11 +322,31 @@ export default {
</template>
</gl-form-group>
+ <gl-form-checkbox
+ v-if="showAddExternalParticipantsFromCC"
+ v-model="addExternalParticipantsFromCc"
+ :disabled="!isIssueTrackerEnabled"
+ >
+ <gl-sprintf :message="$options.i18n.addExternalParticipantsFromCc.label">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+
+ <template #help>
+ <gl-sprintf :message="$options.i18n.addExternalParticipantsFromCc.help">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ {{ $options.i18n.addExternalParticipantsFromCc.helpNotificationExtra }}
+ </template>
+ </gl-form-checkbox>
+
<gl-button
variant="confirm"
class="gl-mt-5"
data-testid="save_service_desk_settings_button"
- data-qa-selector="save_service_desk_settings_button"
:disabled="isTemplateSaving || !isIssueTrackerEnabled"
@click="onSaveTemplate"
>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue
index 315f0743b53..86c4fdcc30a 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue
@@ -84,7 +84,6 @@ export default {
id="service-desk-template-select"
:text="selectedTemplate || $options.i18n.defaultDropdownText"
:header-text="$options.i18n.defaultDropdownText"
- data-qa-selector="service_desk_template_dropdown"
:block="true"
class="service-desk-template-select"
toggle-class="gl-m-0"
diff --git a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
index aafd77bd25e..8ac186e292c 100644
--- a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
+++ b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
@@ -37,6 +37,13 @@ export const I18N_FORM_SMTP_PORT_DESCRIPTION = s__(
export const I18N_FORM_SMTP_USERNAME_LABEL = s__('ServiceDesk|SMTP username');
export const I18N_FORM_SMTP_PASSWORD_LABEL = s__('ServiceDesk|SMTP password');
export const I18N_FORM_SMTP_PASSWORD_DESCRIPTION = s__('ServiceDesk|Minimum 8 characters long.');
+export const I18N_FORM_SMTP_AUTHENTICATION_LABEL = s__('ServiceDesk|SMTP authentication method');
+export const I18N_FORM_SMTP_AUTHENTICATION_NONE = s__(
+ 'ServiceDesk|Let GitLab select a server-supported method (recommended)',
+);
+export const I18N_FORM_SMTP_AUTHENTICATION_PLAIN = s__('ServiceDesk|Plain');
+export const I18N_FORM_SMTP_AUTHENTICATION_LOGIN = s__('ServiceDesk|Login');
+export const I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5 = s__('ServiceDesk|CRAM-MD5');
export const I18N_FORM_SUBMIT_LABEL = s__('ServiceDesk|Save and test connection');
export const I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL = s__(
diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js
index c4d4f42576f..ce223b349bf 100644
--- a/app/assets/javascripts/projects/settings_service_desk/index.js
+++ b/app/assets/javascripts/projects/settings_service_desk/index.js
@@ -21,6 +21,7 @@ export default () => {
incomingEmail,
outgoingName,
projectKey,
+ addExternalParticipantsFromCc,
selectedTemplate,
selectedFileTemplateProjectId,
templates,
@@ -39,6 +40,7 @@ export default () => {
isIssueTrackerEnabled: parseBoolean(issueTrackerEnabled),
outgoingName,
projectKey,
+ addExternalParticipantsFromCc: parseBoolean(addExternalParticipantsFromCc),
selectedTemplate,
selectedFileTemplateProjectId: parseInt(selectedFileTemplateProjectId, 10) || null,
templates: JSON.parse(templates),
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue
index 5b620aa2300..074cddac422 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue
@@ -91,17 +91,10 @@ export default {
};
</script>
<template>
- <div class="ci-status-link">
+ <div class="gl-ml-5">
<gl-loading-icon v-if="isLoading" size="lg" label="Loading pipeline status" />
<a v-else :href="ciStatus.details_path">
- <ci-icon
- v-gl-tooltip
- :title="statusTitle"
- :aria-label="statusTitle"
- :status="ciStatus"
- :size="24"
- data-container="body"
- />
+ <ci-icon :status="ciStatus" :title="statusTitle" :aria-label="statusTitle" />
</a>
</div>
</template>
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index 29034b3bc0e..66da3de516a 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -6,6 +6,10 @@ import { initToggle } from '~/toggles';
import { initAccessDropdown } from '~/projects/settings/init_access_dropdown';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
+const isDropdownDisabled = (dropdown) => {
+ return dropdown?.$options.disabled === '';
+};
+
export default class ProtectedBranchEdit {
constructor(options) {
this.hasLicense = options.hasLicense;
@@ -104,6 +108,9 @@ export default class ProtectedBranchEdit {
}
initSelectedItems(dropdown, accessLevel) {
+ if (isDropdownDisabled(dropdown)) {
+ return;
+ }
this.selectedItems[accessLevel] = dropdown.preselected.map((item) => {
if (item.type === LEVEL_TYPES.USER) return { id: item.id, user_id: item.user_id };
if (item.type === LEVEL_TYPES.ROLE) return { id: item.id, access_level: item.access_level };
@@ -183,7 +190,10 @@ export default class ProtectedBranchEdit {
};
});
- this.selectedItems[accessLevel] = itemsToAdd;
- this[`${accessLevel}_dropdown`]?.setPreselectedItems(itemsToAdd);
+ const dropdown = this[`${accessLevel}_dropdown`];
+ if (!isDropdownDisabled(dropdown)) {
+ this.selectedItems[accessLevel] = itemsToAdd;
+ dropdown?.setPreselectedItems(itemsToAdd);
+ }
}
}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index b5661af352c..b3754cecce4 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -41,7 +41,7 @@ export default class ProtectedTagCreate {
accessLevel: ACCESS_LEVELS.CREATE,
accessLevelsData: gon.create_access_levels,
searchEnabled: dropdownEl.dataset.filter !== undefined,
- testId: 'allowed_to_create_dropdown',
+ testId: 'allowed-to-create-dropdown',
});
this.protectedTagAccessDropdown.$on('select', (selected) => {
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.vue b/app/assets/javascripts/protected_tags/protected_tag_edit.vue
index 82b2ecc5f5c..7fe1dc9c01a 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.vue
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.vue
@@ -101,7 +101,7 @@ export default {
<template>
<access-dropdown
toggle-class="js-allowed-to-create gl-max-w-34"
- test-id="allowed_to_create_dropdown"
+ test-id="allowed-to-create-dropdown"
:has-license="hasLicense"
:access-level="$options.ACCESS_LEVELS.CREATE"
:access-levels-data="accessLevelsData"
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
index 444d6e9cf76..fad15a5d89e 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import ProtectedTagEdit from './protected_tag_edit.vue';
export default class ProtectedTagEditList {
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index c68fbceb4f6..df9f333afe5 100644
--- a/app/assets/javascripts/releases/components/app_edit_new.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -176,8 +176,7 @@ export default {
</p>
<form v-if="showForm" class="js-quick-submit" @submit.prevent="submitForm">
<tag-field />
- <gl-form-group>
- <label for="release-title">{{ __('Release title') }}</label>
+ <gl-form-group :label="__('Release title')">
<gl-form-input
id="release-title"
ref="releaseTitleInput"
@@ -186,17 +185,14 @@ export default {
class="form-control"
/>
</gl-form-group>
- <gl-form-group class="w-50" data-testid="milestones-field">
- <label>{{ __('Milestones') }}</label>
- <div class="d-flex flex-column col-md-6 col-sm-10 pl-0">
- <milestone-combobox
- v-model="releaseMilestones"
- :project-id="projectId"
- :group-id="groupId"
- :group-milestones-available="groupMilestonesAvailable"
- :extra-links="milestoneComboboxExtraLinks"
- />
- </div>
+ <gl-form-group :label="__('Milestones')" class="gl-w-30" data-testid="milestones-field">
+ <milestone-combobox
+ v-model="releaseMilestones"
+ :project-id="projectId"
+ :group-id="groupId"
+ :group-milestones-available="groupMilestonesAvailable"
+ :extra-links="milestoneComboboxExtraLinks"
+ />
</gl-form-group>
<gl-form-group :label="__('Release date')" label-for="release-released-at">
<template #label-description>
@@ -214,8 +210,7 @@ export default {
</template>
<gl-datepicker id="release-released-at" v-model="releasedAt" :default-date="releasedAt" />
</gl-form-group>
- <gl-form-group data-testid="release-notes">
- <label for="release-notes">{{ __('Release notes') }}</label>
+ <gl-form-group :label="__('Release notes')" data-testid="release-notes">
<div class="common-note-form">
<markdown-field
:can-attach-file="true"
diff --git a/app/assets/javascripts/releases/components/release_block_header.vue b/app/assets/javascripts/releases/components/release_block_header.vue
index 070865cf84b..b4c897a8236 100644
--- a/app/assets/javascripts/releases/components/release_block_header.vue
+++ b/app/assets/javascripts/releases/components/release_block_header.vue
@@ -50,7 +50,7 @@ export default {
<template>
<div class="card-header d-flex align-items-center bg-white pr-0">
<h2 class="card-title my-2 mr-auto">
- <gl-link v-if="selfLink" :href="selfLink" class="font-size-inherit">
+ <gl-link v-if="selfLink" :href="selfLink">
{{ release.name }}
</gl-link>
<template v-else>
diff --git a/app/assets/javascripts/repository/components/blob_button_group.vue b/app/assets/javascripts/repository/components/blob_button_group.vue
index 99b861ca104..ed7212eb9a6 100644
--- a/app/assets/javascripts/repository/components/blob_button_group.vue
+++ b/app/assets/javascripts/repository/components/blob_button_group.vue
@@ -92,8 +92,8 @@ export default {
deleteModalTitle() {
return sprintf(__('Delete %{name}'), { name: this.name });
},
- lockBtnQASelector() {
- return this.canLock ? 'lock_button' : 'disabled_lock_button';
+ lockBtnTestId() {
+ return this.canLock ? 'lock-button' : 'disabled-lock-button';
},
},
methods: {
@@ -120,8 +120,7 @@ export default {
:project-path="projectPath"
:is-locked="isLocked"
:can-lock="canLock"
- data-testid="lock"
- :data-qa-selector="lockBtnQASelector"
+ :data-testid="lockBtnTestId"
/>
<gl-button data-testid="replace" @click="showModal($options.replaceBlobModalId)">
{{ $options.i18n.replace }}
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 6565c84fa11..97a1cbda5d0 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -122,6 +122,7 @@ export default {
blobInfo: {},
isEmptyRepository: false,
projectId: null,
+ showBlame: this.$route?.query?.blame === '1',
};
},
computed: {
@@ -202,6 +203,9 @@ export default {
isUsingLfs() {
return this.blobInfo.storedExternally && this.blobInfo.externalStorage === LFS_STORAGE;
},
+ isBlameEnabled() {
+ return this.glFeatures.blobBlameInfo && this.blobInfo.language === 'json'; // This feature is currently scoped to JSON files
+ },
},
watch: {
// Watch the URL 'plain' query value to know if the viewer needs changing.
@@ -289,6 +293,14 @@ export default {
onCopy() {
navigator.clipboard.writeText(this.blobInfo.rawTextBlob);
},
+ handleToggleBlame() {
+ this.switchViewer(SIMPLE_BLOB_VIEWER);
+ this.showBlame = !this.showBlame;
+
+ const blame = this.showBlame === true ? '1' : '0';
+ if (this.$route?.query?.blame === blame) return;
+ this.$router.push({ path: this.$route.path, query: { ...this.$route.query, blame } });
+ },
},
};
</script>
@@ -299,19 +311,21 @@ export default {
<div v-if="blobInfo && !isLoading" id="fileHolder" class="file-holder">
<blob-header
:blob="blobInfo"
- :hide-viewer-switcher="!hasRichViewer || isBinaryFileType || isUsingLfs"
+ :hide-viewer-switcher="isBinaryFileType || isUsingLfs"
:is-binary="isBinaryFileType"
:active-viewer-type="viewer.type"
:has-render-error="hasRenderError"
:show-path="false"
:override-copy="true"
:show-fork-suggestion="showForkSuggestion"
+ :show-blame-toggle="isBlameEnabled"
:project-path="projectPath"
:project-id="projectId"
@viewer-changed="handleViewerChanged"
@copy="onCopy"
@edit="editBlob"
@error="displayError"
+ @blame="handleToggleBlame"
>
<template #actions>
<blob-button-group
@@ -354,6 +368,7 @@ export default {
v-else
:blob="blobInfo"
:chunks="chunks"
+ :show-blame="showBlame"
:project-path="projectPath"
:current-ref="currentRef"
class="blob-viewer"
diff --git a/app/assets/javascripts/repository/components/commit_info.vue b/app/assets/javascripts/repository/components/commit_info.vue
index b6e3cdbb7a3..b6674114a20 100644
--- a/app/assets/javascripts/repository/components/commit_info.vue
+++ b/app/assets/javascripts/repository/components/commit_info.vue
@@ -1,6 +1,6 @@
<script>
import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import SafeHtml from '~/vue_shared/directives/safe_html';
import defaultAvatarUrl from 'images/no_avatar.png';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -26,6 +26,11 @@ export default {
type: Object,
required: true,
},
+ prevBlameLink: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return { showDescription: false };
@@ -35,6 +40,9 @@ export default {
// Strip the newline at the beginning
return this.commit?.descriptionHtml?.replace(/^&#x000A;/, '');
},
+ avatarLinkAltText() {
+ return sprintf(__(`%{username}'s avatar`), { username: this.commit.authorName });
+ },
},
methods: {
toggleShowDescription() {
@@ -58,6 +66,7 @@ export default {
v-if="commit.author"
:link-href="commit.author.webPath"
:img-src="commit.author.avatarUrl"
+ :img-alt="avatarLinkAltText"
:img-size="32"
class="gl-my-2 gl-mr-4"
/>
@@ -67,10 +76,8 @@ export default {
:img-src="commit.authorGravatar || $options.defaultAvatarUrl"
:size="32"
/>
- <div
- class="commit-detail flex-list gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-grow-1 gl-min-w-0"
- >
- <div class="commit-content" data-qa-selector="commit_content">
+ <div class="commit-detail flex-list gl-display-flex gl-flex-grow-1 gl-min-w-0">
+ <div class="commit-content gl-w-full gl-text-truncate" data-testid="commit-content">
<gl-link
v-safe-html:[$options.safeHtmlConfig]="commit.titleHtml"
:href="commit.webPath"
@@ -112,5 +119,6 @@ export default {
<div class="gl-flex-grow-1"></div>
<slot></slot>
</div>
+ <div v-if="prevBlameLink" v-safe-html:[$options.safeHtmlConfig]="prevBlameLink"></div>
</div>
</template>
diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue
index 97171a3282b..079d4c522a8 100644
--- a/app/assets/javascripts/repository/components/delete_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue
@@ -273,7 +273,7 @@ export default {
v-model="form.fields['commit_message'].value"
v-validation:[form.showValidation]
name="commit_message"
- data-qa-selector="commit_message_field"
+ data-testid="commit-message-field"
:state="form.fields['commit_message'].state"
:disabled="loading"
required
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 05d4d9e1f81..7f7a76cd4aa 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -3,7 +3,7 @@ import { GlTooltipDirective, GlButton, GlButtonGroup, GlLoadingIcon } from '@git
import SafeHtml from '~/vue_shared/directives/safe_html';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import { sprintf, s__ } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import SignatureBadge from '~/commit/components/signature_badge.vue';
import getRefMixin from '../mixins/get_ref';
@@ -17,7 +17,7 @@ export default {
CommitInfo,
ClipboardButton,
SignatureBadge,
- CiBadgeLink,
+ CiIcon,
GlButtonGroup,
GlButton,
GlLoadingIcon,
@@ -50,9 +50,6 @@ export default {
pipeline: pipelines?.length && pipelines[0].node,
};
},
- context: {
- isSingleRequest: true,
- },
error(error) {
throw error;
},
@@ -115,12 +112,10 @@ export default {
class="commit-actions gl-display-flex gl-flex-align gl-align-items-center gl-flex-direction-row"
>
<signature-badge v-if="commit.signature" :signature="commit.signature" />
- <div v-if="commit.pipeline" class="ci-status-link">
- <ci-badge-link
+ <div v-if="commit.pipeline" class="gl-ml-5">
+ <ci-icon
:status="commit.pipeline.detailedStatus"
- :details-path="commit.pipeline.detailedStatus.detailsPath"
:aria-label="statusTitle"
- :show-text="false"
class="js-commit-pipeline"
/>
</div>
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 526757e6147..6a81f11eb51 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -248,19 +248,19 @@ export default {
class="ml-1"
/>
</td>
- <td class="d-none d-sm-table-cell tree-commit cursor-default gl-text-secondary">
+ <td class="d-none d-sm-table-cell tree-commit cursor-default">
<gl-link
v-if="commitData"
v-safe-html:[$options.safeHtmlConfig]="commitData.titleHtml"
:href="commitData.commitPath"
:title="commitData.message"
- class="str-truncated-100 tree-commit-link gl-text-secondary"
+ class="str-truncated-100 tree-commit-link gl-text-gray-600"
/>
<gl-intersection-observer @appear="rowAppeared" @disappear="rowDisappeared">
<gl-skeleton-loader v-if="showSkeletonLoader" :lines="1" />
</gl-intersection-observer>
</td>
- <td class="tree-time-ago text-right cursor-default gl-text-secondary">
+ <td class="tree-time-ago text-right cursor-default gl-text-gray-600">
<gl-intersection-observer @appear="rowAppeared" @disappear="rowDisappeared">
<timeago-tooltip v-if="commitData" :time="commitData.committedDate" />
</gl-intersection-observer>
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index 2ff138cabe5..86a5f5107f8 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -1,12 +1,12 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapState, mapGetters } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue';
import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue';
import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue';
import SidebarPortal from '~/super_sidebar/components/sidebar_portal.vue';
import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
import {
SCOPE_ISSUES,
@@ -16,6 +16,7 @@ import {
SCOPE_NOTES,
SCOPE_COMMITS,
SCOPE_MILESTONES,
+ SCOPE_WIKI_BLOBS,
SEARCH_TYPE_ADVANCED,
} from '../constants';
import IssuesFilters from './issues_filters.vue';
@@ -25,6 +26,7 @@ import ProjectsFilters from './projects_filters.vue';
import NotesFilters from './notes_filters.vue';
import CommitsFilters from './commits_filters.vue';
import MilestonesFilters from './milestones_filters.vue';
+import WikiBlobsFilters from './wiki_blobs_filters.vue';
export default {
name: 'GlobalSearchSidebar',
@@ -34,6 +36,7 @@ export default {
BlobsFilters,
ProjectsFilters,
NotesFilters,
+ WikiBlobsFilters,
ScopeLegacyNavigation,
ScopeSidebarNavigation,
SidebarPortal,
@@ -60,20 +63,18 @@ export default {
return this.currentScope === SCOPE_PROJECTS;
},
showNotesFilters() {
- // for now, the feature flag is placed here. Since we have only one filter in notes scope
- return this.currentScope === SCOPE_NOTES && this.glFeatures.searchNotesHideArchivedProjects;
+ return this.currentScope === SCOPE_NOTES;
},
showCommitsFilters() {
- // for now, the feature flag is placed here. Since we have only one filter in commits scope
- return (
- this.currentScope === SCOPE_COMMITS && this.glFeatures.searchCommitsHideArchivedProjects
- );
+ return this.currentScope === SCOPE_COMMITS;
},
showMilestonesFilters() {
- // for now, the feature flag is placed here. Since we have only one filter in milestones scope
+ return this.currentScope === SCOPE_MILESTONES;
+ },
+ showWikiBlobsFilters() {
return (
- this.currentScope === SCOPE_MILESTONES &&
- this.glFeatures.searchMilestonesHideArchivedProjects
+ this.currentScope === SCOPE_WIKI_BLOBS &&
+ this.glFeatures?.searchProjectWikisHideArchivedProjects
);
},
showScopeNavigation() {
@@ -103,6 +104,7 @@ export default {
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
<milestones-filters v-if="showMilestonesFilters" />
+ <wiki-blobs-filters v-if="showWikiBlobsFilters" />
</sidebar-portal>
</section>
@@ -119,6 +121,7 @@ export default {
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
<milestones-filters v-if="showMilestonesFilters" />
+ <wiki-blobs-filters v-if="showWikiBlobsFilters" />
</div>
<small-screen-drawer-navigation class="gl-lg-display-none">
<scope-legacy-navigation />
@@ -129,6 +132,7 @@ export default {
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
<milestones-filters v-if="showMilestonesFilters" />
+ <wiki-blobs-filters v-if="showWikiBlobsFilters" />
</small-screen-drawer-navigation>
</section>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
index ed90e2aaded..96a6f119da2 100644
--- a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
+++ b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
@@ -5,7 +5,16 @@ const checkboxLabel = s__('GlobalSearch|Include archived');
export const TRACKING_NAMESPACE = 'search:archived:select';
export const TRACKING_LABEL_CHECKBOX = 'checkbox';
-const scopes = ['projects', 'issues', 'merge_requests', 'notes', 'blobs', 'commits', 'milestones'];
+const scopes = [
+ 'projects',
+ 'issues',
+ 'merge_requests',
+ 'notes',
+ 'blobs',
+ 'commits',
+ 'milestones',
+ 'wiki_blobs',
+];
const filterParam = 'include_archived';
diff --git a/app/assets/javascripts/search/sidebar/components/blobs_filters.vue b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
index ac36ae6b366..0ed2c24efba 100644
--- a/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
@@ -18,11 +18,8 @@ export default {
computed: {
...mapGetters(['currentScope']),
...mapState(['useSidebarNavigation', 'searchType']),
- showArchivedFilter() {
- return this.glFeatures.searchBlobsHideArchivedProjects;
- },
showDivider() {
- return !this.useSidebarNavigation && this.showArchivedFilter;
+ return !this.useSidebarNavigation;
},
hrClasses() {
return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block'];
@@ -35,6 +32,6 @@ export default {
<filters-template>
<language-filter class="gl-mb-5" />
<hr v-if="showDivider" :class="hrClasses" />
- <archived-filter v-if="showArchivedFilter" class="gl-mb-5" />
+ <archived-filter class="gl-mb-5" />
</filters-template>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/issues_filters.vue b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
index 4a2d3df6921..a77fb34cdba 100644
--- a/app/assets/javascripts/search/sidebar/components/issues_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
@@ -41,10 +41,7 @@ export default {
);
},
showArchivedFilter() {
- return (
- archivedFilterData.scopes.includes(this.currentScope) &&
- this.glFeatures.searchIssuesHideArchivedProjects
- );
+ return archivedFilterData.scopes.includes(this.currentScope);
},
showDivider() {
return !this.useSidebarNavigation;
diff --git a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
index ebd0406bcec..97583730958 100644
--- a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
@@ -55,12 +55,15 @@ export default {
},
i18n: I18N,
computed: {
- ...mapState(['useSidebarNavigation', 'searchLabelString', 'query', 'aggregations']),
+ ...mapState(['useSidebarNavigation', 'searchLabelString', 'query', 'urlQuery', 'aggregations']),
...mapGetters([
'filteredLabels',
'filteredUnselectedLabels',
'filteredAppliedSelectedLabels',
'appliedSelectedLabels',
+ 'unselectedLabels',
+ 'unappliedNewLabels',
+ 'labelAggregationBuckets',
]),
searchInputDescribeBy() {
if (this.isLoggedIn) {
@@ -100,10 +103,10 @@ export default {
return FIRST_DROPDOWN_INDEX;
},
hasSelectedLabels() {
- return this.filteredAppliedSelectedLabels.length > 0;
+ return this.filteredAppliedSelectedLabels?.length > 0;
},
hasUnselectedLabels() {
- return this.filteredUnselectedLabels.length > 0;
+ return this.filteredUnselectedLabels?.length > 0;
},
labelSearchBox() {
return this.$refs.searchLabelInputBox?.$el.querySelector('[role=searchbox]');
@@ -122,25 +125,30 @@ export default {
this.setLabelFilterSearch({ value });
},
},
- selectedFilters: {
+ selectedLabels: {
get() {
return this.combinedSelectedFilters;
},
set(value) {
this.setQuery({ key: this.$options.labelFilterData?.filterParam, value });
-
trackSelectCheckbox(value);
},
},
},
async created() {
- await this.fetchAllAggregation();
+ if (this.urlQuery?.[labelFilterData.filterParam]?.length > 0) {
+ await this.fetchAllAggregation();
+ }
},
methods: {
...mapActions(['fetchAllAggregation', 'setQuery', 'closeLabel', 'setLabelFilterSearch']),
- openDropdown() {
+ async openDropdown() {
this.isFocused = true;
+ if (!this.aggregations.error && this.filteredLabels?.length === 0) {
+ await this.fetchAllAggregation();
+ }
+
trackOpenDropdown();
},
closeDropdown(event) {
@@ -158,16 +166,8 @@ export default {
const { key } = event.target.closest('.gl-label').dataset;
this.closeLabel({ key });
},
- reactiveLabelColor(label) {
- const { color, key } = label;
-
- return this.query?.labels?.some((labelKey) => labelKey === key)
- ? color
- : `rgba(${rgbFromHex(color)}, 0.3)`;
- },
- isLabelClosable(label) {
- const { key } = label;
- return this.query?.labels?.some((labelKey) => labelKey === key);
+ inactiveLabelColor(label) {
+ return `rgba(${rgbFromHex(label.color)}, 0.3)`;
},
},
FIRST_DROPDOWN_INDEX,
@@ -188,13 +188,34 @@ export default {
</h5>
<div class="gl-my-5">
<gl-label
+ v-for="label in unappliedNewLabels"
+ :key="label.key"
+ class="gl-mr-2 gl-mb-2 gl-bg-gray-10"
+ :data-key="label.key"
+ :background-color="inactiveLabelColor(label)"
+ :title="label.title"
+ :show-close-button="false"
+ data-testid="unapplied-label"
+ />
+ <gl-label
+ v-for="label in unselectedLabels"
+ :key="label.key"
+ class="gl-mr-2 gl-mb-2 gl-bg-gray-10"
+ :data-key="label.key"
+ :background-color="inactiveLabelColor(label)"
+ :title="label.title"
+ :show-close-button="false"
+ data-testid="unselected-label"
+ />
+ <gl-label
v-for="label in appliedSelectedLabels"
:key="label.key"
class="gl-mr-2 gl-mb-2 gl-bg-gray-10"
:data-key="label.key"
- :background-color="reactiveLabelColor(label)"
+ :background-color="label.color"
:title="label.title"
- :show-close-button="isLabelClosable(label)"
+ :show-close-button="true"
+ data-testid="label"
@close="onLabelClose"
/>
</div>
@@ -245,7 +266,7 @@ export default {
$options.i18n.DROPDOWN_HEADER
}}</gl-dropdown-section-header>
<gl-dropdown-form>
- <gl-form-checkbox-group v-model="selectedFilters">
+ <gl-form-checkbox-group v-model="selectedLabels">
<label-dropdown-items
v-if="hasSelectedLabels"
:labels="filteredAppliedSelectedLabels"
diff --git a/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue b/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
index 6e476ef7935..f86906ebd26 100644
--- a/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
@@ -1,7 +1,6 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapState } from 'vuex';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { HR_DEFAULT_CLASSES } from '../constants';
import { statusFilterData } from './status_filter/data';
import StatusFilter from './status_filter/index.vue';
@@ -16,15 +15,11 @@ export default {
FiltersTemplate,
ArchivedFilter,
},
- mixins: [glFeatureFlagsMixin()],
computed: {
...mapGetters(['currentScope']),
...mapState(['useSidebarNavigation', 'searchType']),
showArchivedFilter() {
- return (
- archivedFilterData.scopes.includes(this.currentScope) &&
- this.glFeatures.searchMergeRequestsHideArchivedProjects
- );
+ return archivedFilterData.scopes.includes(this.currentScope);
},
showStatusFilter() {
return Object.values(statusFilterData.scopes).includes(this.currentScope);
diff --git a/app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue b/app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue
new file mode 100644
index 00000000000..b1f386d9f4f
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue
@@ -0,0 +1,18 @@
+<script>
+import ArchivedFilter from './archived_filter/index.vue';
+import FiltersTemplate from './filters_template.vue';
+
+export default {
+ name: 'WikiBlobsFilters',
+ components: {
+ ArchivedFilter,
+ FiltersTemplate,
+ },
+};
+</script>
+
+<template>
+ <filters-template>
+ <archived-filter class="gl-mb-5" />
+ </filters-template>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js
index b5446ecbb42..1559155a941 100644
--- a/app/assets/javascripts/search/sidebar/constants/index.js
+++ b/app/assets/javascripts/search/sidebar/constants/index.js
@@ -5,6 +5,8 @@ export const SCOPE_PROJECTS = 'projects';
export const SCOPE_NOTES = 'notes';
export const SCOPE_COMMITS = 'commits';
export const SCOPE_MILESTONES = 'milestones';
+export const SCOPE_WIKI_BLOBS = 'wiki_blobs';
+
export const LABEL_DEFAULT_CLASSES = [
'gl-display-flex',
'gl-flex-direction-row',
diff --git a/app/assets/javascripts/search/store/getters.js b/app/assets/javascripts/search/store/getters.js
index d01fd884bad..de05e9b80b2 100644
--- a/app/assets/javascripts/search/store/getters.js
+++ b/app/assets/javascripts/search/store/getters.js
@@ -1,10 +1,24 @@
-import { findKey } from 'lodash';
+import { findKey, intersection } from 'lodash';
import { languageFilterData } from '~/search/sidebar/components/language_filter/data';
import { labelFilterData } from '~/search/sidebar/components/label_filter/data';
import { formatSearchResultCount, addCountOverLimit } from '~/search/store/utils';
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY, ICON_MAP } from './constants';
+const queryLabelFilters = (state) => state?.query?.[labelFilterData.filterParam] || [];
+const urlQueryLabelFilters = (state) => state?.urlQuery?.[labelFilterData.filterParam] || [];
+
+const appliedSelectedLabelsKeys = (state) =>
+ intersection(urlQueryLabelFilters(state), queryLabelFilters(state));
+
+const unselectedLabelsKeys = (state) =>
+ urlQueryLabelFilters(state)?.filter((label) => !queryLabelFilters(state)?.includes(label));
+
+const unappliedNewLabelKeys = (state) =>
+ state?.query?.labels?.filter((label) => !urlQueryLabelFilters(state)?.includes(label));
+
+export const queryLanguageFilters = (state) => state.query[languageFilterData.filterParam] || [];
+
export const frequentGroups = (state) => {
return state.frequentItems[GROUPS_LOCAL_STORAGE_KEY];
};
@@ -39,25 +53,28 @@ export const filteredLabels = (state) => {
};
export const filteredAppliedSelectedLabels = (state) =>
- filteredLabels(state)?.filter((label) => state?.urlQuery?.labels?.includes(label.key));
+ filteredLabels(state)?.filter((label) => urlQueryLabelFilters(state)?.includes(label.key));
export const appliedSelectedLabels = (state) => {
return labelAggregationBuckets(state)?.filter((label) =>
- state?.urlQuery?.labels?.includes(label.key),
+ appliedSelectedLabelsKeys(state)?.includes(label.key),
);
};
-export const filteredUnselectedLabels = (state) => {
- if (!state?.urlQuery?.labels) {
- return filteredLabels(state);
- }
+export const filteredUnselectedLabels = (state) =>
+ filteredLabels(state)?.filter((label) => !urlQueryLabelFilters(state)?.includes(label.key));
- return filteredLabels(state)?.filter((label) => !state?.urlQuery?.labels?.includes(label.key));
-};
+export const unselectedLabels = (state) =>
+ labelAggregationBuckets(state).filter((label) =>
+ unselectedLabelsKeys(state)?.includes(label.key),
+ );
-export const currentScope = (state) => findKey(state.navigation, { active: true });
+export const unappliedNewLabels = (state) =>
+ labelAggregationBuckets(state).filter((label) =>
+ unappliedNewLabelKeys(state)?.includes(label.key),
+ );
-export const queryLanguageFilters = (state) => state.query[languageFilterData.filterParam] || [];
+export const currentScope = (state) => findKey(state.navigation, { active: true });
export const navigationItems = (state) =>
Object.values(state.navigation).map((item) => ({
diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
index f5f88e12163..d424ec6dfeb 100644
--- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue
+++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
@@ -9,7 +9,7 @@ import {
GlSkeletonLoader,
GlIcon,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import SafeHtml from '~/vue_shared/directives/safe_html';
import Tracking from '~/tracking';
import { __, s__ } from '~/locale';
diff --git a/app/assets/javascripts/sentry/init_sentry.js b/app/assets/javascripts/sentry/init_sentry.js
index 6f32c8c4165..722741b50e4 100644
--- a/app/assets/javascripts/sentry/init_sentry.js
+++ b/app/assets/javascripts/sentry/init_sentry.js
@@ -23,7 +23,7 @@ const initSentry = () => {
const client = new BrowserClient({
// Sentry.init(...) options
dsn: gon.sentry_dsn,
- release: gon.version,
+ release: gon.revision,
allowUrls:
process.env.NODE_ENV === 'production'
? [gon.gitlab_url]
@@ -56,7 +56,7 @@ const initSentry = () => {
hub.bindClient(client);
hub.setTags({
- revision: gon.revision,
+ version: gon.version,
feature_category: gon.feature_category,
page,
});
@@ -75,7 +75,7 @@ const initSentry = () => {
// The _Sentry object is globally exported so it can be used by
// ./sentry_browser_wrapper.js
- // This hack allows us to load a single version of `@sentry/browser`
+ // This hack allows us to load a single version of `~/sentry/sentry_browser_wrapper`
// in the browser, see app/views/layouts/_head.html.haml to find how it is imported.
// eslint-disable-next-line no-underscore-dangle
diff --git a/app/assets/javascripts/sentry/legacy_index.js b/app/assets/javascripts/sentry/legacy_index.js
index 604b982e128..688f8eb0a44 100644
--- a/app/assets/javascripts/sentry/legacy_index.js
+++ b/app/assets/javascripts/sentry/legacy_index.js
@@ -25,7 +25,7 @@ index();
// The _Sentry object is globally exported so it can be used by
// ./sentry_browser_wrapper.js
-// This hack allows us to load a single version of `@sentry/browser`
+// This hack allows us to load a single version of `~/sentry/sentry_browser_wrapper`
// in the browser, see app/views/layouts/_head.html.haml to find how it is imported.
// eslint-disable-next-line no-underscore-dangle
diff --git a/app/assets/javascripts/sentry/sentry_browser_wrapper.js b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
index 03cf53fabef..99f5adf8e89 100644
--- a/app/assets/javascripts/sentry/sentry_browser_wrapper.js
+++ b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
@@ -1,15 +1,23 @@
+/* eslint-disable no-console */
+
// The _Sentry object is globally exported so it can be used here
// This hack allows us to load a single version of `@sentry/browser`
-// in the browser (or none). See app/views/layouts/_head.html.haml
-// to find how it is imported.
+// in the browser (or none).
+
+// See app/views/layouts/_head.html.haml to find how it is imported.
-// This module wraps methods used by our production code.
-// Each export is names as we cannot export the entire namespace from *.
+// This module exports Sentry methods used by our production code.
/** @type {import('@sentry/core').captureException} */
export const captureException = (...args) => {
// eslint-disable-next-line no-underscore-dangle
const Sentry = window._Sentry;
+ // When Sentry is not configured during development, show console error
+ if (process.env.NODE_ENV === 'development' && !Sentry) {
+ console.error('[Sentry stub]', 'captureException(...) called with:', { ...args });
+ return;
+ }
+
Sentry?.captureException(...args);
};
diff --git a/app/assets/javascripts/service_ping_consent.js b/app/assets/javascripts/service_ping_consent.js
deleted file mode 100644
index 7d6e7e81f3b..00000000000
--- a/app/assets/javascripts/service_ping_consent.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import $ from 'jquery';
-import { createAlert } from '~/alert';
-import axios from './lib/utils/axios_utils';
-import { parseBoolean } from './lib/utils/common_utils';
-import { __ } from './locale';
-
-export default () => {
- $('body').on('click', '.js-service-ping-consent-action', (e) => {
- e.preventDefault();
- e.stopImmediatePropagation(); // overwrite rails listener
-
- const { url, checkEnabled, servicePingEnabled } = e.target.dataset;
- const data = {
- application_setting: {
- version_check_enabled: parseBoolean(checkEnabled),
- service_ping_enabled: parseBoolean(servicePingEnabled),
- },
- };
-
- const hideConsentMessage = () =>
- document.querySelector('.service-ping-consent-message .js-close')?.click();
-
- axios
- .put(url, data)
- .then(() => {
- hideConsentMessage();
- })
- .catch(() => {
- hideConsentMessage();
- createAlert({
- message: __('Something went wrong. Try again later.'),
- });
- });
- });
-};
diff --git a/app/assets/javascripts/sessions/new/components/update_email.vue b/app/assets/javascripts/sessions/new/components/update_email.vue
index 124cd671169..f9b9a063808 100644
--- a/app/assets/javascripts/sessions/new/components/update_email.vue
+++ b/app/assets/javascripts/sessions/new/components/update_email.vue
@@ -1,6 +1,7 @@
<script>
import { GlForm, GlFormGroup, GlFormInput, GlButton } from '@gitlab/ui';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
+import { isUserEmail } from '~/lib/utils/forms';
import axios from '~/lib/utils/axios_utils';
import {
I18N_EMAIL,
@@ -10,7 +11,6 @@ import {
I18N_EMAIL_INVALID,
I18N_UPDATE_EMAIL_SUCCESS,
I18N_GENERIC_ERROR,
- EMAIL_REGEXP,
SUCCESS_RESPONSE,
FAILURE_RESPONSE,
} from '../constants';
@@ -48,7 +48,7 @@ export default {
return '';
}
- if (!EMAIL_REGEXP.test(this.email)) {
+ if (!isUserEmail(this.email)) {
return I18N_EMAIL_INVALID;
}
diff --git a/app/assets/javascripts/sessions/new/constants.js b/app/assets/javascripts/sessions/new/constants.js
index e9bd26099aa..eb2bc25d958 100644
--- a/app/assets/javascripts/sessions/new/constants.js
+++ b/app/assets/javascripts/sessions/new/constants.js
@@ -25,6 +25,5 @@ export const I18N_UPDATE_EMAIL_SUCCESS = s__(
);
export const VERIFICATION_CODE_REGEX = /^\d{6}$/;
-export const EMAIL_REGEXP = /^[^@\s]+@[^@\s]+$/; // Taken from DeviseEmailValidator
export const SUCCESS_RESPONSE = 'success';
export const FAILURE_RESPONSE = 'failure';
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
index 609a9355d20..745122afb4a 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
@@ -47,7 +47,6 @@ export default {
class="js-sidebar-dropdown-toggle edit-link btn gl-text-gray-900! gl-ml-auto hide-collapsed btn-default btn-sm gl-button btn-default-tertiary float-right"
href="#"
data-test-id="edit-link"
- data-qa-selector="edit_link"
data-track-action="click_edit_button"
data-track-label="right_sidebar"
data-track-property="assignee"
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index 9d6a8bf47e0..55c5b04dbe3 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -58,7 +58,6 @@ export default {
type="button"
class="gl-button btn-link gl-reset-color!"
data-testid="assign-yourself"
- data-qa-selector="assign_yourself_button"
@click="assignSelf"
>
{{ __('assign yourself') }}
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue
index 5ca18969f0b..06ac2cb715d 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue
@@ -58,7 +58,6 @@ export default {
<template v-for="label in sortedSelectedLabels" v-else>
<gl-label
:key="label.id"
- data-qa-selector="selected_label_content"
:data-qa-label-name="label.title"
:title="label.title"
:description="label.description"
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
index 377200ab804..3d9a5893c67 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
@@ -81,7 +81,6 @@ export default {
:value="searchKey"
:placeholder="__('Search labels')"
:disabled="labelsFetchInProgress"
- data-qa-selector="dropdown_input_field"
data-testid="dropdown-input-field"
@input="$emit('input', $event)"
@keydown.enter="$emit('searchEnter', $event)"
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
index c9e651370f9..1497b229a59 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
@@ -27,11 +27,10 @@ export default {
<gl-sprintf
:message="
__(
- 'Unlock this %{issuableDisplayName}? %{strongStart}Everyone%{strongEnd} will be able to comment.',
+ 'Unlock this discussion? %{strongStart}Everyone%{strongEnd} will be able to comment.',
)
"
>
- <template #issuableDisplayName>{{ issuableDisplayName }}</template>
<template #strong="{ content }"
><strong>{{ content }}</strong></template
>
@@ -42,11 +41,10 @@ export default {
<gl-sprintf
:message="
__(
- 'Lock this %{issuableDisplayName}? Only %{strongStart}project members%{strongEnd} will be able to comment.',
+ 'Lock this discussion? Only %{strongStart}project members%{strongEnd} will be able to comment.',
)
"
>
- <template #issuableDisplayName>{{ issuableDisplayName }}</template>
<template #strong="{ content }"
><strong>{{ content }}</strong></template
>
diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
index 165499696de..977d1d6f668 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -50,12 +50,12 @@ export default {
issueCapitalized: __('Issue'),
mergeRequest: __('merge request'),
mergeRequestCapitalized: __('Merge request'),
- lockingMergeRequest: __('Locking %{issuableDisplayName}'),
- unlockingMergeRequest: __('Unlocking %{issuableDisplayName}'),
- lockMergeRequest: __('Lock %{issuableDisplayName}'),
- unlockMergeRequest: __('Unlock %{issuableDisplayName}'),
- lockedMessage: __('%{issuableDisplayName} locked.'),
- unlockedMessage: __('%{issuableDisplayName} unlocked.'),
+ lockingMergeRequest: __('Locking discussion'),
+ unlockingMergeRequest: __('Unlocking discussion'),
+ lockMergeRequest: __('Lock discussion'),
+ unlockMergeRequest: __('Unlock discussion'),
+ lockedMessage: __('Discussion locked.'),
+ unlockedMessage: __('Discussion unlocked.'),
},
data() {
return {
@@ -152,7 +152,7 @@ export default {
})
.catch(() => {
const alertMessage = __(
- 'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
+ 'Something went wrong trying to change the locked state of the discussion',
);
createAlert({
message: sprintf(alertMessage, { issuableDisplayName: this.issuableDisplayName }),
@@ -170,9 +170,14 @@ export default {
</script>
<template>
- <li v-if="isMovedMrSidebar && isIssuable" class="gl-dropdown-item">
- <button type="button" class="dropdown-item" data-testid="issuable-lock" @click="toggleLocked">
- <span class="gl-dropdown-item-text-wrapper">
+ <li v-if="isMovedMrSidebar && isIssuable" class="gl-new-dropdown-item">
+ <button
+ type="button"
+ class="gl-new-dropdown-item-content"
+ data-testid="issuable-lock"
+ @click="toggleLocked"
+ >
+ <span class="gl-new-dropdown-item-text-wrapper">
<template v-if="isLoading">
<gl-loading-icon inline size="sm" /> {{ lockToggleInProgressText }}
</template>
diff --git a/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue b/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue
index 34a4da946d6..ea8e0c4b950 100644
--- a/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue
+++ b/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue
@@ -1,26 +1,20 @@
<script>
import {
GlIcon,
- GlLoadingIcon,
- GlDropdown,
- GlDropdownForm,
- GlDropdownItem,
- GlSearchBoxByType,
GlButton,
+ GlCollapsibleListbox,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
-
+import { debounce } from 'lodash';
+import { __ } from '~/locale';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import axios from '~/lib/utils/axios_utils';
export default {
components: {
GlIcon,
- GlLoadingIcon,
- GlDropdown,
- GlDropdownForm,
- GlDropdownItem,
- GlSearchBoxByType,
GlButton,
+ GlCollapsibleListbox,
},
directives: {
GlTooltip,
@@ -51,82 +45,58 @@ export default {
},
data() {
return {
- projectsListLoading: false,
- projectsListLoadFailed: false,
- searchKey: '',
projects: [],
- selectedProject: null,
- projectItemClick: false,
+ projectsList: [],
+ selectedProjects: [],
+ noResultsText: '',
+ isSearching: false,
};
},
- computed: {
- hasNoSearchResults() {
- return Boolean(
- !this.projectsListLoading &&
- !this.projectsListLoadFailed &&
- this.searchKey &&
- !this.projects.length,
- );
- },
- failedToLoadResults() {
- return !this.projectsListLoading && this.projectsListLoadFailed;
- },
- },
- watch: {
- searchKey(value = '') {
- this.fetchProjects(value);
- },
+ mounted() {
+ this.fetchProjects = debounce(this.fetchProjects, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
methods: {
- fetchProjects(search = '') {
- this.projectsListLoading = true;
- this.projectsListLoadFailed = false;
- return axios
- .get(this.projectsFetchPath, {
+ triggerSearch() {
+ this.$refs.dropdown.search();
+ },
+ async fetchProjects(search = '') {
+ this.isSearching = true;
+
+ try {
+ const { data } = await axios.get(this.projectsFetchPath, {
params: {
search,
},
- })
- .then(({ data }) => {
- this.projects = data;
- this.$refs.searchInput.focusInput();
- })
- .catch(() => {
- this.projectsListLoadFailed = true;
- })
- .finally(() => {
- this.projectsListLoading = false;
});
- },
- isSelectedProject(project) {
- if (this.selectedProject) {
- return this.selectedProject.id === project.id;
- }
- return false;
- },
- /**
- * This handler is to prevent dropdown
- * from closing when an item is selected
- * and emit an event only when dropdown closes.
- */
- handleDropdownHide(e) {
- if (this.projectItemClick) {
- e.preventDefault();
- this.projectItemClick = false;
- } else {
- this.$emit('dropdown-close');
+ this.projects = data;
+ this.projectsList = data.map((item) => ({
+ value: item.id,
+ text: item.name_with_namespace,
+ }));
+
+ if (!this.projectsList.length) {
+ this.noResultsText = __('No matching results');
+ }
+ } catch (e) {
+ this.noResultsText = __('Failed to load projects');
+ } finally {
+ this.isSearching = false;
}
},
- handleDropdownCloseClick() {
- this.$refs.dropdown.hide();
- },
- handleProjectSelect(project) {
- this.selectedProject = project.id === this.selectedProject?.id ? null : project;
- this.projectItemClick = true;
+ handleProjectSelect(items) {
+ // hack: simulate a single select to prevent the dropdown from closing
+ // todo: switch back to single select when https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2363 is fixed
+ this.selectedProjects = [items[items.length - 1]];
},
handleMoveClick() {
- this.$refs.dropdown.hide();
- this.$emit('move-issuable', this.selectedProject);
+ this.$refs.dropdown.close();
+ this.$emit(
+ 'move-issuable',
+ this.projects.find((item) => item.id === this.selectedProjects[0]),
+ );
+ },
+ handleDropdownHide() {
+ this.$emit('dropdown-close');
},
},
};
@@ -143,79 +113,45 @@ export default {
>
<gl-icon name="arrow-right" />
</div>
- <gl-dropdown
+ <gl-collapsible-listbox
ref="dropdown"
+ v-model="selectedProjects"
+ :items="projectsList"
:block="true"
- :disabled="moveInProgress || disabled"
- class="hide-collapsed"
- toggle-class="js-sidebar-dropdown-toggle"
- @shown="fetchProjects"
- @hide="handleDropdownHide"
+ :multiple="true"
+ :searchable="true"
+ :searching="isSearching"
+ :search-placeholder="__('Search project')"
+ :no-results-text="noResultsText"
+ :header-text="dropdownButtonTitle"
+ @hidden="handleDropdownHide"
+ @shown="triggerSearch"
+ @search="fetchProjects"
+ @select="handleProjectSelect"
>
- <template #button-content
- ><gl-loading-icon v-if="moveInProgress" size="sm" class="gl-mr-3" />{{
- dropdownButtonTitle
- }}</template
- >
- <gl-dropdown-form class="gl-pt-0">
- <div
- data-testid="header"
- class="gl-display-flex gl-pb-3 gl-border-1 gl-border-b-solid gl-border-gray-100"
- >
- <span class="gl-flex-grow-1 gl-text-center gl-font-weight-bold gl-py-1">{{
- dropdownHeaderTitle
- }}</span>
- <gl-button
- variant="link"
- icon="close"
- class="gl-mr-2 gl-w-auto! gl-p-2!"
- :aria-label="__('Close')"
- @click.prevent="handleDropdownCloseClick"
- />
- </div>
- <gl-search-box-by-type
- ref="searchInput"
- v-model.trim="searchKey"
- :placeholder="__('Search project')"
- :debounce="300"
- />
- <div data-testid="content" class="dropdown-content">
- <gl-loading-icon v-if="projectsListLoading" size="lg" class="gl-p-5" />
- <ul v-else>
- <gl-dropdown-item
- v-for="project in projects"
- :key="project.id"
- is-check-item
- :is-checked="isSelectedProject(project)"
- @click.stop.prevent="handleProjectSelect(project)"
- >{{ project.name_with_namespace }}</gl-dropdown-item
- >
- </ul>
- <div v-if="hasNoSearchResults" class="gl-text-center gl-p-3">
- {{ __('No matching results') }}
- </div>
- <div
- v-if="failedToLoadResults"
- data-testid="failed-load-results"
- class="gl-text-center gl-p-3"
- >
- {{ __('Failed to load projects') }}
- </div>
- </div>
- <div
- data-testid="footer"
- class="gl-pt-3 gl-px-3 gl-border-1 gl-border-t-solid gl-border-gray-100"
+ <template #toggle>
+ <gl-button
+ :loading="moveInProgress"
+ size="medium"
+ class="gl-w-full js-sidebar-dropdown-toggle hide-collapsed"
+ data-testid="dropdown-button"
+ :disabled="moveInProgress || disabled"
+ >{{ dropdownButtonTitle }}</gl-button
>
+ </template>
+ <template #footer>
+ <div data-testid="footer" class="gl-p-3">
<gl-button
category="primary"
variant="confirm"
- :disabled="!Boolean(selectedProject)"
- class="gl-w-full issuable-move-button"
+ :disabled="!Boolean(selectedProjects.length)"
+ class="gl-w-full"
+ data-testid="dropdown-move-button"
@click="handleMoveClick"
>{{ __('Move') }}</gl-button
>
</div>
- </gl-dropdown-form>
- </gl-dropdown>
+ </template>
+ </gl-collapsible-listbox>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
index b764d660d63..40893f10109 100644
--- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
+++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
@@ -49,9 +49,6 @@ export default {
error,
});
},
- context: {
- isSingleRequest: true,
- },
},
},
computed: {
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue
index a7db3b3d09f..d8e61c135e7 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue
@@ -40,7 +40,6 @@ export default {
:width="imgSize"
:class="`s${imgSize}`"
class="avatar avatar-inline m-0"
- data-qa-selector="avatar_image"
/>
<gl-icon v-if="hasMergeIcon" name="warning-solid" aria-hidden="true" class="merge-icon" />
</span>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
index ee9edd6a022..1bcbf2167e9 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
@@ -62,7 +62,11 @@ export default {
<collapsed-reviewer-list :users="sortedReviewers" :issuable-type="issuableType" />
<div class="value hide-collapsed">
- <span v-if="hasNoUsers" class="no-value" data-testid="no-value">
+ <span
+ v-if="hasNoUsers"
+ class="no-value gl-display-flex gl-font-base gl-line-height-normal"
+ data-testid="no-value"
+ >
{{ __('None') }}
<template v-if="editable">
-
@@ -71,7 +75,6 @@ export default {
variant="link"
class="gl-ml-2"
data-testid="assign-yourself"
- data-qa-selector="assign_yourself_button"
@click="assignSelf"
>
<span class="gl-text-gray-500 gl-hover-text-blue-800">{{ __('assign yourself') }}</span>
diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity_widget.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity_widget.vue
index e2a3efa096f..e14fee5bfb8 100644
--- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity_widget.vue
+++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity_widget.vue
@@ -112,7 +112,7 @@ export default {
</script>
<template>
- <div ref="sidebarSeverity" class="block">
+ <div ref="sidebarSeverity" class="block" data-testid="severity-block-container">
<sidebar-editable-item
ref="toggle"
:loading="isUpdating"
@@ -131,7 +131,7 @@ export default {
</gl-sprintf>
</gl-tooltip>
</div>
- <div class="hide-collapsed">
+ <div class="hide-collapsed" data-testid="incident-severity">
<severity-token :severity="selectedItem" />
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue b/app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue
index ba0bf783315..7ce1ceb4bb8 100644
--- a/app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'ToggleSidebar',
@@ -10,6 +11,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
collapsed: {
type: Boolean,
@@ -29,7 +31,13 @@ export default {
return this.collapsed ? 'chevron-double-lg-left' : 'chevron-double-lg-right';
},
allCssClasses() {
- return [this.cssClasses, { 'js-sidebar-collapsed': this.collapsed }];
+ return [
+ this.cssClasses,
+ {
+ 'js-sidebar-collapsed': this.collapsed,
+ 'gl-mt-2': this.glFeatures.notificationsTodosButtons,
+ },
+ ];
},
},
watch: {
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 4b6dbdcc2c9..12e60a9ed4e 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -799,8 +799,7 @@ export function mountAssigneesDropdown() {
});
}
-const isAssigneesWidgetShown =
- (isInIssuePage() || isInDesignPage() || isInMRPage()) && gon.features.issueAssigneesWidget;
+const isAssigneesWidgetShown = isInIssuePage() || isInDesignPage() || isInMRPage();
export function mountSidebar(mediator, store) {
mountSidebarTodoWidget();
diff --git a/app/assets/javascripts/sidebar/queries/constants.js b/app/assets/javascripts/sidebar/queries/constants.js
index 0844abc4599..6bcdc01a003 100644
--- a/app/assets/javascripts/sidebar/queries/constants.js
+++ b/app/assets/javascripts/sidebar/queries/constants.js
@@ -12,8 +12,8 @@ import {
WORKSPACE_PROJECT,
} from '~/issues/constants';
import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
-import abuseReportLabelsQuery from '~/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql';
-import createAbuseReportLabelMutation from '~/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql';
+import abuseReportLabelsQuery from '~/admin/abuse_report/graphql/abuse_report_labels.query.graphql';
+import createAbuseReportLabelMutation from '~/admin/abuse_report/graphql/create_abuse_report_label.mutation.graphql';
import createGroupOrProjectLabelMutation from '../components/labels/labels_select_widget/graphql/create_label.mutation.graphql';
import updateTestCaseLabelsMutation from '../components/labels/labels_select_widget/graphql/update_test_case_labels.mutation.graphql';
import epicLabelsQuery from '../components/labels/labels_select_widget/graphql/epic_labels.query.graphql';
diff --git a/app/assets/javascripts/silent_mode_settings/components/app.vue b/app/assets/javascripts/silent_mode_settings/components/app.vue
index 2dd0449448c..a151492c75c 100644
--- a/app/assets/javascripts/silent_mode_settings/components/app.vue
+++ b/app/assets/javascripts/silent_mode_settings/components/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlToggle, GlBadge } from '@gitlab/ui';
+import { GlToggle } from '@gitlab/ui';
import { updateApplicationSettings } from '~/rest_api';
import { createAlert } from '~/alert';
import toast from '~/vue_shared/plugins/global_toast';
@@ -13,11 +13,9 @@ export default {
saveError: s__('SilentMode|There was an error updating the Silent Mode Settings.'),
enabled: __('enabled'),
disabled: __('disabled'),
- experiment: __('Experiment'),
},
components: {
GlToggle,
- GlBadge,
},
props: {
isSilentModeEnabled: {
@@ -62,9 +60,5 @@ export default {
:label="$options.i18n.toggleLabel"
:is-loading="isLoading"
@change="updateSilentModeSettings"
- >
- <template #label
- >{{ $options.i18n.toggleLabel }} <gl-badge>{{ $options.i18n.experiment }}</gl-badge></template
- >
- </gl-toggle>
+ />
</template>
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 11896a75798..1e01da795e8 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -40,9 +40,12 @@ export default class SingleFileDiff {
this.$chevronDownIcon.removeClass('gl-display-none');
}
- $('.js-file-title, .click-to-expand', this.file).on('click', (e) => {
+ $('.js-file-title', this.file).on('click', (e) => {
this.toggleDiff($(e.target));
});
+ $('.click-to-expand', this.file).on('click', (e) => {
+ this.toggleDiff($(e.currentTarget));
+ });
}
toggleDiff($target, cb) {
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index 56ea931fc8c..573b8777ade 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -213,7 +213,7 @@ export default {
</script>
<template>
<div class="detail-page-header">
- <div class="detail-page-header-body">
+ <div class="detail-page-header-body gl-align-items-baseline">
<div
class="snippet-box has-tooltip d-flex align-items-center gl-mr-2 mb-1"
data-testid="snippet-container"
@@ -235,12 +235,20 @@ export default {
<template #author>
<a :href="snippet.author.webUrl" class="d-inline">
<gl-avatar :size="24" :src="snippet.author.avatarUrl" />
- <span class="bold">{{ snippet.author.name }}</span>
+ <span class="bold gl-display-none gl-sm-display-inline">{{
+ snippet.author.name
+ }}</span>
+ <strong
+ v-if="snippet.author.username"
+ data-testid="authored-username"
+ class="gl-display-inline gl-sm-display-none!"
+ >@{{ snippet.author.username }}</strong
+ >
</a>
<gl-emoji
v-if="snippet.author.status"
v-gl-tooltip
- class="gl-vertical-align-baseline font-size-inherit gl-mr-1"
+ class="gl-vertical-align-baseline gl-reset-font-size gl-mr-1"
:title="snippet.author.status.message"
:data-name="snippet.author.status.emoji"
/>
@@ -249,7 +257,7 @@ export default {
</div>
</div>
- <div v-if="hasPersonalSnippetActions" class="detail-page-header-actions">
+ <div v-if="hasPersonalSnippetActions" class="detail-page-header-actions gl-align-self-start">
<div class="d-none d-sm-flex">
<template v-for="(action, index) in personalSnippetActions">
<div
diff --git a/app/assets/javascripts/super_sidebar/components/create_menu.vue b/app/assets/javascripts/super_sidebar/components/create_menu.vue
index 279e689bd8d..e8410a51905 100644
--- a/app/assets/javascripts/super_sidebar/components/create_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/create_menu.vue
@@ -70,7 +70,6 @@ export default {
:toggle-text="$options.i18n.createNew"
:toggle-id="$options.toggleId"
:dropdown-offset="dropdownOffset"
- data-qa-selector="new_menu_toggle"
data-testid="new-menu-toggle"
@shown="dropdownOpen = true"
@hidden="dropdownOpen = false"
diff --git a/app/assets/javascripts/super_sidebar/components/flyout_menu.vue b/app/assets/javascripts/super_sidebar/components/flyout_menu.vue
index e73b9b275ee..414e4a54a8e 100644
--- a/app/assets/javascripts/super_sidebar/components/flyout_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/flyout_menu.vue
@@ -139,8 +139,8 @@ export default {
:key="item.id"
:item="item"
:is-flyout="true"
- @pin-add="(itemId) => $emit('pin-add', itemId)"
- @pin-remove="(itemId) => $emit('pin-remove', itemId)"
+ @pin-add="(itemId, itemTitle) => $emit('pin-add', itemId, itemTitle)"
+ @pin-remove="(itemId, itemTitle) => $emit('pin-remove', itemId, itemTitle)"
/>
</ul>
<svg
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
index b85b163cea9..1a681d6e9bd 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
@@ -2,7 +2,7 @@
import { debounce } from 'lodash';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { GlDisclosureDropdownGroup, GlLoadingIcon } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import Tracking from '~/tracking';
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
index 61fa360c41f..e6137bda401 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
@@ -15,7 +15,14 @@ import { truncate } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { sprintf } from '~/locale';
-import { ARROW_DOWN_KEY, ARROW_UP_KEY, END_KEY, HOME_KEY, ESC_KEY } from '~/lib/utils/keys';
+import {
+ ARROW_DOWN_KEY,
+ ARROW_UP_KEY,
+ END_KEY,
+ HOME_KEY,
+ ESC_KEY,
+ NUMPAD_ENTER_KEY,
+} from '~/lib/utils/keys';
import {
COMMAND_PALETTE,
MIN_SEARCH_TERM,
@@ -215,6 +222,8 @@ export default {
this.focusNextItem(event, elements, 1);
} else if (code === ESC_KEY) {
this.$refs.searchModal.close();
+ } else if (code === NUMPAD_ENTER_KEY) {
+ event.target?.firstChild.click();
} else {
stop = false;
}
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue
index 9167be5c1cc..914d3c393f5 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue
@@ -1,5 +1,6 @@
<script>
import { GlDisclosureDropdownGroup } from '@gitlab/ui';
+import { kebabCase } from 'lodash';
import { PLACES } from '~/vue_shared/global_search/constants';
import { TRACKING_UNKNOWN_ID, TRACKING_UNKNOWN_PANEL } from '~/super_sidebar/constants';
import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from '../command_palette/constants';
@@ -20,7 +21,7 @@ export default {
group() {
return {
name: this.$options.i18n.PLACES,
- items: this.contextSwitcherLinks.map(({ title, link }) => ({
+ items: this.contextSwitcherLinks.map(({ title, link, ...rest }) => ({
text: title,
href: link,
extraAttrs: {
@@ -35,6 +36,12 @@ export default {
// QA attributes
'data-testid': 'places-item-link',
'data-qa-places-item': title,
+
+ // Any other data- attributes (e.g., for @rails/ujs)
+ ...Object.entries(rest).reduce((acc, [name, value]) => {
+ if (name.startsWith('data')) acc[kebabCase(name)] = value;
+ return acc;
+ }, {}),
},
})),
};
diff --git a/app/assets/javascripts/super_sidebar/components/menu_section.vue b/app/assets/javascripts/super_sidebar/components/menu_section.vue
index 91b781b8235..a672e254004 100644
--- a/app/assets/javascripts/super_sidebar/components/menu_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/menu_section.vue
@@ -145,8 +145,8 @@ export default {
:items="item.items"
@mouseover="isMouseOverFlyout = true"
@mouseleave="isMouseOverFlyout = false"
- @pin-add="(itemId) => $emit('pin-add', itemId)"
- @pin-remove="(itemId) => $emit('pin-remove', itemId)"
+ @pin-add="(itemId, itemTitle) => $emit('pin-add', itemId, itemTitle)"
+ @pin-remove="(itemId, itemTitle) => $emit('pin-remove', itemId, itemTitle)"
/>
<gl-collapse
@@ -162,8 +162,8 @@ export default {
v-for="subItem of item.items"
:key="`${item.title}-${subItem.title}`"
:item="subItem"
- @pin-add="(itemId) => $emit('pin-add', itemId)"
- @pin-remove="(itemId) => $emit('pin-remove', itemId)"
+ @pin-add="(itemId, itemTitle) => $emit('pin-add', itemId, itemTitle)"
+ @pin-remove="(itemId, itemTitle) => $emit('pin-remove', itemId, itemTitle)"
/>
</ul>
</slot>
diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue
index 5416f86abeb..3ae33bf8b37 100644
--- a/app/assets/javascripts/super_sidebar/components/nav_item.vue
+++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue
@@ -70,14 +70,16 @@ export default {
return {
isMouseIn: false,
canClickPinButton: false,
- pillCount: this.item.pill_count,
};
},
computed: {
+ pillData() {
+ return this.item.pill_count;
+ },
hasPill() {
return (
- Number.isFinite(this.pillCount) ||
- (typeof this.pillCount === 'string' && this.pillCount !== '')
+ Number.isFinite(this.pillData) ||
+ (typeof this.pillData === 'string' && this.pillData !== '')
);
},
isPinnable() {
@@ -188,12 +190,22 @@ export default {
eventHub.$off('updatePillValue', this.updatePillValue);
},
methods: {
+ pinAdd() {
+ this.$emit('pin-add', this.item.id, this.item.title);
+ },
+ pinRemove() {
+ this.$emit('pin-remove', this.item.id, this.item.title);
+ },
togglePointerEvents() {
this.canClickPinButton = this.isMouseIn;
},
updatePillValue({ value, itemId }) {
if (this.item.id === itemId) {
- this.pillCount = value;
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/428246
+ // fixing this linting issue is causing the pills not to async update
+ //
+ // eslint-disable-next-line vue/no-mutating-props
+ this.item.pill_count = value;
}
},
},
@@ -214,7 +226,6 @@ export default {
class="gl-relative gl-display-flex gl-align-items-center gl-min-h-7 gl-gap-3 gl-mb-1 gl-py-2 gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-focus--focus show-on-focus-or-hover--control hide-on-focus-or-hover--control"
:class="computedLinkClasses"
data-testid="nav-item-link"
- data-qa-selector="nav_item_link"
>
<div
:class="[isActive ? 'gl-opacity-10' : 'gl-opacity-0']"
@@ -258,7 +269,7 @@ export default {
'hide-on-focus-or-hover--target transition-opacity-on-hover--target': isPinnable,
}"
>
- {{ pillCount }}
+ {{ pillData }}
</gl-badge>
</span>
</component>
@@ -273,7 +284,7 @@ export default {
data-testid="nav-item-unpin"
icon="thumbtack-solid"
size="small"
- @click="$emit('pin-remove', item.id)"
+ @click="pinRemove"
@transitionend="togglePointerEvents"
/>
<gl-button
@@ -286,7 +297,7 @@ export default {
data-testid="nav-item-pin"
icon="thumbtack"
size="small"
- @click="$emit('pin-add', item.id)"
+ @click="pinAdd"
@transitionend="togglePointerEvents"
/>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/pinned_section.vue b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
index ea3e9e9df1f..05040218164 100644
--- a/app/assets/javascripts/super_sidebar/components/pinned_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
@@ -84,8 +84,8 @@ export default {
return { ...i, title };
});
},
- onPinRemove(itemId) {
- this.$emit('pin-remove', itemId);
+ onPinRemove(itemId, itemTitle) {
+ this.$emit('pin-remove', itemId, itemTitle);
},
},
};
@@ -113,7 +113,7 @@ export default {
:key="item.id"
:item="item"
is-in-pinned-section
- @pin-remove="onPinRemove"
+ @pin-remove="onPinRemove(item.id, item.title)"
/>
</draggable>
<li v-else class="gl-text-secondary gl-font-sm gl-py-3" style="margin-left: 2.5rem">
diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
index 772072c0996..c04addf5262 100644
--- a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
@@ -1,6 +1,7 @@
<script>
-import * as Sentry from '@sentry/browser';
import { GlBreakpointInstance, breakpoints } from '@gitlab/ui/dist/utils';
+import { s__, sprintf } from '~/locale';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { PANELS_WITH_PINS } from '../constants';
@@ -16,7 +17,10 @@ export default {
PinnedSection,
},
mixins: [glFeatureFlagsMixin()],
-
+ i18n: {
+ pinAdded: s__('Navigation|%{title} added to pinned items'),
+ pinRemoved: s__('Navigation|%{title} removed from pinned items'),
+ },
provide() {
return {
pinnedItemIds: this.changedPinnedItemIds,
@@ -111,12 +115,22 @@ export default {
window.removeEventListener('resize', this.decideFlyoutState);
},
methods: {
- createPin(itemId) {
+ createPin(itemId, itemTitle) {
this.changedPinnedItemIds.ids.push(itemId);
+ this.$toast.show(
+ sprintf(this.$options.i18n.pinAdded, {
+ title: itemTitle,
+ }),
+ );
this.updatePins();
},
- destroyPin(itemId) {
+ destroyPin(itemId, itemTitle) {
this.changedPinnedItemIds.ids = this.changedPinnedItemIds.ids.filter((id) => id !== itemId);
+ this.$toast.show(
+ sprintf(this.$options.i18n.pinRemoved, {
+ title: itemTitle,
+ }),
+ );
this.updatePins();
},
movePin(fromId, toId, isDownwards) {
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
index 88ea4d828b7..3c47245a1a6 100644
--- a/app/assets/javascripts/super_sidebar/components/user_bar.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -115,6 +115,7 @@ export default {
<gl-badge
v-if="sidebarData.gitlab_com_and_canary"
variant="success"
+ data-testid="canary-badge-link"
:href="sidebarData.canary_toggle_com_url"
size="sm"
>
diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue
index 891e883b6c0..5712b716f48 100644
--- a/app/assets/javascripts/super_sidebar/components/user_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue
@@ -8,7 +8,6 @@ import {
} from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, __, sprintf } from '~/locale';
-import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import Tracking from '~/tracking';
import PersistentUserCallout from '~/persistent_user_callout';
import { USER_MENU_TRACKING_DEFAULTS, DROPDOWN_Y_OFFSET, IMPERSONATING_OFFSET } from '../constants';
@@ -39,14 +38,13 @@ export default {
GlDisclosureDropdownGroup,
GlDisclosureDropdownItem,
GlButton,
- NewNavToggle,
UserMenuProfileItem,
},
directives: {
SafeHtml,
},
mixins: [Tracking.mixin()],
- inject: ['toggleNewNavEndpoint', 'isImpersonating'],
+ inject: ['isImpersonating'],
props: {
data: {
required: true,
@@ -301,13 +299,6 @@ export default {
/>
</gl-disclosure-dropdown-group>
- <gl-disclosure-dropdown-group bordered>
- <template #group-label>
- <span class="gl-font-sm">{{ $options.i18n.newNavigation.sectionTitle }}</span>
- </template>
- <new-nav-toggle :endpoint="toggleNewNavEndpoint" enabled new-navigation />
- </gl-disclosure-dropdown-group>
-
<gl-disclosure-dropdown-group
v-if="data.can_sign_out"
bordered
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
index f9e488ea5ee..9e540175b48 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { GlToast } from '@gitlab/ui';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import { initStatusTriggers } from '../header';
import { JS_TOGGLE_EXPAND_CLASS } from './constants';
@@ -10,6 +11,8 @@ import {
import SuperSidebar from './components/super_sidebar.vue';
import SuperSidebarToggle from './components/super_sidebar_toggle.vue';
+Vue.use(GlToast);
+
const getTrialStatusWidgetData = (sidebarData) => {
if (sidebarData.trial_status_widget_data_attrs && sidebarData.trial_status_popover_data_attrs) {
const {
@@ -63,13 +66,7 @@ export const initSuperSidebar = () => {
if (!el) return false;
- const {
- rootPath,
- sidebar,
- toggleNewNavEndpoint,
- forceDesktopExpandedSidebar,
- commandPalette,
- } = el.dataset;
+ const { rootPath, sidebar, forceDesktopExpandedSidebar, commandPalette } = el.dataset;
bindSuperSidebarCollapsedEvents(forceDesktopExpandedSidebar);
initSuperSidebarCollapsedState(parseBoolean(forceDesktopExpandedSidebar));
@@ -95,7 +92,6 @@ export const initSuperSidebar = () => {
name: 'SuperSidebarRoot',
provide: {
rootPath,
- toggleNewNavEndpoint,
isImpersonating,
...getTrialStatusWidgetData(sidebarData),
commandPaletteCommands,
diff --git a/app/assets/javascripts/super_sidebar/utils.js b/app/assets/javascripts/super_sidebar/utils.js
index d2fb72adb85..3d6eef62ad2 100644
--- a/app/assets/javascripts/super_sidebar/utils.js
+++ b/app/assets/javascripts/super_sidebar/utils.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import AccessorUtilities from '~/lib/utils/accessor';
import { FREQUENT_ITEMS, FIFTEEN_MINUTES_IN_MS } from '~/frequent_items/constants';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/tags/components/delete_tag_modal.vue b/app/assets/javascripts/tags/components/delete_tag_modal.vue
index c4f9db70d2a..9a0cc026223 100644
--- a/app/assets/javascripts/tags/components/delete_tag_modal.vue
+++ b/app/assets/javascripts/tags/components/delete_tag_modal.vue
@@ -151,7 +151,6 @@ export default {
ref="deleteTagButton"
:disabled="deleteButtonDisabled"
variant="danger"
- data-qa-selector="delete_tag_confirmation_button"
data-testid="delete-tag-confirmation-button"
@click="submitForm"
>{{ buttonText }}</gl-button
diff --git a/app/assets/javascripts/terraform/components/init_command_modal.vue b/app/assets/javascripts/terraform/components/init_command_modal.vue
index 74c41700f43..7962c8573df 100644
--- a/app/assets/javascripts/terraform/components/init_command_modal.vue
+++ b/app/assets/javascripts/terraform/components/init_command_modal.vue
@@ -40,15 +40,14 @@ export default {
},
methods: {
getModalInfoCopyStr() {
- const stateNameEncoded = this.stateName
- ? encodeURIComponent(this.stateName)
- : '<YOUR-STATE-NAME>';
+ const stateNameEncoded = this.stateName ? encodeURIComponent(this.stateName) : 'default';
return `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
+export TF_STATE_NAME=${stateNameEncoded}
terraform init \\
- -backend-config="address=${this.terraformApiUrl}/${stateNameEncoded}" \\
- -backend-config="lock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\
- -backend-config="unlock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\
+ -backend-config="address=${this.terraformApiUrl}/$TF_STATE_NAME" \\
+ -backend-config="lock_address=${this.terraformApiUrl}/$TF_STATE_NAME/lock" \\
+ -backend-config="unlock_address=${this.terraformApiUrl}/$TF_STATE_NAME/lock" \\
-backend-config="username=${this.username}" \\
-backend-config="password=$GITLAB_ACCESS_TOKEN" \\
-backend-config="lock_method=POST" \\
diff --git a/app/assets/javascripts/terraform/components/states_table.vue b/app/assets/javascripts/terraform/components/states_table.vue
index c88c528a632..273cd599308 100644
--- a/app/assets/javascripts/terraform/components/states_table.vue
+++ b/app/assets/javascripts/terraform/components/states_table.vue
@@ -11,14 +11,14 @@ import {
} from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__, sprintf } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import StateActions from './states_table_actions.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
GlAlert,
GlBadge,
GlLink,
@@ -198,10 +198,10 @@ export default {
:id="`terraformJobStatusContainer${item.name}`"
class="gl-my-2"
>
- <ci-badge-link
+ <ci-icon
:id="`terraformJobStatus${item.name}`"
:status="pipelineDetailedStatus(item)"
- class="gl-py-1"
+ show-status-text
/>
<gl-tooltip
diff --git a/app/assets/javascripts/time_tracking/components/timelogs_app.vue b/app/assets/javascripts/time_tracking/components/timelogs_app.vue
index 7bb9b6c52a5..8464384ac7c 100644
--- a/app/assets/javascripts/time_tracking/components/timelogs_app.vue
+++ b/app/assets/javascripts/time_tracking/components/timelogs_app.vue
@@ -1,5 +1,4 @@
<script>
-import * as Sentry from '@sentry/browser';
import {
GlButton,
GlFormGroup,
@@ -8,6 +7,7 @@ import {
GlKeysetPagination,
GlDatepicker,
} from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import { formatTimeSpent } from '~/lib/utils/datetime_utility';
import { s__ } from '~/locale';
diff --git a/app/assets/javascripts/token_access/components/inbound_token_access.vue b/app/assets/javascripts/token_access/components/inbound_token_access.vue
index 7e55f56279e..345db1752f6 100644
--- a/app/assets/javascripts/token_access/components/inbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/inbound_token_access.vue
@@ -46,12 +46,6 @@ export default {
columnClass: 'gl-w-40p',
},
{
- key: 'namespace',
- label: __('Namespace'),
- thClass: 'gl-border-t-none!',
- columnClass: 'gl-w-40p',
- },
- {
key: 'actions',
label: '',
tdClass: 'gl-text-right',
diff --git a/app/assets/javascripts/token_access/components/outbound_token_access.vue b/app/assets/javascripts/token_access/components/outbound_token_access.vue
index 43aa9b94b3a..846b0d1791f 100644
--- a/app/assets/javascripts/token_access/components/outbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/outbound_token_access.vue
@@ -54,12 +54,6 @@ export default {
columnClass: 'gl-w-40p',
},
{
- key: 'namespace',
- label: __('Namespace'),
- thClass: 'gl-border-t-none!',
- columnClass: 'gl-w-40p',
- },
- {
key: 'actions',
label: '',
tdClass: 'gl-text-right',
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 ee88b4ec339..4245b39dec1 100644
--- a/app/assets/javascripts/token_access/components/token_projects_table.vue
+++ b/app/assets/javascripts/token_access/components/token_projects_table.vue
@@ -29,9 +29,6 @@ export default {
removeProject(project) {
this.$emit('removeProject', project);
},
- namespaceFallback(namespace) {
- return namespace?.fullPath || '';
- },
},
};
</script>
@@ -50,13 +47,7 @@ export default {
</template>
<template #cell(project)="{ item }">
- <span data-testid="token-access-project-name">{{ item.name }}</span>
- </template>
-
- <template #cell(namespace)="{ item }">
- <span data-testid="token-access-project-namespace">
- {{ namespaceFallback(item.namespace) }}
- </span>
+ <span data-testid="token-access-project-name">{{ item.fullPath }}</span>
</template>
<template #cell(actions)="{ item }">
diff --git a/app/assets/javascripts/token_access/graphql/cache_config.js b/app/assets/javascripts/token_access/graphql/cache_config.js
new file mode 100644
index 00000000000..2db534b7eb5
--- /dev/null
+++ b/app/assets/javascripts/token_access/graphql/cache_config.js
@@ -0,0 +1,14 @@
+export default {
+ typePolicies: {
+ Project: {
+ fields: {
+ ciCdSettings: {
+ merge: true,
+ },
+ ciJobTokenScope: {
+ merge: true,
+ },
+ },
+ },
+ },
+};
diff --git a/app/assets/javascripts/token_access/index.js b/app/assets/javascripts/token_access/index.js
index 9258d5eba45..45bd1921dbd 100644
--- a/app/assets/javascripts/token_access/index.js
+++ b/app/assets/javascripts/token_access/index.js
@@ -2,11 +2,12 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import TokenAccessApp from './components/token_access_app.vue';
+import cacheConfig from './graphql/cache_config';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient({}, { cacheConfig }),
});
export const initTokenAccess = (containerId = 'js-ci-token-access-app') => {
diff --git a/app/assets/javascripts/tracking/constants.js b/app/assets/javascripts/tracking/constants.js
index 46278152879..bc416b20e80 100644
--- a/app/assets/javascripts/tracking/constants.js
+++ b/app/assets/javascripts/tracking/constants.js
@@ -1,5 +1,7 @@
export const SNOWPLOW_JS_SOURCE = 'gitlab-javascript';
+export const MAX_LOCAL_STORAGE_QUEUE_SIZE = 100;
+
export const DEFAULT_SNOWPLOW_OPTIONS = {
namespace: 'gl',
hostname: window.location.hostname,
@@ -15,6 +17,7 @@ export const DEFAULT_SNOWPLOW_OPTIONS = {
forms: { allow: [] },
fields: { allow: [] },
},
+ maxLocalStorageQueueSize: MAX_LOCAL_STORAGE_QUEUE_SIZE,
};
export const ACTION_ATTR_SELECTOR = '[data-track-action]';
diff --git a/app/assets/javascripts/tracking/dispatch_snowplow_event.js b/app/assets/javascripts/tracking/dispatch_snowplow_event.js
index 99e4a6aa3c7..91512292eb6 100644
--- a/app/assets/javascripts/tracking/dispatch_snowplow_event.js
+++ b/app/assets/javascripts/tracking/dispatch_snowplow_event.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import getStandardContext from './get_standard_context';
export function dispatchSnowplowEvent(
diff --git a/app/assets/javascripts/users/profile/actions/components/user_actions_app.vue b/app/assets/javascripts/users/profile/actions/components/user_actions_app.vue
index 5dfa9c67852..f994cad6881 100644
--- a/app/assets/javascripts/users/profile/actions/components/user_actions_app.vue
+++ b/app/assets/javascripts/users/profile/actions/components/user_actions_app.vue
@@ -83,7 +83,13 @@ export default {
<template>
<span>
- <gl-disclosure-dropdown icon="ellipsis_v" category="tertiary" no-caret :items="dropdownItems" />
+ <gl-disclosure-dropdown
+ data-testid="user-profile-actions"
+ icon="ellipsis_v"
+ category="tertiary"
+ no-caret
+ :items="dropdownItems"
+ />
<abuse-category-selector
v-if="reportedUserId"
:reported-user-id="reportedUserId"
diff --git a/app/assets/javascripts/users/profile/components/report_abuse_button.vue b/app/assets/javascripts/users/profile/components/report_abuse_button.vue
deleted file mode 100644
index 0e41a214888..00000000000
--- a/app/assets/javascripts/users/profile/components/report_abuse_button.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-<script>
-import { GlButton, GlTooltipDirective } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
-
-import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
-
-export default {
- name: 'ReportAbuseButton',
- components: {
- GlButton,
- AbuseCategorySelector,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- inject: ['reportedUserId', 'reportedFromUrl'],
- i18n: {
- reportAbuse: s__('ReportAbuse|Report abuse to administrator'),
- },
- data() {
- return {
- open: false,
- };
- },
- computed: {
- buttonTooltipText() {
- return this.$options.i18n.reportAbuse;
- },
- },
- methods: {
- toggleDrawer(open) {
- this.open = open;
- },
- hideTooltips() {
- this.$root.$emit(BV_HIDE_TOOLTIP);
- },
- },
-};
-</script>
-<template>
- <span>
- <gl-button
- v-gl-tooltip="buttonTooltipText"
- category="primary"
- :aria-label="buttonTooltipText"
- icon="error"
- @click="toggleDrawer(true)"
- @mouseout="hideTooltips"
- />
- <abuse-category-selector
- :reported-user-id="reportedUserId"
- :reported-from-url="reportedFromUrl"
- :show-drawer="open"
- @close-drawer="toggleDrawer(false)"
- />
- </span>
-</template>
diff --git a/app/assets/javascripts/users/profile/index.js b/app/assets/javascripts/users/profile/index.js
deleted file mode 100644
index 3ae3cc2de98..00000000000
--- a/app/assets/javascripts/users/profile/index.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import Vue from 'vue';
-import ReportAbuseButton from './components/report_abuse_button.vue';
-
-export const initReportAbuse = () => {
- const el = document.getElementById('js-report-abuse');
-
- if (!el) return false;
-
- const { reportAbusePath, reportedUserId, reportedFromUrl } = el.dataset;
-
- return new Vue({
- el,
- name: 'ReportAbuseButtonRoot',
- provide: {
- reportAbusePath,
- reportedUserId: reportedUserId ? parseInt(reportedUserId, 10) : null,
- reportedFromUrl,
- },
- render(createElement) {
- return createElement(ReportAbuseButton);
- },
- });
-};
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 974b53caa15..524f2c045e6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton, GlSprintf } from '@gitlab/ui';
import { createAlert } from '~/alert';
+import { visitUrl } from '~/lib/utils/url_utility';
import { STATUS_MERGED } from '~/issues/constants';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status';
@@ -114,6 +115,13 @@ export default {
return this.userHasApproved && !this.userCanApprove && this.mr.state !== STATUS_MERGED;
},
approvalText() {
+ // Repeating a text of this to keep i18n easier to do (vs, construcing a compound string)
+ if (this.requireSamlAuthToApprove) {
+ return this.isApproved && this.approvedBy.length > 0
+ ? s__('mrWidget|Approve additionally with SAML')
+ : s__('mrWidget|Approve with SAML');
+ }
+
return this.isApproved && this.approvedBy.length > 0
? s__('mrWidget|Approve additionally')
: s__('mrWidget|Approve');
@@ -161,14 +169,20 @@ export default {
.join(', ')
.concat('.');
},
+ requireSamlAuthToApprove() {
+ return this.mr.requireSamlAuthToApprove;
+ },
},
methods: {
approve() {
+ if (this.requireSamlAuthToApprove) {
+ this.approveWithSamlAuth();
+ return;
+ }
if (this.requirePasswordToApprove) {
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
return;
}
-
this.updateApproval(
() => this.service.approveMergeRequest(),
() =>
@@ -179,6 +193,10 @@ export default {
),
);
},
+ approveWithSamlAuth() {
+ // Intentionally direct to SAML Identity Provider for renewed authorization even if SSO session exists
+ visitUrl(this.mr.samlApprovalPath);
+ },
approveWithAuth(data) {
this.updateApproval(
() => this.service.approveMergeRequestWithAuth(data),
@@ -236,7 +254,7 @@ export default {
};
</script>
<template>
- <div class="js-mr-approvals mr-section-container mr-widget-workflow">
+ <div v-if="approvals" class="js-mr-approvals mr-section-container mr-widget-workflow">
<state-container
:is-loading="$apollo.queries.approvals.loading"
:mr="mr"
@@ -258,7 +276,7 @@ export default {
:category="action.category"
:loading="isApproving"
class="gl-mr-3"
- data-qa-selector="approve_button"
+ data-testid="approve-button"
@click="action.action"
>
{{ action.text }}
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 367395f4446..b2c44dee230 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
@@ -137,7 +137,7 @@ export default {
</script>
<template>
- <div data-qa-selector="approvals_summary_content">
+ <div data-testid="approvals-summary-content">
<span class="gl-font-weight-bold">{{ approvalLeftMessage }}</span>
<template v-if="hasApprovers">
<span v-if="approvalLeftMessage">{{ message }}</span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue
index 303952c787e..32c3f19014b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue
@@ -72,6 +72,8 @@ export default {
<template>
<merge-checks-message :check="check">
- <action-buttons v-if="!isLoading" :tertiary-buttons="tertiaryActionsButtons" />
+ <template #failed>
+ <action-buttons v-if="!isLoading" :tertiary-buttons="tertiaryActionsButtons" />
+ </template>
</merge-checks-message>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
new file mode 100644
index 00000000000..431348e1d57
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
@@ -0,0 +1,6 @@
+export const COMPONENTS = {
+ conflict: () => import('./conflicts.vue'),
+ unresolved_discussions: () => import('./unresolved_discussions.vue'),
+ need_rebase: () => import('./rebase.vue'),
+ default: () => import('./message.vue'),
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
index d0d749aa441..058b9e1fe99 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
@@ -1,10 +1,25 @@
<script>
+import { __ } from '~/locale';
import StatusIcon from '../widget/status_icon.vue';
const ICON_NAMES = {
failed: 'failed',
- allowed_to_fail: 'neutral',
- passed: 'success',
+ inactive: 'neutral',
+ success: 'success',
+};
+
+const FAILURE_REASONS = {
+ broken_status: __('Cannot merge the source into the target branch, due to a conflict.'),
+ ci_must_pass: __('Pipeline must succeed.'),
+ conflict: __('Merge conflicts must be resolved.'),
+ discussions_not_resolved: __('Unresolved discussions must be resolved.'),
+ draft_status: __('Merge request must not be draft.'),
+ not_open: __('Merge request must be open.'),
+ need_rebase: __('Merge request must be rebased, because a fast-forward merge is not possible.'),
+ not_approved: __('All required approvals must be given.'),
+ policies_denied: __('Denied licenses must be removed or approved.'),
+ merge_request_blocked: __('Merge request is blocked by another merge request.'),
+ status_checks_must_pass: __('Status checks must pass.'),
};
export default {
@@ -25,7 +40,10 @@ export default {
},
computed: {
iconName() {
- return ICON_NAMES[this.check.result];
+ return ICON_NAMES[this.check.status.toLowerCase()];
+ },
+ failureReason() {
+ return FAILURE_REASONS[this.check.identifier.toLowerCase()];
},
},
};
@@ -36,9 +54,10 @@ export default {
<div class="gl-display-flex">
<status-icon :icon-name="iconName" :level="2" />
<div class="gl-w-full gl-min-w-0">
- <div class="gl-display-flex">{{ check.failureReason }}</div>
+ <div class="gl-display-flex">{{ failureReason }}</div>
</div>
<slot></slot>
+ <slot v-if="check.status === 'FAILED'" name="failed"></slot>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js
new file mode 100644
index 00000000000..c0ac1818ffa
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js
@@ -0,0 +1,85 @@
+import createMockApollo from 'helpers/mock_apollo_helper';
+import rebaseStateQuery from '../../queries/states/rebase.query.graphql';
+import Rebase from './rebase.vue';
+
+const service = {
+ rebase: () => new Promise(() => {}),
+};
+
+const defaultRender = ({ apolloProvider, check, mr, canCreatePipelineInTargetProject }) => ({
+ components: { Rebase },
+ apolloProvider,
+ provide: {
+ canCreatePipelineInTargetProject,
+ },
+ data() {
+ return { service, mr: { ...mr, targetProjectFullPath: 'gitlab-org/gitlab' }, check };
+ },
+ template: '<rebase :mr="mr" :service="service" :check="check" />',
+});
+
+const Template = ({
+ failed,
+ pushToSourceBranch,
+ rebaseInProgress,
+ onlyAllowMergeIfPipelineSucceeds,
+ canCreatePipelineInTargetProject,
+}) => {
+ const requestHandlers = [
+ [
+ rebaseStateQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: '1',
+ mergeRequest: {
+ id: '2',
+ rebaseInProgress,
+ targetBranch: 'main',
+ userPermissions: {
+ pushToSourceBranch,
+ },
+ pipelines: {
+ nodes: [
+ {
+ id: '1',
+ project: {
+ id: '2',
+ fullPath: 'gitlab/gitlab',
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ }),
+ ],
+ ];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ return defaultRender({
+ apolloProvider,
+ check: {
+ identifier: 'need_rebase',
+ status: failed ? 'failed' : 'passed',
+ },
+ mr: { onlyAllowMergeIfPipelineSucceeds },
+ canCreatePipelineInTargetProject,
+ });
+};
+
+export const Default = Template.bind({});
+Default.args = {
+ failed: true,
+ pushToSourceBranch: true,
+ rebaseInProgress: false,
+ onlyAllowMergeIfPipelineSucceeds: false,
+ canCreatePipelineInTargetProject: false,
+};
+
+export default {
+ title: 'vue_merge_request_widget/merge_checks/rebase',
+ component: Rebase,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue
new file mode 100644
index 00000000000..72140c22a89
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue
@@ -0,0 +1,220 @@
+<script>
+import { GlModal, GlLink } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { createAlert } from '~/alert';
+import toast from '~/vue_shared/plugins/global_toast';
+import simplePoll from '~/lib/utils/simple_poll';
+import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
+import rebaseQuery from '../../queries/states/rebase.query.graphql';
+import eventHub from '../../event_hub';
+import ActionButtons from '../action_buttons.vue';
+import MergeChecksMessage from './message.vue';
+
+export default {
+ name: 'MergeChecksRebase',
+ components: {
+ GlModal,
+ GlLink,
+ MergeChecksMessage,
+ ActionButtons,
+ },
+ mixins: [mergeRequestQueryVariablesMixin],
+ apollo: {
+ state: {
+ query: rebaseQuery,
+ variables() {
+ return this.mergeRequestQueryVariables;
+ },
+ update: (data) => data.project.mergeRequest,
+ },
+ },
+ inject: {
+ canCreatePipelineInTargetProject: {
+ default: false,
+ },
+ },
+ props: {
+ check: {
+ type: Object,
+ required: true,
+ },
+ mr: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ service: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ state: {},
+ isMakingRequest: false,
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.state.loading;
+ },
+ rebaseInProgress() {
+ return this.state.rebaseInProgress;
+ },
+ showRebaseWithoutPipeline() {
+ return (
+ !this.mr.onlyAllowMergeIfPipelineSucceeds ||
+ (this.mr.onlyAllowMergeIfPipelineSucceeds && this.mr.allowMergeOnSkippedPipeline)
+ );
+ },
+ isForkMergeRequest() {
+ return this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath;
+ },
+ isLatestPipelineCreatedInTargetProject() {
+ const latestPipeline = this.state.pipelines.nodes[0];
+
+ return latestPipeline?.project?.fullPath === this.mr.targetProjectFullPath;
+ },
+ shouldShowSecurityWarning() {
+ return (
+ this.canCreatePipelineInTargetProject &&
+ this.isForkMergeRequest &&
+ !this.isLatestPipelineCreatedInTargetProject
+ );
+ },
+ tertiaryActionsButtons() {
+ if (this.check.result === 'success') return [];
+
+ return [
+ {
+ text: s__('mrWidget|Rebase'),
+ loading: this.isMakingRequest || this.rebaseInProgress,
+ testId: 'standard-rebase-button',
+ onClick: () => this.tryRebase(),
+ },
+ this.showRebaseWithoutPipeline && {
+ text: s__('mrWidget|Rebase without pipeline'),
+ loading: this.isMakingRequest || this.rebaseInProgress,
+ testId: 'rebase-without-ci-button',
+ onClick: () => this.rebaseWithoutCi(),
+ },
+ ].filter((b) => b);
+ },
+ },
+ methods: {
+ rebase({ skipCi = false } = {}) {
+ this.isMakingRequest = true;
+
+ this.service
+ .rebase({ skipCi })
+ .then(() => simplePoll(this.checkRebaseStatus))
+ .catch((error) => {
+ this.isMakingRequest = false;
+
+ if (!error.response?.data?.merge_error) {
+ createAlert({
+ message: __('Something went wrong. Please try again.'),
+ });
+ }
+ });
+ },
+ rebaseWithoutCi() {
+ return this.rebase({ skipCi: true });
+ },
+ tryRebase() {
+ if (this.shouldShowSecurityWarning) {
+ this.$refs.modal.show();
+ } else {
+ this.rebase();
+ }
+ },
+ checkRebaseStatus(continuePolling, stopPolling) {
+ this.service
+ .poll()
+ .then((res) => res.data)
+ .then((res) => {
+ if (res.rebase_in_progress || res.should_be_rebased) {
+ continuePolling();
+ } else {
+ this.isMakingRequest = false;
+
+ if (!res.merge_error?.length) {
+ toast(__('Rebase completed'));
+ }
+
+ eventHub.$emit('MRWidgetRebaseSuccess');
+ stopPolling();
+ }
+ })
+ .catch(() => {
+ this.isMakingRequest = false;
+ createAlert({
+ message: __('Something went wrong. Please try again.'),
+ });
+ stopPolling();
+ });
+ },
+ },
+ modal: {
+ id: 'rebase-security-risk-modal',
+ title: s__('mrWidget|Are you sure you want to rebase?'),
+ actionPrimary: {
+ text: s__('mrWidget|Rebase'),
+ attributes: {
+ variant: 'danger',
+ },
+ },
+ actionCancel: {
+ text: __('Cancel'),
+ attributes: {
+ variant: 'default',
+ },
+ },
+ },
+ runPipelinesInTheParentProjectHelpPath: helpPagePath(
+ '/ci/pipelines/merge_request_pipelines.html',
+ {
+ anchor: 'run-pipelines-in-the-parent-project',
+ },
+ ),
+};
+</script>
+
+<template>
+ <merge-checks-message :check="check">
+ <template #failed>
+ <action-buttons v-if="!isLoading" :tertiary-buttons="tertiaryActionsButtons" />
+ </template>
+ <gl-modal
+ ref="modal"
+ :modal-id="$options.modal.id"
+ :title="$options.modal.title"
+ :action-primary="$options.modal.actionPrimary"
+ :action-cancel="$options.modal.actionCancel"
+ @primary="rebase"
+ >
+ <p>
+ {{
+ s__(
+ 'Pipelines|Rebasing creates a pipeline that runs code originating from a forked project merge request. Consequently there are potential security implications, such as the exposure of CI variables.',
+ )
+ }}
+ </p>
+ <p>
+ {{
+ s__(
+ "Pipelines|You should review the code thoroughly before running this pipeline with the parent project's CI/CD resources.",
+ )
+ }}
+ </p>
+ <p>
+ {{ s__('Pipelines|If you are unsure, ask a project maintainer to review it for you.') }}
+ </p>
+ <gl-link :href="$options.runPipelinesInTheParentProjectHelpPath" target="_blank">
+ {{ s__('Pipelines|More Information') }}
+ </gl-link>
+ </gl-modal>
+ </merge-checks-message>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/unresolved_discussions.vue
new file mode 100644
index 00000000000..a6970d9c795
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/unresolved_discussions.vue
@@ -0,0 +1,39 @@
+<script>
+import { s__ } from '~/locale';
+import notesEventHub from '~/notes/event_hub';
+import ActionButtons from '../action_buttons.vue';
+import MergeChecksMessage from './message.vue';
+
+export default {
+ name: 'MergeChecksUnresolvedDiscussions',
+ components: {
+ MergeChecksMessage,
+ ActionButtons,
+ },
+ props: {
+ check: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ tertiaryActionsButtons() {
+ return [
+ {
+ text: s__('mrWidget|Go to first unresolved thread'),
+ category: 'default',
+ onClick: () => notesEventHub.$emit('jumpToFirstUnresolvedDiscussion'),
+ },
+ ];
+ },
+ },
+};
+</script>
+
+<template>
+ <merge-checks-message :check="check">
+ <template #failed>
+ <action-buttons :tertiary-buttons="tertiaryActionsButtons" />
+ </template>
+ </merge-checks-message>
+</template>
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 3e2f3ab4103..0f692f23142 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
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIntersectionObserver } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
import { sprintf, s__, __ } from '~/locale';
@@ -102,7 +102,7 @@ export default {
return this.statusIcon(this.collapsedData);
},
tertiaryActionsButtons() {
- return this.tertiaryButtons ? this.tertiaryButtons() : undefined;
+ return 'tertiaryButtons' in this ? this.tertiaryButtons() : undefined;
},
hydratedSummary() {
const structuredOutput = this.summary(this.collapsedData);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
index 1c57226f887..77dc5b1d0da 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
@@ -15,9 +15,9 @@ const defaultRender = (apolloProvider) => ({
components: { MergeChecks },
apolloProvider,
data() {
- return { mr: { conflictResolutionPath: 'https://gitlab.com' } };
+ return { service: {}, mr: { conflictResolutionPath: 'https://gitlab.com' } };
},
- template: '<merge-checks :mr="mr" />',
+ template: '<merge-checks :mr="mr" :service="service" />',
});
const Template = ({ canMerge, failed, pushToSourceBranch }) => {
@@ -32,16 +32,14 @@ const Template = ({ canMerge, failed, pushToSourceBranch }) => {
mergeRequest: {
id: 1,
userPermissions: { canMerge },
- mergeChecks: [
+ mergeabilityChecks: [
{
- failureReason: 'Unresolved discussions',
- identifier: 'unresolved_discussions',
- result: failed ? 'failed' : 'passed',
+ identifier: 'DISCUSSIONS_NOT_RESOLVED',
+ status: failed ? 'FAILED' : 'SUCCESS',
},
{
- failureReason: 'Resolve conflicts',
- identifier: 'conflicts',
- result: failed ? 'failed' : 'passed',
+ identifier: 'CONFLICT',
+ status: failed ? 'FAILED' : 'SUCCESS',
},
],
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
index fa84c0a4a6f..ac403c2c6f2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
@@ -1,16 +1,12 @@
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
-import { n__, __, sprintf } from '~/locale';
+import { __, n__, sprintf } from '~/locale';
+import { COMPONENTS } from '~/vue_merge_request_widget/components/checks/constants';
import mergeRequestQueryVariablesMixin from '../mixins/merge_request_query_variables';
import mergeChecksQuery from '../queries/merge_checks.query.graphql';
import StateContainer from './state_container.vue';
import BoldText from './bold_text.vue';
-const COMPONENTS = {
- conflicts: () => import('./checks/conflicts.vue'),
- default: () => import('./checks/message.vue'),
-};
-
export default {
apollo: {
state: {
@@ -35,6 +31,10 @@ export default {
type: Object,
required: true,
},
+ service: {
+ type: Object,
+ required: true,
+ },
},
data() {
return {
@@ -68,10 +68,10 @@ export default {
);
},
checks() {
- return this.state.mergeChecks || [];
+ return this.state.mergeabilityChecks || [];
},
failedChecks() {
- return this.checks.filter((c) => c.result === 'failed');
+ return this.checks.filter((c) => c.status.toLowerCase() === 'failed');
},
},
methods: {
@@ -79,7 +79,7 @@ export default {
this.collapsed = !this.collapsed;
},
checkComponent(check) {
- return COMPONENTS[check.identifier] || COMPONENTS.default;
+ return COMPONENTS[check.identifier.toLowerCase()] || COMPONENTS.default;
},
},
};
@@ -122,6 +122,7 @@ export default {
}"
:check="check"
:mr="mr"
+ :service="service"
/>
</div>
</div>
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 2e104f2b93b..efc74241941 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
@@ -10,7 +10,7 @@ import {
} from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, n__ } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
import PipelineArtifacts from '~/ci/pipelines_page/components/pipelines_artifacts.vue';
import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
@@ -21,7 +21,7 @@ import { MT_MERGE_STRATEGY } from '../constants';
export default {
name: 'MRWidgetPipeline',
components: {
- CiBadgeLink,
+ CiIcon,
GlLink,
GlLoadingIcon,
GlIcon,
@@ -194,13 +194,7 @@ export default {
</p>
</template>
<template v-else-if="hasPipeline">
- <ci-badge-link
- :status="status"
- :href="status.details_path"
- size="md"
- :show-text="false"
- class="gl-align-self-start gl-mt-2 gl-mr-3"
- />
+ <ci-icon :status="status" class="gl-align-self-start gl-mt-2 gl-mr-3" />
<div class="ci-widget-container d-flex">
<div class="ci-widget-content">
<div class="media-body">
@@ -208,7 +202,9 @@ export default {
data-testid="pipeline-info-container"
class="gl-display-flex gl-flex-wrap gl-align-items-center gl-justify-content-space-between"
>
- <p class="mr-pipeline-title gl-m-0! gl-mr-3! gl-font-weight-bold gl-text-gray-900">
+ <p
+ class="mr-pipeline-title gl-align-self-start gl-m-0! gl-mr-3! gl-font-weight-bold gl-text-gray-900"
+ >
{{ pipeline.details.event_type_name }}
<gl-link :href="pipeline.path" class="pipeline-id" data-testid="pipeline-id"
>#{{ pipeline.id }}</gl-link
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index ea3f324b8f2..370e07b397c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -28,7 +28,7 @@ export default {
};
</script>
<template>
- <div class="gl-w-6 gl-h-6 gl-display-flex gl-align-self-center gl-mr-3">
+ <div class="gl-w-6 gl-h-6 gl-display-flex gl-align-self-start gl-mr-3">
<div class="gl-display-flex gl-m-auto">
<gl-icon v-if="isMerged" name="merge" :size="16" class="gl-text-blue-500" />
<gl-icon v-else-if="isClosed" name="merge-request-close" :size="16" class="gl-text-red-500" />
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 45958d7fb8d..c70213ad8a2 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
@@ -99,11 +99,7 @@ export default {
<p class="gl-mt-2">
<gl-sprintf :message="$options.SP_HELP_CONTENT">
<template #link="{ content }">
- <gl-link
- data-testid="help"
- :href="$options.SP_HELP_URL"
- target="_blank"
- class="font-size-inherit"
+ <gl-link data-testid="help" :href="$options.SP_HELP_URL" target="_blank"
>{{ content }}
</gl-link>
</template>
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 ac434c5be4e..3c2d8efaffc 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
@@ -1,10 +1,9 @@
<script>
import {
- GlIcon,
GlButton,
GlButtonGroup,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlFormCheckbox,
GlSprintf,
GlLink,
@@ -15,6 +14,7 @@ import { isEmpty, isNil } from 'lodash';
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
import { createAlert } from '~/alert';
+import { fetchPolicies } from '~/lib/graphql';
import { TYPENAME_MERGE_REQUEST } from '~/graphql_shared/constants';
import { STATUS_CLOSED, STATUS_MERGED } from '~/issues/constants';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
@@ -25,11 +25,14 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import readyToMergeSubscription from '~/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
AUTO_MERGE_STRATEGIES,
MT_MERGE_STRATEGY,
PIPELINE_FAILED_STATE,
STATE_MACHINE,
+ MT_SKIP_TRAIN,
+ MT_RESTART_TRAIN,
} from '../../constants';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
@@ -61,6 +64,10 @@ export default {
},
manual: true,
result({ data }) {
+ if (!data.project) {
+ return;
+ }
+
if (Object.keys(this.state).length === 0) {
this.removeSourceBranch =
data.project.mergeRequest.shouldRemoveSourceBranch ||
@@ -121,13 +128,12 @@ export default {
SquashBeforeMerge,
CommitEdit,
CommitMessageDropdown,
- GlIcon,
GlSprintf,
GlLink,
GlButton,
GlButtonGroup,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlFormCheckbox,
GlSkeletonLoader,
MergeFailedPipelineConfirmationDialog,
@@ -139,6 +145,10 @@ export default {
import(
'ee_component/vue_merge_request_widget/components/merge_train_failed_pipeline_confirmation_dialog.vue'
),
+ MergeTrainRestartTrainConfirmationDialog: () =>
+ import(
+ 'ee_component/vue_merge_request_widget/components/merge_train_restart_train_confirmation_dialog.vue'
+ ),
AddedCommitMessage,
RelatedLinks,
HelpPopover,
@@ -148,7 +158,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [readyToMergeMixin, mergeRequestQueryVariablesMixin],
+ mixins: [readyToMergeMixin, mergeRequestQueryVariablesMixin, glFeatureFlagsMixin()],
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
@@ -168,6 +178,10 @@ export default {
squashCommitMessageIsTouched: false,
isPipelineFailedModalVisibleMergeTrain: false,
isPipelineFailedModalVisibleNormalMerge: false,
+ isMergeTrainBeingForceMerged: false,
+ mergeTrainMergeType: MT_RESTART_TRAIN,
+ skipMergeTrain: false,
+ mergeTrainsSkipAllowed: this.mr.mergeTrainsSkipAllowed,
editCommitMessage: false,
};
},
@@ -319,6 +333,12 @@ export default {
title: this.autoMergePopoverSettings.title,
};
},
+ isSkipMergeTrainAvailable() {
+ return this.mergeTrainsSkipAllowed && this.glFeatures.mergeTrainsSkipTrain;
+ },
+ displaySkipMergeTrainOptions() {
+ return this.shouldDisplayMergeImmediatelyDropdownOptions && this.isSkipMergeTrainAvailable;
+ },
},
watch: {
'mr.state': function mrStateWatcher() {
@@ -329,6 +349,12 @@ export default {
eventHub.$on('ApprovalUpdated', this.updateGraphqlState);
eventHub.$on('MRWidgetUpdateRequested', this.updateGraphqlState);
eventHub.$on('mr.discussion.updated', this.updateGraphqlState);
+
+ if (this.glFeatures.widgetPipelinePassSubscriptionUpdate) {
+ this.$apollo.queries.state.setOptions({
+ fetchPolicy: fetchPolicies.NO_CACHE,
+ });
+ }
},
beforeDestroy() {
eventHub.$off('ApprovalUpdated', this.updateGraphqlState);
@@ -377,6 +403,7 @@ export default {
auto_merge_strategy: useAutoMerge ? this.preferredAutoMergeStrategy : undefined,
should_remove_source_branch: this.removeSourceBranch === true,
squash: this.squashBeforeMerge,
+ skip_merge_train: this.skipMergeTrain,
};
// If users can't alter the squash message (e.g. for 1-commit merge requests),
@@ -428,6 +455,17 @@ export default {
this.handleMergeButtonClick(false, true);
}
},
+ handleMergeTrainMergeImmediatelyButtonClick(type) {
+ this.mergeTrainMergeType = type;
+ this.isMergeTrainBeingForceMerged = true;
+ },
+ processMergeTrain() {
+ if (this.mergeTrainMergeType === MT_SKIP_TRAIN) {
+ this.skipMergeTrain = true;
+ }
+
+ this.handleMergeButtonClick(false, true, true);
+ },
onMergeImmediatelyConfirmation() {
this.handleMergeButtonClick(false, true, true);
},
@@ -491,6 +529,8 @@ export default {
sourceDivergedFromTargetText: s__('mrWidget|The source branch is %{link} the target branch.'),
divergedCommits: (count) => n__('%d commit behind', '%d commits behind', count),
},
+ MT_SKIP_TRAIN,
+ MT_RESTART_TRAIN,
};
</script>
@@ -520,7 +560,7 @@ export default {
<div class="mr-widget-body-controls gl-display-flex gl-align-items-center gl-flex-wrap">
<template v-if="shouldShowMergeControls">
<div
- class="gl-display-flex gl-sm-flex-direction-column gl-md-align-items-center gl-flex-wrap gl-w-full"
+ class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-md-align-items-center gl-flex-wrap gl-w-full"
>
<gl-form-checkbox
v-if="canRemoveSourceBranch"
@@ -637,32 +677,57 @@ export default {
@click="handleMergeButtonClick(isAutoMergeAvailable)"
>{{ mergeButtonText }}</gl-button
>
- <gl-dropdown
+ <gl-disclosure-dropdown
v-if="shouldShowMergeImmediatelyDropdown"
v-gl-tooltip.hover.focus="__('Select merge moment')"
:disabled="isMergeButtonDisabled"
variant="confirm"
+ class="gl-mr-0"
data-testid="merge-immediately-dropdown"
+ icon="chevron-down"
toggle-class="btn-icon js-merge-moment"
+ :toggle-text="__('Select a merge moment')"
+ text-sr-only
+ no-caret
>
- <template #button-content>
- <gl-icon name="chevron-down" class="mr-0" />
- <span class="sr-only">{{ __('Select merge moment') }}</span>
- </template>
- <gl-dropdown-item
- icon-name="warning"
- button-class="accept-merge-request"
+ <gl-disclosure-dropdown-item
+ v-if="
+ !shouldDisplayMergeImmediatelyDropdownOptions || !isSkipMergeTrainAvailable
+ "
data-testid="merge-immediately-button"
- @click="handleMergeImmediatelyButtonClick"
+ @action="handleMergeImmediatelyButtonClick"
+ >
+ <template #list-item> {{ __('Merge immediately') }} </template>
+ </gl-disclosure-dropdown-item>
+ <gl-disclosure-dropdown-item
+ v-if="displaySkipMergeTrainOptions"
+ data-testid="mt-merge-now-restart-button"
+ @action="handleMergeTrainMergeImmediatelyButtonClick($options.MT_RESTART_TRAIN)"
>
- {{ __('Merge immediately') }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <template #list-item>
+ <strong>{{ __(`Merge now and restart train`) }}</strong>
+ <p class="gl-text-gray-400 gl-font-sm gl-mb-0">
+ {{ __('Restart merge train pipelines with the merged changes.') }}
+ </p>
+ </template>
+ </gl-disclosure-dropdown-item>
+ <gl-disclosure-dropdown-item
+ v-if="displaySkipMergeTrainOptions"
+ data-testid="mt-merge-now-skip-restart-button"
+ @action="handleMergeTrainMergeImmediatelyButtonClick($options.MT_SKIP_TRAIN)"
+ >
+ <template #list-item>
+ <strong>{{ __(`Merge now and don't restart train`) }}</strong>
+ <p class="gl-text-gray-400 gl-font-sm gl-mb-0">
+ {{ __('Merge train pipelines continue without the merged changes.') }}
+ </p>
+ </template>
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown>
</gl-button-group>
<template v-if="showAutoMergeHelperText">
<div
class="gl-ml-4 gl-text-gray-500 gl-font-sm"
- data-qa-selector="auto_merge_helper_text"
data-testid="auto-merge-helper-text"
>
{{ autoMergeHelperText }}
@@ -730,12 +795,16 @@ export default {
class="mr-ready-merge-related-links gl-display-inline"
/>
</li>
+ <li v-if="state.autoMergeEnabled" class="gl-line-height-normal">
+ {{ s__('mrWidget|Auto-merge enabled') }}
+ </li>
</ul>
</div>
</div>
</div>
</div>
<merge-immediately-confirmation-dialog
+ v-if="mr.mergeImmediatelyDocsPath"
ref="confirmationDialog"
:docs-url="mr.mergeImmediatelyDocsPath"
@mergeImmediately="onMergeImmediatelyConfirmation"
@@ -745,6 +814,13 @@ export default {
@startMergeTrain="onStartMergeTrainConfirmation"
@cancel="isPipelineFailedModalVisibleMergeTrain = false"
/>
+ <merge-train-restart-train-confirmation-dialog
+ v-if="isSkipMergeTrainAvailable"
+ :visible="isMergeTrainBeingForceMerged"
+ :merge-train-type="mergeTrainMergeType"
+ @processMergeTrainMerge="processMergeTrain"
+ @cancel="isMergeTrainBeingForceMerged = false"
+ />
<merge-failed-pipeline-confirmation-dialog
:visible="isPipelineFailedModalVisibleNormalMerge"
@mergeWithFailedPipeline="onMergeWithFailedPipelineConfirmation"
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 7fc4a06cbae..267facb0a50 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
@@ -143,7 +143,9 @@ export default {
:collapsed="mr.mergeDetailsCollapsed"
@toggle="() => mr.toggleMergeDetails()"
>
- <span class="gl-ml-0! gl-text-body! gl-flex-grow-1">
+ <span
+ class="gl-display-inline-flex gl-align-self-start gl-pt-2 gl-ml-0! gl-text-body! gl-flex-grow-1"
+ >
<bold-text :message="$options.i18n.removeDraftStatus" />
</span>
<template #actions>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
index 8249dffcc27..08e803bffc9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
@@ -9,6 +9,8 @@ export default {
MrTerraformWidget: () => import('~/vue_merge_request_widget/extensions/terraform/index.vue'),
MrCodeQualityWidget: () =>
import('~/vue_merge_request_widget/extensions/code_quality/index.vue'),
+ MrAccessibilityWidget: () =>
+ import('~/vue_merge_request_widget/extensions/accessibility/index.vue'),
},
props: {
@@ -31,12 +33,17 @@ export default {
return this.mr.codequalityReportsPath ? 'MrCodeQualityWidget' : undefined;
},
+ accessibilityWidget() {
+ return this.mr.accessibilityReportPath ? 'MrAccessibilityWidget' : undefined;
+ },
+
widgets() {
return [
this.codeQualityWidget,
this.testReportWidget,
this.terraformPlansWidget,
'MrSecurityWidget',
+ this.accessibilityWidget,
].filter((w) => w);
},
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
index 72c041759d9..d4375690ad1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
@@ -1,5 +1,5 @@
<script>
-import { GlBadge, GlLink } from '@gitlab/ui';
+import { GlBadge, GlLink, GlTooltipDirective } from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { generateText } from '../extensions/utils';
import ContentRow from './widget_content_row.vue';
@@ -15,6 +15,7 @@ export default {
},
directives: {
SafeHtml,
+ GlTooltip: GlTooltipDirective,
},
props: {
data: {
@@ -78,7 +79,11 @@ export default {
<div class="gl-display-flex gl-flex-grow-1">
<div class="gl-display-flex gl-flex-grow-1 gl-align-items-baseline">
<div>
- <p v-safe-html="generatedText" class="gl-mb-0 gl-mr-1"></p>
+ <p
+ v-gl-tooltip="{ title: data.tooltipText, boundary: 'viewport' }"
+ v-safe-html="generatedText"
+ class="gl-mb-0 gl-mr-1"
+ ></p>
<gl-link v-if="data.link" :href="data.link.href">{{ data.link.text }}</gl-link>
<p
v-if="data.supportingText"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
index d17be3e4037..0eb50b9ff4f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlButton, GlLink, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { logError } from '~/lib/logger';
import SafeHtml from '~/vue_shared/directives/safe_html';
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index 1a469f9b7bb..071f95a28fa 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -6,7 +6,7 @@ import { stateToComponentMap as classStateMap, stateKey } from './stores/state_m
export const FOUR_MINUTES_IN_MS = 1000 * 60 * 4;
export const STATE_QUERY_POLLING_INTERVAL_DEFAULT = 5000;
-export const STATE_QUERY_POLLING_INTERVAL_BACKOFF = 2;
+export const STATE_QUERY_POLLING_INTERVAL_BACKOFF = 1.2;
export const SUCCESS = 'success';
export const WARNING = 'warning';
@@ -202,3 +202,6 @@ export const DETAILED_MERGE_STATUS = {
CI_STILL_RUNNING: 'CI_STILL_RUNNING',
EXTERNAL_STATUS_CHECKS: 'EXTERNAL_STATUS_CHECKS',
};
+
+export const MT_SKIP_TRAIN = 'skip';
+export const MT_RESTART_TRAIN = 'restart';
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.vue
index 0fb5e13ad82..2ae16eef410 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.vue
@@ -1,24 +1,37 @@
+<script>
import { uniqueId } from 'lodash';
import { __, n__, s__, sprintf } from '~/locale';
import axios from '~/lib/utils/axios_utils';
+import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue';
import { EXTENSION_ICONS } from '../../constants';
export default {
name: 'WidgetAccessibility',
- enablePolling: true,
i18n: {
loading: s__('Reports|Accessibility scanning results are being parsed'),
error: s__('Reports|Accessibility scanning failed loading results'),
},
- props: ['accessibilityReportPath'],
+ components: {
+ MrWidget,
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ collapsedData: {},
+ content: [],
+ };
+ },
computed: {
statusIcon() {
- return this.collapsedData.status === 'failed'
+ return this.collapsedData?.status === 'failed'
? EXTENSION_ICONS.warning
: EXTENSION_ICONS.success;
},
- },
- methods: {
summary() {
const numOfResults = this.collapsedData?.summary?.errored || 0;
@@ -37,13 +50,20 @@ export default {
false,
);
- return numOfResults === 0 ? successText : warningText;
+ return numOfResults === 0 ? { title: successText } : { title: warningText };
},
shouldCollapse() {
return this.collapsedData?.summary?.errored > 0;
},
+ },
+ methods: {
fetchCollapsedData() {
- return axios.get(this.accessibilityReportPath);
+ return axios.get(this.mr.accessibilityReportPath).then((response) => {
+ this.collapsedData = response.data;
+ this.content = this.getContent(response.data);
+
+ return response;
+ });
},
fetchFullData() {
return Promise.resolve(this.prepareReports());
@@ -74,9 +94,7 @@ export default {
formatMessage(message) {
return sprintf(s__('AccessibilityReport|Message: %{message}'), { message });
},
- prepareReports() {
- const { collapsedData } = this;
-
+ getContent(collapsedData) {
const newErrors = collapsedData.new_errors.map((error) => {
return {
header: __('New'),
@@ -121,3 +139,16 @@ export default {
},
},
};
+</script>
+<template>
+ <mr-widget
+ :error-text="$options.i18n.error"
+ :status-icon-name="statusIcon"
+ :loading-text="$options.i18n.loading"
+ :widget-name="$options.name"
+ :summary="summary"
+ :content="content"
+ :is-collapsible="shouldCollapse"
+ :fetch-collapsed-data="fetchCollapsedData"
+ />
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue
index cd3a98effa3..e87b5d20ca0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue
@@ -1,5 +1,5 @@
<script>
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { SEVERITY_ICONS_MR_WIDGET } from '~/ci/reports/codequality_report/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue
index e7d8de97f20..a36a58c68de 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown, GlDropdownItem, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__, sprintf } from '~/locale';
@@ -10,11 +10,7 @@ export default {
name: 'WidgetSecurityReportsCE',
components: {
MrWidget,
- GlDropdown,
- GlDropdownItem,
- },
- directives: {
- GlTooltip,
+ GlDisclosureDropdown,
},
i18n: {
apiError: s__(
@@ -76,17 +72,23 @@ export default {
summary() {
return { title: this.$options.i18n.scansHaveRun };
},
+ listboxOptions() {
+ return this.artifacts.map(({ name, path }) => ({
+ text: sprintf(s__('SecurityReports|Download %{artifactName}'), {
+ artifactName: name,
+ }),
+ href: path,
+ extraAttrs: {
+ download: '',
+ rel: 'nofollow',
+ },
+ }));
+ },
},
methods: {
handleIsLoading(value) {
this.isLoading = value;
},
-
- artifactText({ name }) {
- return sprintf(s__('SecurityReports|Download %{artifactName}'), {
- artifactName: name,
- });
- },
},
widgetHelpPopover: {
options: { title: s__('ciReport|Security scan results') },
@@ -116,26 +118,12 @@ export default {
@is-loading="handleIsLoading"
>
<template #action-buttons>
- <div class="gl-ml-3">
- <gl-dropdown
- v-gl-tooltip
- icon="download"
- size="small"
- category="tertiary"
- variant="confirm"
- right
- >
- <gl-dropdown-item
- v-for="artifact in artifacts"
- :key="artifact.path"
- :href="artifact.path"
- :data-testid="`download-${artifact.name}`"
- download
- >
- {{ artifactText(artifact) }}
- </gl-dropdown-item>
- </gl-dropdown>
- </div>
+ <gl-disclosure-dropdown
+ class="gl-ml-3"
+ size="small"
+ icon="download"
+ :items="listboxOptions"
+ />
</template>
</mr-widget>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue
index 1b03b9c04e1..c12bc6456a5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue
@@ -19,7 +19,9 @@ import {
import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants';
export default {
- name: 'WidgetTestReport',
+ // widget name does not match file path because widget name must match telemetry event names
+ // see https://gitlab.com/gitlab-org/gitlab/-/issues/427061
+ name: 'WidgetTestSummary',
components: {
MrWidget,
MrWidgetRow,
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js b/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js
index 564e9321d54..8bb2f2898eb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js
@@ -18,8 +18,16 @@ export default {
iid: `${this.mr.iid}`,
};
},
- update: (data) => data.project.mergeRequest,
+ update: (data) => data.project?.mergeRequest,
result({ data }) {
+ // This case can occur when backend returns an empty project due to expired session.
+ // See https://gitlab.com/gitlab-org/gitlab/-/issues/413627 for more information.
+ if (!data.project) {
+ // Needed to suppress several errors.
+ this.mr.setApprovals({});
+ return;
+ }
+
const { mergeRequest } = data.project;
this.disableCommittersApproval = data.project.mergeRequestsDisableCommittersApproval;
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
index 2f49252a06b..623b504fcc1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
@@ -21,13 +21,6 @@ export default {
this.mr.preventMerge,
);
},
- mergeDisabledText() {
- if (this.pipeline?.status === PIPELINE_SKIPPED_STATUS) {
- return MERGE_DISABLED_SKIPPED_PIPELINE_TEXT;
- }
-
- return MERGE_DISABLED_TEXT;
- },
pipelineMustSucceedConflictText() {
return PIPELINE_MUST_SUCCEED_CONFLICT_TEXT;
},
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 02d73cf9cbd..cc116b42f1e 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,9 +1,6 @@
<script>
import { isEmpty, clamp } from 'lodash';
-import {
- registerExtension,
- registeredExtensions,
-} from '~/vue_merge_request_widget/components/extensions';
+import { registeredExtensions } from '~/vue_merge_request_widget/components/extensions';
import SafeHtml from '~/vue_shared/directives/safe_html';
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';
@@ -55,7 +52,6 @@ import eventHub from './event_hub';
import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
import getStateQuery from './queries/get_state.query.graphql';
import getStateSubscription from './queries/get_state.subscription.graphql';
-import accessibilityExtension from './extensions/accessibility';
import ReportWidgetContainer from './components/report_widget_container.vue';
import MrWidgetReadyToMerge from './components/states/new_ready_to_merge.vue';
@@ -235,9 +231,6 @@ export default {
false,
);
},
- shouldShowAccessibilityReport() {
- return Boolean(this.mr?.accessibilityReportPath);
- },
formattedHumanAccess() {
return (this.mr.humanAccess || '').toLowerCase();
},
@@ -268,11 +261,6 @@ export default {
this.initPostMergeDeploymentsPolling();
}
},
- shouldShowAccessibilityReport(newVal) {
- if (newVal) {
- this.registerAccessibilityExtension();
- }
- },
},
mounted() {
MRWidgetService.fetchInitialData()
@@ -507,11 +495,6 @@ export default {
dismissSuggestPipelines() {
this.mr.isDismissedSuggestPipeline = true;
},
- registerAccessibilityExtension() {
- if (this.shouldShowAccessibilityReport) {
- registerExtension(accessibilityExtension);
- }
- },
},
};
</script>
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql
index 6b602a0095c..fcaddcc2a42 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql
@@ -6,7 +6,10 @@ query mergeChecks($projectPath: ID!, $iid: String!) {
userPermissions {
canMerge
}
- mergeChecks @client
+ mergeabilityChecks {
+ identifier
+ status
+ }
}
}
}
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 6803d609dbc..e84b3f53b53 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
@@ -10,7 +10,7 @@ import {
GlTab,
GlButton,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import SafeHtml from '~/vue_shared/directives/safe_html';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
import { fetchPolicies } from '~/lib/graphql';
@@ -30,7 +30,7 @@ import AlertSidebar from './alert_sidebar.vue';
import AlertSummaryRow from './alert_summary_row.vue';
import SystemNote from './system_notes/system_note.vue';
-const containerEl = document.querySelector('.page-with-contextual-sidebar');
+const containerEl = document.querySelector('.layout-page');
export default {
i18n: {
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 2d3815439a6..056388f690d 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
@@ -284,13 +284,7 @@ export default {
>
<div v-if="userName" class="gl-display-inline-flex gl-mt-2" data-testid="assigned-users">
<span class="gl-relative gl-mr-4">
- <img
- :alt="userName"
- :src="userImg"
- :width="32"
- class="avatar avatar-inline gl-m-0 s32"
- data-qa-selector="avatar_image"
- />
+ <img :alt="userName" :src="userImg" :width="32" class="avatar avatar-inline gl-m-0 s32" />
</span>
<span class="gl-display-flex gl-flex-direction-column gl-overflow-hidden">
<strong class="dropdown-menu-user-full-name">
diff --git a/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue b/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue
index ffbcdefc924..93e1fc4a0c2 100644
--- a/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue
+++ b/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue
@@ -1,6 +1,6 @@
<script>
-import * as Sentry from '@sentry/browser';
import { GlFormInput } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import {
DurationParseError,
outputChronicDuration,
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
deleted file mode 100644
index abbeac0e098..00000000000
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ /dev/null
@@ -1,157 +0,0 @@
-<script>
-import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
-import CiIcon from './ci_icon.vue';
-
-/**
- * Renders CI Badge link with CI icon and status text based on
- * API response shared between all places where it is used.
- *
- * Receives status object containing:
- * status: {
- * details_path or detailsPath: "/gitlab-org/gitlab-foss/pipelines/8150156" // url
- * group:"running" // used for CSS class
- * icon: "icon_status_running" // used to render the icon
- * label:"running" // used for potential tooltip
- * text:"running" // text rendered
- * }
- *
- * Used in:
- * - Pipelines table - first column
- * - Jobs table - first column
- * - Pipeline show view - header
- * - Job show view - header
- * - MR widget
- * - Terraform table
- * - On-demand scans list
- */
-
-const badgeSizeOptions = {
- sm: 'sm',
- md: 'md',
- lg: 'lg',
-};
-
-export default {
- components: {
- CiIcon,
- GlBadge,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- status: {
- type: Object,
- required: true,
- },
- showText: {
- type: Boolean,
- required: false,
- default: true,
- },
- size: {
- type: String,
- required: false,
- default: badgeSizeOptions.md,
- validator(value) {
- return badgeSizeOptions[value] !== undefined;
- },
- },
- showTooltip: {
- type: Boolean,
- required: false,
- default: true,
- },
- useLink: {
- type: Boolean,
- default: true,
- required: false,
- },
- },
- computed: {
- isNotLargeBadgeSize() {
- return this.size !== badgeSizeOptions.lg;
- },
- title() {
- return this.showTooltip && !this.showText ? this.status?.text : '';
- },
- detailsPath() {
- // For now, this can either come from graphQL with camelCase or REST API in snake_case
- if (!this.useLink) {
- return null;
- }
- return this.status.detailsPath || this.status.details_path;
- },
- badgeStyles() {
- switch (this.status.icon) {
- case 'status_success':
- return {
- textColor: 'gl-text-green-700',
- variant: 'success',
- };
- case 'status_warning':
- return {
- textColor: 'gl-text-orange-700',
- variant: 'warning',
- };
- case 'status_failed':
- return {
- textColor: 'gl-text-red-700',
- variant: 'danger',
- };
- case 'status_running':
- return {
- textColor: 'gl-text-blue-700',
- variant: 'info',
- };
- case 'status_pending':
- return {
- textColor: 'gl-text-orange-700',
- variant: 'warning',
- };
- case 'status_canceled':
- return {
- textColor: 'gl-text-gray-700',
- variant: 'neutral',
- };
- case 'status_manual':
- return {
- textColor: 'gl-text-gray-700',
- variant: 'neutral',
- };
- // default covers the styles for the remainder of CI
- // statuses that are not explicitly stated here
- default:
- return {
- textColor: 'gl-text-gray-600',
- variant: 'muted',
- };
- }
- },
- },
-};
-</script>
-<template>
- <gl-badge
- v-gl-tooltip
- :class="{ 'gl-px-2': !showText && isNotLargeBadgeSize }"
- :title="title"
- :href="detailsPath"
- :size="size"
- :variant="badgeStyles.variant"
- data-testid="ci-badge-link"
- @click="$emit('ciStatusBadgeClick')"
- >
- <ci-icon :status="status" />
-
- <template v-if="showText">
- <span
- class="gl-ml-2 gl-white-space-nowrap"
- :class="badgeStyles.textColor"
- data-testid="ci-badge-text"
- >
- {{ status.text }}
- </span>
- </template>
- </gl-badge>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index 6670b931416..a2b6b4642c9 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -1,99 +1,115 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlBadge, GlTooltipDirective, GlIcon } from '@gitlab/ui';
/**
* Renders CI icon based on API response shared between all places where it is used.
*
* Receives status object containing:
* status: {
- * group:"running" // used for CSS class
- * icon: "icon_status_running" // used to render the icon
+ * icon: "status_running" // used to render the icon and CSS class
+ * text: "Running",
+ * detailsPath: '/project1/jobs/1' // can also be details_path
* }
*
- * Used in:
- * - Extended MR Popover
- * - Jobs show view header
- * - Jobs show view sidebar
- * - Jobs table
- * - Linked pipelines
- * - Pipeline graph
- * - Pipeline mini graph
- * - Pipeline show view badge
- * - Pipelines table Badge
*/
-/*
- * These sizes are defined in gitlab-ui/src/scss/variables.scss
- * under '$gl-icon-sizes'
- */
-const validSizes = [8, 12, 14, 16, 24, 32, 48, 72];
-
export default {
components: {
+ GlBadge,
GlIcon,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
status: {
type: Object,
required: true,
validator(status) {
- const { group, icon } = status;
- return (
- typeof group === 'string' &&
- group.length &&
- typeof icon === 'string' &&
- icon.startsWith('status_')
- );
+ const { icon } = status;
+ return typeof icon === 'string' && icon.startsWith('status_');
},
},
- size: {
- type: Number,
- required: false,
- default: 16,
- validator(value) {
- return validSizes.includes(value);
- },
- },
- isActive: {
+ showStatusText: {
type: Boolean,
required: false,
default: false,
},
- isBorderless: {
+ showTooltip: {
type: Boolean,
required: false,
- default: false,
+ default: true,
},
- isInteractive: {
+ useLink: {
type: Boolean,
+ default: true,
required: false,
- default: false,
- },
- cssClasses: {
- type: String,
- required: false,
- default: '',
},
},
computed: {
- wrapperStyleClasses() {
- const status = this.status.group;
- return `ci-status-icon ci-status-icon-${status} gl-rounded-full gl-justify-content-center gl-line-height-0`;
+ title() {
+ if (this.showTooltip) {
+ // show tooltip only when not showing text already
+ return !this.showStatusText ? this.status?.text : null;
+ }
+ return null;
+ },
+ ariaLabel() {
+ // show aria-label only when text is not rendered
+ if (!this.showStatusText) {
+ return this.status?.text;
+ }
+ return null;
+ },
+ href() {
+ // href can come from GraphQL (camelCase) or REST API (snake_case)
+ if (this.useLink) {
+ return this.status.detailsPath || this.status.details_path;
+ }
+ return null;
},
icon() {
- return this.isBorderless ? `${this.status.icon}_borderless` : this.status.icon;
+ if (this.status.icon) {
+ return `${this.status.icon}_borderless`;
+ }
+ return null;
+ },
+ variant() {
+ switch (this.status.icon) {
+ case 'status_success':
+ return 'success';
+ case 'status_warning':
+ case 'status_pending':
+ return 'warning';
+ case 'status_failed':
+ return 'danger';
+ case 'status_running':
+ return 'info';
+ // default covers the styles for the remainder of CI
+ // statuses that are not explicitly stated here
+ default:
+ return 'neutral';
+ }
},
},
};
</script>
<template>
- <span
- :class="[
- wrapperStyleClasses,
- { interactive: isInteractive, active: isActive, borderless: isBorderless },
- ]"
- :style="{ height: `${size}px`, width: `${size}px` }"
+ <gl-badge
+ v-gl-tooltip
+ class="ci-icon gl-p-2"
+ :class="`ci-icon-variant-${variant}`"
+ :variant="variant"
+ :title="title"
+ :aria-label="ariaLabel"
+ :href="href"
+ size="md"
+ data-testid="ci-icon"
+ @click="$emit('ciStatusBadgeClick')"
>
- <gl-icon :name="icon" :size="size" :class="cssClasses" :aria-label="status.icon" />
- </span>
+ <span class="ci-icon-gl-icon-wrapper"><gl-icon :name="icon" /></span
+ ><span v-if="showStatusText" class="gl-mx-2 gl-white-space-nowrap" data-testid="ci-icon-text">{{
+ status.text
+ }}</span>
+ </gl-badge>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue b/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue
index f62bfb551df..55767c5f4bc 100644
--- a/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue
@@ -1,11 +1,5 @@
<script>
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
- GlSearchBoxByType,
- GlSprintf,
-} from '@gitlab/ui';
+import { GlDisclosureDropdown, GlIcon, GlSearchBoxByType, GlSprintf } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { __, n__, s__, sprintf } from '~/locale';
@@ -16,12 +10,16 @@ export const i18n = {
searchFiles: __('Search files'),
};
+const variantCssColorMap = {
+ success: 'gl-text-green-500',
+ danger: 'gl-text-red-500',
+};
+
export default {
i18n,
components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
+ GlDisclosureDropdown,
+ GlIcon,
GlSearchBoxByType,
GlSprintf,
},
@@ -54,6 +52,15 @@ export default {
? fuzzaldrinPlus.filter(this.files, this.search, { key: 'name' })
: this.files;
},
+ dropdownItems() {
+ return this.filteredFiles.map((file) => {
+ return {
+ ...file,
+ text: file.name || this.$options.i18n.noFileNameAvailable,
+ iconColor: variantCssColorMap[file.iconColor],
+ };
+ });
+ },
messageChanged() {
return sprintf(
n__(
@@ -64,21 +71,21 @@ export default {
{ count: this.changed },
);
},
-
- additionsText() {
- return n__('Diffs|%d addition', 'Diffs|%d additions', this.added);
- },
- deletionsText() {
- return n__('Diffs|%d deletion', 'Diffs|%d deletions', this.deleted);
- },
},
methods: {
- jumpToFile(fileHash) {
- window.location.hash = fileHash;
- },
focusInput() {
this.$refs.search.focusInput();
},
+ focusFirstItem() {
+ if (!this.filteredFiles.length) return;
+ this.$el.querySelector('.gl-new-dropdown-item:first-child').focus();
+ },
+ additionsText(numberOfChanges = this.added) {
+ return n__('Diffs|%d addition', 'Diffs|%d additions', numberOfChanges);
+ },
+ deletionsText(numberOfChanges = this.deleted) {
+ return n__('Diffs|%d deletion', 'Diffs|%d deletions', numberOfChanges);
+ },
},
};
</script>
@@ -87,15 +94,15 @@ export default {
<div>
<gl-sprintf :message="messageChanged">
<template #dropdown="{ content: dropdownText }">
- <gl-dropdown
+ <gl-disclosure-dropdown
+ :toggle-text="dropdownText"
+ :items="dropdownItems"
category="tertiary"
variant="confirm"
- :text="dropdownText"
data-testid="diff-stats-dropdown"
class="gl-vertical-align-baseline"
toggle-class="gl-px-0! gl-font-weight-bold!"
- menu-class="gl-w-auto!"
- no-flip
+ fluid-width
@shown="focusInput"
>
<template #header>
@@ -103,35 +110,38 @@ export default {
ref="search"
v-model.trim="search"
:placeholder="$options.i18n.searchFiles"
+ class="gl-mx-3 gl-my-4"
+ @keydown.down="focusFirstItem"
/>
+ <span v-if="!filteredFiles.length" class="gl-mx-3">
+ {{ $options.i18n.noFilesFound }}
+ </span>
</template>
- <gl-dropdown-item
- v-for="file in filteredFiles"
- :key="file.href"
- :icon-name="file.icon"
- :icon-color="file.iconColor"
- @click="jumpToFile(file.href)"
- >
- <div class="gl-display-flex">
- <span v-if="file.name" class="gl-font-weight-bold gl-mr-3 gl-text-truncate">{{
- file.name
- }}</span>
- <span v-else class="gl-mr-3 gl-font-weight-bold gl-font-style-italic gl-gray-400">{{
- $options.i18n.noFileNameAvailable
- }}</span>
- <span class="gl-ml-auto gl-white-space-nowrap">
- <span class="gl-text-green-600">+{{ file.added }}</span>
- <span class="gl-text-red-500">-{{ file.removed }}</span>
- </span>
+ <template #list-item="{ item }">
+ <div class="gl-display-flex gl-gap-3 gl-align-items-center gl-overflow-hidden">
+ <gl-icon :name="item.icon" :class="item.iconColor" class="gl-flex-shrink-0" />
+ <div class="gl-flex-grow-1 gl-overflow-hidden">
+ <div class="gl-display-flex">
+ <span
+ class="gl-font-weight-bold gl-mr-3 gl-flex-grow-1"
+ :class="item.name ? 'gl-text-truncate' : 'gl-font-style-italic gl-gray-400'"
+ >{{ item.text }}</span
+ >
+ <span class="gl-ml-auto gl-white-space-nowrap" aria-hidden="true">
+ <span class="gl-text-green-600">+{{ item.added }}</span>
+ <span class="gl-text-red-500">-{{ item.removed }}</span>
+ </span>
+ <span class="gl-sr-only"
+ >{{ additionsText(item.added) }}, {{ deletionsText(item.removed) }}</span
+ >
+ </div>
+ <div class="gl-text-gray-700 gl-overflow-hidden gl-text-overflow-ellipsis">
+ {{ item.path }}
+ </div>
+ </div>
</div>
- <div class="gl-text-gray-700 gl-overflow-hidden gl-text-overflow-ellipsis">
- {{ file.path }}
- </div>
- </gl-dropdown-item>
- <gl-dropdown-text v-if="!filteredFiles.length">
- {{ $options.i18n.noFilesFound }}
- </gl-dropdown-text>
- </gl-dropdown>
+ </template>
+ </gl-disclosure-dropdown>
</template>
</gl-sprintf>
<span
@@ -140,12 +150,20 @@ export default {
>
<gl-sprintf :message="$options.i18n.messageAdditionsDeletions">
<template #additions>
- <span class="gl-text-green-600 gl-font-weight-bold">{{ additionsText }}</span>
+ <span class="gl-text-green-600 gl-font-weight-bold">{{ additionsText() }}</span>
</template>
<template #deletions>
- <span class="gl-text-red-500 gl-font-weight-bold">{{ deletionsText }}</span>
+ <span class="gl-text-red-500 gl-font-weight-bold">{{ deletionsText() }}</span>
</template>
</gl-sprintf>
</span>
</div>
</template>
+
+<style scoped>
+/* TODO: Use max-height prop when gitlab-ui got updated.
+See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2374 */
+::v-deep .gl-new-dropdown-inner {
+ max-height: 310px;
+}
+</style>
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/constants.js b/app/assets/javascripts/vue_shared/components/entity_select/constants.js
index 0fb5a2d5534..5bad907c9f9 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/constants.js
+++ b/app/assets/javascripts/vue_shared/components/entity_select/constants.js
@@ -14,3 +14,13 @@ export const PROJECT_TOGGLE_TEXT = s__('ProjectSelect|Search for project');
export const PROJECT_HEADER_TEXT = s__('ProjectSelect|Select a project');
export const FETCH_PROJECTS_ERROR = __('Unable to fetch projects. Reload the page to try again.');
export const FETCH_PROJECT_ERROR = __('Unable to fetch project. Reload the page to try again.');
+
+// Organizations
+export const ORGANIZATION_TOGGLE_TEXT = s__('Organization|Search for an organization');
+export const ORGANIZATION_HEADER_TEXT = s__('Organization|Select an organization');
+export const FETCH_ORGANIZATIONS_ERROR = s__(
+ 'Organization|Unable to fetch organizations. Reload the page to try again.',
+);
+export const FETCH_ORGANIZATION_ERROR = s__(
+ 'Organization|Unable to fetch organizations. Reload the page to try again.',
+);
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
index 970c24c6e87..1a215454ab6 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
@@ -22,6 +22,11 @@ export default {
type: String,
required: true,
},
+ description: {
+ type: String,
+ required: false,
+ default: '',
+ },
inputName: {
type: String,
required: true,
@@ -31,7 +36,7 @@ export default {
required: true,
},
initialSelection: {
- type: String,
+ type: [String, Number],
required: false,
default: null,
},
@@ -57,6 +62,11 @@ export default {
required: false,
default: null,
},
+ toggleClass: {
+ type: [String, Array, Object],
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -152,6 +162,7 @@ export default {
this.searching = true;
const name = await this.fetchInitialSelectionText(this.initialSelection);
+
this.selectedValue = this.initialSelection;
this.selectedText = name;
this.pristine = false;
@@ -178,7 +189,7 @@ export default {
</script>
<template>
- <gl-form-group :label="label">
+ <gl-form-group :label="label" :description="description">
<slot name="error"></slot>
<template v-if="Boolean($scopedSlots.label)" #label>
<slot name="label"></slot>
@@ -196,6 +207,7 @@ export default {
:no-results-text="noResultsText"
:infinite-scroll="hasMoreItems"
:infinite-scroll-loading="infiniteScrollLoading"
+ :toggle-class="toggleClass"
searchable
@shown="onShown"
@search="search"
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
index eb7b20fa4c1..8a338551fbe 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import Api, { DEFAULT_PER_PAGE } from '~/api';
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue
new file mode 100644
index 00000000000..d068d86d95b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue
@@ -0,0 +1,150 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import getCurrentUserOrganizationsQuery from '~/organizations/index/graphql/organizations.query.graphql';
+import getOrganizationQuery from '~/organizations/shared/graphql/queries/organization.query.graphql';
+import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPENAME_ORGANIZATION } from '~/graphql_shared/constants';
+import {
+ ORGANIZATION_TOGGLE_TEXT,
+ ORGANIZATION_HEADER_TEXT,
+ FETCH_ORGANIZATIONS_ERROR,
+ FETCH_ORGANIZATION_ERROR,
+} from './constants';
+import EntitySelect from './entity_select.vue';
+
+export default {
+ name: 'OrganizationSelect',
+ components: {
+ GlAlert,
+ EntitySelect,
+ },
+ props: {
+ block: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ label: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ description: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ inputName: {
+ type: String,
+ required: true,
+ },
+ inputId: {
+ type: String,
+ required: true,
+ },
+ initialSelection: {
+ type: [String, Number],
+ required: false,
+ default: null,
+ },
+ clearable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ toggleClass: {
+ type: [String, Array, Object],
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ errorMessage: '',
+ };
+ },
+ methods: {
+ async fetchOrganizations() {
+ try {
+ const {
+ data: {
+ currentUser: {
+ organizations: { nodes },
+ },
+ },
+ } = await this.$apollo.query({
+ query: getCurrentUserOrganizationsQuery,
+ // TODO: implement search support - https://gitlab.com/gitlab-org/gitlab/-/issues/429999.
+ });
+
+ return {
+ items: nodes.map((organization) => ({
+ text: organization.name,
+ value: getIdFromGraphQLId(organization.id),
+ })),
+ // TODO: implement pagination - https://gitlab.com/gitlab-org/gitlab/-/issues/429999.
+ totalPages: 1,
+ };
+ } catch (error) {
+ this.handleError({ message: FETCH_ORGANIZATIONS_ERROR, error });
+
+ return { items: [], totalPages: 0 };
+ }
+ },
+ async fetchOrganizationName(id) {
+ try {
+ const {
+ data: {
+ organization: { name },
+ },
+ } = await this.$apollo.query({
+ query: getOrganizationQuery,
+ variables: { id: convertToGraphQLId(TYPENAME_ORGANIZATION, id) },
+ });
+
+ return name;
+ } catch (error) {
+ this.handleError({ message: FETCH_ORGANIZATION_ERROR, error });
+
+ return '';
+ }
+ },
+ handleError({ message, error }) {
+ Sentry.captureException(error);
+ this.errorMessage = message;
+ },
+ dismissError() {
+ this.errorMessage = '';
+ },
+ },
+ i18n: {
+ toggleText: ORGANIZATION_TOGGLE_TEXT,
+ selectGroup: ORGANIZATION_HEADER_TEXT,
+ },
+};
+</script>
+
+<template>
+ <entity-select
+ :block="block"
+ :label="label"
+ :description="description"
+ :input-name="inputName"
+ :input-id="inputId"
+ :initial-selection="initialSelection"
+ :clearable="clearable"
+ :header-text="$options.i18n.selectGroup"
+ :default-toggle-text="$options.i18n.toggleText"
+ :fetch-items="fetchOrganizations"
+ :fetch-initial-selection-text="fetchOrganizationName"
+ :toggle-class="toggleClass"
+ v-on="$listeners"
+ >
+ <template #error>
+ <gl-alert v-if="errorMessage" class="gl-mb-3" variant="danger" @dismiss="dismissError">{{
+ errorMessage
+ }}</gl-alert>
+ </template>
+ </entity-select>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue
index 13a825a68f6..8c371e3d4ce 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Api from '~/api';
import SafeHtml from '~/vue_shared/directives/safe_html';
import {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index 346384e3023..d39e4d2ee42 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -292,7 +292,9 @@ export default {
this.recentSearchesService.save(resultantSearches);
this.recentSearches = [];
},
- handleFilterSubmit() {
+ async handleFilterSubmit() {
+ this.blurSearchInput();
+ await this.$nextTick();
const filterTokens = uniqueTokens(this.filterValue);
this.filterValue = filterTokens;
@@ -309,7 +311,6 @@ export default {
// https://gitlab.com/gitlab-org/gitlab-foss/issues/30821
});
}
- this.blurSearchInput();
this.$emit('onFilter', this.removeQuotesEnclosure(filterTokens));
},
historyTokenOptionTitle(historyToken) {
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 23de8dd5596..3857dd9c55d 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
@@ -7,9 +7,10 @@ import {
GlDropdownText,
GlLoadingIcon,
} from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { debounce, last } from 'lodash';
import { stripQuotes } from '~/lib/utils/text_utility';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { DEBOUNCE_DELAY, FILTERS_NONE_ANY, OPERATOR_NOT, OPERATOR_OR } from '../constants';
import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
@@ -22,6 +23,7 @@ export default {
GlDropdownText,
GlLoadingIcon,
},
+ mixins: [glFeatureFlagMixin()],
props: {
config: {
type: Object,
@@ -70,6 +72,11 @@ export default {
required: false,
default: undefined,
},
+ multiSelectValues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
@@ -94,7 +101,11 @@ export default {
return this.preloadedSuggestions.map((tokenValue) => tokenValue[this.valueIdentifier]);
},
activeTokenValue() {
- return this.getActiveTokenValue(this.suggestions, this.value.data);
+ const data =
+ this.glFeatures.groupMultiSelectTokens && Array.isArray(this.value.data)
+ ? last(this.value.data)
+ : this.value.data;
+ return this.getActiveTokenValue(this.suggestions, data);
},
availableDefaultSuggestions() {
if ([OPERATOR_NOT, OPERATOR_OR].includes(this.value.operator)) {
@@ -146,10 +157,14 @@ export default {
watch: {
active: {
immediate: true,
- handler(newValue) {
- if (!newValue && !this.suggestions.length) {
- const search = this.searchTerm ? this.searchTerm : this.value.data;
- this.$emit('fetch-suggestions', search);
+ handler(active) {
+ if (!active && !this.suggestions.length) {
+ // data could be a string or an array of strings
+ const selectedItems = [this.value.data].flat();
+ selectedItems.forEach((item) => {
+ const search = this.searchTerm ? this.searchTerm : item;
+ this.$emit('fetch-suggestions', search);
+ });
}
},
},
@@ -163,6 +178,9 @@ export default {
},
methods: {
handleInput: debounce(function debouncedSearch({ data, operator }) {
+ // in multiSelect mode, data could be an array
+ if (Array.isArray(data)) return;
+
// Prevent fetching suggestions when data or operator is not present
if (data || operator) {
this.searchKey = data;
@@ -181,8 +199,11 @@ export default {
}
}, DEBOUNCE_DELAY),
handleTokenValueSelected(selectedValue) {
- const activeTokenValue = this.getActiveTokenValue(this.suggestions, selectedValue);
+ if (this.glFeatures.groupMultiSelectTokens) {
+ this.$emit('token-selected', selectedValue);
+ }
+ const activeTokenValue = this.getActiveTokenValue(this.suggestions, selectedValue);
// Make sure that;
// 1. Recently used values feature is enabled
// 2. User has actually selected a value
@@ -210,6 +231,7 @@ export default {
:config="config"
:value="value"
:active="active"
+ :multi-select-values="multiSelectValues"
v-bind="$attrs"
v-on="$listeners"
@input="handleInput"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
index 4601287b417..c5326ead60d 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
@@ -1,11 +1,12 @@
<script>
-import { GlAvatar, GlFilteredSearchSuggestion } from '@gitlab/ui';
+import { GlAvatar, GlIcon, GlIntersperse, GlFilteredSearchSuggestion } from '@gitlab/ui';
import { compact } from 'lodash';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { OPTIONS_NONE_ANY } from '../constants';
import BaseToken from './base_token.vue';
@@ -14,8 +15,11 @@ export default {
components: {
BaseToken,
GlAvatar,
+ GlIcon,
+ GlIntersperse,
GlFilteredSearchSuggestion,
},
+ mixins: [glFeatureFlagMixin()],
props: {
config: {
type: Object,
@@ -32,8 +36,11 @@ export default {
},
data() {
return {
+ // current users visible in list
users: this.config.initialUsers || [],
+ allUsers: this.config.initialUsers || [],
loading: false,
+ selectedUsernames: [],
};
},
computed: {
@@ -49,13 +56,69 @@ export default {
fetchUsersQuery() {
return this.config.fetchUsers ? this.config.fetchUsers : this.fetchUsersBySearchTerm;
},
+ multiSelectEnabled() {
+ return this.config.multiSelect && this.glFeatures.groupMultiSelectTokens;
+ },
+ },
+ watch: {
+ value: {
+ deep: true,
+ immediate: true,
+ handler(newValue) {
+ const { data } = newValue;
+
+ if (!this.multiSelectEnabled) {
+ return;
+ }
+
+ // don't add empty values to selectedUsernames
+ if (!data) {
+ return;
+ }
+
+ if (Array.isArray(data)) {
+ this.selectedUsernames = data;
+ // !active so we don't add strings while searching, e.g. r, ro, roo
+ // !includes so we don't add the same usernames (if @input is emitted twice)
+ } else if (!this.active && !this.selectedUsernames.includes(data)) {
+ this.selectedUsernames = this.selectedUsernames.concat(data);
+ }
+ },
+ },
},
methods: {
getActiveUser(users, data) {
return users.find((user) => user.username.toLowerCase() === data.toLowerCase());
},
getAvatarUrl(user) {
- return user.avatarUrl || user.avatar_url;
+ return user?.avatarUrl || user?.avatar_url;
+ },
+ displayNameFor(username) {
+ return this.getActiveUser(this.allUsers, username)?.name || `@${username}`;
+ },
+ avatarFor(username) {
+ const user = this.getActiveUser(this.allUsers, username);
+ return this.getAvatarUrl(user);
+ },
+ addCheckIcon(username) {
+ return this.multiSelectEnabled && this.selectedUsernames.includes(username);
+ },
+ addPadding(username) {
+ return this.multiSelectEnabled && !this.selectedUsernames.includes(username);
+ },
+ handleSelected(username) {
+ if (!this.multiSelectEnabled) {
+ return;
+ }
+
+ const index = this.selectedUsernames.indexOf(username);
+ if (index > -1) {
+ this.selectedUsernames.splice(index, 1);
+ } else {
+ this.selectedUsernames.push(username);
+ }
+
+ this.$emit('input', { ...this.value, data: '' });
},
fetchUsersBySearchTerm(search) {
return this.$apollo
@@ -79,6 +142,7 @@ export default {
// TODO: rm when completed https://gitlab.com/gitlab-org/gitlab/-/issues/345756
this.users = Array.isArray(res) ? compact(res) : compact(res.data);
+ this.allUsers = this.allUsers.concat(this.users);
})
.catch(() =>
createAlert({
@@ -103,18 +167,32 @@ export default {
:get-active-token-value="getActiveUser"
:default-suggestions="defaultUsers"
:preloaded-suggestions="preloadedUsers"
+ :multi-select-values="selectedUsernames"
v-bind="$attrs"
@fetch-suggestions="fetchUsers"
+ @token-selected="handleSelected"
v-on="$listeners"
>
<template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
- <gl-avatar
- v-if="activeTokenValue"
- :size="16"
- :src="getAvatarUrl(activeTokenValue)"
- class="gl-mr-2"
- />
- {{ activeTokenValue ? activeTokenValue.name : inputValue }}
+ <gl-intersperse v-if="multiSelectEnabled" separator=",">
+ <span
+ v-for="(username, index) in selectedUsernames"
+ :key="username"
+ :class="{ 'gl-ml-2': index > 0 }"
+ ><gl-avatar :size="16" :src="avatarFor(username)" class="gl-mr-1" />{{
+ displayNameFor(username)
+ }}</span
+ >
+ </gl-intersperse>
+ <template v-else>
+ <gl-avatar
+ v-if="activeTokenValue"
+ :size="16"
+ :src="getAvatarUrl(activeTokenValue)"
+ class="gl-mr-2"
+ />
+ {{ activeTokenValue ? activeTokenValue.name : inputValue }}
+ </template>
</template>
<template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
@@ -122,7 +200,15 @@ export default {
:key="user.username"
:value="user.username"
>
- <div class="gl-display-flex">
+ <div
+ class="gl-display-flex gl-align-items-center"
+ :class="{ 'gl-pl-6': addPadding(user.username) }"
+ >
+ <gl-icon
+ v-if="addCheckIcon(user.username)"
+ name="check"
+ class="gl-mr-3 gl-text-secondary gl-flex-shrink-0"
+ />
<gl-avatar :size="32" :src="getAvatarUrl(user)" />
<div>
<div>{{ user.name }}</div>
diff --git a/app/assets/javascripts/vue_shared/components/form/errors_alert.stories.js b/app/assets/javascripts/vue_shared/components/form/errors_alert.stories.js
new file mode 100644
index 00000000000..7c32e38a299
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/form/errors_alert.stories.js
@@ -0,0 +1,21 @@
+import ErrorsAlert from './errors_alert.vue';
+
+export default {
+ component: ErrorsAlert,
+ title: 'vue_shared/form/errors_alert',
+};
+
+const defaultProps = {
+ errors: ['Name must be at least 5 characters.', 'Name cannot contain special characters.'],
+};
+
+const Template = (args) => ({
+ components: { ErrorsAlert },
+ data() {
+ return { errors: args.errors };
+ },
+ template: `<errors-alert v-model="errors" />`,
+});
+
+export const Default = Template.bind({});
+Default.args = defaultProps;
diff --git a/app/assets/javascripts/vue_shared/components/form/errors_alert.vue b/app/assets/javascripts/vue_shared/components/form/errors_alert.vue
new file mode 100644
index 00000000000..3e33168781b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/form/errors_alert.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { n__ } from '~/locale';
+
+export default {
+ components: { GlAlert },
+ model: {
+ prop: 'errors',
+ },
+ props: {
+ errors: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ return n__(
+ 'The form contains the following error:',
+ 'The form contains the following errors:',
+ this.errors.length,
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-alert
+ v-if="errors.length"
+ class="gl-mb-5"
+ :title="title"
+ variant="danger"
+ @dismiss="$emit('input', [])"
+ >
+ <ul class="gl-pl-5 gl-mb-0">
+ <li v-for="error in errors" :key="error">
+ {{ error }}
+ </li>
+ </ul>
+ </gl-alert>
+</template>
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 d97f1ae6135..0455685627d 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
@@ -94,8 +94,12 @@ export default {
computedValueIsVisible() {
return !this.showToggleVisibilityButton || this.valueIsVisible;
},
- inputType() {
- return this.computedValueIsVisible ? 'text' : 'password';
+ formInputClass() {
+ return [
+ 'gl-font-monospace! gl-cursor-default!',
+ { 'input-copy-show-disc': !this.computedValueIsVisible },
+ this.formInputGroupProps.class,
+ ];
},
},
mounted() {
@@ -157,10 +161,9 @@ export default {
ref="input"
:readonly="readonly"
:width="size"
- class="gl-font-monospace! gl-cursor-default!"
+ :class="formInputClass"
v-bind="formInputGroupProps"
:value="value"
- :type="inputType"
@input="handleInput"
@click="handleClick"
/>
@@ -194,3 +197,8 @@ export default {
</template>
</gl-form-group>
</template>
+<style>
+.input-copy-show-disc {
+ -webkit-text-security: disc;
+}
+</style>
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/constants.js b/app/assets/javascripts/vue_shared/components/list_selector/constants.js
new file mode 100644
index 00000000000..cff9c56a1c0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_selector/constants.js
@@ -0,0 +1,6 @@
+import { __ } from '~/locale';
+
+export const CONFIG = {
+ users: { title: __('Users'), icon: 'user', filterKey: 'username', showNamespaceDropdown: true },
+ groups: { title: __('Groups'), icon: 'group', filterKey: 'name' },
+};
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/group_item.vue b/app/assets/javascripts/vue_shared/components/list_selector/group_item.vue
new file mode 100644
index 00000000000..2d24cc5553b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_selector/group_item.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlAvatar, GlButton } from '@gitlab/ui';
+import { sprintf, __ } from '~/locale';
+
+export default {
+ name: 'GroupItem',
+ components: {
+ GlAvatar,
+ GlButton,
+ },
+ props: {
+ data: {
+ type: Object,
+ required: true,
+ },
+ canDelete: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ deleteButtonLabel() {
+ return sprintf(__('Delete %{name}'), { name: this.name });
+ },
+ fullName() {
+ return this.data.fullName;
+ },
+ name() {
+ return this.data.name;
+ },
+ avatarUrl() {
+ return this.data.avatarUrl;
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="gl-display-flex gl-align-items-center gl-gap-3" @click="$emit('select', name)">
+ <gl-avatar :alt="fullName" :size="32" :src="avatarUrl" />
+ <span class="gl-display-flex gl-flex-direction-column gl-flex-grow-1">
+ <span class="gl-font-weight-bold">{{ fullName }}</span>
+ <span class="gl-text-gray-600">@{{ name }}</span>
+ </span>
+
+ <gl-button
+ v-if="canDelete"
+ icon="remove"
+ :aria-label="deleteButtonLabel"
+ category="tertiary"
+ @click="$emit('delete', name)"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/index.vue b/app/assets/javascripts/vue_shared/components/list_selector/index.vue
new file mode 100644
index 00000000000..b8480a0c496
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_selector/index.vue
@@ -0,0 +1,193 @@
+<script>
+import { GlCard, GlIcon, GlCollapsibleListbox, GlSearchBoxByType } from '@gitlab/ui';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { createAlert } from '~/alert';
+import { __ } from '~/locale';
+import groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql';
+import Api from '~/api';
+import UserItem from './user_item.vue';
+import GroupItem from './group_item.vue';
+import { CONFIG } from './constants';
+
+const I18N = {
+ allGroups: __('All groups'),
+ projectGroups: __('Project groups'),
+ apiErrorMessage: __('An error occurred while fetching. Please try again.'),
+};
+
+export default {
+ name: 'ListSelector',
+ i18n: I18N,
+ components: {
+ GlCard,
+ GlIcon,
+ GlSearchBoxByType,
+ GlCollapsibleListbox,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ type: {
+ type: String,
+ required: true,
+ },
+ selectedItems: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ groupPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ searchValue: '',
+ isProjectNamespace: 'true',
+ selected: [],
+ items: [],
+ };
+ },
+ computed: {
+ config() {
+ return CONFIG[this.type];
+ },
+ isUserVariant() {
+ return this.type === 'users';
+ },
+ component() {
+ return this.isUserVariant ? UserItem : GroupItem;
+ },
+ namespaceDropdownText() {
+ return parseBoolean(this.isProjectNamespace)
+ ? this.$options.i18n.projectGroups
+ : this.$options.i18n.allGroups;
+ },
+ },
+ methods: {
+ async handleSearchInput(search) {
+ this.$refs.results.open();
+
+ try {
+ if (this.isUserVariant) {
+ this.items = await this.fetchUsersBySearchTerm(search);
+ } else {
+ this.items = await this.fetchGroupsBySearchTerm(search);
+ }
+ } catch (e) {
+ createAlert({
+ message: this.$options.i18n.apiErrorMessage,
+ });
+ }
+ },
+ async fetchUsersBySearchTerm(search) {
+ let users = [];
+ if (parseBoolean(this.isProjectNamespace)) {
+ users = await Api.projectUsers(this.projectPath, search);
+ } else {
+ const groupMembers = await Api.groupMembers(this.groupPath, { query: search });
+ users = groupMembers?.data || [];
+ }
+
+ return users?.map((user) => ({ text: user.name, value: user.username, ...user }));
+ },
+ fetchGroupsBySearchTerm(search) {
+ return this.$apollo
+ .query({
+ query: groupsAutocompleteQuery,
+ variables: { search },
+ })
+ .then(({ data }) =>
+ data?.groups.nodes.map((group) => ({
+ text: group.fullName,
+ value: group.name,
+ ...group,
+ })),
+ );
+ },
+ getItemByKey(key) {
+ return this.items.find((item) => item[this.config.filterKey] === key);
+ },
+ handleSelectItem(key) {
+ this.$emit('select', this.getItemByKey(key));
+ },
+ handleDeleteItem(key) {
+ this.$emit('delete', key);
+ },
+ handleSelectNamespace() {
+ this.items = [];
+ this.searchValue = '';
+ },
+ },
+ namespaceOptions: [
+ { text: I18N.projectGroups, value: 'true' },
+ { text: I18N.allGroups, value: 'false' },
+ ],
+};
+</script>
+
+<template>
+ <gl-card header-class="gl-new-card-header gl-border-none" body-class="gl-card-footer">
+ <template #header
+ ><strong data-testid="list-selector-title"
+ >{{ title }}
+ <span class="gl-text-gray-700 gl-ml-3"
+ ><gl-icon :name="config.icon" /> {{ selectedItems.length }}</span
+ ></strong
+ ></template
+ >
+
+ <div class="gl-display-flex gl-gap-3" :class="{ 'gl-mb-4': selectedItems.length }">
+ <gl-collapsible-listbox
+ ref="results"
+ v-model="selected"
+ class="list-selector gl-display-block gl-flex-grow-1"
+ :items="items"
+ multiple
+ @shown="$refs.search.focusInput()"
+ >
+ <template #toggle>
+ <gl-search-box-by-type
+ ref="search"
+ v-model="searchValue"
+ autofocus
+ debounce="500"
+ @input="handleSearchInput"
+ />
+ </template>
+
+ <template #list-item="{ item }">
+ <component :is="component" :data="item" @select="handleSelectItem" />
+ </template>
+ </gl-collapsible-listbox>
+
+ <gl-collapsible-listbox
+ v-if="config.showNamespaceDropdown"
+ v-model="isProjectNamespace"
+ :toggle-text="namespaceDropdownText"
+ :items="$options.namespaceOptions"
+ @select="handleSelectNamespace"
+ />
+ </div>
+
+ <component
+ :is="component"
+ v-for="(item, index) of selectedItems"
+ :key="index"
+ :class="{ 'gl-border-t': index > 0 }"
+ class="gl-p-3"
+ :data="item"
+ can-delete
+ @delete="handleDeleteItem"
+ />
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/user_item.vue b/app/assets/javascripts/vue_shared/components/list_selector/user_item.vue
new file mode 100644
index 00000000000..fdbc767db81
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_selector/user_item.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlAvatar, GlButton } from '@gitlab/ui';
+import { sprintf, __ } from '~/locale';
+
+export default {
+ name: 'UserItem',
+ components: {
+ GlAvatar,
+ GlButton,
+ },
+ props: {
+ data: {
+ type: Object,
+ required: true,
+ },
+ canDelete: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ deleteButtonLabel() {
+ return sprintf(__('Delete %{name}'), { name: this.name });
+ },
+ name() {
+ return this.data.name;
+ },
+ username() {
+ return this.data.username;
+ },
+ avatarUrl() {
+ return this.data.avatarUrl;
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="gl-display-flex gl-align-items-center gl-gap-3" @click="$emit('select', username)">
+ <gl-avatar :alt="name" :size="32" :src="avatarUrl" />
+ <span class="gl-display-flex gl-flex-direction-column gl-flex-grow-1">
+ <span class="gl-font-weight-bold">{{ name }}</span>
+ <span class="gl-text-gray-600">@{{ username }}</span>
+ </span>
+
+ <gl-button
+ v-if="canDelete"
+ icon="remove"
+ :aria-label="deleteButtonLabel"
+ category="tertiary"
+ @click="$emit('delete', username)"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 741bdfd211b..cc3c95a047b 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -492,7 +492,7 @@ export default {
tracking-property="quickAction"
/>
<comment-templates-dropdown
- v-if="!previewMarkdown && newCommentTemplatePath && glFeatures.savedReplies"
+ v-if="!previewMarkdown && newCommentTemplatePath"
:new-comment-template-path="newCommentTemplatePath"
@select="insertSavedReply"
/>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
index 4a3c3cf0053..73c030b23dc 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -190,7 +190,7 @@ export default {
renderMarkdown(markdown) {
const url = setUrlParams(
{ render_quick_actions: this.supportsQuickActions },
- joinPaths(gon.relative_url_root || window.location.origin, this.renderMarkdownPath),
+ joinPaths(window.location.origin, gon.relative_url_root, this.renderMarkdownPath),
);
return axios.post(url, { text: markdown }).then(({ data }) => data.body);
},
diff --git a/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js b/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
index 27237f2f16b..6d74c1d083a 100644
--- a/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
+++ b/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { helpPagePath } from '~/helpers/help_page_helper';
import axios from '~/lib/utils/axios_utils';
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 0ec8b6e2a0a..3bee539688b 100644
--- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
@@ -64,7 +64,7 @@ export default {
});
},
lockedContextText() {
- return sprintf(__('This %{noteableTypeText} is locked.'), {
+ return sprintf(__('The discussion in this %{noteableTypeText} is locked.'), {
noteableTypeText: this.noteableTypeText,
});
},
@@ -80,7 +80,7 @@ export default {
<gl-sprintf
:message="
__(
- 'This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and %{lockedLinkStart}locked%{lockedLinkEnd}.',
+ 'This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and its %{lockedLinkStart}discussion is locked%{lockedLinkEnd}.',
)
"
>
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 81cbbf951ad..6a5884e4857 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -30,12 +30,10 @@ import { renderGFM } from '~/behaviors/markdown/render_gfm';
import TimelineEntryItem from './timeline_entry_item.vue';
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
-const MR_ICON_COLORS = {
+const ICON_COLORS = {
check: 'gl-bg-green-100 gl-text-green-700',
'merge-request-close': 'gl-bg-red-100 gl-text-red-700',
merge: 'gl-bg-blue-100 gl-text-blue-700',
-};
-const ICON_COLORS = {
'issue-close': 'gl-bg-blue-100 gl-text-blue-700',
};
@@ -76,6 +74,9 @@ export default {
noteAnchorId() {
return `note_${this.note.id}`;
},
+ isAllowedIcon() {
+ return Object.keys(ICON_COLORS).includes(this.note.system_note_icon_name);
+ },
isTargetNote() {
return this.targetNoteHash === this.noteAnchorId;
},
@@ -95,15 +96,8 @@ export default {
isMergeRequest() {
return this.getNoteableData.noteableType === 'MergeRequest';
},
- hasIconColors() {
- if (!this.isMergeRequest) return true;
-
- return this.isMergeRequest && MR_ICON_COLORS[this.note.system_note_icon_name];
- },
iconBgClass() {
- const colors = this.isMergeRequest ? MR_ICON_COLORS : ICON_COLORS;
-
- return colors[this.note.system_note_icon_name] || 'gl-bg-gray-50 gl-text-gray-600';
+ return ICON_COLORS[this.note.system_note_icon_name] || 'gl-bg-gray-50 gl-text-gray-600';
},
},
mounted() {
@@ -140,17 +134,16 @@ export default {
:class="[
iconBgClass,
{
- 'mr-system-note-empty gl-bg-gray-900!': !hasIconColors,
- 'gl-w-6 gl-h-6 gl-mt-n1 gl-ml-2': !isMergeRequest,
- 'mr-system-note-icon': isMergeRequest,
+ 'system-note-icon': isAllowedIcon,
+ 'system-note-tiny-dot gl-bg-gray-900!': !isAllowedIcon,
},
]"
class="gl-float-left gl--flex-center gl-rounded-full gl-relative timeline-icon"
>
<gl-icon
- v-if="note.system_note_icon_name && hasIconColors"
+ v-if="isAllowedIcon"
:name="note.system_note_icon_name"
- :size="isMergeRequest ? 12 : 16"
+ :size="12"
data-testid="timeline-icon"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue
index 9bce9402afa..e2fd4477f0a 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue
@@ -2,7 +2,6 @@
import { GlTooltipDirective } from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import CommitInfo from '~/repository/components/commit_info.vue';
-import { calculateBlameOffset, toggleBlameClasses } from '../utils';
export default {
name: 'BlameInfo',
@@ -14,25 +13,11 @@ export default {
SafeHtml,
},
props: {
- blameData: {
+ blameInfo: {
type: Array,
required: true,
},
},
- computed: {
- blameInfo() {
- return this.blameData.map((blame, index) => ({
- ...blame,
- blameOffset: calculateBlameOffset(blame.lineno, index),
- }));
- },
- },
- mounted() {
- toggleBlameClasses(this.blameData, true);
- },
- destroyed() {
- toggleBlameClasses(this.blameData, false);
- },
};
</script>
<template>
@@ -41,10 +26,11 @@ export default {
<commit-info
v-for="(blame, index) in blameInfo"
:key="index"
- :class="{ 'gl-border-t': index !== 0 }"
+ :class="{ 'gl-border-t': blame.blameOffset !== '0px' }"
class="gl-display-flex gl-absolute gl-px-3"
:style="{ top: blame.blameOffset }"
:commit="blame.commit"
+ :prev-blame-link="blame.commitData && blame.commitData.projectBlameLink"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
index 8dac6327a99..3b6dcace8fe 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
@@ -56,7 +56,6 @@ export default {
data() {
return {
hasAppeared: false,
- isLoading: true,
};
},
computed: {
@@ -68,17 +67,6 @@ export default {
return getPageSearchString(this.blamePath, page);
},
},
- created() {
- if (this.chunkIndex === 0) {
- // Display first chunk ASAP in order to improve perceived performance
- this.isLoading = false;
- return;
- }
-
- window.requestIdleCallback(() => {
- this.isLoading = false;
- });
- },
methods: {
handleChunkAppear() {
this.hasAppeared = true;
@@ -91,37 +79,37 @@ export default {
};
</script>
<template>
- <gl-intersection-observer @appear="handleChunkAppear">
- <div class="gl-display-flex">
- <div v-if="shouldHighlight" class="gl-display-flex gl-flex-direction-column">
- <div
- v-for="(n, index) in totalLines"
- :key="index"
- data-testid="line-numbers"
- class="gl-p-0! gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers"
+ <div class="gl-display-flex">
+ <div v-if="shouldHighlight" class="gl-display-flex gl-flex-direction-column">
+ <div
+ v-for="(n, index) in totalLines"
+ :key="index"
+ data-testid="line-numbers"
+ class="gl-p-0! gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers"
+ >
+ <a
+ class="gl-user-select-none gl-shadow-none! file-line-blame"
+ :href="`${blamePath}${pageSearchString}#L${calculateLineNumber(index)}`"
+ ></a>
+ <a
+ :id="`L${calculateLineNumber(index)}`"
+ class="gl-user-select-none gl-shadow-none! file-line-num"
+ :href="`#L${calculateLineNumber(index)}`"
+ :data-line-number="calculateLineNumber(index)"
>
- <a
- class="gl-user-select-none gl-shadow-none! file-line-blame"
- :href="`${blamePath}${pageSearchString}#L${calculateLineNumber(index)}`"
- ></a>
- <a
- :id="`L${calculateLineNumber(index)}`"
- class="gl-user-select-none gl-shadow-none! file-line-num"
- :href="`#L${calculateLineNumber(index)}`"
- :data-line-number="calculateLineNumber(index)"
- >
- {{ calculateLineNumber(index) }}
- </a>
- </div>
+ {{ calculateLineNumber(index) }}
+ </a>
</div>
+ </div>
- <div v-else-if="!isLoading" class="line-numbers gl-p-0! gl-mr-3 gl-text-transparent">
- <!-- Placeholder for line numbers while content is not highlighted -->
- </div>
+ <div v-else class="line-numbers gl-p-0! gl-mr-3 gl-text-transparent">
+ <!-- Placeholder for line numbers while content is not highlighted -->
+ </div>
+ <gl-intersection-observer class="gl-w-full" @appear="handleChunkAppear">
<pre
class="gl-m-0 gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-0"
- ><code v-if="shouldHighlight" v-safe-html="highlightedContent" data-testid="content"></code><code v-else-if="!isLoading" v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre>
- </div>
- </gl-intersection-observer>
+ ><code v-if="shouldHighlight" v-safe-html="highlightedContent" data-testid="content"></code><code v-else v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre>
+ </gl-intersection-observer>
+ </div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql b/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql
new file mode 100644
index 00000000000..a5f3f348cfc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql
@@ -0,0 +1,36 @@
+#import "~/graphql_shared/fragments/author.fragment.graphql"
+
+query getBlameData($fullPath: ID!, $filePath: String!, $fromLine: Int, $toLine: Int) {
+ project(fullPath: $fullPath) {
+ id
+ repository {
+ blobs(paths: [$filePath]) {
+ nodes {
+ id
+ blame(fromLine: $fromLine, toLine: $toLine) {
+ firstLine
+ groups {
+ lineno
+ span
+ commit {
+ id
+ titleHtml
+ message
+ authoredDate
+ authorGravatar
+ webPath
+ author {
+ ...Author
+ }
+ sha
+ }
+ commitData {
+ projectBlameLink
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
index c7353ed6785..dcefa66c403 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
@@ -1,10 +1,15 @@
<script>
+import { debounce } from 'lodash';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import SafeHtml from '~/vue_shared/directives/safe_html';
import Tracking from '~/tracking';
import addBlobLinksTracking from '~/blob/blob_links_tracking';
import LineHighlighter from '~/blob/line_highlighter';
import { EVENT_ACTION, EVENT_LABEL_VIEWER } from './constants';
import Chunk from './components/chunk_new.vue';
+import Blame from './components/blame_info.vue';
+import { calculateBlameOffset, shouldRender, toggleBlameClasses } from './utils';
+import blameDataQuery from './queries/blame_data.query.graphql';
/*
* Note, this is a new experimental version of the SourceViewer, it is not ready for production use.
@@ -15,6 +20,7 @@ export default {
name: 'SourceViewerNew',
components: {
Chunk,
+ Blame,
},
directives: {
SafeHtml,
@@ -30,13 +36,55 @@ export default {
required: false,
default: () => [],
},
+ showBlame: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
lineHighlighter: new LineHighlighter(),
+ blameData: [],
+ renderedChunks: [],
};
},
+ computed: {
+ blameInfo() {
+ return this.blameData.reduce((result, blame, index) => {
+ if (shouldRender(this.blameData, index)) {
+ result.push({
+ ...blame,
+ blameOffset: calculateBlameOffset(blame.lineno, index),
+ });
+ }
+
+ return result;
+ }, []);
+ },
+ },
+ watch: {
+ showBlame: {
+ handler(shouldShow) {
+ toggleBlameClasses(this.blameData, shouldShow);
+ this.requestBlameInfo(this.renderedChunks[0]);
+ },
+ immediate: true,
+ },
+ blameData: {
+ handler(blameData) {
+ if (!this.showBlame) return;
+ toggleBlameClasses(blameData, true);
+ },
+ immediate: true,
+ },
+ },
created() {
+ this.handleAppear = debounce(this.handleChunkAppear, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
this.track(EVENT_ACTION, { label: EVENT_LABEL_VIEWER, property: this.blob.language });
addBlobLinksTracking();
},
@@ -44,10 +92,39 @@ export default {
this.selectLine();
},
methods: {
+ async handleChunkAppear(chunkIndex, handleOverlappingChunk = true) {
+ if (!this.renderedChunks.includes(chunkIndex)) {
+ this.renderedChunks.push(chunkIndex);
+ await this.requestBlameInfo(chunkIndex);
+
+ if (chunkIndex > 0 && handleOverlappingChunk) {
+ // request the blame information for overlapping chunk incase it is visible in the DOM
+ this.handleChunkAppear(chunkIndex - 1, false);
+ }
+ }
+ },
+ async requestBlameInfo(chunkIndex) {
+ const chunk = this.chunks[chunkIndex];
+ if (!this.showBlame || !chunk) return;
+
+ const { data } = await this.$apollo.query({
+ query: blameDataQuery,
+ variables: {
+ fullPath: this.projectPath,
+ filePath: this.blob.path,
+ fromLine: chunk.startingFrom + 1,
+ toLine: chunk.startingFrom + chunk.totalLines,
+ },
+ });
+
+ const blob = data?.project?.repository?.blobs?.nodes[0];
+ const blameGroups = blob?.blame?.groups;
+ const isDuplicate = this.blameData.includes(blameGroups[0]);
+ if (blameGroups && !isDuplicate) this.blameData.push(...blameGroups);
+ },
async selectLine() {
await this.$nextTick();
- const scrollEnabled = false;
- this.lineHighlighter.highlightHash(this.$route.hash, scrollEnabled);
+ this.lineHighlighter.highlightHash(this.$route.hash);
},
},
userColorScheme: window.gon.user_color_scheme,
@@ -55,24 +132,27 @@ export default {
</script>
<template>
- <div
- class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto"
- :class="$options.userColorScheme"
- data-type="simple"
- :data-path="blob.path"
- data-qa-selector="blob_viewer_file_content"
- >
- <chunk
- v-for="(chunk, _, index) in chunks"
- :key="index"
- :chunk-index="index"
- :is-highlighted="Boolean(chunk.isHighlighted)"
- :raw-content="chunk.rawContent"
- :highlighted-content="chunk.highlightedContent"
- :total-lines="chunk.totalLines"
- :starting-from="chunk.startingFrom"
- :blame-path="blob.blamePath"
- @appear="selectLine"
- />
+ <div class="gl-display-flex">
+ <blame v-if="showBlame && blameInfo.length" :blame-info="blameInfo" />
+
+ <div
+ class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto gl-w-full"
+ :class="$options.userColorScheme"
+ data-type="simple"
+ :data-path="blob.path"
+ >
+ <chunk
+ v-for="(chunk, index) in chunks"
+ :key="index"
+ :chunk-index="index"
+ :is-highlighted="Boolean(chunk.isHighlighted)"
+ :raw-content="chunk.rawContent"
+ :highlighted-content="chunk.highlightedContent"
+ :total-lines="chunk.totalLines"
+ :starting-from="chunk.startingFrom"
+ :blame-path="blob.blamePath"
+ @appear="() => handleAppear(index)"
+ />
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
index af01653fc0d..596829b51a4 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
@@ -1,6 +1,7 @@
const BLAME_INFO_CLASSLIST = ['gl-border-t', 'gl-border-gray-500', 'gl-pt-3!'];
const PADDING_BOTTOM_LARGE = 'gl-pb-6!';
const PADDING_BOTTOM_SMALL = 'gl-pb-3!';
+const VIEWER_SELECTOR = '.file-holder .blob-viewer';
const findLineNumberElement = (lineNumber) => document.getElementById(`L${lineNumber}`);
@@ -8,8 +9,18 @@ const findLineContentElement = (lineNumber) => document.getElementById(`LC${line
export const calculateBlameOffset = (lineNumber) => {
if (lineNumber === 1) return '0px';
- const lineContentOffset = findLineContentElement(lineNumber)?.offsetTop;
- return `${lineContentOffset}px`;
+ const blobViewerOffset = document.querySelector(VIEWER_SELECTOR)?.getBoundingClientRect().top;
+ const lineContentOffset = findLineContentElement(lineNumber)?.getBoundingClientRect().top;
+ return `${lineContentOffset - blobViewerOffset}px`;
+};
+
+export const shouldRender = (data, index) => {
+ const prevBlame = data[index - 1];
+ const currBlame = data[index];
+ const identicalSha = currBlame.commit.sha === prevBlame?.commit?.sha;
+ const lineNumberSmaller = currBlame.lineno < prevBlame?.lineno;
+
+ return !identicalSha || lineNumberSmaller;
};
export const toggleBlameClasses = (blameData, isVisible) => {
@@ -17,7 +28,9 @@ export const toggleBlameClasses = (blameData, isVisible) => {
* Adds/removes classes to line number/content elements to match the line with the blame info
* */
const method = isVisible ? 'add' : 'remove';
- blameData.forEach(({ lineno, span }) => {
+ blameData.forEach(({ lineno, span }, index) => {
+ if (!shouldRender(blameData, index)) return;
+
const lineNumberEl = findLineNumberElement(lineno)?.parentElement;
const lineContentEl = findLineContentElement(lineno);
const lineNumberSpanEl = findLineNumberElement(lineno + span - 1)?.parentElement;
diff --git a/app/assets/javascripts/vue_shared/components/toggle_labels.vue b/app/assets/javascripts/vue_shared/components/toggle_labels.vue
index 05c837e32f0..db20e1288aa 100644
--- a/app/assets/javascripts/vue_shared/components/toggle_labels.vue
+++ b/app/assets/javascripts/vue_shared/components/toggle_labels.vue
@@ -54,7 +54,6 @@ export default {
label-position="left"
aria-describedby="board-labels-toggle-text"
data-testid="show-labels-toggle"
- data-qa-selector="show_labels_toggle"
class="gl-flex-direction-row"
@change="setShowLabels"
/>
diff --git a/app/assets/javascripts/vue_shared/components/users_table/constants.js b/app/assets/javascripts/vue_shared/components/users_table/constants.js
new file mode 100644
index 00000000000..2a063a1be33
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/users_table/constants.js
@@ -0,0 +1,3 @@
+export const USER_AVATAR_SIZE = 32;
+
+export const LENGTH_OF_USER_NOTE_TOOLTIP = 100;
diff --git a/app/assets/javascripts/admin/users/components/user_avatar.vue b/app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue
index dd354794cf3..5d86f90880d 100644
--- a/app/assets/javascripts/admin/users/components/user_avatar.vue
+++ b/app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue
@@ -1,7 +1,7 @@
<script>
import { GlAvatarLabeled, GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { truncate } from '~/lib/utils/text_utility';
-import { USER_AVATAR_SIZE, LENGTH_OF_USER_NOTE_TOOLTIP } from '../constants';
+import { USER_AVATAR_SIZE, LENGTH_OF_USER_NOTE_TOOLTIP } from './constants';
export default {
directives: {
@@ -23,12 +23,21 @@ export default {
},
},
computed: {
+ subLabel() {
+ if (this.user.email) {
+ return {
+ label: this.user.email,
+ link: `mailto:${this.user.email}`,
+ };
+ }
+
+ return {
+ label: `@${this.user.username}`,
+ };
+ },
adminUserHref() {
return this.adminUserPath.replace('id', this.user.username);
},
- adminUserMailto() {
- return `mailto:${this.user.email}`;
- },
userNoteShort() {
return truncate(this.user.note, LENGTH_OF_USER_NOTE_TOOLTIP);
},
@@ -48,9 +57,9 @@ export default {
:size="$options.USER_AVATAR_SIZE"
:src="user.avatarUrl"
:label="user.name"
- :sub-label="user.email"
+ :sub-label="subLabel.label"
:label-link="adminUserHref"
- :sub-label-link="adminUserMailto"
+ :sub-label-link="subLabel.link"
>
<template #meta>
<div v-if="user.note" class="gl-text-gray-500 gl-p-1">
diff --git a/app/assets/javascripts/admin/users/components/users_table.vue b/app/assets/javascripts/vue_shared/components/users_table/users_table.vue
index 65737be1e67..be164bb07a3 100644
--- a/app/assets/javascripts/admin/users/components/users_table.vue
+++ b/app/assets/javascripts/vue_shared/components/users_table/users_table.vue
@@ -1,12 +1,8 @@
<script>
import { GlSkeletonLoader, GlTable } from '@gitlab/ui';
-import { createAlert } from '~/alert';
-import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils';
import { thWidthPercent } from '~/lib/utils/table_utility';
-import { s__, __ } from '~/locale';
+import { __ } from '~/locale';
import UserDate from '~/vue_shared/components/user_date.vue';
-import getUsersGroupCountsQuery from '../graphql/queries/get_users_group_counts.query.graphql';
-import UserActions from './user_actions.vue';
import UserAvatar from './user_avatar.vue';
export default {
@@ -14,7 +10,6 @@ export default {
GlSkeletonLoader,
GlTable,
UserAvatar,
- UserActions,
UserDate,
},
props: {
@@ -22,49 +17,20 @@ export default {
type: Array,
required: true,
},
- paths: {
- type: Object,
+ adminUserPath: {
+ type: String,
required: true,
},
- },
- data() {
- return {
- groupCounts: [],
- };
- },
- apollo: {
groupCounts: {
- query: getUsersGroupCountsQuery,
- variables() {
- return {
- usernames: this.users.map((user) => user.username),
- };
- },
- update(data) {
- const nodes = data?.users?.nodes || [];
- const parsedIds = convertNodeIdsFromGraphQLIds(nodes);
-
- return parsedIds.reduce((acc, { id, groupCount }) => {
- acc[id] = groupCount || 0;
- return acc;
- }, {});
- },
- error(error) {
- createAlert({
- message: this.$options.i18n.groupCountFetchError,
- captureError: true,
- error,
- });
- },
- skip() {
- return !this.users.length;
- },
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ groupCountsLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- },
- i18n: {
- groupCountFetchError: s__(
- 'AdminUsers|Could not load user group counts. Please refresh the page to try again.',
- ),
},
fields: [
{
@@ -112,7 +78,7 @@ export default {
:tbody-tr-attr="{ 'data-testid': 'user-row-content' }"
>
<template #cell(name)="{ item: user }">
- <user-avatar :user="user" :admin-user-path="paths.adminUser" />
+ <user-avatar :user="user" :admin-user-path="adminUserPath" />
</template>
<template #cell(createdAt)="{ item: { createdAt } }">
@@ -125,17 +91,19 @@ export default {
<template #cell(groupCount)="{ item: { id } }">
<div :data-testid="`user-group-count-${id}`">
- <gl-skeleton-loader v-if="$apollo.loading" :width="40" :lines="1" />
- <span v-else>{{ groupCounts[id] }}</span>
+ <gl-skeleton-loader v-if="groupCountsLoading" :width="40" :lines="1" />
+ <span v-else>{{ groupCounts[id] || 0 }}</span>
</div>
</template>
<template #cell(projectsCount)="{ item: { id, projectsCount } }">
- <div :data-testid="`user-project-count-${id}`">{{ projectsCount }}</div>
+ <div :data-testid="`user-project-count-${id}`">
+ {{ projectsCount || 0 }}
+ </div>
</template>
<template #cell(settings)="{ item: user }">
- <user-actions :user="user" :paths="paths" :show-button-labels="true" />
+ <slot name="user-actions" :user="user"></slot>
</template>
</gl-table>
</div>
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 9fb0add5522..441b4c31b3a 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -335,7 +335,7 @@ export default {
:variant="isBlob ? 'confirm' : 'default'"
:category="isBlob ? 'primary' : 'secondary'"
:toggle-text="$options.i18n.toggleText"
- data-qa-selector="action_dropdown"
+ data-testid="action-dropdown"
fluid-width
block
@shown="$emit('shown')"
@@ -347,7 +347,7 @@ export default {
v-for="action in actions"
:key="action.key"
:item="action"
- :data-qa-selector="`${action.key}_menu_item`"
+ :data-testid="`${action.key}-menu-item`"
@action="executeAction(action)"
>
<template #list-item>
diff --git a/app/assets/javascripts/vue_shared/directives/safe_html.js b/app/assets/javascripts/vue_shared/directives/safe_html.js
index 450c7fc1bc5..c731f742771 100644
--- a/app/assets/javascripts/vue_shared/directives/safe_html.js
+++ b/app/assets/javascripts/vue_shared/directives/safe_html.js
@@ -11,7 +11,7 @@ const DEFAULT_CONFIG = {
const transform = (el, binding) => {
if (binding.oldValue !== binding.value) {
- const config = { ...DEFAULT_CONFIG, ...(binding.arg ?? {}) };
+ const config = { ...DEFAULT_CONFIG, ...binding.arg };
el.textContent = '';
diff --git a/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js b/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js
index 79946ebaecd..a1abb079cc2 100644
--- a/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js
+++ b/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js
@@ -2,12 +2,11 @@ export default (Vue) => {
Vue.mixin({
provide() {
return {
- glFeatures:
- {
- ...window.gon?.features,
- // TODO: extract into glLicensedFeatures https://gitlab.com/gitlab-org/gitlab/-/issues/322460
- ...window.gon?.licensed_features,
- } || {},
+ glFeatures: {
+ ...window.gon?.features,
+ // TODO: extract into glLicensedFeatures https://gitlab.com/gitlab-org/gitlab/-/issues/322460
+ ...window.gon?.licensed_features,
+ },
};
},
});
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 45fde45f516..dae3ddfe016 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
@@ -74,6 +74,11 @@ export default {
required: false,
default: 0,
},
+ workspaceType: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
isUpdated() {
@@ -161,6 +166,7 @@ export default {
:issuable="issuable"
:status-icon="statusIcon"
:enable-edit="enableEdit"
+ :workspace-type="workspaceType"
@edit-issuable="$emit('edit-issuable', $event)"
>
<template #status-badge>
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 a9b5e3a66a8..62a2b44e660 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
@@ -221,7 +221,7 @@ export default {
@click="handleRightSidebarToggleClick"
/>
</div>
- <div class="detail-page-header-actions gl-display-flex">
+ <div class="detail-page-header-actions gl-align-self-center gl-display-flex">
<slot name="header-actions"></slot>
</div>
</div>
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 3878c16c8d0..040f49c7c25 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
@@ -147,6 +147,7 @@ export default {
:description-help-path="descriptionHelpPath"
:task-list-update-path="taskListUpdatePath"
:task-list-lock-version="taskListLockVersion"
+ :workspace-type="workspaceType"
@edit-issuable="$emit('edit-issuable', $event)"
@task-list-update-success="$emit('task-list-update-success', $event)"
@task-list-update-failure="$emit('task-list-update-failure')"
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 da71adc8abd..5387e39e3eb 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,5 +1,6 @@
<script>
import { GlIcon, GlBadge, GlButton, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { STATUS_OPEN } from '~/issues/constants';
import { __ } from '~/locale';
@@ -13,6 +14,7 @@ export default {
GlBadge,
GlButton,
GlIntersectionObserver,
+ ConfidentialityBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -31,6 +33,11 @@ export default {
type: Boolean,
required: true,
},
+ workspaceType: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -79,9 +86,7 @@ export default {
class="issue-sticky-header gl-fixed gl-z-index-3 gl-bg-white gl-border-1 gl-border-b-solid gl-border-b-gray-100 gl-py-3"
data-testid="header"
>
- <div
- class="issue-sticky-header-text gl-display-flex gl-align-items-baseline gl-mx-auto gl-px-5"
- >
+ <div class="issue-sticky-header-text gl-display-flex gl-align-items-baseline gl-mx-auto">
<gl-badge
class="gl-white-space-nowrap gl-mr-3 gl-align-self-center"
:variant="badgeVariant"
@@ -91,6 +96,12 @@ export default {
<slot name="status-badge"></slot>
</span>
</gl-badge>
+ <confidentiality-badge
+ v-if="issuable.confidential"
+ class="gl-white-space-nowrap gl-mr-3 gl-align-self-center"
+ :issuable-type="issuable.type"
+ :workspace-type="workspaceType"
+ />
<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/webhooks/components/push_events.vue b/app/assets/javascripts/webhooks/components/push_events.vue
index 91d7e21500a..b5e0a4b2348 100644
--- a/app/assets/javascripts/webhooks/components/push_events.vue
+++ b/app/assets/javascripts/webhooks/components/push_events.vue
@@ -43,7 +43,7 @@ export default {
value="all_branches"
data-testid="rule_all_branches"
>
- <div data-qa-selector="strategy_radio_all">{{ __('All branches') }}</div>
+ <div>{{ __('All branches') }}</div>
</gl-form-radio>
<!-- wildcard -->
@@ -52,7 +52,7 @@ export default {
value="wildcard"
data-testid="rule_wildcard"
>
- <div data-qa-selector="strategy_radio_wildcard">
+ <div>
{{ s__('Webhooks|Wildcard pattern') }}
</div>
</gl-form-radio>
@@ -61,7 +61,6 @@ export default {
v-if="branchFilterStrategyData === 'wildcard'"
v-model="pushEventsBranchFilterData"
name="hook[push_events_branch_filter]"
- data-qa-selector="webhook_branch_filter_field"
data-testid="webhook_branch_filter_field"
/>
</div>
@@ -85,7 +84,7 @@ export default {
value="regex"
data-testid="rule_regex"
>
- <div data-qa-selector="strategy_radio_regex">
+ <div>
{{ s__('Webhooks|Regular expression') }}
</div>
</gl-form-radio>
@@ -94,7 +93,6 @@ export default {
v-if="branchFilterStrategyData === 'regex'"
v-model="pushEventsBranchFilterData"
name="hook[push_events_branch_filter]"
- data-qa-selector="webhook_branch_filter_field"
data-testid="webhook_branch_filter_field"
/>
</div>
diff --git a/app/assets/javascripts/work_items/components/notes/system_note.vue b/app/assets/javascripts/work_items/components/notes/system_note.vue
index 7903adea9bd..31cfe387b6e 100644
--- a/app/assets/javascripts/work_items/components/notes/system_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/system_note.vue
@@ -26,6 +26,11 @@ import { __ } from '~/locale';
import NoteHeader from '~/notes/components/note_header.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+const ALLOWED_ICONS = ['issue-close'];
+const ICON_COLORS = {
+ 'issue-close': 'gl-bg-blue-100! gl-text-blue-700',
+};
+
export default {
i18n: {
deleteButtonLabel: __('Remove description history'),
@@ -66,6 +71,12 @@ export default {
noteAnchorId() {
return `note_${this.noteId}`;
},
+ getIconColor() {
+ return ICON_COLORS[this.note.systemNoteIconName] || '';
+ },
+ isAllowedIcon() {
+ return ALLOWED_ICONS.includes(this.note.systemNoteIconName);
+ },
isTargetNote() {
return this.targetNoteHash === this.noteAnchorId;
},
@@ -102,9 +113,16 @@ export default {
class="note system-note note-wrapper"
>
<div
- class="gl-float-left gl--flex-center gl-rounded-full gl-mt-n1 gl-ml-2 gl-w-6 gl-h-6 gl-bg-gray-50 gl-text-gray-600"
+ :class="[
+ getIconColor,
+ {
+ 'gl-bg-gray-50 gl-text-gray-600 system-note-icon': isAllowedIcon,
+ 'system-note-tiny-dot gl-bg-gray-900!': !isAllowedIcon,
+ },
+ ]"
+ class="gl-float-left gl--flex-center gl-rounded-full gl-relative"
>
- <gl-icon :name="note.systemNoteIconName" />
+ <gl-icon v-if="isAllowedIcon" :size="12" :name="note.systemNoteIconName" />
</div>
<div class="timeline-content">
<div class="note-header">
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
index c867e53dc30..c3b7b7a2953 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
@@ -1,5 +1,5 @@
<script>
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
import { ASC } from '~/notes/constants';
import { __ } from '~/locale';
@@ -105,7 +105,7 @@ export default {
};
},
update(data) {
- return data.workspace.workItems.nodes[0];
+ return data.workspace.workItems.nodes[0] ?? {};
},
skip() {
return !this.workItemIid;
@@ -150,13 +150,13 @@ export default {
};
},
isProjectArchived() {
- return this.workItem?.project?.archived;
+ return this.workItem.archived;
},
canCreateNote() {
- return this.workItem?.userPermissions?.createNote;
+ return this.workItem.userPermissions?.createNote;
},
workItemState() {
- return this.workItem?.state;
+ return this.workItem.state;
},
commentButtonText() {
return this.isNewDiscussion ? __('Comment') : __('Reply');
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
index c7d8a50f402..1e6bd9ff1ac 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
@@ -8,7 +8,7 @@ import { STATE_OPEN, TRACKING_CATEGORY_SHOW, TASK_TYPE_NAME } from '~/work_items
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
+import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
import CommentFieldLayout from '~/notes/components/comment_field_layout.vue';
export default {
@@ -29,7 +29,7 @@ export default {
MarkdownEditor,
GlFormCheckbox,
GlIcon,
- WorkItemStateToggleButton,
+ WorkItemStateToggle,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -195,7 +195,6 @@ export default {
:autocomplete-data-sources="autocompleteDataSources"
:form-field-props="formFieldProps"
:add-spacing-classes="false"
- data-testid="work-item-add-comment"
use-bottom-toolbar
supports-quick-actions
:autofocus="autofocus"
@@ -230,7 +229,7 @@ export default {
@click="$emit('submitForm', { commentText, isNoteInternal })"
>{{ commentButtonTextComputed }}
</gl-button>
- <work-item-state-toggle-button
+ <work-item-state-toggle
v-if="isNewDiscussion"
class="gl-ml-3"
:work-item-id="workItemId"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
index f4c654f054c..11aecc65803 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
@@ -1,6 +1,6 @@
<script>
import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
import Tracking from '~/tracking';
@@ -96,6 +96,7 @@ export default {
data() {
return {
isEditing: false,
+ workItem: {},
};
},
computed: {
@@ -163,13 +164,13 @@ export default {
return this.authorId === this.currentUserId;
},
isWorkItemAuthor() {
- return getIdFromGraphQLId(this.workItem?.author?.id) === this.authorId;
+ return getIdFromGraphQLId(this.workItem.author?.id) === this.authorId;
},
projectName() {
- return this.workItem?.project?.name;
+ return this.workItem.namespace?.name;
},
isWorkItemConfidential() {
- return this.workItem?.confidential;
+ return this.workItem.confidential;
},
},
apollo: {
@@ -184,7 +185,7 @@ export default {
};
},
update(data) {
- return data.workspace?.workItems?.nodes[0];
+ return data.workspace?.workItems?.nodes[0] ?? {};
},
skip() {
return !this.workItemIid;
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
index 2cdf8b5ea9d..cb9a560f9e1 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
@@ -5,7 +5,7 @@ import {
GlDisclosureDropdown,
GlDisclosureDropdownItem,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __, sprintf } from '~/locale';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
@@ -207,7 +207,6 @@ export default {
<gl-button
v-if="showEdit"
v-gl-tooltip
- data-testid="edit-work-item-note"
data-track-action="click_button"
data-track-label="edit_button"
category="tertiary"
@@ -219,7 +218,6 @@ export default {
<gl-disclosure-dropdown
ref="dropdown"
v-gl-tooltip
- data-testid="work-item-note-actions"
icon="ellipsis_v"
text-sr-only
placement="right"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
index 17d22e66530..75a8a7b29c0 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
@@ -1,5 +1,5 @@
<script>
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import AwardsList from '~/vue_shared/components/awards_list.vue';
import { getMutation, optimisticAwardUpdate } from '../../notes/award_utils';
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue
index bccbec903b4..e073fddeddb 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue
@@ -27,5 +27,8 @@ export default {
</script>
<template>
- <div v-safe-html="signedOutText" class="disabled-comment gl-text-center gl-relative"></div>
+ <div
+ v-safe-html="signedOutText"
+ class="disabled-comment gl-text-center gl-text-secondary gl-relative"
+ ></div>
</template>
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
index 49813edf6fc..cbe7de4abcd 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
@@ -1,6 +1,6 @@
<script>
-import { GlLabel, GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { GlLabel, GlLink, GlIcon, GlTooltipDirective, GlButton } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
import { isScopedLabel } from '~/lib/utils/common_utils';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/shared/work_item_link_child_metadata.vue';
@@ -15,21 +15,21 @@ import {
WIDGET_TYPE_LABELS,
WORK_ITEM_NAME_TO_ICON_MAP,
} from '../../constants';
-import WorkItemLinksMenu from './work_item_links_menu.vue';
export default {
i18n: {
confidential: __('Confidential'),
created: __('Created'),
closed: __('Closed'),
+ remove: s__('WorkItem|Remove'),
},
components: {
GlLabel,
GlLink,
GlIcon,
+ GlButton,
RichTimestampTooltip,
WorkItemLinkChildMetadata,
- WorkItemLinksMenu,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -52,6 +52,16 @@ export default {
required: false,
default: false,
},
+ showLabels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ isFocused: false,
+ };
},
computed: {
labels() {
@@ -106,6 +116,12 @@ export default {
}
return false;
},
+ showRemove() {
+ return this.canUpdate && this.isFocused;
+ },
+ displayLabels() {
+ return this.showLabels && this.labels.length;
+ },
},
methods: {
showScopedLabel(label) {
@@ -117,8 +133,12 @@ export default {
<template>
<div
- class="item-body work-item-link-child gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-pl-3 gl-pr-2 gl-py-2 gl-mx-n2 gl-rounded-base"
+ class="item-body work-item-link-child gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-pl-3 gl-pr-2 gl-py-2 gl-mx-n2 gl-rounded-base gl-gap-3"
data-testid="links-child"
+ @mouseover="isFocused = true"
+ @mouseleave="isFocused = false"
+ @focusin="isFocused = true"
+ @focusout="isFocused = false"
>
<div class="item-contents gl-display-flex gl-flex-grow-1 gl-flex-wrap gl-min-w-0">
<div
@@ -168,7 +188,7 @@ export default {
class="gl-ml-6 ml-xl-0"
/>
</div>
- <div v-if="labels.length" class="gl-display-flex gl-flex-wrap gl-flex-basis-full gl-ml-6">
+ <div v-if="displayLabels" class="gl-display-flex gl-flex-wrap gl-flex-basis-full gl-ml-6">
<gl-label
v-for="label in labels"
:key="label.id"
@@ -181,10 +201,16 @@ export default {
/>
</div>
</div>
- <div v-if="canUpdate" class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex">
- <work-item-links-menu
- data-testid="links-menu"
- @removeChild="$emit('removeChild', childItem)"
+ <div v-if="canUpdate">
+ <gl-button
+ :class="{ 'gl-visibility-visible': showRemove }"
+ class="gl-visibility-hidden"
+ category="tertiary"
+ size="small"
+ icon="close"
+ :aria-label="$options.i18n.remove"
+ data-testid="remove-work-item-link"
+ @click="$emit('removeChild', childItem)"
/>
</div>
</div>
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue b/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue
deleted file mode 100644
index 12b7bade31d..00000000000
--- a/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<script>
-import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
-
-export default {
- components: {
- GlDisclosureDropdownItem,
- GlDisclosureDropdown,
- },
-};
-</script>
-
-<template>
- <div class="gl-ml-5">
- <gl-disclosure-dropdown
- category="tertiary"
- toggle-class="btn-icon btn-sm"
- icon="ellipsis_v"
- data-testid="work_items_links_menu"
- :aria-label="__(`More actions`)"
- text-sr-only
- no-caret
- >
- <gl-disclosure-dropdown-item @action="$emit('removeChild')">
- <template #list-item>{{ s__('WorkItem|Remove') }}</template>
- </gl-disclosure-dropdown-item>
- </gl-disclosure-dropdown>
- </div>
-</template>
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
index 3595ab631df..c122db6c902 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
@@ -1,20 +1,29 @@
<script>
-import { GlTokenSelector } from '@gitlab/ui';
+import { GlTokenSelector, GlAlert } from '@gitlab/ui';
import { debounce } from 'lodash';
+
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { isNumeric } from '~/lib/utils/number_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { highlighter } from 'ee_else_ce/gfm_auto_complete';
+import groupWorkItemsQuery from '../../graphql/group_work_items.query.graphql';
import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
import {
WORK_ITEMS_TYPE_MAP,
I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER,
+ I18N_WORK_ITEM_SEARCH_ERROR,
sprintfWorkItem,
} from '../../constants';
export default {
components: {
GlTokenSelector,
+ GlAlert,
},
+ directives: { SafeHtml },
+ inject: ['isGroup'],
props: {
value: {
type: Array,
@@ -47,30 +56,37 @@ export default {
},
apollo: {
availableWorkItems: {
- query: projectWorkItemsQuery,
+ query() {
+ return this.isGroup ? groupWorkItemsQuery : projectWorkItemsQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
- searchTerm: this.search?.title || this.search,
+ searchTerm: '',
types: this.childrenType ? [this.childrenType] : [],
- in: this.search ? 'TITLE' : undefined,
+ isNumber: false,
};
},
skip() {
return !this.searchStarted;
},
update(data) {
- return data.workspace.workItems.nodes.filter(
- (wi) => !this.childrenIds.includes(wi.id) && this.parentWorkItemId !== wi.id,
- );
+ return [
+ ...this.filterItems(data.workspace.workItemsByIid?.nodes),
+ ...this.filterItems(data.workspace.workItems.nodes),
+ ];
+ },
+ error() {
+ this.error = sprintfWorkItem(I18N_WORK_ITEM_SEARCH_ERROR, this.childrenTypeName);
},
},
},
data() {
return {
availableWorkItems: [],
- search: '',
+ query: '',
searchStarted: false,
+ error: '',
};
},
computed: {
@@ -101,7 +117,24 @@ export default {
methods: {
getIdFromGraphQLId,
setSearchKey(value) {
- this.search = value;
+ this.query = value;
+
+ // Query parameters for searching by text
+ const variables = {
+ searchTerm: value,
+ in: value ? 'TITLE' : undefined,
+ iid: null,
+ isNumber: false,
+ };
+
+ // Check if it is a number, add iid as query parameter
+ if (isNumeric(value) && value) {
+ variables.iid = value;
+ variables.isNumber = true;
+ }
+
+ // Fetch combined results of search by iid and search by title.
+ this.$apollo.queries.availableWorkItems.refetch(variables);
},
handleFocus() {
this.searchStarted = true;
@@ -125,33 +158,58 @@ export default {
}
});
},
+ formatResults(input) {
+ if (!this.query) {
+ return input;
+ }
+
+ return highlighter(`<span class="gl-text-black-normal">${input}</span>`, this.query);
+ },
+ unsetError() {
+ this.error = '';
+ },
+ filterItems(items) {
+ return (
+ items?.filter(
+ (wi) => !this.childrenIds.includes(wi.id) && this.parentWorkItemId !== wi.id,
+ ) || []
+ );
+ },
},
};
</script>
<template>
- <gl-token-selector
- ref="tokenSelector"
- v-model="workItemsToAdd"
- :dropdown-items="availableWorkItems"
- :loading="isLoading"
- :placeholder="addInputPlaceholder"
- menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
- :container-class="tokenSelectorContainerClass"
- data-testid="work-item-token-select-input"
- @text-input="debouncedSearchKeyUpdate"
- @focus="handleFocus"
- @mouseover.native="handleMouseOver"
- @mouseout.native="handleMouseOut"
- @token-add="focusInputText"
- @token-remove="focusInputText"
- @blur="handleBlur"
- >
- <template #token-content="{ token }"> {{ token.iid }} {{ token.title }} </template>
- <template #dropdown-item-content="{ dropdownItem }">
- <div class="gl-display-flex">
- <div class="gl-text-secondary gl-font-sm gl-mr-4">{{ dropdownItem.iid }}</div>
- <div class="gl-text-truncate">{{ dropdownItem.title }}</div>
- </div>
- </template>
- </gl-token-selector>
+ <div>
+ <gl-alert v-if="error" variant="danger" class="gl-mb-3" @dismiss="unsetError">
+ {{ error }}
+ </gl-alert>
+ <gl-token-selector
+ ref="tokenSelector"
+ v-model="workItemsToAdd"
+ :dropdown-items="availableWorkItems"
+ :loading="isLoading"
+ :placeholder="addInputPlaceholder"
+ menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
+ :container-class="tokenSelectorContainerClass"
+ data-testid="work-item-token-select-input"
+ @text-input="debouncedSearchKeyUpdate"
+ @focus="handleFocus"
+ @mouseover.native="handleMouseOver"
+ @mouseout.native="handleMouseOut"
+ @token-add="focusInputText"
+ @token-remove="focusInputText"
+ @blur="handleBlur"
+ >
+ <template #token-content="{ token }"> {{ token.iid }} {{ token.title }} </template>
+ <template #dropdown-item-content="{ dropdownItem }">
+ <div class="gl-display-flex">
+ <div
+ v-safe-html="formatResults(dropdownItem.iid)"
+ class="gl-text-secondary gl-font-sm gl-mr-4"
+ ></div>
+ <div v-safe-html="formatResults(dropdownItem.title)" class="gl-text-truncate"></div>
+ </div>
+ </template>
+ </gl-token-selector>
+ </div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue
index 02d2ea24ca0..0a71fbc9a34 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -8,7 +8,7 @@ import {
GlToggle,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
@@ -20,12 +20,12 @@ import {
I18N_WORK_ITEM_DELETE,
I18N_WORK_ITEM_ARE_YOU_SURE_DELETE,
TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
- TEST_ID_NOTIFICATIONS_TOGGLE_ACTION,
TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
TEST_ID_DELETE_ACTION,
TEST_ID_PROMOTE_ACTION,
TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
TEST_ID_COPY_REFERENCE_ACTION,
+ TEST_ID_TOGGLE_ACTION,
I18N_WORK_ITEM_ERROR_CONVERTING,
WORK_ITEM_TYPE_VALUE_KEY_RESULT,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
@@ -36,11 +36,12 @@ import {
import updateWorkItemNotificationsMutation from '../graphql/update_work_item_notifications.mutation.graphql';
import convertWorkItemMutation from '../graphql/work_item_convert.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
+import WorkItemStateToggle from './work_item_state_toggle.vue';
export default {
i18n: {
- enableTaskConfidentiality: s__('WorkItem|Turn on confidentiality'),
- disableTaskConfidentiality: s__('WorkItem|Turn off confidentiality'),
+ enableConfidentiality: s__('WorkItem|Turn on confidentiality'),
+ disableConfidentiality: s__('WorkItem|Turn off confidentiality'),
notifications: s__('WorkItem|Notifications'),
notificationOn: s__('WorkItem|Notifications turned on.'),
notificationOff: s__('WorkItem|Notifications turned off.'),
@@ -54,25 +55,30 @@ export default {
GlDropdownDivider,
GlModal,
GlToggle,
+ WorkItemStateToggle,
},
directives: {
GlModal: GlModalDirective,
},
mixins: [Tracking.mixin({ label: 'actions_menu' })],
isLoggedIn: isLoggedIn(),
- notificationsToggleTestId: TEST_ID_NOTIFICATIONS_TOGGLE_ACTION,
notificationsToggleFormTestId: TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
confidentialityTestId: TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
copyReferenceTestId: TEST_ID_COPY_REFERENCE_ACTION,
copyCreateNoteEmailTestId: TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
deleteActionTestId: TEST_ID_DELETE_ACTION,
promoteActionTestId: TEST_ID_PROMOTE_ACTION,
+ stateToggleTestId: TEST_ID_TOGGLE_ACTION,
inject: ['isGroup'],
props: {
fullPath: {
type: String,
required: true,
},
+ workItemState: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: false,
@@ -128,6 +134,11 @@ export default {
required: false,
default: false,
},
+ workItemParentId: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
apollo: {
workItemTypes: {
@@ -165,6 +176,11 @@ export default {
canPromoteToObjective() {
return this.canUpdate && this.workItemType === WORK_ITEM_TYPE_VALUE_KEY_RESULT;
},
+ confidentialItemText() {
+ return this.isConfidential
+ ? this.$options.i18n.disableConfidentiality
+ : this.$options.i18n.enableConfidentiality;
+ },
objectiveWorkItemTypeId() {
return this.workItemTypes.find((type) => type.name === WORK_ITEM_TYPE_VALUE_OBJECTIVE).id;
},
@@ -267,7 +283,7 @@ export default {
icon="ellipsis_v"
data-testid="work-item-actions-dropdown"
text-sr-only
- :text="__('More actions')"
+ :toggle-text="__('More actions')"
category="tertiary"
:auto-close="false"
no-caret
@@ -282,7 +298,6 @@ export default {
<gl-toggle
:value="subscribedToNotifications"
:label="$options.i18n.notifications"
- :data-testid="$options.notificationsToggleTestId"
class="work-item-notification-toggle"
label-position="left"
@change="toggleNotifications($event)"
@@ -299,49 +314,56 @@ export default {
>
<template #list-item>{{ __('Promote to objective') }}</template>
</gl-disclosure-dropdown-item>
- <template v-if="canUpdate && !isParentConfidential">
- <gl-disclosure-dropdown-item
- :data-testid="$options.confidentialityTestId"
- @action="handleToggleWorkItemConfidentiality"
- ><template #list-item>{{
- isConfidential
- ? $options.i18n.disableTaskConfidentiality
- : $options.i18n.enableTaskConfidentiality
- }}</template></gl-disclosure-dropdown-item
- >
- </template>
+
+ <gl-disclosure-dropdown-item
+ v-if="canUpdate && !isParentConfidential"
+ :data-testid="$options.confidentialityTestId"
+ @action="handleToggleWorkItemConfidentiality"
+ >
+ <template #list-item>{{ confidentialItemText }}</template>
+ </gl-disclosure-dropdown-item>
+
+ <work-item-state-toggle
+ v-if="canUpdate"
+ :data-testid="$options.stateToggleTestId"
+ :work-item-id="workItemId"
+ :work-item-state="workItemState"
+ :work-item-parent-id="workItemParentId"
+ :work-item-type="workItemType"
+ show-as-dropdown-item
+ />
+
<gl-disclosure-dropdown-item
- ref="workItemReference"
:data-testid="$options.copyReferenceTestId"
:data-clipboard-text="workItemReference"
@action="copyToClipboard(workItemReference, $options.i18n.referenceCopied)"
- ><template #list-item>{{
- $options.i18n.copyReference
- }}</template></gl-disclosure-dropdown-item
>
- <template v-if="$options.isLoggedIn && workItemCreateNoteEmail">
- <gl-disclosure-dropdown-item
- ref="workItemCreateNoteEmail"
- :data-testid="$options.copyCreateNoteEmailTestId"
- :data-clipboard-text="workItemCreateNoteEmail"
- @action="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)"
- ><template #list-item>{{
- i18n.copyCreateNoteEmail
- }}</template></gl-disclosure-dropdown-item
- >
- </template>
- <gl-dropdown-divider v-if="canDelete" />
+ <template #list-item>{{ $options.i18n.copyReference }}</template>
+ </gl-disclosure-dropdown-item>
+
<gl-disclosure-dropdown-item
- v-if="canDelete"
- :data-testid="$options.deleteActionTestId"
- variant="danger"
- @action="handleDelete"
+ v-if="$options.isLoggedIn && workItemCreateNoteEmail"
+ :data-testid="$options.copyCreateNoteEmailTestId"
+ :data-clipboard-text="workItemCreateNoteEmail"
+ @action="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)"
>
- <template #list-item
- ><span class="text-danger">{{ i18n.deleteWorkItem }}</span></template
- >
+ <template #list-item>{{ i18n.copyCreateNoteEmail }}</template>
</gl-disclosure-dropdown-item>
+
+ <template v-if="canDelete">
+ <gl-dropdown-divider />
+ <gl-disclosure-dropdown-item
+ :data-testid="$options.deleteActionTestId"
+ variant="danger"
+ @action="handleDelete"
+ >
+ <template #list-item>
+ <span class="text-danger">{{ i18n.deleteWorkItem }}</span>
+ </template>
+ </gl-disclosure-dropdown-item>
+ </template>
</gl-disclosure-dropdown>
+
<gl-modal
ref="modal"
modal-id="work-item-confirm-delete"
diff --git a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
index fd01d855782..7d09a003926 100644
--- a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
@@ -13,6 +13,7 @@ import {
WIDGET_TYPE_WEIGHT,
WORK_ITEM_TYPE_VALUE_KEY_RESULT,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ WORK_ITEM_TYPE_VALUE_TASK,
} from '../constants';
import WorkItemDueDate from './work_item_due_date.vue';
import WorkItemAssignees from './work_item_assignees.vue';
@@ -98,7 +99,8 @@ export default {
showWorkItemParent() {
return (
this.workItemType === WORK_ITEM_TYPE_VALUE_OBJECTIVE ||
- this.workItemType === WORK_ITEM_TYPE_VALUE_KEY_RESULT
+ this.workItemType === WORK_ITEM_TYPE_VALUE_KEY_RESULT ||
+ this.workItemType === WORK_ITEM_TYPE_VALUE_TASK
);
},
workItemParent() {
diff --git a/app/assets/javascripts/work_items/components/work_item_award_emoji.vue b/app/assets/javascripts/work_items/components/work_item_award_emoji.vue
index 44bd17b59a2..f806946509f 100644
--- a/app/assets/javascripts/work_items/components/work_item_award_emoji.vue
+++ b/app/assets/javascripts/work_items/components/work_item_award_emoji.vue
@@ -1,13 +1,14 @@
<script>
-import * as Sentry from '@sentry/browser';
import { produce } from 'immer';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import AwardsList from '~/vue_shared/components/awards_list.vue';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { TYPENAME_USER } from '~/graphql_shared/constants';
-import workItemAwardEmojiQuery from '../graphql/award_emoji.query.graphql';
+import groupWorkItemAwardEmojiQuery from '../graphql/group_award_emoji.query.graphql';
+import projectWorkItemAwardEmojiQuery from '../graphql/award_emoji.query.graphql';
import updateAwardEmojiMutation from '../graphql/update_award_emoji.mutation.graphql';
import {
EMOJI_THUMBSDOWN,
@@ -23,6 +24,7 @@ export default {
components: {
AwardsList,
},
+ inject: ['isGroup'],
props: {
workItemId: {
type: String,
@@ -75,7 +77,9 @@ export default {
},
apollo: {
awardEmoji: {
- query: workItemAwardEmojiQuery,
+ query() {
+ return this.isGroup ? groupWorkItemAwardEmojiQuery : projectWorkItemAwardEmojiQuery;
+ },
variables() {
return {
iid: this.workItemIid,
@@ -116,7 +120,7 @@ export default {
after: this.pageInfo?.endCursor,
},
});
- } catch (error) {
+ } catch {
this.$emit('error', I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR);
}
},
@@ -139,7 +143,7 @@ export default {
return this.awardEmoji.nodes;
}
- // else make a copy of unmutable list and return the list after adding the new emoji
+ // else make a copy of immutable list and return the list after adding the new emoji
const awardEmojiNodes = [...this.awardEmoji.nodes];
awardEmojiNodes.push({
name,
@@ -162,7 +166,7 @@ export default {
},
updateWorkItemAwardEmojiWidgetCache({ cache, name, toggledOn }) {
const query = {
- query: workItemAwardEmojiQuery,
+ query: this.isGroup ? groupWorkItemAwardEmojiQuery : projectWorkItemAwardEmojiQuery,
variables: {
fullPath: this.workItemFullpath,
iid: this.workItemIid,
@@ -234,7 +238,6 @@ export default {
<template>
<div v-if="!isLoading" class="gl-mt-3">
<awards-list
- data-testid="work-item-award-list"
:awards="awards"
:can-award-emoji="$options.isLoggedIn"
:current-user-id="currentUserId"
diff --git a/app/assets/javascripts/work_items/components/work_item_created_updated.vue b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
index 460b5d35187..d352d66196a 100644
--- a/app/assets/javascripts/work_items/components/work_item_created_updated.vue
+++ b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
@@ -86,7 +86,7 @@ export default {
</script>
<template>
- <div class="gl-mb-3 gl-text-gray-700">
+ <div class="gl-mb-3 gl-text-gray-700 gl-mt-3">
<work-item-state-badge v-if="workItemState" :work-item-state="workItemState" />
<gl-loading-icon v-if="updateInProgress" :inline="true" class="gl-mr-3" />
<confidentiality-badge
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index b7f3ac93cdb..77c573b47e4 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert, GlButton, GlForm, GlFormGroup } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
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';
@@ -244,13 +244,7 @@ export default {
@keydown.ctrl.enter="updateWorkItem"
/>
<div class="gl-display-flex">
- <gl-alert
- v-if="hasConflicts"
- :dismissible="false"
- variant="danger"
- class="gl-w-full"
- data-testid="work-item-description-conflicts"
- >
+ <gl-alert v-if="hasConflicts" :dismissible="false" variant="danger" class="gl-w-full">
<p>
{{
s__(
diff --git a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
index 07e03eba1d1..124e05db431 100644
--- a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
@@ -114,7 +114,7 @@ export default {
v-else
ref="gfm-content"
v-safe-html="descriptionHtml"
- class="md gl-mb-5 gl-min-h-8"
+ class="md gl-mb-5 gl-min-h-8 gl-clearfix"
data-testid="work-item-description"
@change="toggleCheckboxes"
></div>
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 53929775684..45d3aa564a5 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -50,7 +50,6 @@ import WorkItemDescription from './work_item_description.vue';
import WorkItemNotes from './work_item_notes.vue';
import WorkItemDetailModal from './work_item_detail_modal.vue';
import WorkItemAwardEmoji from './work_item_award_emoji.vue';
-import WorkItemStateToggleButton from './work_item_state_toggle_button.vue';
import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue';
import WorkItemTypeIcon from './work_item_type_icon.vue';
@@ -61,7 +60,6 @@ export default {
},
isLoggedIn: isLoggedIn(),
components: {
- WorkItemStateToggleButton,
GlAlert,
GlButton,
GlLoadingIcon,
@@ -146,9 +144,9 @@ export default {
if (isEmpty(this.workItem)) {
this.setEmptyState();
}
- if (!this.isModal && this.workItem.project) {
- const path = this.workItem.project?.fullPath
- ? ` · ${this.workItem.project.fullPath}`
+ if (!this.isModal && this.workItem.namespace) {
+ const path = this.workItem.namespace.fullPath
+ ? ` · ${this.workItem.namespace.fullPath}`
: '';
document.title = `${this.workItem.title} · ${this.workItem?.workItemType?.name}${path}`;
@@ -181,19 +179,19 @@ export default {
return this.workItemType ? `#${this.workItem.iid}` : '';
},
canUpdate() {
- return this.workItem?.userPermissions?.updateWorkItem;
+ return this.workItem.userPermissions?.updateWorkItem;
},
canDelete() {
- return this.workItem?.userPermissions?.deleteWorkItem;
+ return this.workItem.userPermissions?.deleteWorkItem;
},
canSetWorkItemMetadata() {
- return this.workItem?.userPermissions?.setWorkItemMetadata;
+ return this.workItem.userPermissions?.setWorkItemMetadata;
},
canAssignUnassignUser() {
return this.workItemAssignees && this.canSetWorkItemMetadata;
},
projectFullPath() {
- return this.workItem?.project?.fullPath;
+ return this.workItem.namespace?.fullPath;
},
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
@@ -222,7 +220,7 @@ export default {
return this.parentWorkItem?.webUrl;
},
workItemIconName() {
- return this.workItem?.workItemType?.iconName;
+ return this.workItem.workItemType?.iconName;
},
noAccessSvgPath() {
return `data:image/svg+xml;utf8,${encodeURIComponent(noAccessSvg)}`;
@@ -274,6 +272,18 @@ export default {
showWorkItemLinkedItems() {
return this.hasLinkedWorkItems && this.workItemLinkedItems;
},
+ titleClassHeader() {
+ return {
+ 'gl-sm-display-none!': this.parentWorkItem,
+ 'gl-w-full': !this.parentWorkItem,
+ };
+ },
+ titleClassComponent() {
+ return {
+ 'gl-sm-display-block!': !this.parentWorkItem,
+ 'gl-display-none gl-sm-display-block!': this.parentWorkItem,
+ };
+ },
},
mounted() {
if (this.modalWorkItemIid) {
@@ -285,7 +295,7 @@ export default {
},
methods: {
isWidgetPresent(type) {
- return this.workItem?.widgets?.find((widget) => widget.type === type);
+ return this.workItem.widgets?.find((widget) => widget.type === type);
},
toggleConfidentiality(confidentialStatus) {
this.updateInProgress = true;
@@ -409,7 +419,20 @@ export default {
</gl-skeleton-loader>
</div>
<template v-else>
- <div class="gl-display-flex gl-align-items-center" data-testid="work-item-body">
+ <div class="gl-sm-display-none! gl-display-flex">
+ <gl-button
+ v-if="isModal"
+ class="gl-ml-auto"
+ category="tertiary"
+ data-testid="work-item-close"
+ icon="close"
+ :aria-label="__('Close')"
+ @click="$emit('close')"
+ />
+ </div>
+ <div
+ class="gl-display-block gl-sm-display-flex! gl-align-items-flex-start gl-flex-direction-column gl-sm-flex-direction-row gl-gap-3 gl-pt-3"
+ >
<ul
v-if="parentWorkItem"
class="list-unstyled gl-display-flex gl-min-w-0 gl-mr-auto gl-mb-0 gl-z-index-0"
@@ -440,53 +463,55 @@ export default {
</li>
</ul>
<div
- v-else-if="!error && !workItemLoading"
- class="gl-mr-auto"
+ v-if="!error && !workItemLoading"
+ :class="titleClassHeader"
data-testid="work-item-type"
>
- <work-item-type-icon
- :work-item-icon-name="workItemIconName"
+ <work-item-title
+ v-if="workItem.title"
+ ref="title"
+ class="gl-sm-display-block!"
+ :work-item-id="workItem.id"
+ :work-item-title="workItem.title"
:work-item-type="workItemType"
- show-text
+ :work-item-parent-id="workItemParentId"
+ :can-update="canUpdate"
+ @error="updateError = $event"
+ />
+ </div>
+ <div class="detail-page-header-actions gl-display-flex gl-align-self-start gl-gap-3">
+ <work-item-todos
+ v-if="showWorkItemCurrentUserTodos"
+ :work-item-id="workItem.id"
+ :work-item-iid="workItemIid"
+ :work-item-fullpath="projectFullPath"
+ :current-user-todos="currentUserTodos"
+ @error="updateError = $event"
+ />
+ <work-item-actions
+ :full-path="fullPath"
+ :work-item-id="workItem.id"
+ :subscribed-to-notifications="workItemNotificationsSubscribed"
+ :work-item-type="workItemType"
+ :work-item-type-id="workItemTypeId"
+ :can-delete="canDelete"
+ :can-update="canUpdate"
+ :is-confidential="workItem.confidential"
+ :is-parent-confidential="parentWorkItemConfidentiality"
+ :work-item-reference="workItem.reference"
+ :work-item-create-note-email="workItem.createNoteEmail"
+ :is-modal="isModal"
+ :work-item-state="workItem.state"
+ :work-item-parent-id="workItemParentId"
+ @deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
+ @toggleWorkItemConfidentiality="toggleConfidentiality"
+ @error="updateError = $event"
+ @promotedToObjective="$emit('promotedToObjective', workItemIid)"
/>
- {{ workItemBreadcrumbReference }}
</div>
- <work-item-state-toggle-button
- v-if="canUpdate"
- :work-item-id="workItem.id"
- :work-item-state="workItem.state"
- :work-item-parent-id="workItemParentId"
- :work-item-type="workItemType"
- @error="updateError = $event"
- />
- <work-item-todos
- v-if="showWorkItemCurrentUserTodos"
- :work-item-id="workItem.id"
- :work-item-iid="workItemIid"
- :work-item-fullpath="projectFullPath"
- :current-user-todos="currentUserTodos"
- @error="updateError = $event"
- />
- <work-item-actions
- :full-path="fullPath"
- :work-item-id="workItem.id"
- :subscribed-to-notifications="workItemNotificationsSubscribed"
- :work-item-type="workItemType"
- :work-item-type-id="workItemTypeId"
- :can-delete="canDelete"
- :can-update="canUpdate"
- :is-confidential="workItem.confidential"
- :is-parent-confidential="parentWorkItemConfidentiality"
- :work-item-reference="workItem.reference"
- :work-item-create-note-email="workItem.createNoteEmail"
- :is-modal="isModal"
- @deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
- @toggleWorkItemConfidentiality="toggleConfidentiality"
- @error="updateError = $event"
- @promotedToObjective="$emit('promotedToObjective', workItemIid)"
- />
<gl-button
v-if="isModal"
+ class="gl-display-none gl-sm-display-block!"
category="tertiary"
data-testid="work-item-close"
icon="close"
@@ -496,8 +521,9 @@ export default {
</div>
<div>
<work-item-title
- v-if="workItem.title"
+ v-if="workItem.title && parentWorkItem"
ref="title"
+ :class="titleClassComponent"
:work-item-id="workItem.id"
:work-item-title="workItem.title"
:work-item-type="workItemType"
diff --git a/app/assets/javascripts/work_items/components/work_item_due_date.vue b/app/assets/javascripts/work_items/components/work_item_due_date.vue
index 1aa62a2b906..704fe6fb11d 100644
--- a/app/assets/javascripts/work_items/components/work_item_due_date.vue
+++ b/app/assets/javascripts/work_items/components/work_item_due_date.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlDatepicker, GlFormGroup } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { getDateWithUTC, newDateAsLocaleTime } from '~/lib/utils/datetime/date_calculation_utility';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue
index 3cdbf816421..7a5d3b1155f 100644
--- a/app/assets/javascripts/work_items/components/work_item_labels.vue
+++ b/app/assets/javascripts/work_items/components/work_item_labels.vue
@@ -3,7 +3,8 @@ import { GlTokenSelector, GlLabel, GlSkeletonLoader } from '@gitlab/ui';
import { debounce, uniqueId, without } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
-import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
+import groupLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/group_labels.query.graphql';
+import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
@@ -90,7 +91,9 @@ export default {
},
},
searchLabels: {
- query: labelSearchQuery,
+ query() {
+ return this.isGroup ? groupLabelsQuery : projectLabelsQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
index f4de7c1dddc..b6ea09edbd4 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
@@ -1,7 +1,7 @@
<script>
-import * as Sentry from '@sentry/browser';
import produce from 'immer';
import Draggable from 'vuedraggable';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
@@ -50,6 +50,11 @@ export default {
required: false,
default: false,
},
+ showLabels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -151,9 +156,6 @@ export default {
update(data) {
return data.workspace.workItems.nodes[0];
},
- context: {
- isSingleRequest: true,
- },
});
},
prefetchWorkItem({ iid }) {
@@ -280,6 +282,7 @@ export default {
:confidential="child.confidential"
:work-item-type="workItemType"
:has-indirect-children="hasIndirectChildren"
+ :show-labels="showLabels"
@mouseover="prefetchWorkItem(child)"
@mouseout="clearPrefetching"
@removeChild="removeChild"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
index 847a3585ac4..49454c3d9f3 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __, s__ } from '~/locale';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { createAlert } from '~/alert';
@@ -49,6 +49,11 @@ export default {
required: false,
default: '',
},
+ showLabels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -231,6 +236,7 @@ export default {
:can-update="canUpdate"
:parent-work-item-id="issuableGid"
:work-item-type="workItemType"
+ :show-labels="showLabels"
@click="$emit('click', $event)"
@removeChild="$emit('removeChild', childItem)"
/>
@@ -241,6 +247,7 @@ export default {
:work-item-id="issuableGid"
:work-item-type="workItemType"
:children="children"
+ :show-labels="showLabels"
@removeChild="removeChild"
@click="$emit('click', $event)"
/>
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
index 7fa6ac2c57f..dd0a26c0b9c 100644
--- 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
@@ -5,6 +5,7 @@ import {
GlIcon,
GlLoadingIcon,
GlTooltipDirective,
+ GlToggle,
} from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { s__ } from '~/locale';
@@ -15,7 +16,12 @@ import { isMetaKey } from '~/lib/utils/common_utils';
import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
-import { FORM_TYPES, WIDGET_ICONS, WORK_ITEM_STATUS_TEXT } from '../../constants';
+import {
+ FORM_TYPES,
+ WIDGET_ICONS,
+ WORK_ITEM_STATUS_TEXT,
+ I18N_WORK_ITEM_SHOW_LABELS,
+} from '../../constants';
import { findHierarchyWidgetChildren } from '../../utils';
import { removeHierarchyChild } from '../../graphql/cache_utils';
import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
@@ -36,6 +42,7 @@ export default {
WorkItemDetailModal,
AbuseCategorySelector,
WorkItemChildrenWrapper,
+ GlToggle,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -65,9 +72,6 @@ export default {
update(data) {
return data.workspace.workItems.nodes[0] ?? {};
},
- context: {
- isSingleRequest: true,
- },
skip() {
return !this.iid;
},
@@ -107,6 +111,7 @@ export default {
reportedUserId: 0,
reportedUrl: '',
widgetName: 'tasks',
+ showLabels: true,
};
},
computed: {
@@ -204,6 +209,7 @@ export default {
addChildButtonLabel: s__('WorkItem|Add'),
addChildOptionLabel: s__('WorkItem|Existing task'),
createChildOptionLabel: s__('WorkItem|New task'),
+ showLabelsLabel: I18N_WORK_ITEM_SHOW_LABELS,
},
WIDGET_TYPE_TASK_ICON: WIDGET_ICONS.TASK,
WORK_ITEM_STATUS_TEXT,
@@ -227,6 +233,14 @@ export default {
</span>
</template>
<template #header-right>
+ <gl-toggle
+ class="gl-mr-4"
+ :value="showLabels"
+ :label="$options.i18n.showLabelsLabel"
+ label-position="left"
+ label-id="relationship-toggle-labels"
+ @change="showLabels = $event"
+ />
<gl-disclosure-dropdown
v-if="canUpdate && canAddTask"
placement="right"
@@ -282,6 +296,7 @@ export default {
:full-path="fullPath"
:work-item-id="issuableGid"
:work-item-iid="iid"
+ :show-labels="showLabels"
@error="error = $event"
@show-modal="openChild"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
index b61b3b2e0d3..3d09a90169c 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
@@ -1,10 +1,12 @@
<script>
+import { GlToggle } from '@gitlab/ui';
import {
FORM_TYPES,
WIDGET_TYPE_HIERARCHY,
WORK_ITEMS_TREE_TEXT_MAP,
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
WORK_ITEM_TYPE_ENUM_KEY_RESULT,
+ I18N_WORK_ITEM_SHOW_LABELS,
} from '../../constants';
import WidgetWrapper from '../widget_wrapper.vue';
import OkrActionsSplitButton from './okr_actions_split_button.vue';
@@ -21,6 +23,7 @@ export default {
WidgetWrapper,
WorkItemLinksForm,
WorkItemChildrenWrapper,
+ GlToggle,
},
props: {
fullPath: {
@@ -68,6 +71,7 @@ export default {
formType: null,
childType: null,
widgetName: 'tasks',
+ showLabels: true,
};
},
computed: {
@@ -99,6 +103,9 @@ export default {
this.$emit('show-modal', { event, modalWorkItem: child });
},
},
+ i18n: {
+ showLabelsLabel: I18N_WORK_ITEM_SHOW_LABELS,
+ },
};
</script>
@@ -114,6 +121,14 @@ export default {
{{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].title }}
</template>
<template #header-right>
+ <gl-toggle
+ class="gl-mr-4"
+ :value="showLabels"
+ :label="$options.i18n.showLabelsLabel"
+ label-position="left"
+ label-id="relationship-toggle-labels"
+ @change="showLabels = $event"
+ />
<okr-actions-split-button
v-if="canUpdate"
@showCreateObjectiveForm="
@@ -160,6 +175,7 @@ export default {
:work-item-id="workItemId"
:work-item-iid="workItemIid"
:work-item-type="workItemType"
+ :show-labels="showLabels"
@error="error = $event"
@show-modal="showModal"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
index 401223c3593..af181fa4e7e 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
@@ -22,6 +22,11 @@ export default {
required: false,
default: false,
},
+ showLabels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
};
</script>
@@ -35,6 +40,7 @@ export default {
:issuable-gid="workItemId"
:child-item="child"
:work-item-type="workItemType"
+ :show-labels="showLabels"
@removeChild="$emit('removeChild', $event)"
@click="$emit('click', Object.assign($event, { childItem: child }))"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue
index a2cbb7f7598..9c6fa158169 100644
--- a/app/assets/javascripts/work_items/components/work_item_milestone.vue
+++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue
@@ -1,15 +1,7 @@
<script>
-import {
- GlFormGroup,
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
- GlSkeletonLoader,
- GlSearchBoxByType,
- GlDropdownText,
-} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import { GlCollapsibleListbox, GlFormGroup, GlSkeletonLoader } from '@gitlab/ui';
import { debounce } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
import { s__, __ } from '~/locale';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
@@ -22,7 +14,8 @@ import {
TRACKING_CATEGORY_SHOW,
} from '../constants';
-const noMilestoneId = 'no-milestone-id';
+export const noMilestoneId = 'no-milestone-id';
+const noMilestoneItem = { text: s__('WorkItem|No milestone'), value: noMilestoneId };
export default {
i18n: {
@@ -37,13 +30,9 @@ export default {
EXPIRED_TEXT: __('(expired)'),
},
components: {
+ GlCollapsibleListbox,
GlFormGroup,
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
GlSkeletonLoader,
- GlSearchBoxByType,
- GlDropdownText,
},
mixins: [Tracking.mixin()],
props: {
@@ -74,11 +63,23 @@ export default {
data() {
return {
localMilestone: this.workItemMilestone,
+ localMilestoneId: this.workItemMilestone?.id,
searchTerm: '',
shouldFetch: false,
updateInProgress: false,
- isFocused: false,
milestones: [],
+ dropdownGroups: [
+ {
+ text: this.$options.i18n.NO_MILESTONE,
+ textSrOnly: true,
+ options: [noMilestoneItem],
+ },
+ {
+ text: __('Milestones'),
+ textSrOnly: true,
+ options: [],
+ },
+ ],
};
},
computed: {
@@ -103,23 +104,29 @@ export default {
isLoadingMilestones() {
return this.$apollo.queries.milestones.loading;
},
- isNoMilestone() {
- return this.localMilestone?.id === noMilestoneId || !this.localMilestone?.id;
+ milestonesList() {
+ return (
+ this.milestones.map(({ id, title, expired }) => {
+ return {
+ value: id,
+ text: title,
+ expired,
+ };
+ }) ?? []
+ );
},
- dropdownClasses() {
- return {
- 'gl-text-gray-500!': this.canUpdate && this.isNoMilestone,
- 'is-not-focused': !this.isFocused,
- 'gl-min-w-20': true,
- };
+ toggleClasses() {
+ const toggleClasses = ['gl-max-w-full'];
+
+ if (this.localMilestoneId === noMilestoneId) {
+ toggleClasses.push('gl-text-gray-500!');
+ }
+ return toggleClasses;
},
},
watch: {
- workItemMilestone: {
- handler(newVal) {
- this.localMilestone = newVal;
- },
- deep: true,
+ milestones() {
+ this.dropdownGroups[1].options = this.milestonesList;
},
},
created() {
@@ -152,15 +159,11 @@ export default {
this.localMilestone = milestone;
},
onDropdownShown() {
- this.$refs.search.focusInput();
this.shouldFetch = true;
- this.isFocused = true;
},
onDropdownHide() {
- this.isFocused = false;
this.searchTerm = '';
this.shouldFetch = false;
- this.updateMilestone();
},
setSearchKey(value) {
this.searchTerm = value;
@@ -169,6 +172,9 @@ export default {
return this.localMilestone?.id === milestone?.id;
},
updateMilestone() {
+ this.localMilestone =
+ this.milestones.find(({ id }) => id === this.localMilestoneId) ?? noMilestoneItem;
+
if (this.workItemMilestone?.id === this.localMilestone?.id) {
return;
}
@@ -182,8 +188,7 @@ export default {
input: {
id: this.workItemId,
milestoneWidget: {
- milestoneId:
- this.localMilestone?.id === 'no-milestone-id' ? null : this.localMilestone?.id,
+ milestoneId: this.localMilestoneId === noMilestoneId ? null : this.localMilestoneId,
},
},
},
@@ -222,50 +227,45 @@ export default {
>
{{ dropdownText }}
</span>
- <gl-dropdown
+
+ <gl-collapsible-listbox
v-else
id="milestone-value"
+ v-model="localMilestoneId"
+ :items="dropdownGroups"
+ category="tertiary"
data-testid="work-item-milestone-dropdown"
- class="gl-pl-0 gl-max-w-full work-item-field-value"
- :toggle-class="dropdownClasses"
- :text="dropdownText"
+ class="gl-max-w-full"
+ :toggle-text="dropdownText"
:loading="updateInProgress"
+ :toggle-class="toggleClasses"
+ searchable
+ @select="updateMilestone"
@shown="onDropdownShown"
- @hide="onDropdownHide"
+ @hidden="onDropdownHide"
+ @search="debouncedSearchKeyUpdate"
>
- <template #header>
- <gl-search-box-by-type ref="search" :value="searchTerm" @input="debouncedSearchKeyUpdate" />
+ <template #list-item="{ item }">
+ {{ item.text }}
+ <span v-if="item.expired">{{ $options.i18n.EXPIRED_TEXT }}</span>
</template>
- <gl-dropdown-item
- data-testid="no-milestone"
- is-check-item
- :is-checked="isNoMilestone"
- @click="handleMilestoneClick({ id: 'no-milestone-id' })"
- >
- {{ $options.i18n.NO_MILESTONE }}
- </gl-dropdown-item>
- <gl-dropdown-divider />
- <gl-dropdown-text v-if="isLoadingMilestones">
- <gl-skeleton-loader :height="90">
+ <template #footer>
+ <gl-skeleton-loader v-if="isLoadingMilestones" :height="90">
<rect width="380" height="10" x="10" y="15" rx="4" />
<rect width="280" height="10" x="10" y="30" rx="4" />
<rect width="380" height="10" x="10" y="50" rx="4" />
<rect width="280" height="10" x="10" y="65" rx="4" />
</gl-skeleton-loader>
- </gl-dropdown-text>
- <template v-else-if="milestones.length">
- <gl-dropdown-item
- v-for="milestone in milestones"
- :key="milestone.id"
- is-check-item
- :is-checked="isMilestoneChecked(milestone)"
- @click="handleMilestoneClick(milestone)"
+
+ <div
+ v-else-if="!milestones.length"
+ aria-live="assertive"
+ class="gl-pl-7 gl-pr-5 gl-py-3 gl-font-base gl-text-gray-600"
+ data-testid="no-results-text"
>
- {{ milestone.title }}
- <template v-if="milestone.expired">{{ $options.i18n.EXPIRED_TEXT }}</template>
- </gl-dropdown-item>
+ {{ $options.i18n.NO_MATCHING_RESULTS }}
+ </div>
</template>
- <gl-dropdown-text v-else>{{ $options.i18n.NO_MATCHING_RESULTS }}</gl-dropdown-text>
- </gl-dropdown>
+ </gl-collapsible-listbox>
</gl-form-group>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
index fe8aea99f53..6756acd4495 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -1,7 +1,7 @@
<script>
import { GlSkeletonLoader, GlModal } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { uniqueId } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __ } from '~/locale';
import { scrollToTargetOnResize } from '~/lib/utils/resize_observer';
import { TYPENAME_DISCUSSION, TYPENAME_NOTE } from '~/graphql_shared/constants';
@@ -170,9 +170,6 @@ export default {
apollo: {
workItemNotes: {
query: workItemNotesByIidQuery,
- context: {
- isSingleRequest: true,
- },
variables() {
return {
fullPath: this.fullPath,
diff --git a/app/assets/javascripts/work_items/components/work_item_parent.vue b/app/assets/javascripts/work_items/components/work_item_parent.vue
index e16299f482f..ce30f7985cf 100644
--- a/app/assets/javascripts/work_items/components/work_item_parent.vue
+++ b/app/assets/javascripts/work_items/components/work_item_parent.vue
@@ -1,18 +1,20 @@
<script>
import { GlFormGroup, GlCollapsibleListbox } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { debounce } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import { removeHierarchyChild } from '../graphql/cache_utils';
+import groupWorkItemsQuery from '../graphql/group_work_items.query.graphql';
import projectWorkItemsQuery from '../graphql/project_work_items.query.graphql';
import {
I18N_WORK_ITEM_ERROR_UPDATING,
sprintfWorkItem,
- WORK_ITEM_TYPE_ENUM_OBJECTIVE,
+ SUPPORTED_PARENT_TYPE_MAP,
} from '../constants';
export default {
@@ -31,7 +33,7 @@ export default {
GlCollapsibleListbox,
},
mixins: [glFeatureFlagMixin()],
- inject: ['fullPath'],
+ inject: ['fullPath', 'isGroup'],
props: {
workItemId: {
type: String,
@@ -60,7 +62,7 @@ export default {
searchStarted: false,
availableWorkItems: [],
localSelectedItem: this.parent?.id,
- isNotFocused: true,
+ oldParent: this.parent,
};
},
computed: {
@@ -80,13 +82,8 @@ export default {
workItems() {
return this.availableWorkItems.map(({ id, title }) => ({ text: title, value: id }));
},
- listboxCategory() {
- return this.searchStarted ? 'secondary' : 'tertiary';
- },
- listboxClasses() {
- return {
- 'is-not-focused': this.isNotFocused && !this.searchStarted,
- };
+ parentType() {
+ return SUPPORTED_PARENT_TYPE_MAP[this.workItemType];
},
},
watch: {
@@ -101,13 +98,17 @@ export default {
},
apollo: {
availableWorkItems: {
- query: projectWorkItemsQuery,
+ query() {
+ return this.isGroup ? groupWorkItemsQuery : projectWorkItemsQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
searchTerm: this.search,
- types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
+ types: this.parentType,
in: this.search ? 'TITLE' : undefined,
+ iid: null,
+ isNumber: false,
};
},
skip() {
@@ -146,6 +147,14 @@ export default {
},
},
},
+ update: (cache) =>
+ removeHierarchyChild({
+ cache,
+ fullPath: this.fullPath,
+ iid: this.oldParent?.iid,
+ isGroup: this.isGroup,
+ workItem: { id: this.workItemId },
+ }),
});
if (errors.length) {
@@ -171,19 +180,10 @@ export default {
},
onListboxShown() {
this.searchStarted = true;
- this.isNotFocused = false;
},
onListboxHide() {
this.searchStarted = false;
this.search = '';
- this.isNotFocused = true;
- },
- setListboxFocused() {
- // This is to match the caret behaviour of parent listbox
- // to the other dropdown fields of work items
- if (document.activeElement.parentElement.id !== 'work-item-parent-listbox-value') {
- this.isNotFocused = true;
- }
},
},
};
@@ -206,30 +206,20 @@ export default {
>
{{ listboxText }}
</span>
- <div
- v-else
- :class="{ 'gl-max-w-max-content': !workItemsMvc2Enabled }"
- @mouseover="isNotFocused = false"
- @mouseleave="setListboxFocused"
- @focusout="isNotFocused = true"
- @focusin="isNotFocused = false"
- >
+ <div v-else :class="{ 'gl-max-w-max-content': !workItemsMvc2Enabled }">
<gl-collapsible-listbox
id="work-item-parent-listbox-value"
class="gl-max-w-max-content"
data-testid="work-item-parent-listbox"
- block
searchable
- :no-caret="isNotFocused && !searchStarted"
is-check-centered
- :category="listboxCategory"
+ category="tertiary"
:searching="isLoading"
:header-text="$options.i18n.assignParentLabel"
:no-results-text="$options.i18n.noMatchingResults"
:loading="updateInProgress"
:items="workItems"
:toggle-text="listboxText"
- :toggle-class="listboxClasses"
:selected="localSelectedItem"
:reset-button-label="$options.i18n.unAssign"
@reset="unAssignParent"
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
index d242db95896..c98bd6ce1e9 100644
--- a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
@@ -4,6 +4,7 @@ import { GlFormGroup, GlForm, GlFormRadioGroup, GlButton, GlAlert } from '@gitla
import { __, s__ } from '~/locale';
import WorkItemTokenInput from '../shared/work_item_token_input.vue';
import addLinkedItemsMutation from '../../graphql/add_linked_items.mutation.graphql';
+import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import {
LINK_ITEM_FORM_HEADER_LABEL,
@@ -23,6 +24,7 @@ export default {
GlAlert,
WorkItemTokenInput,
},
+ inject: ['isGroup'],
props: {
workItemId: {
type: String,
@@ -121,7 +123,7 @@ export default {
},
) => {
const queryArgs = {
- query: workItemByIidQuery,
+ query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: { fullPath: this.workItemFullPath, iid: this.workItemIid },
};
const sourceData = cache.readQuery(queryArgs);
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
index 002c1786044..e70c79ea68f 100644
--- a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
@@ -19,6 +19,11 @@ export default {
type: Boolean,
required: true,
},
+ showLabels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
};
</script>
@@ -42,6 +47,7 @@ export default {
:child-item="linkedItem.workItem"
:can-update="canUpdate"
:show-task-icon="true"
+ :show-labels="showLabels"
@click="$emit('showModal', { event: $event, child: linkedItem.workItem })"
@removeChild="$emit('removeLinkedItem', linkedItem.workItem)"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
index 20427fe96c4..790804a8934 100644
--- a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
@@ -1,6 +1,6 @@
<script>
import { produce } from 'immer';
-import { GlLoadingIcon, GlIcon, GlButton, GlLink } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon, GlButton, GlLink, GlToggle } from '@gitlab/ui';
import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -8,7 +8,11 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import removeLinkedItemsMutation from '../../graphql/remove_linked_items.mutation.graphql';
-import { WIDGET_TYPE_LINKED_ITEMS, LINKED_CATEGORIES_MAP } from '../../constants';
+import {
+ WIDGET_TYPE_LINKED_ITEMS,
+ LINKED_CATEGORIES_MAP,
+ I18N_WORK_ITEM_SHOW_LABELS,
+} from '../../constants';
import WidgetWrapper from '../widget_wrapper.vue';
import WorkItemRelationshipList from './work_item_relationship_list.vue';
@@ -24,6 +28,7 @@ export default {
WidgetWrapper,
WorkItemRelationshipList,
WorkItemAddRelationshipForm,
+ GlToggle,
},
inject: ['isGroup'],
props: {
@@ -60,9 +65,6 @@ export default {
update(data) {
return data.workspace.workItems.nodes[0] ?? {};
},
- context: {
- isSingleRequest: true,
- },
skip() {
return !this.workItemIid;
},
@@ -97,6 +99,7 @@ export default {
linksBlocks: [],
isShownLinkItemForm: false,
widgetName: 'linkeditems',
+ showLabels: true,
};
},
computed: {
@@ -150,7 +153,7 @@ export default {
return;
}
const queryArgs = {
- query: workItemByIidQuery,
+ query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: { fullPath: this.workItemFullPath, iid: this.workItemIid },
};
const sourceData = cache.readQuery(queryArgs);
@@ -200,6 +203,7 @@ export default {
blockingTitle: s__('WorkItem|Blocking'),
blockedByTitle: s__('WorkItem|Blocked by'),
addLinkedWorkItemButtonLabel: s__('WorkItem|Add'),
+ showLabelsLabel: I18N_WORK_ITEM_SHOW_LABELS,
},
};
</script>
@@ -222,11 +226,18 @@ export default {
</div>
</template>
<template #header-right>
+ <gl-toggle
+ :value="showLabels"
+ :label="$options.i18n.showLabelsLabel"
+ label-position="left"
+ label-id="relationship-toggle-labels"
+ @change="showLabels = $event"
+ />
<gl-button
v-if="canAdminWorkItemLink"
data-testid="link-item-add-button"
size="small"
- class="gl-ml-3"
+ class="gl-ml-4"
@click="showLinkItemForm"
>
<slot name="add-button-text">{{ $options.i18n.addLinkedWorkItemButtonLabel }}</slot>
@@ -264,6 +275,7 @@ export default {
:linked-items="linksBlocks"
:heading="$options.i18n.blockingTitle"
:can-update="canAdminWorkItemLink"
+ :show-labels="showLabels"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
@removeLinkedItem="removeLinkedItem"
/>
@@ -276,6 +288,7 @@ export default {
:linked-items="linksIsBlockedBy"
:heading="$options.i18n.blockedByTitle"
:can-update="canAdminWorkItemLink"
+ :show-labels="showLabels"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
@removeLinkedItem="removeLinkedItem"
/>
@@ -284,6 +297,7 @@ export default {
:linked-items="linksRelatesTo"
:heading="$options.i18n.relatedToTitle"
:can-update="canAdminWorkItemLink"
+ :show-labels="showLabels"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
@removeLinkedItem="removeLinkedItem"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue b/app/assets/javascripts/work_items/components/work_item_state_toggle.vue
index 0ea30845466..581ef9ec945 100644
--- a/app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue
+++ b/app/assets/javascripts/work_items/components/work_item_state_toggle.vue
@@ -1,9 +1,8 @@
<script>
-import { GlButton } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import { GlButton, GlDisclosureDropdownItem, GlLoadingIcon } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
-import { __, sprintf } from '~/locale';
-import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { __ } from '~/locale';
import { getUpdateWorkItemMutation } from '~/work_items/components/update_work_item';
import {
sprintfWorkItem,
@@ -17,6 +16,8 @@ import {
export default {
components: {
GlButton,
+ GlDisclosureDropdownItem,
+ GlLoadingIcon,
},
mixins: [Tracking.mixin()],
props: {
@@ -37,6 +38,11 @@ export default {
required: false,
default: null,
},
+ showAsDropdownItem: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -51,9 +57,7 @@ export default {
const baseText = this.isWorkItemOpen
? __('Close %{workItemType}')
: __('Reopen %{workItemType}');
- return capitalizeFirstCharacter(
- sprintf(baseText, { workItemType: this.workItemType.toLowerCase() }),
- );
+ return sprintfWorkItem(baseText, this.workItemType);
},
tracking() {
return {
@@ -62,6 +66,12 @@ export default {
property: `type_${this.workItemType}`,
};
},
+ toggleInProgressText() {
+ const baseText = this.isWorkItemOpen
+ ? __('Closing %{workItemType}')
+ : __('Reopening %{workItemType}');
+ return sprintfWorkItem(baseText, this.workItemType);
+ },
},
methods: {
async updateWorkItem() {
@@ -104,10 +114,18 @@ export default {
</script>
<template>
- <gl-button
- :loading="updateInProgress"
- data-testid="work-item-state-toggle"
- @click="updateWorkItem"
- >{{ toggleWorkItemStateText }}</gl-button
- >
+ <gl-disclosure-dropdown-item v-if="showAsDropdownItem" @action="updateWorkItem">
+ <template #list-item>
+ <template v-if="updateInProgress">
+ <gl-loading-icon inline size="sm" />
+ {{ toggleInProgressText }}
+ </template>
+ <template v-else>
+ {{ toggleWorkItemStateText }}
+ </template>
+ </template>
+ </gl-disclosure-dropdown-item>
+ <gl-button v-else :loading="updateInProgress" @click="updateWorkItem">{{
+ toggleWorkItemStateText
+ }}</gl-button>
</template>
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 c52a6854fad..9b5803421dd 100644
--- a/app/assets/javascripts/work_items/components/work_item_title.vue
+++ b/app/assets/javascripts/work_items/components/work_item_title.vue
@@ -1,10 +1,12 @@
<script>
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
import {
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_UPDATING,
TRACKING_CATEGORY_SHOW,
+ WORK_ITEM_TITLE_MAX_LENGTH,
+ I18N_MAX_CHARS_IN_WORK_ITEM_TITLE_MESSAGE,
} from '../constants';
import { getUpdateWorkItemMutation } from './update_work_item';
import ItemTitle from './item_title.vue';
@@ -56,6 +58,11 @@ export default {
return;
}
+ if (updatedTitle.length > WORK_ITEM_TITLE_MAX_LENGTH) {
+ this.$emit('error', sprintfWorkItem(I18N_MAX_CHARS_IN_WORK_ITEM_TITLE_MESSAGE));
+ return;
+ }
+
const input = {
id: this.workItemId,
title: updatedTitle,
diff --git a/app/assets/javascripts/work_items/components/work_item_todos.vue b/app/assets/javascripts/work_items/components/work_item_todos.vue
index e6d7f2067ba..62518616398 100644
--- a/app/assets/javascripts/work_items/components/work_item_todos.vue
+++ b/app/assets/javascripts/work_items/components/work_item_todos.vue
@@ -175,17 +175,12 @@ export default {
<template>
<gl-button
v-gl-tooltip.hover
- data-testid="work-item-todos-action"
:loading="isLoading"
:title="buttonLabel"
- category="tertiary"
+ category="secondary"
:aria-label="buttonLabel"
@click="onToggle"
>
- <gl-icon
- data-testid="work-item-todos-icon"
- :class="{ 'gl-fill-blue-500': pendingTodo }"
- :name="buttonIcon"
- />
+ <gl-icon :class="{ 'gl-fill-blue-500': pendingTodo }" :name="buttonIcon" />
</gl-button>
</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index a64172acff4..daa72204609 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -35,6 +35,7 @@ export const WORK_ITEM_TYPE_ENUM_TEST_CASE = 'TEST_CASE';
export const WORK_ITEM_TYPE_ENUM_REQUIREMENTS = 'REQUIREMENTS';
export const WORK_ITEM_TYPE_ENUM_OBJECTIVE = 'OBJECTIVE';
export const WORK_ITEM_TYPE_ENUM_KEY_RESULT = 'KEY_RESULT';
+export const WORK_ITEM_TYPE_ENUM_EPIC = 'EPIC';
export const WORK_ITEM_TYPE_VALUE_EPIC = 'Epic';
export const WORK_ITEM_TYPE_VALUE_INCIDENT = 'Incident';
@@ -45,6 +46,8 @@ export const WORK_ITEM_TYPE_VALUE_REQUIREMENTS = 'Requirements';
export const WORK_ITEM_TYPE_VALUE_KEY_RESULT = 'Key Result';
export const WORK_ITEM_TYPE_VALUE_OBJECTIVE = 'Objective';
+export const WORK_ITEM_TITLE_MAX_LENGTH = 255;
+
export const i18n = {
fetchErrorTitle: s__('WorkItem|Work item not found'),
fetchError: s__(
@@ -91,8 +94,9 @@ export const I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR = s__(
export const I18N_WORK_ITEM_CREATE_BUTTON_LABEL = s__('WorkItem|Create %{workItemType}');
export const I18N_WORK_ITEM_ADD_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}');
export const I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}s');
-export const I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER = s__(
- 'WorkItem|Search existing %{workItemType}s',
+export const I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER = s__('WorkItem|Search existing items');
+export const I18N_WORK_ITEM_SEARCH_ERROR = s__(
+ 'WorkItem|Something went wrong while fetching the %{workItemType}. Please try again.',
);
export const I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL = s__(
'WorkItem|This %{workItemType} is confidential and should only be visible to team members with at least Reporter access',
@@ -108,6 +112,11 @@ export const I18N_WORK_ITEM_ERROR_COPY_EMAIL = s__(
'WorkItem|Something went wrong while copying the %{workItemType} email address. Please try again.',
);
+export const I18N_MAX_CHARS_IN_WORK_ITEM_TITLE_MESSAGE = sprintf(
+ s__('WorkItem|Title cannot have more than %{WORK_ITEM_TITLE_MAX_LENGTH} characters.'),
+ { WORK_ITEM_TITLE_MAX_LENGTH },
+);
+
export const I18N_WORK_ITEM_COPY_CREATE_NOTE_EMAIL = s__(
'WorkItem|Copy %{workItemType} email address',
);
@@ -122,6 +131,7 @@ export const I18N_MAX_WORK_ITEMS_NOTE_LABEL = sprintf(
s__('WorkItem|Add a maximum of %{MAX_WORK_ITEMS} items at a time.'),
{ MAX_WORK_ITEMS },
);
+export const I18N_WORK_ITEM_SHOW_LABELS = s__('WorkItem|Show labels');
export const sprintfWorkItem = (msg, workItemTypeArg, parentWorkItemType = '') => {
const workItemType = workItemTypeArg || s__('WorkItem|item');
@@ -178,6 +188,11 @@ export const WORK_ITEMS_TYPE_MAP = {
name: s__('WorkItem|Key result'),
value: WORK_ITEM_TYPE_VALUE_KEY_RESULT,
},
+ [WORK_ITEM_TYPE_ENUM_EPIC]: {
+ icon: `epic`,
+ name: s__('WorkItem|Epic'),
+ value: WORK_ITEM_TYPE_VALUE_EPIC,
+ },
};
export const WORK_ITEMS_TREE_TEXT_MAP = {
@@ -246,12 +261,12 @@ export const WORK_ITEM_ACTIVITY_SORT_OPTIONS = [
];
export const TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION = 'confidentiality-toggle-action';
-export const TEST_ID_NOTIFICATIONS_TOGGLE_ACTION = 'notifications-toggle-action';
export const TEST_ID_NOTIFICATIONS_TOGGLE_FORM = 'notifications-toggle-form';
export const TEST_ID_DELETE_ACTION = 'delete-action';
export const TEST_ID_PROMOTE_ACTION = 'promote-action';
export const TEST_ID_COPY_REFERENCE_ACTION = 'copy-reference-action';
export const TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION = 'copy-create-note-email-action';
+export const TEST_ID_TOGGLE_ACTION = 'state-toggle-action';
export const TODO_ADD_ICON = 'todo-add';
export const TODO_DONE_ICON = 'todo-done';
@@ -288,3 +303,9 @@ export const LINK_ITEM_FORM_HEADER_LABEL = {
[WORK_ITEM_TYPE_VALUE_KEY_RESULT]: s__('WorkItem|The current key result'),
[WORK_ITEM_TYPE_VALUE_TASK]: s__('WorkItem|The current task'),
};
+
+export const SUPPORTED_PARENT_TYPE_MAP = {
+ [WORK_ITEM_TYPE_VALUE_OBJECTIVE]: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
+ [WORK_ITEM_TYPE_VALUE_KEY_RESULT]: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
+ [WORK_ITEM_TYPE_VALUE_TASK]: [WORK_ITEM_TYPE_ENUM_ISSUE],
+};
diff --git a/app/assets/javascripts/work_items/graphql/award_emoji.query.graphql b/app/assets/javascripts/work_items/graphql/award_emoji.query.graphql
index 82a532e1bea..0b9dc546df3 100644
--- a/app/assets/javascripts/work_items/graphql/award_emoji.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/award_emoji.query.graphql
@@ -1,7 +1,7 @@
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
#import "~/work_items/graphql/award_emoji.fragment.graphql"
-query workItemAwardEmojis($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) {
+query projectWorkItemAwardEmojis($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) {
workspace: project(fullPath: $fullPath) {
id
workItems(iid: $iid) {
diff --git a/app/assets/javascripts/work_items/graphql/group_award_emoji.query.graphql b/app/assets/javascripts/work_items/graphql/group_award_emoji.query.graphql
new file mode 100644
index 00000000000..cdf8c7cad04
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/group_award_emoji.query.graphql
@@ -0,0 +1,27 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+#import "~/work_items/graphql/award_emoji.fragment.graphql"
+
+query groupWorkItemAwardEmojis($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) {
+ workspace: group(fullPath: $fullPath) {
+ id
+ workItems(iid: $iid) {
+ nodes {
+ id
+ iid
+ widgets {
+ ... on WorkItemWidgetAwardEmoji {
+ type
+ awardEmoji(first: $pageSize, after: $after) {
+ pageInfo {
+ ...PageInfo
+ }
+ nodes {
+ ...AwardEmojiFragment
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/group_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/group_work_items.query.graphql
new file mode 100644
index 00000000000..5332e21a0cb
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/group_work_items.query.graphql
@@ -0,0 +1,17 @@
+query groupWorkItems(
+ $searchTerm: String
+ $fullPath: ID!
+ $types: [IssueType!]
+ $in: [IssuableSearchableField!]
+) {
+ workspace: group(fullPath: $fullPath) {
+ id
+ workItems(search: $searchTerm, types: $types, in: $in) {
+ nodes {
+ id
+ iid
+ title
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
index 2be436aa8c2..3aeaaa1116a 100644
--- a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
@@ -3,6 +3,8 @@ query projectWorkItems(
$fullPath: ID!
$types: [IssueType!]
$in: [IssuableSearchableField!]
+ $iid: String = null
+ $isNumber: Boolean!
) {
workspace: project(fullPath: $fullPath) {
id
@@ -11,8 +13,13 @@ query projectWorkItems(
id
iid
title
- state
- confidential
+ }
+ }
+ workItemsByIid: workItems(iid: $iid, types: $types) @include(if: $isNumber) {
+ nodes {
+ id
+ iid
+ title
}
}
}
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 fac99310890..ef43b9c026d 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -4,6 +4,7 @@
fragment WorkItem on WorkItem {
id
iid
+ archived
title
state
description
@@ -13,10 +14,9 @@ fragment WorkItem on WorkItem {
closedAt
reference(full: true)
createNoteEmail
- project {
+ namespace {
id
fullPath
- archived
name
}
author {
diff --git a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
index a853018a931..58f74dccd4d 100644
--- a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
+++ b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
@@ -1,5 +1,5 @@
<script>
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import { STATUS_OPEN } from '~/issues/constants';
diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue
index 31e790254d9..435a1233dce 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -103,7 +103,7 @@ export default {
data: {
workspace: {
__typename: TYPENAME_PROJECT,
- id: workItem.project.id,
+ id: workItem.namespace.id,
workItems: {
__typename: 'WorkItemConnection',
nodes: [workItem],
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 40228b93e01..ce8ccb2bc08 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -31,7 +31,3 @@
@media print {
@import 'print';
}
-
-/* Rules for overriding cloaking in startup-general.scss */
-@import 'startup/cloaking';
-@include cloak-startup-scss(block);
diff --git a/app/assets/stylesheets/application_utilities.scss b/app/assets/stylesheets/application_utilities.scss
index 817e983a0ec..8bec12784ed 100644
--- a/app/assets/stylesheets/application_utilities.scss
+++ b/app/assets/stylesheets/application_utilities.scss
@@ -10,3 +10,5 @@
// Gitlab UI util classes
@import '@gitlab/ui/src/scss/utilities';
+
+@import 'tmp_utilities'; \ No newline at end of file
diff --git a/app/assets/stylesheets/components/detail_page.scss b/app/assets/stylesheets/components/detail_page.scss
index de8142924f9..a5fd57f6c57 100644
--- a/app/assets/stylesheets/components/detail_page.scss
+++ b/app/assets/stylesheets/components/detail_page.scss
@@ -36,7 +36,6 @@
}
.detail-page-header-actions {
- align-self: center;
flex: 0 0 auto;
&:not(.is-merge-request) {
@@ -67,6 +66,8 @@
}
.description {
+ @include clearfix;
+
margin-top: 6px;
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 8a64b0999b6..88509dbc4a1 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -191,25 +191,6 @@
color: $gray-700;
}
- // deprecated class
- &.btn-text-field {
- width: 100%;
- text-align: left;
- padding: 6px 16px;
- border-color: $border-color;
- color: $gray-darkest;
- background-color: $white;
-
- &:hover,
- &:active,
- &:focus {
- cursor: text;
- box-shadow: none;
- border-color: lighten($blue-300, 20%);
- color: $gray-darkest;
- }
- }
-
&.dot-highlight::after {
content: '';
background-color: $blue-500;
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index 4bf109a0bff..8f07ef73554 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -901,7 +901,6 @@ table.code {
@media (max-width: map-get($grid-breakpoints, lg)-1) {
.diffs .files {
- @include fixed-width-container;
flex-direction: column;
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 613e504c771..eb627b036fe 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -247,6 +247,7 @@ span.idiff {
border-bottom: 1px solid $border-color;
padding: $gl-padding-8 $gl-padding;
margin: 0;
+ min-height: px-to-rem(42px);
border-radius: $border-radius-default $border-radius-default 0 0;
@include media-breakpoint-up(md) {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 32735679ded..e269ea68e41 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -630,11 +630,18 @@ $search-input-field-x-min-width: 200px;
header.navbar-gitlab.super-sidebar-logged-out {
background-color: $brand-charcoal !important;
+ li.nav-item > button,
li.nav-item > a {
- @include gl-text-white;
+ @include gl-text-gray-100;
@include gl-font-weight-normal;
&:hover,
+ &:focus,
+ &:active {
+ @include gl-text-white
+ }
+
+ &:hover,
&:focus {
background-color: $brand-gray-04;
text-decoration: none;
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index a63ce66e681..a93c2191016 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -33,7 +33,7 @@
padding-right: 10px;
white-space: pre;
- &:empty::before {
+ &:empty::before, span:empty::before {
content: '\200b';
}
}
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 37a2264122d..bfd55fbb53d 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -1,55 +1,37 @@
-@mixin icon-styles($primary-color, $svg-color) {
+@mixin icon-styles($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: $svg-color;
- }
-
- &:hover,
- &.active {
- box-shadow: 0 0 0 1px $primary-color;
- }
+ fill: $color;
}
}
.ci-status-icon-success,
.ci-status-icon-passed {
- @include icon-styles($green-500, $green-100);
+ @include icon-styles($green-500);
}
.ci-status-icon-error,
.ci-status-icon-failed {
- @include icon-styles($red-500, $red-100);
+ @include icon-styles($red-500);
}
.ci-status-icon-pending,
.ci-status-icon-waiting-for-resource,
+.ci-status-icon-waiting-for-callback,
.ci-status-icon-failed-with-warnings,
.ci-status-icon-success-with-warnings {
- @include icon-styles($orange-500, $orange-100);
+ @include icon-styles($orange-500);
}
.ci-status-icon-running {
- @include icon-styles($blue-500, $blue-100);
+ @include icon-styles($blue-500);
}
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-scheduled,
.ci-status-icon-manual {
- @include icon-styles($gray-900, $gray-100);
+ @include icon-styles($gray-900);
}
.ci-status-icon-notification,
@@ -57,7 +39,58 @@
.ci-status-icon-created,
.ci-status-icon-skipped,
.ci-status-icon-notfound {
- @include icon-styles($gray-500, $gray-100);
+ @include icon-styles($gray-500);
+}
+
+.ci-icon {
+ // .ci-icon class is used at
+ // - app/assets/javascripts/vue_shared/components/ci_icon.vue
+ // - app/helpers/ci/status_helper.rb
+ .ci-icon-gl-icon-wrapper {
+ @include gl-rounded-full;
+ @include gl-line-height-0;
+ }
+
+ // Makes the borderless CI icons appear slightly bigger than the default 16px.
+ // Could be fixed by making the SVG fill up the canvas in a follow up issue.
+ .gl-icon {
+ // fill: currentColor;
+ width: 20px;
+ height: 20px;
+ margin: -2px;
+ }
+
+ @mixin ci-icon-style($bg-color, $color, $gl-dark-bg-color: null, $gl-dark-color: null) {
+ .ci-icon-gl-icon-wrapper {
+ background-color: $bg-color;
+ color: $color;
+
+ .gl-dark & {
+ background-color: $gl-dark-bg-color;
+ color: $gl-dark-color;
+ }
+ }
+ }
+
+ &.ci-icon-variant-success {
+ @include ci-icon-style($green-500, $white, $green-600, $green-50)
+ }
+
+ &.ci-icon-variant-warning {
+ @include ci-icon-style($orange-500, $white, $orange-600, $orange-50)
+ }
+
+ &.ci-icon-variant-danger {
+ @include ci-icon-style($red-500, $white, $red-600, $red-50)
+ }
+
+ &.ci-icon-variant-info {
+ @include ci-icon-style($white, $blue-500, $blue-600, $blue-50)
+ }
+
+ &.ci-icon-variant-neutral {
+ @include ci-icon-style($white, $gray-500)
+ }
}
.password-status-icon-success {
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 171f070d776..33c8a0254fd 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -4,12 +4,6 @@ html {
&.touch .tooltip {
display: none !important;
}
-
- @include media-breakpoint-up(sm) {
- &.logged-out-marketing-header {
- --header-height: 72px;
- }
- }
}
body {
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index df107798a87..0f6fdf18ea0 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -30,15 +30,6 @@
max-width: $max-width;
}
-/**
- * Mixin for fixed width container
- */
-@mixin fixed-width-container {
- max-width: $limited-layout-width - ($gl-padding * 2);
- margin-left: auto;
- margin-right: auto;
-}
-
/*
* Base mixin for lists in GitLab
*/
diff --git a/app/assets/stylesheets/framework/page_header.scss b/app/assets/stylesheets/framework/page_header.scss
index c2bd475ab90..ad183a64cc5 100644
--- a/app/assets/stylesheets/framework/page_header.scss
+++ b/app/assets/stylesheets/framework/page_header.scss
@@ -34,12 +34,4 @@
margin-left: 8px;
}
}
-
- .ci-status-link {
- svg {
- position: relative;
- top: 2px;
- margin: 0 2px 0 3px;
- }
- }
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 0619d5f166e..168aa704a69 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -2,7 +2,11 @@
width: 100%;
.container-fluid {
- padding: 0 $gl-padding;
+ padding: 0 $container-margin;
+
+ @include media-breakpoint-up(xl) {
+ padding: 0 $container-margin-xl;
+ }
&.container-blank {
background: none;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index a4bb39e0764..ab8547c3fef 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -468,8 +468,10 @@ $content-wrapper-padding: 100px;
$header-zindex: 1000;
$zindex-dropdown-menu: 300;
$ide-statusbar-height: 25px;
-$fixed-layout-width: 1280px;
-$limited-layout-width: 990px;
+$limited-layout-width: 1006px;
+$fixed-layout-width: 1296px;
+$container-margin: $gl-padding;
+$container-margin-xl: $gl-padding-24;
$container-text-max-width: 540px;
$border-radius-default: 4px;
$border-radius-small: 2px;
@@ -485,7 +487,7 @@ $performance-bar-height: 2.5rem;
$system-header-height: 16px;
$system-footer-height: $system-header-height;
$mr-sticky-header-height: 72px;
-$mr-review-bar-height: calc(2rem + 13px);
+$mr-review-bar-height: calc(2rem + 16px);
$flash-height: 52px;
$context-header-height: 60px;
$top-bar-height: 48px;
@@ -655,8 +657,8 @@ $status-icon-size: 22px;
*/
$discord: #5865f2;
$linkedin: #2867b2;
+$mastodon: #6364ff;
$skype: #0078d7;
-$twitter: #1d9bf0;
/*
* Award emoji
@@ -715,10 +717,10 @@ $blame-blue: #254e77;
*/
$builds-log-bg: #111;
$job-log-highlight-height: 18px;
-$job-log-line-padding: 55px;
+$job-log-line-padding: 63px;
$job-line-number-width: 50px;
-$job-line-number-margin: 43px;
-$job-arrow-margin: 55px;
+$job-line-number-margin: 51px;
+$job-arrow-margin: 63px;
/*
* Calendar
@@ -810,7 +812,7 @@ $ci-action-icon-size: 22px;
$ci-action-icon-size-lg: 24px;
$pipeline-dropdown-line-height: 20px;
$ci-action-dropdown-button-size: 24px;
-$ci-action-dropdown-svg-size: 12px;
+$ci-action-dropdown-svg-size: 16px;
/*
CI variable lists
diff --git a/app/assets/stylesheets/page_bundles/_system_note_styles.scss b/app/assets/stylesheets/page_bundles/_system_note_styles.scss
new file mode 100644
index 00000000000..68e2b747c52
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/_system_note_styles.scss
@@ -0,0 +1,59 @@
+/**
+Shared styles for system note dot and icon styles used for MR, Issue, Work Item
+*/
+.system-note-tiny-dot {
+ width: 8px;
+ height: 8px;
+ margin-top: 6px;
+ margin-left: 12px;
+ margin-right: 8px;
+ border: 2px solid var(--gray-50, $gray-50);
+ }
+
+ .system-note-icon {
+ width: 20px;
+ height: 20px;
+ margin-left: 6px;
+
+ &.gl-bg-green-100 {
+ --bg-color: var(--green-100, #{$green-100});
+ }
+
+ &.gl-bg-red-100 {
+ --bg-color: var(--red-100, #{$red-100});
+ }
+
+ &.gl-bg-blue-100 {
+ --bg-color: var(--blue-100, #{$blue-100});
+ }
+ }
+
+ .system-note-icon:not(.mr-system-note-empty)::before {
+ content: '';
+ display: block;
+ position: absolute;
+ left: calc(50% - 1px);
+ bottom: 100%;
+ width: 2px;
+ height: 20px;
+ background: linear-gradient(to bottom, transparent, var(--bg-color));
+
+ .system-note:first-child & {
+ display: none;
+ }
+ }
+
+ .system-note-icon:not(.mr-system-note-empty)::after {
+ content: '';
+ display: block;
+ position: absolute;
+ left: calc(50% - 1px);
+ top: 100%;
+ width: 2px;
+ height: 20px;
+ background: linear-gradient(to bottom, var(--bg-color), transparent);
+
+ .system-note:last-child & {
+ display: none;
+ }
+ } \ No newline at end of file
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index 5aca697ae26..22e42d0a7f7 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -39,6 +39,12 @@
width: 400px;
}
+ &.board-add-new-list {
+ @include media-breakpoint-down(sm) {
+ width: 100%;
+ }
+ }
+
&.is-collapsed {
.board-title-text > span,
.issue-count-badge > span {
diff --git a/app/assets/stylesheets/page_bundles/branches.scss b/app/assets/stylesheets/page_bundles/branches.scss
index daf828fb559..973ba1afb17 100644
--- a/app/assets/stylesheets/page_bundles/branches.scss
+++ b/app/assets/stylesheets/page_bundles/branches.scss
@@ -42,6 +42,10 @@
.branches-list .branch-item:not(:last-of-type) {
border-bottom: 1px solid $border-color;
+
+ .gl-dark & {
+ border-bottom-color: $gray-800;
+ }
}
.branch-item {
diff --git a/app/assets/stylesheets/page_bundles/build.scss b/app/assets/stylesheets/page_bundles/build.scss
index 16fc0e7ebae..6165ee6e8b4 100644
--- a/app/assets/stylesheets/page_bundles/build.scss
+++ b/app/assets/stylesheets/page_bundles/build.scss
@@ -48,14 +48,6 @@
border: 1px solid var(--border-color, $border-color);
padding: 8px $gl-padding 12px;
border-radius: $border-radius-default;
-
- svg {
- position: relative;
- top: 3px;
- margin-right: 5px;
- width: 22px;
- height: 22px;
- }
}
.build-loader-animation {
diff --git a/app/assets/stylesheets/page_bundles/ci_status.scss b/app/assets/stylesheets/page_bundles/ci_status.scss
index 17886ab954a..f2129aa6841 100644
--- a/app/assets/stylesheets/page_bundles/ci_status.scss
+++ b/app/assets/stylesheets/page_bundles/ci_status.scss
@@ -48,6 +48,7 @@
&.ci-pending,
&.ci-waiting-for-resource,
+ &.ci-waiting-for-callback,
&.ci-failed-with-warnings,
&.ci-success-with-warnings {
@include status-color(
diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss
index 07614c5271a..05563f8e314 100644
--- a/app/assets/stylesheets/page_bundles/issuable.scss
+++ b/app/assets/stylesheets/page_bundles/issuable.scss
@@ -1,33 +1,5 @@
@import 'mixins_and_variables_and_functions';
-
-.limit-container-width {
- .flash-container,
- .detail-page-header,
- .page-content-header,
- .commit-box,
- .info-well,
- .commit-ci-menu,
- .files-changed-inner,
- .limited-header-width,
- .limited-width-notes {
- @include fixed-width-container;
- }
-
- .issuable-details {
- .detail-page-description,
- .mr-source-target,
- .mr-state-widget,
- .merge-manually {
- @include fixed-width-container;
- }
- }
-
- .merge-request-details {
- .emoji-list-container {
- @include fixed-width-container;
- }
- }
-}
+@import 'system_note_styles';
.issuable-details {
section {
@@ -114,29 +86,6 @@
}
}
-/*
- * Following overrides are done to prevent
- * legacy dropdown styles from influencing
- * GitLab UI components used within GlDropdown
- */
-.issuable-move-dropdown {
- .b-dropdown-form {
- @include gl-p-0;
- }
-
- .gl-search-box-by-type button.gl-clear-icon-button:hover {
- @include gl-bg-transparent;
-
- &:focus {
- @include gl-focus($inset: true);
- }
- }
-
- .issuable-move-button:not(.disabled):hover {
- @include gl-text-white;
- }
-}
-
.suggestion-footer {
font-size: 12px;
line-height: 15px;
diff --git a/app/assets/stylesheets/page_bundles/merge_request.scss b/app/assets/stylesheets/page_bundles/merge_request.scss
index e429c0c149e..8dc4401e72c 100644
--- a/app/assets/stylesheets/page_bundles/merge_request.scss
+++ b/app/assets/stylesheets/page_bundles/merge_request.scss
@@ -88,20 +88,6 @@ $comparison-empty-state-height: 62px;
.merge-request-title {
margin-bottom: 2px;
-
- .ci-status-link {
- svg {
- height: 16px;
- width: 16px;
- position: relative;
- top: 3px;
- }
-
- &:hover,
- &:focus {
- text-decoration: none;
- }
- }
}
}
}
@@ -147,10 +133,6 @@ $comparison-empty-state-height: 62px;
padding: 0;
background: transparent;
}
-
- .ci-status-link {
- margin-right: 5px;
- }
}
.merge-request-select {
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index b00e1813696..847cd3f2ff4 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -258,15 +258,15 @@ $tabs-holder-z-index: 250;
position: sticky;
top: calc(#{$calc-application-header-height} + #{$mr-tabs-height} + #{$diff-file-header-top});
+ // height calc is fully delegated to the tree_list_height.vue component
+ height: 0;
min-height: 300px;
- height: calc(#{$calc-application-viewport-height} - (#{$mr-tabs-height} + #{$diff-file-header-top}));
.drag-handle {
bottom: 16px;
}
&.is-sidebar-moved {
- height: calc(#{$calc-application-viewport-height} - (#{$mr-sticky-header-height} + #{$diff-file-header-top}));
top: calc(#{$calc-application-header-height} + #{$mr-sticky-header-height} + #{$diff-file-header-top});
}
}
@@ -379,6 +379,10 @@ $tabs-holder-z-index: 250;
.deployment-info {
margin-bottom: $gl-padding-8;
}
+
+ .gl-button {
+ margin-left: 0;
+ }
}
> *:not(:last-child) {
@@ -645,6 +649,9 @@ $tabs-holder-z-index: 250;
// to the end of the line or to force it to a
// new line if there is not enough space.
flex-grow: 999;
+ // Avoid layout shift of title when Mini Graph
+ // moves below title
+ padding-top: 5px;
}
.label-branch {
@@ -981,7 +988,7 @@ $tabs-holder-z-index: 250;
.merge-request-tabs-container {
&.is-merge-request {
@include gl-mx-auto;
- max-width: $fixed-layout-width - ($gl-padding * 2);
+ max-width: $fixed-layout-width - ($container-margin-xl * 2);
}
}
}
@@ -994,24 +1001,13 @@ $tabs-holder-z-index: 250;
}
}
-.submit-review-dropdown {
- &.show .dropdown-menu {
- width: calc(100vw - 20px);
- max-width: 680px;
- max-height: calc(100vh - 50px);
-
- .gl-dropdown-inner {
- max-height: none !important;
- }
- }
-
- .gl-dropdown-contents {
- padding: $gl-spacing-scale-4 !important;
- }
+.submit-review-dropdown .gl-new-dropdown-panel {
+ max-width: none;
+}
- .md-preview-holder {
- max-height: 182px;
- }
+.submit-review-dropdown-form {
+ width: calc(100vw - 20px);
+ max-width: 680px;
}
.submit-review-dropdown-animated {
@@ -1112,7 +1108,7 @@ $tabs-holder-z-index: 250;
display: flex;
align-items: center;
width: 100%;
- height: $toggle-sidebar-height;
+ height: var(--mr-review-bar-height);
padding-left: $contextual-sidebar-width;
padding-right: $right-sidebar-collapsed-width;
background: var(--white, $white);
@@ -1128,14 +1124,14 @@ $tabs-holder-z-index: 250;
padding-right: 0;
}
- .dropdown {
+ .submit-review-dropdown {
margin-left: $grid-size;
}
}
.review-bar-content {
max-width: $limited-layout-width;
- padding: 0 $gl-padding;
+ padding: 0 $container-margin;
width: 100%;
margin: 0 auto;
}
@@ -1198,63 +1194,6 @@ $tabs-holder-z-index: 250;
}
}
-.mr-system-note-icon {
- width: 20px;
- height: 20px;
- margin-left: 6px;
-
- &.gl-bg-green-100 {
- --bg-color: var(--green-100, #{$green-100});
- }
-
- &.gl-bg-red-100 {
- --bg-color: var(--red-100, #{$red-100});
- }
-
- &.gl-bg-blue-100 {
- --bg-color: var(--blue-100, #{$blue-100});
- }
-}
-
-.mr-system-note-icon:not(.mr-system-note-empty)::before {
- content: '';
- display: block;
- position: absolute;
- left: calc(50% - 1px);
- bottom: 100%;
- width: 2px;
- height: 20px;
- background: linear-gradient(to bottom, transparent, var(--bg-color));
-
- .system-note:first-child & {
- display: none;
- }
-}
-
-.mr-system-note-icon:not(.mr-system-note-empty)::after {
- content: '';
- display: block;
- position: absolute;
- left: calc(50% - 1px);
- top: 100%;
- width: 2px;
- height: 20px;
- background: linear-gradient(to bottom, var(--bg-color), transparent);
-
- .system-note:last-child & {
- display: none;
- }
-}
-
-.mr-system-note-empty {
- width: 8px;
- height: 8px;
- margin-top: 6px;
- margin-left: 12px;
- margin-right: 8px;
- border: 2px solid var(--gray-50, $gray-50);
-}
-
.diff-file-discussions-wrapper {
@include gl-w-full;
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index 98e9e2b3c27..aaec277cf08 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -125,21 +125,27 @@
// They are here to still access a variable or because they use magic values.
// scoped to the graph. Do not add other styles.
.gl-pipeline-min-h {
- min-height: $dropdown-max-height-lg;
+ min-height: calc(#{$dropdown-max-height-lg} + #{$gl-spacing-scale-6});
}
.gl-pipeline-job-width {
width: 100%;
- max-width: 400px;
}
.gl-pipeline-job-width\! {
width: 100% !important;
- max-width: 400px !important;
}
.gl-downstream-pipeline-job-width {
width: 8rem;
+
+ .pipeline-graph-container & {
+ width: 100%;
+
+ @media (min-width: $breakpoint-sm) {
+ width: 8rem;
+ }
+ }
}
.gl-linked-pipeline-padding {
@@ -154,8 +160,8 @@
// Action Icons in big pipeline-graph nodes
&.ci-action-icon-wrapper {
- height: 30px;
- width: 30px;
+ height: 24px;
+ width: 24px;
border-radius: 100%;
display: block;
padding: 0;
@@ -163,6 +169,10 @@
}
}
+.stage-column-title .gl-ci-action-icon-container {
+ right: 11px;
+}
+
.split-report-section {
border-bottom: 1px solid var(--gray-50, $gray-50);
@@ -242,3 +252,69 @@
}
}
}
+
+.pipeline-graph-container {
+ .stage-column.is-stage-view:not(:last-of-type)::after {
+ content: "";
+ position: absolute;
+ top: 100%;
+ left: $gl-spacing-scale-6;
+ width: 2px;
+ height: $gl-spacing-scale-5 * 2;
+ background-color: $gray-200;
+
+ @media (min-width: $breakpoint-sm) {
+ top: 1.25rem;
+ left: 100%;
+ width: $gl-spacing-scale-5 * 2;
+ height: 2px;
+ }
+ }
+
+ .stage-column,
+ .stage-column.is-stage-view {
+ min-width: 1px;
+
+ @media (min-width: $breakpoint-sm) {
+ min-width: inherit;
+ max-width: $gl-spacing-scale-48;
+
+ &:first-of-type {
+ margin-left: $gl-spacing-scale-6;
+ }
+ }
+ }
+
+ .linked-pipeline-container[aria-expanded=true] {
+ @media (max-width: $breakpoint-sm) {
+ width: 100%;
+
+ > div {
+ border-bottom-left-radius: 0;
+ }
+
+ > div > button {
+ border-bottom-right-radius: 0 !important;
+ }
+ }
+ }
+
+ .linked-pipelines-column,
+ .pipeline-show-container,
+ .pipeline-links-container {
+ @media (max-width: $breakpoint-sm) {
+ width: 100%;
+ }
+ }
+
+ .pipeline-graph {
+ @media (max-width: $breakpoint-sm) {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+ }
+
+ .pipeline-graph .pipeline-graph {
+ background-color: $gray-100;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/pipelines.scss b/app/assets/stylesheets/page_bundles/pipelines.scss
index f9c49b0e6ca..bcc0ad112ac 100644
--- a/app/assets/stylesheets/page_bundles/pipelines.scss
+++ b/app/assets/stylesheets/page_bundles/pipelines.scss
@@ -14,10 +14,6 @@
// - app/assets/javascripts/commit/pipelines/pipelines_bundle.js
.pipelines {
- .badge {
- margin-bottom: 3px;
- }
-
.pipeline-actions {
min-width: 170px; //Guarantees buttons don't break in several lines.
diff --git a/app/assets/stylesheets/page_bundles/profile.scss b/app/assets/stylesheets/page_bundles/profile.scss
index dbe82f583d1..2c08db048fd 100644
--- a/app/assets/stylesheets/page_bundles/profile.scss
+++ b/app/assets/stylesheets/page_bundles/profile.scss
@@ -235,13 +235,17 @@
}
.twitter-icon {
- color: $twitter;
+ color: var(--gl-text-color, $gl-text-color);
}
.discord-icon {
color: $discord;
}
+.mastodon-icon {
+ color: $mastodon;
+}
+
.key-created-at {
line-height: 42px;
}
diff --git a/app/assets/stylesheets/page_bundles/projects.scss b/app/assets/stylesheets/page_bundles/projects.scss
index 99c84026762..d252afd0b29 100644
--- a/app/assets/stylesheets/page_bundles/projects.scss
+++ b/app/assets/stylesheets/page_bundles/projects.scss
@@ -320,10 +320,6 @@
}
}
- .ci-status-link {
- @include gl-text-decoration-none;
- }
-
&:not(.compact) {
.controls {
@include media-breakpoint-up(lg) {
@@ -369,10 +365,6 @@
}
}
}
-
- .ci-status-link {
- @include gl-display-inline-flex;
- }
}
.icon-container {
diff --git a/app/assets/stylesheets/page_bundles/wiki.scss b/app/assets/stylesheets/page_bundles/wiki.scss
index 4fb07328493..81e6b4c1191 100644
--- a/app/assets/stylesheets/page_bundles/wiki.scss
+++ b/app/assets/stylesheets/page_bundles/wiki.scss
@@ -148,7 +148,8 @@
margin: 0;
}
- ul.wiki-pages ul {
+ ul.wiki-pages ul,
+ ul.wiki-pages li:not(.wiki-directory){
padding-left: 20px;
}
@@ -161,6 +162,16 @@
}
}
+.right-sidebar.wiki-sidebar {
+ .active > .wiki-list {
+ a,
+ .wiki-list-expand-button,
+ .wiki-list-collapse-button {
+ color: $white;
+ }
+ }
+}
+
ul.wiki-pages-list.content-list {
a {
color: var(--blue-600, $blue-600);
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index 01c6fde80da..ec73f27ed09 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -1,7 +1,8 @@
@import 'mixins_and_variables_and_functions';
+@import 'system_note_styles';
$work-item-field-inset-shadow: inset 0 0 0 $gl-border-size-1 var(--gray-200, $gray-200) !important;
-$work-item-overview-right-sidebar-width: 340px;
+$work-item-overview-right-sidebar-width: 23rem;
$work-item-sticky-header-height: 52px;
.gl-token-selector-token-container {
@@ -67,6 +68,7 @@ $work-item-sticky-header-height: 52px;
}
}
+//TODO: remove all the styles related to `gl-dropdown` when all `.work-item-dropdown`s are migrated
.work-item-dropdown {
// duplicate classname because we are fighting with gl-button styles
.gl-dropdown-toggle.gl-dropdown-toggle {
@@ -95,24 +97,25 @@ $work-item-sticky-header-height: 52px;
// need to override the listbox styles to match with dropdown
// till the dropdown are converted to listbox
- .gl-new-dropdown-toggle {
+ .gl-new-dropdown-toggle.gl-new-dropdown-toggle {
&:hover,
&:focus {
- background: none !important;
box-shadow: $work-item-field-inset-shadow;
background-color: $input-bg;
- }
- .is-not-focused {
- &.gl-new-dropdown-button-text {
- margin: 0 0.25rem;
+ .gl-dark & {
+ // $input-bg is overridden in dark mode but that does not
+ // work in page bundles currently, manually override here
+ background-color: var(--gray-50, $input-bg);
}
}
- }
- .gl-new-dropdown-toggle.is-not-focused {
- .gl-new-dropdown-button-text {
- margin: 0 0.25rem;
+ &:not(:hover, :focus) {
+ box-shadow: none;
+
+ .gl-new-dropdown-chevron {
+ visibility: hidden;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 8b093e7bb7b..72ea586979f 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -131,7 +131,7 @@
}
.committer {
- color: $gl-text-color-tertiary;
+ color: $gl-text-color-secondary;
.commit-author-link {
color: $gl-text-color;
@@ -144,7 +144,6 @@
vertical-align: text-bottom;
}
- > .ci-status-link,
> .btn,
> .commit-sha-group {
margin-left: $gl-padding;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 36efe42aed1..e82a689fe5d 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -81,17 +81,6 @@ ul.related-merge-requests > li gl-emoji {
}
}
-.related-merge-requests {
- .ci-status-link {
- display: block;
- margin-right: 5px;
- }
-
- svg {
- display: block;
- }
-}
-
@include media-breakpoint-down(xs) {
.detail-page-header {
.issuable-meta {
@@ -262,6 +251,14 @@ ul.related-merge-requests > li gl-emoji {
}
}
+.issue-sticky-header-text {
+ padding: 0 $container-margin;
+
+ @include media-breakpoint-up(xl) {
+ padding: 0 $container-margin-xl;
+ }
+}
+
.issuable-header-slide-enter-active,
.issuable-header-slide-leave-active {
@include gl-transition-medium;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 2722893d04c..8e0fab04ab2 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -10,16 +10,13 @@ $icon-size-diff: $avatar-icon-size - $system-note-icon-size;
$system-note-icon-m-top: $avatar-m-top + $icon-size-diff - 1.3rem;
$system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
-@mixin vertical-line($left) {
- &::before {
- content: '';
- border-left: 2px solid var(--gray-50, $gray-50);
- position: absolute;
- top: 16px;
- bottom: 0;
- left: calc(#{$left} - 1px);
- height: calc(100% + 20px);
- }
+@mixin vertical-line($top, $left) {
+ content: '';
+ position: absolute;
+ width: 2px;
+ left: $left;
+ top: $top;
+ height: calc(100% - #{$top});
}
@mixin outline-comment() {
@@ -32,12 +29,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
.limited-width-notes {
.main-notes-list::before,
.timeline-entry:last-child::before {
- content: '';
- position: absolute;
- width: 2px;
- left: 15px;
- top: 15px;
- height: calc(100% - 15px);
+ @include vertical-line(15px, 15px);
}
.main-notes-list::before {
@@ -1143,6 +1135,24 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
}
}
+.user-activity-content {
+ &::before {
+ @include vertical-line(80px, 25px);
+ background: var(--gray-50, $gray-50);
+ }
+
+ .system-note-image {
+ @include gl--flex-center;
+ top: 14px;
+ width: 22px;
+ height: 22px;
+
+ svg {
+ fill: $gray-600 !important;
+ }
+ }
+}
+
//This needs to be deleted when Snippet/Commit comments are convered to Vue
// See https://gitlab.com/gitlab-org/gitlab-foss/issues/53918#note_117038785
.unstyled-comments {
diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index c3662c3e6ea..3015cfec34f 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -67,7 +67,6 @@ nav.navbar-collapse.collapse,
.nav,
.btn,
ul.notes-form,
-.ci-status-link::after,
.issuable-gutter-toggle,
.gutter-toggle,
.issuable-details .content-block-small,
diff --git a/app/assets/stylesheets/startup/_cloaking.scss b/app/assets/stylesheets/startup/_cloaking.scss
deleted file mode 100644
index f60d72a51fb..00000000000
--- a/app/assets/stylesheets/startup/_cloaking.scss
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- Prevent flashing of content when using startup.css
- */
-@mixin cloak-startup-scss($display) {
- // General selector for cloaking until ready
- .cloak-startup,
- // Breadcrumbs and alerts on the top of the page
- .content-wrapper > .alert-wrapper,
- // Content on pages
- #content-body,
- // Prevent flashing of haml generated modal contents
- .modal-dialog {
- display: $display;
- }
-}
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
deleted file mode 100644
index 60cbcffd506..00000000000
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ /dev/null
@@ -1,1928 +0,0 @@
-// DO NOT EDIT! This is auto-generated from "yarn run generate:startup_css"
-// Please see the feedback issue for more details and help:
-// https://gitlab.com/gitlab-org/gitlab/-/issues/331812
-@charset "UTF-8";
-:root {
- --white: #333238;
-}
-*,
-*::before,
-*::after {
- box-sizing: border-box;
-}
-html {
- font-family: sans-serif;
- line-height: 1.15;
-}
-aside,
-header {
- display: block;
-}
-body {
- margin: 0;
- font-family: var(--default-regular-font, "GitLab Sans"), -apple-system,
- BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell,
- "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
- "Segoe UI Symbol", "Noto Color Emoji";
- font-size: 1rem;
- font-weight: 400;
- line-height: 1.5;
- color: #ececef;
- text-align: left;
- background-color: #1f1e24;
-}
-ul {
- margin-top: 0;
- margin-bottom: 1rem;
-}
-ul ul {
- margin-bottom: 0;
-}
-strong {
- font-weight: bolder;
-}
-a {
- color: #428fdc;
- text-decoration: none;
- background-color: transparent;
-}
-a:not([href]):not([class]) {
- color: inherit;
- text-decoration: none;
-}
-kbd {
- font-family: var(--default-mono-font, "GitLab Mono"), "JetBrains Mono",
- "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono",
- "Courier New", "andale mono", "lucida console", monospace;
- font-size: 1em;
-}
-img {
- vertical-align: middle;
- border-style: none;
-}
-svg {
- overflow: hidden;
- vertical-align: middle;
-}
-button {
- border-radius: 0;
-}
-input,
-button {
- margin: 0;
- font-family: inherit;
- font-size: inherit;
- line-height: inherit;
-}
-button,
-input {
- overflow: visible;
-}
-button {
- text-transform: none;
-}
-[role="button"] {
- cursor: pointer;
-}
-button:not(:disabled),
-[type="button"]:not(:disabled) {
- cursor: pointer;
-}
-button::-moz-focus-inner,
-[type="button"]::-moz-focus-inner {
- padding: 0;
- border-style: none;
-}
-[type="search"] {
- outline-offset: -2px;
-}
-.list-unstyled {
- padding-left: 0;
- list-style: none;
-}
-kbd {
- padding: 0.2rem 0.4rem;
- font-size: 90%;
- color: #333238;
- background-color: #ececef;
- border-radius: 0.2rem;
-}
-kbd kbd {
- padding: 0;
- font-size: 100%;
- font-weight: 600;
-}
-.container-fluid {
- width: 100%;
- padding-right: 15px;
- padding-left: 15px;
- margin-right: auto;
- margin-left: auto;
-}
-.form-control {
- display: block;
- width: 100%;
- height: 32px;
- padding: 0.375rem 0.75rem;
- font-size: 0.875rem;
- font-weight: 400;
- line-height: 1.5;
- color: #ececef;
- background-color: #333238;
- background-clip: padding-box;
- border: 1px solid #737278;
- border-radius: 0.25rem;
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.form-control::placeholder {
- color: #a4a3a8;
- opacity: 1;
-}
-.form-control:disabled {
- background-color: #24232a;
- opacity: 1;
-}
-.btn {
- display: inline-block;
- font-weight: 400;
- color: #ececef;
- text-align: center;
- vertical-align: middle;
- user-select: none;
- background-color: transparent;
- border: 1px solid transparent;
- padding: 0.375rem 0.75rem;
- font-size: 1rem;
- line-height: 20px;
- border-radius: 0.25rem;
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.btn:disabled {
- opacity: 0.65;
-}
-.btn:not(:disabled):not(.disabled) {
- cursor: pointer;
-}
-.collapse:not(.show) {
- display: none;
-}
-.dropdown {
- position: relative;
-}
-.dropdown-menu {
- position: absolute;
- top: 100%;
- left: 0;
- z-index: 1000;
- display: none;
- float: left;
- min-width: 10rem;
- padding: 0.5rem 0;
- margin: 0.125rem 0 0;
- font-size: 1rem;
- color: #ececef;
- text-align: left;
- list-style: none;
- background-color: #333238;
- background-clip: padding-box;
- border: 1px solid rgba(255, 255, 255, 0.15);
- border-radius: 0.25rem;
-}
-.nav {
- display: flex;
- flex-wrap: wrap;
- padding-left: 0;
- margin-bottom: 0;
- list-style: none;
-}
-.navbar {
- position: relative;
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
- padding: 0.25rem 0.5rem;
-}
-.navbar .container-fluid {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
-}
-.navbar-nav {
- display: flex;
- flex-direction: column;
- padding-left: 0;
- margin-bottom: 0;
- list-style: none;
-}
-.navbar-nav .dropdown-menu {
- position: static;
- float: none;
-}
-.navbar-collapse {
- flex-basis: 100%;
- flex-grow: 1;
- align-items: center;
-}
-.navbar-toggler {
- padding: 0.25rem 0.75rem;
- font-size: 1.25rem;
- line-height: 1;
- background-color: transparent;
- border: 1px solid transparent;
- border-radius: 0.25rem;
-}
-@media (max-width: 575.98px) {
- .navbar-expand-sm > .container-fluid {
- padding-right: 0;
- padding-left: 0;
- }
-}
-@media (min-width: 576px) {
- .navbar-expand-sm {
- flex-flow: row nowrap;
- justify-content: flex-start;
- }
- .navbar-expand-sm .navbar-nav {
- flex-direction: row;
- }
- .navbar-expand-sm .navbar-nav .dropdown-menu {
- position: absolute;
- }
- .navbar-expand-sm > .container-fluid {
- flex-wrap: nowrap;
- }
- .navbar-expand-sm .navbar-collapse {
- display: flex !important;
- flex-basis: auto;
- }
- .navbar-expand-sm .navbar-toggler {
- display: none;
- }
-}
-.badge {
- display: inline-block;
- padding: 0.25em 0.4em;
- font-size: 75%;
- font-weight: 600;
- line-height: 1;
- text-align: center;
- white-space: nowrap;
- vertical-align: baseline;
- border-radius: 0.25rem;
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.badge:empty {
- display: none;
-}
-.btn .badge {
- position: relative;
- top: -1px;
-}
-.badge-pill {
- padding-right: 0.6em;
- padding-left: 0.6em;
- border-radius: 10rem;
-}
-.badge-success {
- color: #fbfafd;
- background-color: #2da160;
-}
-.badge-info {
- color: #fbfafd;
- background-color: #428fdc;
-}
-.badge-warning {
- color: #fbfafd;
- background-color: #c17d10;
-}
-.rounded-circle {
- border-radius: 50% !important;
-}
-.d-none {
- display: none !important;
-}
-.d-block {
- display: block !important;
-}
-@media (min-width: 576px) {
- .d-sm-none {
- display: none !important;
- }
- .d-sm-inline-block {
- display: inline-block !important;
- }
-}
-@media (min-width: 768px) {
- .d-md-block {
- display: block !important;
- }
-}
-@media (min-width: 992px) {
- .d-lg-none {
- display: none !important;
- }
-}
-.sr-only {
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- margin: -1px;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- white-space: nowrap;
- border: 0;
-}
-.gl-avatar {
- display: inline-flex;
- border-width: 1px;
- border-style: solid;
- border-color: rgba(251, 250, 253, 0.08);
- overflow: hidden;
- flex-shrink: 0;
-}
-.gl-avatar-s24 {
- width: 1.5rem;
- height: 1.5rem;
- font-size: 0.75rem;
- line-height: 1rem;
- border-radius: 0.25rem;
-}
-.gl-avatar-circle {
- border-radius: 50%;
-}
-.gl-badge {
- display: inline-flex;
- align-items: center;
- font-size: 0.75rem;
- font-weight: 400;
- line-height: 1rem;
- padding-top: 0.25rem;
- padding-bottom: 0.25rem;
- padding-left: 0.5rem;
- padding-right: 0.5rem;
-}
-.gl-badge.sm {
- padding-top: 0;
- padding-bottom: 0;
-}
-.gl-badge.badge-info {
- background-color: #064787;
- color: #9dc7f1;
-}
-a.gl-badge.badge-info.active,
-a.gl-badge.badge-info:active {
- color: #e9f3fc;
- background-color: #0b5cad;
-}
-a.gl-badge.badge-info:active {
- box-shadow: 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
- outline: none;
-}
-.gl-badge.badge-success {
- background-color: #0d532a;
- color: #91d4a8;
-}
-a.gl-badge.badge-success.active,
-a.gl-badge.badge-success:active {
- color: #ecf4ee;
- background-color: #24663b;
-}
-a.gl-badge.badge-success:active {
- box-shadow: 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
- outline: none;
-}
-.gl-badge.badge-warning {
- background-color: #703800;
- color: #e9be74;
-}
-a.gl-badge.badge-warning.active,
-a.gl-badge.badge-warning:active {
- color: #fdf1dd;
- background-color: #8f4700;
-}
-a.gl-badge.badge-warning:active {
- box-shadow: 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
- outline: none;
-}
-.gl-button .gl-badge {
- top: 0;
-}
-.gl-form-input,
-.gl-form-input.form-control {
- background-color: #333238;
- font-family: var(--default-regular-font, "GitLab Sans"), -apple-system,
- BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell,
- "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
- "Segoe UI Symbol", "Noto Color Emoji";
- font-size: 0.875rem;
- line-height: 1rem;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- height: auto;
- color: #ececef;
- box-shadow: inset 0 0 0 1px #737278;
- border-style: none;
- appearance: none;
- -moz-appearance: none;
-}
-.gl-form-input:disabled,
-.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: #1f1e24;
- box-shadow: inset 0 0 0 1px #434248;
-}
-.gl-form-input:disabled,
-.gl-form-input.form-control:disabled {
- cursor: not-allowed;
- color: #89888d;
-}
-.gl-form-input::placeholder,
-.gl-form-input.form-control::placeholder {
- color: #737278;
-}
-.gl-icon {
- fill: currentColor;
-}
-.gl-icon.s12 {
- width: 12px;
- height: 12px;
-}
-.gl-icon.s16 {
- width: 16px;
- height: 16px;
-}
-.gl-icon.s32 {
- width: 32px;
- height: 32px;
-}
-.gl-link {
- font-size: 0.875rem;
- color: #428fdc;
-}
-.gl-link:active {
- color: #9dc7f1;
-}
-.gl-link:active {
- text-decoration: underline;
- outline: 2px solid #1f75cb;
- outline-offset: 2px;
-}
-.gl-button {
- display: inline-flex;
-}
-.gl-button:not(.btn-link):active {
- text-decoration: none;
-}
-.gl-button.gl-button {
- border-width: 0;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- background-color: transparent;
- line-height: 1rem;
- color: #ececef;
- fill: currentColor;
- box-shadow: inset 0 0 0 1px #535158;
- justify-content: center;
- align-items: center;
- font-size: 0.875rem;
- border-radius: 0.25rem;
-}
-.gl-button.gl-button .gl-button-text {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- padding-top: 1px;
- padding-bottom: 1px;
- margin-top: -1px;
- margin-bottom: -1px;
-}
-.gl-button.gl-button.btn-default {
- background-color: #333238;
-}
-.gl-button.gl-button.btn-default:active,
-.gl-button.gl-button.btn-default.active {
- box-shadow: inset 0 0 0 1px #a4a3a8, 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
- outline: none;
- background-color: #434248;
-}
-.gl-button.gl-button.btn-default:active .gl-icon,
-.gl-button.gl-button.btn-default.active .gl-icon {
- color: #ececef;
-}
-.gl-button.gl-button.btn-default .gl-icon {
- color: #89888d;
-}
-.gl-search-box-by-type-search-icon {
- color: #89888d;
- width: 1rem;
- position: absolute;
- left: 0.5rem;
- top: calc(50% - 16px / 2);
-}
-.gl-search-box-by-type {
- display: flex;
- position: relative;
-}
-.gl-search-box-by-type-input,
-.gl-search-box-by-type-input.gl-form-input {
- height: 2rem;
- padding-right: 2rem;
- padding-left: 1.75rem;
-}
-body {
- font-size: 0.875rem;
-}
-button,
-html [type="button"],
-[role="button"] {
- cursor: pointer;
-}
-strong {
- font-weight: bold;
-}
-svg {
- vertical-align: baseline;
-}
-.form-control {
- font-size: 0.875rem;
-}
-.hidden {
- display: none !important;
- visibility: hidden !important;
-}
-.badge:not(.gl-badge) {
- padding: 4px 5px;
- font-size: 12px;
- font-style: normal;
- font-weight: 400;
- display: inline-block;
-}
-.divider {
- height: 0;
- margin: 4px 0;
- overflow: hidden;
- border-top: 1px solid #434248;
-}
-.toggle-sidebar-button .collapse-text,
-.toggle-sidebar-button .icon-chevron-double-lg-left {
- color: #bfbfc3;
-}
-html {
- overflow-y: scroll;
-}
-.layout-page {
- padding-top: calc(
- var(--header-height, 48px) +
- calc(var(--system-header-height) + var(--performance-bar-height))
- );
- padding-bottom: var(--system-footer-height);
-}
-@media (min-width: 576px) {
- .logged-out-marketing-header {
- --header-height: 72px;
- }
-}
-.btn {
- border-radius: 4px;
- font-size: 0.875rem;
- font-weight: 400;
- padding: 6px 10px;
- background-color: #333238;
- border-color: #434248;
- color: #ececef;
- color: #ececef;
- white-space: nowrap;
-}
-.btn:active {
- background-color: #333238;
- box-shadow: none;
-}
-.btn:active,
-.btn.active {
- background-color: #434248;
- border-color: #4f4f4f;
- color: #ececef;
-}
-.btn svg {
- height: 15px;
- width: 15px;
-}
-.btn svg:not(:last-child) {
- margin-right: 5px;
-}
-.badge.badge-pill:not(.gl-badge) {
- font-weight: 400;
- background-color: rgba(255, 255, 255, 0.07);
- color: #bfbfc3;
- vertical-align: baseline;
-}
-:root {
- --performance-bar-height: 0px;
- --system-header-height: 0px;
- --top-bar-height: 0px;
- --system-footer-height: 0px;
- --mr-review-bar-height: 0px;
- --breakpoint-xs: 0;
- --breakpoint-sm: 576px;
- --breakpoint-md: 768px;
- --breakpoint-lg: 992px;
- --breakpoint-xl: 1200px;
-}
-.with-top-bar {
- --top-bar-height: 48px;
-}
-@media (min-width: 768px) {
- .page-with-contextual-sidebar {
- --application-bar-left: 56px;
- }
-}
-@media (min-width: 1200px) {
- .page-with-contextual-sidebar {
- --application-bar-left: 256px;
- }
- .page-with-icon-sidebar {
- --application-bar-left: 56px;
- }
- .page-with-super-sidebar {
- --application-bar-left: 256px;
- }
- .page-with-super-sidebar-collapsed {
- --application-bar-left: 0px;
- }
-}
-.gl-font-sm {
- font-size: 12px;
-}
-.dropdown {
- position: relative;
-}
-.dropdown-menu {
- display: none;
- position: absolute;
- width: auto;
- top: 100%;
- z-index: 300;
- min-width: 240px;
- max-width: 500px;
- margin-top: 4px;
- margin-bottom: 24px;
- font-size: 0.875rem;
- font-weight: 400;
- padding: 8px 0;
- background-color: #333238;
- border: 1px solid #434248;
- border-radius: 0.25rem;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-}
-.dropdown-menu ul {
- margin: 0;
- padding: 0;
-}
-.dropdown-menu li {
- display: block;
- text-align: left;
- list-style: none;
-}
-.dropdown-menu li > a,
-.dropdown-menu li > button {
- background: transparent;
- border: 0;
- border-radius: 0;
- box-shadow: none;
- display: block;
- font-weight: 400;
- position: relative;
- padding: 8px 12px;
- color: #ececef;
- line-height: 16px;
- white-space: normal;
- overflow: hidden;
- text-align: left;
- width: 100%;
-}
-.dropdown-menu li > a:active,
-.dropdown-menu li > button:active {
- background-color: #4e4c53;
- color: #ececef;
- 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 #333238,
- inset 0 0 0 1px #333238;
- outline: none;
-}
-.dropdown-menu .divider {
- height: 1px;
- margin: 0.25rem 0;
- padding: 0;
- background-color: #434248;
-}
-.dropdown-menu .badge.badge-pill + span:not(.badge):not(.badge-pill) {
- margin-right: 40px;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab li.dropdown {
- position: static;
- }
- .navbar-gitlab li.dropdown.user-counter {
- margin-left: 8px !important;
- }
- .navbar-gitlab li.dropdown.user-counter > a {
- padding: 0 4px !important;
- }
- header.navbar-gitlab .dropdown .dropdown-menu {
- width: 100%;
- min-width: 100%;
- }
-}
-input {
- border-radius: 0.25rem;
- color: #ececef;
- background-color: #333238;
-}
-input[type="search"] {
- appearance: textfield;
-}
-.form-control {
- border-radius: 4px;
- padding: 6px 10px;
-}
-.form-control::placeholder {
- color: #737278;
-}
-kbd {
- display: inline-block;
- padding: 3px 5px;
- font-size: 0.75rem;
- line-height: 10px;
- color: var(--gray-700, #bfbfc3);
- vertical-align: unset;
- background-color: var(--gray-10, #1f1e24);
- border-width: 1px;
- border-style: solid;
- border-color: var(--gray-100, #434248) var(--gray-100, #434248)
- var(--gray-200, #535158);
- border-image: none;
- border-radius: 3px;
- box-shadow: 0 -1px 0 var(--gray-200, #535158) inset;
-}
-.navbar-gitlab {
- padding: 0 16px;
- z-index: 1000;
- margin-bottom: 0;
- min-height: var(--header-height, 48px);
- border: 0;
- position: fixed;
- top: calc(var(--system-header-height) + var(--performance-bar-height));
- left: 0;
- right: 0;
- border-radius: 0;
-}
-.navbar-gitlab .close-icon {
- display: none;
-}
-.navbar-gitlab .header-content {
- width: 100%;
- display: flex;
- justify-content: space-between;
- position: relative;
- min-height: var(--header-height, 48px);
- padding-left: 0;
-}
-.navbar-gitlab .header-content .title {
- padding-right: 0;
- color: currentColor;
- display: flex;
- position: relative;
- margin: 0;
- font-size: 18px;
- vertical-align: top;
- white-space: nowrap;
-}
-.navbar-gitlab .header-content .title img {
- height: 24px;
-}
-.navbar-gitlab .header-content .title a:not(.canary-badge) {
- display: flex;
- align-items: center;
- padding: 2px 8px;
- margin: 4px 2px 4px -8px;
- border-radius: 4px;
-}
-.navbar-gitlab .header-content .title a:not(.canary-badge):active {
- box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6), 0 0 0 3px #1068bf;
- outline: none;
-}
-.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
- margin: 0 2px;
-}
-.navbar-gitlab .header-search-form {
- min-width: 320px;
-}
-@media (min-width: 768px) and (max-width: 1199.98px) {
- .navbar-gitlab .header-search-form {
- min-width: 200px;
- }
-}
-.navbar-gitlab .header-search-form .keyboard-shortcut-helper {
- transform: translateY(calc(50% - 2px));
- box-shadow: none;
- border-color: transparent;
-}
-.navbar-gitlab .navbar-collapse {
- flex: 0 0 auto;
- border-top: 0;
- padding: 0;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .navbar-collapse {
- flex: 1 1 auto;
- }
-}
-.navbar-gitlab .navbar-collapse .nav {
- flex-wrap: nowrap;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .navbar-collapse .nav > li:not(.d-none) a {
- margin-left: 0;
- }
-}
-.navbar-gitlab .container-fluid {
- padding: 0;
-}
-.navbar-gitlab .container-fluid .user-counter svg {
- margin-right: 3px;
-}
-.navbar-gitlab .container-fluid .navbar-toggler {
- position: relative;
- right: -10px;
- border-radius: 0;
- min-width: 45px;
- padding: 0;
- margin: 8px 8px 8px 0;
- font-size: 14px;
- text-align: center;
- color: currentColor;
-}
-.navbar-gitlab .container-fluid .navbar-toggler.active {
- color: currentColor;
- background-color: transparent;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .container-fluid .navbar-nav {
- display: flex;
- padding-right: 10px;
- flex-direction: row;
- }
-}
-.navbar-gitlab
- .container-fluid
- .navbar-nav
- li
- .badge.badge-pill:not(.gl-badge) {
- box-shadow: none;
- font-weight: 600;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .container-fluid .nav > li.header-user {
- padding-left: 10px;
- }
-}
-.navbar-gitlab .container-fluid .nav > li > a {
- will-change: color;
- margin: 4px 0;
- padding: 6px 8px;
- height: 32px;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .container-fluid .nav > li > a {
- padding: 0;
- }
-}
-.navbar-gitlab .container-fluid .nav > li > a.header-user-dropdown-toggle {
- margin-left: 2px;
-}
-.navbar-gitlab
- .container-fluid
- .nav
- > li
- > a.header-user-dropdown-toggle
- .header-user-avatar {
- margin-right: 0;
-}
-.navbar-gitlab .container-fluid .nav > li .header-new-dropdown-toggle {
- margin-right: 0;
-}
-.navbar-sub-nav > li > a,
-.navbar-sub-nav > li > button,
-.navbar-nav > li > a,
-.navbar-nav > li > button {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 6px 8px;
- margin: 4px 2px;
- font-size: 12px;
- color: currentColor;
- border-radius: 4px;
- height: 32px;
- font-weight: 600;
-}
-.navbar-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,
-.navbar-nav > li > button {
- background: transparent;
- border: 0;
-}
-.navbar-sub-nav .dropdown-menu,
-.navbar-nav .dropdown-menu {
- position: absolute;
-}
-.navbar-sub-nav {
- display: flex;
- align-items: center;
- height: 100%;
- margin: 0 0 0 6px;
-}
-.caret-down,
-.btn .caret-down {
- top: 0;
- height: 11px;
- width: 11px;
- margin-left: 4px;
- fill: currentColor;
-}
-.header-user .dropdown-menu,
-.header-new .dropdown-menu {
- margin-top: 4px;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .container-fluid {
- font-size: 18px;
- }
- .navbar-gitlab .container-fluid .navbar-nav {
- table-layout: fixed;
- width: 100%;
- margin: 0;
- text-align: right;
- }
- .navbar-gitlab .container-fluid .navbar-collapse {
- margin-left: -8px;
- margin-right: -10px;
- }
- .navbar-gitlab .container-fluid .navbar-collapse .nav > li:not(.d-none) {
- flex: 1;
- }
- .header-user-dropdown-toggle {
- text-align: center;
- }
- .header-user-avatar {
- float: none;
- }
-}
-.header-user-avatar {
- float: left;
- margin-right: 5px;
- border-radius: 50%;
- border: 1px solid #333238;
-}
-.notification-dot {
- background-color: #9e5400;
- height: 12px;
- width: 12px;
- pointer-events: none;
- visibility: hidden;
- top: 3px;
-}
-.tanuki-logo .tanuki {
- fill: #e24329;
-}
-.tanuki-logo .left-cheek,
-.tanuki-logo .right-cheek {
- fill: #fc6d26;
-}
-.tanuki-logo .chin {
- fill: #fca326;
-}
-.context-header {
- position: relative;
- margin-right: 2px;
- width: 256px;
-}
-.context-header > a,
-.context-header > button {
- font-weight: 600;
- display: flex;
- width: 100%;
- align-items: center;
- padding: 10px 16px 10px 10px;
- color: #ececef;
- background-color: transparent;
- border: 0;
- text-align: left;
-}
-.context-header .avatar-container {
- flex: 0 0 32px;
- background-color: #333238;
-}
-.context-header .sidebar-context-title {
- overflow: hidden;
- text-overflow: ellipsis;
- color: #ececef;
-}
-@media (min-width: 768px) {
- .page-with-contextual-sidebar {
- padding-left: 56px;
- }
-}
-@media (min-width: 1200px) {
- .page-with-contextual-sidebar {
- padding-left: 256px;
- }
-}
-@media (min-width: 768px) {
- .page-with-icon-sidebar {
- padding-left: 56px;
- }
-}
-.nav-sidebar {
- position: fixed;
- bottom: var(--system-footer-height);
- left: 0;
- z-index: 600;
- width: 256px;
- top: calc(
- var(--header-height, 48px) +
- calc(var(--system-header-height) + var(--performance-bar-height)) +
- var(--top-bar-height)
- );
- background-color: #1f1e24;
- border-right: 1px solid #e9e9e9;
- transform: translate3d(0, 0, 0);
-}
-.nav-sidebar.sidebar-collapsed-desktop {
- width: 56px;
-}
-.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll {
- overflow-x: hidden;
-}
-.nav-sidebar.sidebar-collapsed-desktop .badge.badge-pill:not(.fly-out-badge),
-.nav-sidebar.sidebar-collapsed-desktop .nav-item-name,
-.nav-sidebar.sidebar-collapsed-desktop .collapse-text {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
-}
-.nav-sidebar.sidebar-collapsed-desktop .sidebar-top-level-items > li > a {
- min-height: unset;
-}
-.nav-sidebar.sidebar-collapsed-desktop .fly-out-top-item:not(.divider) {
- display: block !important;
-}
-.nav-sidebar.sidebar-collapsed-desktop .avatar-container {
- margin: 0 auto;
-}
-.nav-sidebar.sidebar-collapsed-desktop li.active:not(.fly-out-top-item) > a {
- background-color: rgba(41, 41, 97, 0.08);
-}
-.nav-sidebar a {
- text-decoration: none;
- color: #ececef;
-}
-.nav-sidebar li {
- white-space: nowrap;
-}
-.nav-sidebar li .nav-item-name {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-.nav-sidebar li > a,
-.nav-sidebar li > .fly-out-top-item-container {
- height: 2rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- display: flex;
- align-items: center;
- border-radius: 0.25rem;
- width: auto;
- margin: 1px 8px;
-}
-.nav-sidebar li.active > a {
- font-weight: 600;
-}
-.nav-sidebar li.active:not(.fly-out-top-item) > a:not(.has-sub-items) {
- background-color: rgba(251, 250, 253, 0.08);
-}
-.nav-sidebar ul {
- padding-left: 0;
- list-style: none;
-}
-@media (max-width: 767.98px) {
- .nav-sidebar {
- left: -256px;
- }
-}
-.nav-sidebar .nav-icon-container {
- display: flex;
- margin-right: 8px;
-}
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item {
- display: none;
-}
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- a,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item.active
- a,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- .fly-out-top-item-container {
- margin-left: 0;
- margin-right: 0;
- padding-left: 1rem;
- padding-right: 1rem;
- cursor: default;
- pointer-events: none;
- font-size: 0.75rem;
- margin-top: -0.25rem;
- margin-bottom: -0.25rem;
- margin-top: 0;
- position: relative;
- color: #333238;
- background: var(--black, #fff);
-}
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- a
- strong,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item.active
- a
- strong,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- .fly-out-top-item-container
- strong {
- font-weight: 400;
-}
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- a::before,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item.active
- a::before,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- .fly-out-top-item-container::before {
- position: absolute;
- content: "";
- display: block;
- top: 50%;
- left: -0.25rem;
- margin-top: -0.25rem;
- width: 0;
- height: 0;
- border-top: 0.25rem solid transparent;
- border-bottom: 0.25rem solid transparent;
- border-right: 0.25rem solid #fff;
- border-right-color: var(--black, #fff);
-}
-@media (min-width: 576px) {
- .nav-sidebar a.has-sub-items + .sidebar-sub-level-items {
- min-width: 150px;
- }
-}
-.nav-sidebar a.has-sub-items + .sidebar-sub-level-items .fly-out-top-item {
- display: none;
-}
-.nav-sidebar a.has-sub-items + .sidebar-sub-level-items .fly-out-top-item a,
-.nav-sidebar
- a.has-sub-items
- + .sidebar-sub-level-items
- .fly-out-top-item.active
- a,
-.nav-sidebar
- a.has-sub-items
- + .sidebar-sub-level-items
- .fly-out-top-item
- .fly-out-top-item-container {
- margin-left: 0;
- margin-right: 0;
- padding-left: 1rem;
- padding-right: 1rem;
- cursor: default;
- pointer-events: none;
- font-size: 0.75rem;
- margin-top: 0;
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
-}
-@media (min-width: 768px) and (max-width: 1199px) {
- .nav-sidebar:not(.sidebar-expanded-mobile) {
- width: 56px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll {
- overflow-x: hidden;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- .badge.badge-pill:not(.fly-out-badge),
- .nav-sidebar:not(.sidebar-expanded-mobile) .nav-item-name,
- .nav-sidebar:not(.sidebar-expanded-mobile) .collapse-text {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items > li > a {
- min-height: unset;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .fly-out-top-item:not(.divider) {
- display: block !important;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .avatar-container {
- margin: 0 auto;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- li.active:not(.fly-out-top-item)
- > a {
- background-color: rgba(41, 41, 97, 0.08);
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
- height: 60px;
- width: 56px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
- padding: 10px 4px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-context-title {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
- height: auto;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
- padding: 0.25rem;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- .sidebar-top-level-items
- > li
- .sidebar-sub-level-items:not(.flyout-list) {
- display: none;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .nav-icon-container {
- margin-right: 0;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button {
- width: 55px;
- padding: 0 21px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- .toggle-sidebar-button
- .collapse-text {
- display: none;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- .toggle-sidebar-button
- .icon-chevron-double-lg-left {
- transform: rotate(180deg);
- margin: 0;
- }
-}
-.nav-sidebar-inner-scroll {
- height: 100%;
- width: 100%;
- overflow-x: hidden;
- overflow-y: auto;
-}
-.nav-sidebar-inner-scroll > div.context-header {
- margin-top: 0.25rem;
-}
-.nav-sidebar-inner-scroll > div.context-header a {
- height: 2rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- display: flex;
- align-items: center;
- border-radius: 0.25rem;
- width: auto;
- margin: 1px 8px;
- padding: 0.25rem;
- margin-bottom: 0.25rem;
- margin-top: 0.125rem;
- height: auto;
-}
-.nav-sidebar-inner-scroll > div.context-header a .avatar-container {
- font-weight: 400;
- flex: none;
-}
-.sidebar-top-level-items {
- margin-bottom: 60px;
-}
-.sidebar-top-level-items .context-header a {
- padding: 0.25rem;
- margin-bottom: 0.25rem;
- margin-top: 0.125rem;
- height: auto;
-}
-.sidebar-top-level-items .context-header a .avatar-container {
- font-weight: 400;
- flex: none;
-}
-.sidebar-top-level-items
- > li.active
- .sidebar-sub-level-items:not(.is-fly-out-only) {
- display: block;
-}
-.sidebar-top-level-items li > a.gl-link {
- color: #ececef;
-}
-.sidebar-top-level-items li > a.gl-link:active {
- text-decoration: none;
-}
-.sidebar-sub-level-items {
- padding-top: 0;
- padding-bottom: 0;
- display: none;
-}
-.sidebar-sub-level-items:not(.fly-out-list) li > a {
- padding-left: 2.25rem;
-}
-.toggle-sidebar-button,
-.close-nav-button {
- height: 48px;
- padding: 0 16px;
- background-color: #24232a;
- border: 0;
- color: #bfbfc3;
- display: flex;
- align-items: center;
- background-color: #1f1e24;
- position: fixed;
- bottom: 0;
- width: 255px;
-}
-.toggle-sidebar-button .collapse-text,
-.toggle-sidebar-button .icon-chevron-double-lg-left,
-.close-nav-button .collapse-text,
-.close-nav-button .icon-chevron-double-lg-left {
- color: inherit;
-}
-.collapse-text {
- white-space: nowrap;
- overflow: hidden;
-}
-.sidebar-collapsed-desktop .context-header {
- height: 60px;
- width: 56px;
-}
-.sidebar-collapsed-desktop .context-header a {
- padding: 10px 4px;
-}
-.sidebar-collapsed-desktop .sidebar-context-title {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
-}
-.sidebar-collapsed-desktop .context-header {
- height: auto;
-}
-.sidebar-collapsed-desktop .context-header a {
- padding: 0.25rem;
-}
-.sidebar-collapsed-desktop
- .sidebar-top-level-items
- > li
- .sidebar-sub-level-items:not(.flyout-list) {
- display: none;
-}
-.sidebar-collapsed-desktop .nav-icon-container {
- margin-right: 0;
-}
-.sidebar-collapsed-desktop .toggle-sidebar-button {
- width: 55px;
- padding: 0 21px;
-}
-.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text {
- display: none;
-}
-.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-left {
- transform: rotate(180deg);
- margin: 0;
-}
-.close-nav-button {
- display: none;
-}
-@media (max-width: 767.98px) {
- .close-nav-button {
- display: flex;
- }
- .toggle-sidebar-button {
- display: none;
- }
-}
-.super-sidebar {
- display: flex;
- flex-direction: column;
- position: fixed;
- top: calc(
- var(--header-height, 48px) +
- calc(var(--system-header-height) + var(--performance-bar-height))
- );
- bottom: var(--system-footer-height);
- left: 0;
- background-color: var(--gray-10, #1f1e24);
- border-right: 1px solid rgba(251, 250, 253, 0.08);
- transform: translate3d(0, 0, 0);
- width: 256px;
- z-index: 600;
-}
-.super-sidebar.super-sidebar-loading {
- transform: translate3d(-100%, 0, 0);
-}
-@media (min-width: 1200px) {
- .super-sidebar.super-sidebar-loading {
- transform: translate3d(0, 0, 0);
- }
-}
-@media (prefers-reduced-motion: no-preference) {
-}
-.page-with-super-sidebar {
- padding-left: 0;
-}
-@media (prefers-reduced-motion: no-preference) {
-}
-@media (min-width: 1200px) {
- .page-with-super-sidebar {
- padding-left: 256px;
- }
-}
-.page-with-super-sidebar-collapsed .super-sidebar {
- transform: translate3d(-100%, 0, 0);
-}
-@media (min-width: 1200px) {
- .page-with-super-sidebar-collapsed {
- padding-left: 0;
- }
-}
-input::-moz-placeholder {
- color: #737278;
- opacity: 1;
-}
-input::-ms-input-placeholder {
- color: #737278;
-}
-input:-ms-input-placeholder {
- color: #737278;
-}
-svg {
- fill: currentColor;
-}
-svg.s12 {
- width: 12px;
- height: 12px;
-}
-svg.s16 {
- width: 16px;
- height: 16px;
-}
-svg.s32 {
- width: 32px;
- height: 32px;
-}
-svg.s12 {
- vertical-align: -1px;
-}
-svg.s16 {
- vertical-align: -3px;
-}
-.avatar,
-.avatar-container {
- float: left;
- margin-right: 16px;
- border-radius: 50%;
-}
-.avatar.s16,
-.avatar-container.s16 {
- width: 16px;
- height: 16px;
- margin-right: 8px;
-}
-.avatar.s32,
-.avatar-container.s32 {
- width: 32px;
- height: 32px;
- margin-right: 8px;
-}
-.avatar {
- transition-property: none;
- width: 40px;
- height: 40px;
- padding: 0;
- background: #212027;
- overflow: hidden;
- box-shadow: inset 0 0 0 1px rgba(251, 250, 253, 0.1);
-}
-.avatar.avatar-tile {
- border-radius: 0;
- border: 0;
-}
-.identicon {
- text-align: center;
- vertical-align: top;
- color: #ececef;
- background-color: #333238;
-}
-.identicon.s16 {
- font-size: 10px;
- line-height: 16px;
-}
-.identicon.s32 {
- font-size: 14px;
- line-height: 32px;
-}
-.identicon.bg1 {
- background-color: #660e00;
-}
-.identicon.bg2 {
- background-color: #232150;
-}
-.identicon.bg3 {
- background-color: #1a1a40;
-}
-.identicon.bg4 {
- background-color: #033464;
-}
-.identicon.bg5 {
- background-color: #0a4020;
-}
-.identicon.bg6 {
- background-color: #5c2900;
-}
-.identicon.bg7 {
- background-color: #333238;
-}
-.avatar-container {
- overflow: hidden;
- display: flex;
-}
-.avatar-container a {
- width: 100%;
- height: 100%;
- display: flex;
- text-decoration: none;
-}
-.avatar-container .avatar {
- border-radius: 0;
- border: 0;
- height: auto;
- width: 100%;
- margin: 0;
- align-self: center;
-}
-.rect-avatar {
- border-radius: 2px;
-}
-.rect-avatar.s16 {
- border-radius: 2px;
-}
-.rect-avatar.s16 .avatar {
- border-radius: 2px;
-}
-.rect-avatar.s32 {
- border-radius: 4px;
-}
-.rect-avatar.s32 .avatar {
- border-radius: 4px;
-}
-:root {
- color-scheme: dark;
- --gray-10: #1f1e24;
- --gray-50: #333238;
- --gray-100: #434248;
- --gray-200: #535158;
- --gray-700: #bfbfc3;
- --gray-900: #ececef;
- --border-color: #434248;
- --white: #333238;
- --black: #fff;
-}
-body.gl-dark {
- color-scheme: dark;
- --gray-10: #1f1e24;
- --border-color: #434248;
- --white: #333238;
- --black: #fff;
-}
-.nav-sidebar,
-.toggle-sidebar-button,
-.close-nav-button {
- background-color: #29282d;
- border-right: 1px solid #333238;
-}
-.gl-avatar:not(.gl-avatar-identicon),
-.avatar-container,
-.avatar {
- background: rgba(251, 250, 253, 0.04);
-}
-.gl-avatar {
- border-style: none;
- box-shadow: inset 0 0 0 1px rgba(251, 250, 253, 0.1);
-}
-body.gl-dark {
- --gl-theme-accent: #737278;
-}
-body.gl-dark .navbar-gitlab {
- background-color: #ececef;
-}
-body.gl-dark .navbar-gitlab .navbar-collapse {
- color: #ececef;
-}
-body.gl-dark .navbar-gitlab .container-fluid .navbar-toggler {
- border-left: 1px solid #a3a2a6;
- color: #ececef;
-}
-body.gl-dark .navbar-gitlab .navbar-sub-nav > li.active > a,
-body.gl-dark .navbar-gitlab .navbar-sub-nav > li.active > button,
-body.gl-dark .navbar-gitlab .navbar-nav > li.active > a,
-body.gl-dark .navbar-gitlab .navbar-nav > li.active > button {
- color: #ececef;
- background-color: #333238;
-}
-body.gl-dark .navbar-gitlab .navbar-sub-nav {
- color: #ececef;
-}
-body.gl-dark .navbar-gitlab .nav > li {
- color: #ececef;
-}
-body.gl-dark .navbar-gitlab .nav > li.header-search {
- color: #ececef;
-}
-body.gl-dark .navbar-gitlab .nav > li > a .notification-dot {
- border: 2px solid #ececef;
-}
-body.gl-dark
- .navbar-gitlab
- .nav
- > li
- > a.header-help-dropdown-toggle
- .notification-dot {
- background-color: #ececef;
-}
-body.gl-dark
- .navbar-gitlab
- .nav
- > li
- > a.header-user-dropdown-toggle
- .header-user-avatar {
- border-color: #ececef;
-}
-body.gl-dark .navbar-gitlab .nav > li.active > a {
- color: #ececef;
- background-color: #333238;
-}
-body.gl-dark .navbar-gitlab .nav > li.active > a .notification-dot {
- border-color: #333238;
-}
-body.gl-dark
- .navbar-gitlab
- .nav
- > li.active
- > a.header-help-dropdown-toggle
- .notification-dot {
- background-color: #ececef;
-}
-body.gl-dark .header-search-form {
- background-color: rgba(236, 236, 239, 0.2) !important;
- border-radius: 4px;
-}
-body.gl-dark .header-search-form svg.gl-search-box-by-type-search-icon {
- color: rgba(236, 236, 239, 0.8);
-}
-body.gl-dark .header-search-form input {
- background-color: transparent;
- color: rgba(236, 236, 239, 0.8);
- box-shadow: inset 0 0 0 1px rgba(236, 236, 239, 0.4);
-}
-body.gl-dark .header-search-form input::placeholder {
- color: rgba(236, 236, 239, 0.8);
-}
-body.gl-dark .header-search-form input:active::placeholder {
- color: #737278;
-}
-body.gl-dark .header-search-form .keyboard-shortcut-helper {
- color: #ececef;
- background-color: rgba(236, 236, 239, 0.2);
-}
-body.gl-dark .nav-sidebar li.active > a {
- color: #ececef;
-}
-body.gl-dark .nav-sidebar .fly-out-top-item a,
-body.gl-dark .nav-sidebar .fly-out-top-item.active a,
-body.gl-dark .nav-sidebar .fly-out-top-item .fly-out-top-item-container {
- background-color: var(--gray-100, #333238);
- color: var(--gray-900, #ececef);
-}
-body.gl-dark .navbar-gitlab {
- background-color: var(--gray-50);
- box-shadow: 0 1px 0 0 var(--gray-100);
-}
-body.gl-dark .navbar-gitlab .navbar-sub-nav li.active > a,
-body.gl-dark .navbar-gitlab .navbar-sub-nav li.active > button,
-body.gl-dark .navbar-gitlab .navbar-nav li.active > a,
-body.gl-dark .navbar-gitlab .navbar-nav li.active > button {
- color: var(--gl-text-color);
- background-color: var(--gray-200);
-}
-body.gl-dark .navbar-gitlab .header-search-form {
- background-color: var(--gray-100) !important;
- box-shadow: inset 0 0 0 1px var(--border-color) !important;
-}
-body.gl-dark .navbar-gitlab .header-search-form:active {
- background-color: var(--gray-100) !important;
- box-shadow: inset 0 0 0 1px var(--blue-200) !important;
-}
-
-.tab-width-8 {
- tab-size: 8;
-}
-.gl-sr-only {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
-}
-.gl-border-none\! {
- border-style: none !important;
-}
-.gl-display-none {
- display: none;
-}
-.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;
- }
-}
-@media (min-width: 992px) {
- .gl-lg-display-block {
- display: block;
- }
-}
-.gl-align-items-center {
- align-items: center;
-}
-.gl-align-items-stretch {
- align-items: stretch;
-}
-.gl-flex-grow-0\! {
- flex-grow: 0 !important;
-}
-.gl-flex-grow-1 {
- flex-grow: 1;
-}
-.gl-flex-basis-half\! {
- flex-basis: 50% !important;
-}
-.gl-justify-content-end {
- justify-content: flex-end;
-}
-.gl-relative {
- position: relative;
-}
-.gl-absolute {
- position: absolute;
-}
-.gl-top-0 {
- top: 0;
-}
-.gl-right-3 {
- right: 0.5rem;
-}
-.gl-w-full {
- width: 100%;
-}
-.gl-px-3 {
- padding-left: 0.5rem;
- padding-right: 0.5rem;
-}
-.gl-pr-2 {
- padding-right: 0.25rem;
-}
-.gl-pt-0 {
- padding-top: 0;
-}
-.gl-mr-auto {
- margin-right: auto;
-}
-.gl-mr-3 {
- margin-right: 0.5rem;
-}
-.gl-ml-n2 {
- margin-left: -0.25rem;
-}
-.gl-ml-3 {
- margin-left: 0.5rem;
-}
-.gl-mx-0\! {
- margin-left: 0 !important;
- margin-right: 0 !important;
-}
-.gl-text-right {
- text-align: right;
-}
-.gl-white-space-nowrap {
- white-space: nowrap;
-}
-.gl-font-sm {
- font-size: 0.75rem;
-}
-.gl-font-weight-bold {
- font-weight: 600;
-}
-.gl-z-index-1 {
- z-index: 1;
-}
-
-@import "startup/cloaking";
-@include cloak-startup-scss(none);
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
deleted file mode 100644
index 04c44dd9603..00000000000
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ /dev/null
@@ -1,1781 +0,0 @@
-// DO NOT EDIT! This is auto-generated from "yarn run generate:startup_css"
-// Please see the feedback issue for more details and help:
-// https://gitlab.com/gitlab-org/gitlab/-/issues/331812
-@charset "UTF-8";
-:root {
- --white: #fff;
-}
-*,
-*::before,
-*::after {
- box-sizing: border-box;
-}
-html {
- font-family: sans-serif;
- line-height: 1.15;
-}
-aside,
-header {
- display: block;
-}
-body {
- margin: 0;
- font-family: var(--default-regular-font, "GitLab Sans"), -apple-system,
- BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell,
- "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
- "Segoe UI Symbol", "Noto Color Emoji";
- font-size: 1rem;
- font-weight: 400;
- line-height: 1.5;
- color: #333238;
- text-align: left;
- background-color: #fff;
-}
-ul {
- margin-top: 0;
- margin-bottom: 1rem;
-}
-ul ul {
- margin-bottom: 0;
-}
-strong {
- font-weight: bolder;
-}
-a {
- color: #1f75cb;
- text-decoration: none;
- background-color: transparent;
-}
-a:not([href]):not([class]) {
- color: inherit;
- text-decoration: none;
-}
-kbd {
- font-family: var(--default-mono-font, "GitLab Mono"), "JetBrains Mono",
- "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono",
- "Courier New", "andale mono", "lucida console", monospace;
- font-size: 1em;
-}
-img {
- vertical-align: middle;
- border-style: none;
-}
-svg {
- overflow: hidden;
- vertical-align: middle;
-}
-button {
- border-radius: 0;
-}
-input,
-button {
- margin: 0;
- font-family: inherit;
- font-size: inherit;
- line-height: inherit;
-}
-button,
-input {
- overflow: visible;
-}
-button {
- text-transform: none;
-}
-[role="button"] {
- cursor: pointer;
-}
-button:not(:disabled),
-[type="button"]:not(:disabled) {
- cursor: pointer;
-}
-button::-moz-focus-inner,
-[type="button"]::-moz-focus-inner {
- padding: 0;
- border-style: none;
-}
-[type="search"] {
- outline-offset: -2px;
-}
-.list-unstyled {
- padding-left: 0;
- list-style: none;
-}
-kbd {
- padding: 0.2rem 0.4rem;
- font-size: 90%;
- color: #fff;
- background-color: #333238;
- border-radius: 0.2rem;
-}
-kbd kbd {
- padding: 0;
- font-size: 100%;
- font-weight: 600;
-}
-.container-fluid {
- width: 100%;
- padding-right: 15px;
- padding-left: 15px;
- margin-right: auto;
- margin-left: auto;
-}
-.form-control {
- display: block;
- width: 100%;
- height: 32px;
- padding: 0.375rem 0.75rem;
- font-size: 0.875rem;
- font-weight: 400;
- line-height: 1.5;
- color: #333238;
- background-color: #fff;
- background-clip: padding-box;
- border: 1px solid #89888d;
- border-radius: 0.25rem;
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.form-control::placeholder {
- color: #626168;
- opacity: 1;
-}
-.form-control:disabled {
- background-color: #fbfafd;
- opacity: 1;
-}
-.btn {
- display: inline-block;
- font-weight: 400;
- color: #333238;
- text-align: center;
- vertical-align: middle;
- user-select: none;
- background-color: transparent;
- border: 1px solid transparent;
- padding: 0.375rem 0.75rem;
- font-size: 1rem;
- line-height: 20px;
- border-radius: 0.25rem;
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.btn:disabled {
- opacity: 0.65;
-}
-.btn:not(:disabled):not(.disabled) {
- cursor: pointer;
-}
-.collapse:not(.show) {
- display: none;
-}
-.dropdown {
- position: relative;
-}
-.dropdown-menu {
- position: absolute;
- top: 100%;
- left: 0;
- z-index: 1000;
- display: none;
- float: left;
- min-width: 10rem;
- padding: 0.5rem 0;
- margin: 0.125rem 0 0;
- font-size: 1rem;
- color: #333238;
- text-align: left;
- list-style: none;
- background-color: #fff;
- background-clip: padding-box;
- border: 1px solid rgba(0, 0, 0, 0.15);
- border-radius: 0.25rem;
-}
-.nav {
- display: flex;
- flex-wrap: wrap;
- padding-left: 0;
- margin-bottom: 0;
- list-style: none;
-}
-.navbar {
- position: relative;
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
- padding: 0.25rem 0.5rem;
-}
-.navbar .container-fluid {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
-}
-.navbar-nav {
- display: flex;
- flex-direction: column;
- padding-left: 0;
- margin-bottom: 0;
- list-style: none;
-}
-.navbar-nav .dropdown-menu {
- position: static;
- float: none;
-}
-.navbar-collapse {
- flex-basis: 100%;
- flex-grow: 1;
- align-items: center;
-}
-.navbar-toggler {
- padding: 0.25rem 0.75rem;
- font-size: 1.25rem;
- line-height: 1;
- background-color: transparent;
- border: 1px solid transparent;
- border-radius: 0.25rem;
-}
-@media (max-width: 575.98px) {
- .navbar-expand-sm > .container-fluid {
- padding-right: 0;
- padding-left: 0;
- }
-}
-@media (min-width: 576px) {
- .navbar-expand-sm {
- flex-flow: row nowrap;
- justify-content: flex-start;
- }
- .navbar-expand-sm .navbar-nav {
- flex-direction: row;
- }
- .navbar-expand-sm .navbar-nav .dropdown-menu {
- position: absolute;
- }
- .navbar-expand-sm > .container-fluid {
- flex-wrap: nowrap;
- }
- .navbar-expand-sm .navbar-collapse {
- display: flex !important;
- flex-basis: auto;
- }
- .navbar-expand-sm .navbar-toggler {
- display: none;
- }
-}
-.badge {
- display: inline-block;
- padding: 0.25em 0.4em;
- font-size: 75%;
- font-weight: 600;
- line-height: 1;
- text-align: center;
- white-space: nowrap;
- vertical-align: baseline;
- border-radius: 0.25rem;
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.badge:empty {
- display: none;
-}
-.btn .badge {
- position: relative;
- top: -1px;
-}
-.badge-pill {
- padding-right: 0.6em;
- padding-left: 0.6em;
- border-radius: 10rem;
-}
-.badge-success {
- color: #fff;
- background-color: #108548;
-}
-.badge-info {
- color: #fff;
- background-color: #1f75cb;
-}
-.badge-warning {
- color: #fff;
- background-color: #ab6100;
-}
-.rounded-circle {
- border-radius: 50% !important;
-}
-.d-none {
- display: none !important;
-}
-.d-block {
- display: block !important;
-}
-@media (min-width: 576px) {
- .d-sm-none {
- display: none !important;
- }
- .d-sm-inline-block {
- display: inline-block !important;
- }
-}
-@media (min-width: 768px) {
- .d-md-block {
- display: block !important;
- }
-}
-@media (min-width: 992px) {
- .d-lg-none {
- display: none !important;
- }
-}
-.sr-only {
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- margin: -1px;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- white-space: nowrap;
- border: 0;
-}
-.gl-avatar {
- display: inline-flex;
- border-width: 1px;
- border-style: solid;
- border-color: rgba(31, 30, 36, 0.08);
- overflow: hidden;
- flex-shrink: 0;
-}
-.gl-avatar-s24 {
- width: 1.5rem;
- height: 1.5rem;
- font-size: 0.75rem;
- line-height: 1rem;
- border-radius: 0.25rem;
-}
-.gl-avatar-circle {
- border-radius: 50%;
-}
-.gl-badge {
- display: inline-flex;
- align-items: center;
- font-size: 0.75rem;
- font-weight: 400;
- line-height: 1rem;
- padding-top: 0.25rem;
- padding-bottom: 0.25rem;
- padding-left: 0.5rem;
- padding-right: 0.5rem;
-}
-.gl-badge.sm {
- padding-top: 0;
- padding-bottom: 0;
-}
-.gl-badge.badge-info {
- background-color: #cbe2f9;
- color: #0b5cad;
-}
-a.gl-badge.badge-info.active,
-a.gl-badge.badge-info:active {
- color: #033464;
- background-color: #9dc7f1;
-}
-a.gl-badge.badge-info:active {
- box-shadow: 0 0 0 1px #fff, 0 0 0 3px #428fdc;
- outline: none;
-}
-.gl-badge.badge-success {
- background-color: #c3e6cd;
- color: #24663b;
-}
-a.gl-badge.badge-success.active,
-a.gl-badge.badge-success:active {
- color: #0a4020;
- background-color: #91d4a8;
-}
-a.gl-badge.badge-success:active {
- box-shadow: 0 0 0 1px #fff, 0 0 0 3px #428fdc;
- outline: none;
-}
-.gl-badge.badge-warning {
- background-color: #f5d9a8;
- color: #8f4700;
-}
-a.gl-badge.badge-warning.active,
-a.gl-badge.badge-warning:active {
- color: #5c2900;
- background-color: #e9be74;
-}
-a.gl-badge.badge-warning:active {
- box-shadow: 0 0 0 1px #fff, 0 0 0 3px #428fdc;
- outline: none;
-}
-.gl-button .gl-badge {
- top: 0;
-}
-.gl-form-input,
-.gl-form-input.form-control {
- background-color: #fff;
- font-family: var(--default-regular-font, "GitLab Sans"), -apple-system,
- BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell,
- "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
- "Segoe UI Symbol", "Noto Color Emoji";
- font-size: 0.875rem;
- line-height: 1rem;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- height: auto;
- color: #333238;
- box-shadow: inset 0 0 0 1px #89888d;
- border-style: none;
- appearance: none;
- -moz-appearance: none;
-}
-.gl-form-input:disabled,
-.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: #fbfafd;
- box-shadow: inset 0 0 0 1px #dcdcde;
-}
-.gl-form-input:disabled,
-.gl-form-input.form-control:disabled {
- cursor: not-allowed;
- color: #737278;
-}
-.gl-form-input::placeholder,
-.gl-form-input.form-control::placeholder {
- color: #89888d;
-}
-.gl-icon {
- fill: currentColor;
-}
-.gl-icon.s12 {
- width: 12px;
- height: 12px;
-}
-.gl-icon.s16 {
- width: 16px;
- height: 16px;
-}
-.gl-icon.s32 {
- width: 32px;
- height: 32px;
-}
-.gl-link {
- font-size: 0.875rem;
- color: #1f75cb;
-}
-.gl-link:active {
- color: #0b5cad;
-}
-.gl-link:active {
- text-decoration: underline;
- outline: 2px solid #428fdc;
- outline-offset: 2px;
-}
-.gl-button {
- display: inline-flex;
-}
-.gl-button:not(.btn-link):active {
- text-decoration: none;
-}
-.gl-button.gl-button {
- border-width: 0;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- background-color: transparent;
- line-height: 1rem;
- color: #333238;
- fill: currentColor;
- box-shadow: inset 0 0 0 1px #bfbfc3;
- justify-content: center;
- align-items: center;
- font-size: 0.875rem;
- border-radius: 0.25rem;
-}
-.gl-button.gl-button .gl-button-text {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- padding-top: 1px;
- padding-bottom: 1px;
- margin-top: -1px;
- margin-bottom: -1px;
-}
-.gl-button.gl-button.btn-default {
- background-color: #fff;
-}
-.gl-button.gl-button.btn-default:active,
-.gl-button.gl-button.btn-default.active {
- box-shadow: inset 0 0 0 1px #626168, 0 0 0 1px #fff, 0 0 0 3px #428fdc;
- outline: none;
- background-color: #dcdcde;
-}
-.gl-button.gl-button.btn-default:active .gl-icon,
-.gl-button.gl-button.btn-default.active .gl-icon {
- color: #333238;
-}
-.gl-button.gl-button.btn-default .gl-icon {
- color: #737278;
-}
-.gl-search-box-by-type-search-icon {
- color: #737278;
- width: 1rem;
- position: absolute;
- left: 0.5rem;
- top: calc(50% - 16px / 2);
-}
-.gl-search-box-by-type {
- display: flex;
- position: relative;
-}
-.gl-search-box-by-type-input,
-.gl-search-box-by-type-input.gl-form-input {
- height: 2rem;
- padding-right: 2rem;
- padding-left: 1.75rem;
-}
-body {
- font-size: 0.875rem;
-}
-button,
-html [type="button"],
-[role="button"] {
- cursor: pointer;
-}
-strong {
- font-weight: bold;
-}
-svg {
- vertical-align: baseline;
-}
-.form-control {
- font-size: 0.875rem;
-}
-.hidden {
- display: none !important;
- visibility: hidden !important;
-}
-.badge:not(.gl-badge) {
- padding: 4px 5px;
- font-size: 12px;
- font-style: normal;
- font-weight: 400;
- display: inline-block;
-}
-.divider {
- height: 0;
- margin: 4px 0;
- overflow: hidden;
- border-top: 1px solid #dcdcde;
-}
-.toggle-sidebar-button .collapse-text,
-.toggle-sidebar-button .icon-chevron-double-lg-left {
- color: #737278;
-}
-html {
- overflow-y: scroll;
-}
-.layout-page {
- padding-top: calc(
- var(--header-height, 48px) +
- calc(var(--system-header-height) + var(--performance-bar-height))
- );
- padding-bottom: var(--system-footer-height);
-}
-@media (min-width: 576px) {
- .logged-out-marketing-header {
- --header-height: 72px;
- }
-}
-.btn {
- border-radius: 4px;
- font-size: 0.875rem;
- font-weight: 400;
- padding: 6px 10px;
- background-color: #fff;
- border-color: #dcdcde;
- color: #333238;
- color: #333238;
- white-space: nowrap;
-}
-.btn:active {
- background-color: #ececef;
- box-shadow: none;
-}
-.btn:active,
-.btn.active {
- background-color: #e6e6ea;
- border-color: #dedee3;
- color: #333238;
-}
-.btn svg {
- height: 15px;
- width: 15px;
-}
-.btn svg:not(:last-child) {
- margin-right: 5px;
-}
-.badge.badge-pill:not(.gl-badge) {
- font-weight: 400;
- background-color: rgba(0, 0, 0, 0.07);
- color: #535158;
- vertical-align: baseline;
-}
-:root {
- --performance-bar-height: 0px;
- --system-header-height: 0px;
- --top-bar-height: 0px;
- --system-footer-height: 0px;
- --mr-review-bar-height: 0px;
- --breakpoint-xs: 0;
- --breakpoint-sm: 576px;
- --breakpoint-md: 768px;
- --breakpoint-lg: 992px;
- --breakpoint-xl: 1200px;
-}
-.with-top-bar {
- --top-bar-height: 48px;
-}
-@media (min-width: 768px) {
- .page-with-contextual-sidebar {
- --application-bar-left: 56px;
- }
-}
-@media (min-width: 1200px) {
- .page-with-contextual-sidebar {
- --application-bar-left: 256px;
- }
- .page-with-icon-sidebar {
- --application-bar-left: 56px;
- }
- .page-with-super-sidebar {
- --application-bar-left: 256px;
- }
- .page-with-super-sidebar-collapsed {
- --application-bar-left: 0px;
- }
-}
-.gl-font-sm {
- font-size: 12px;
-}
-.dropdown {
- position: relative;
-}
-.dropdown-menu {
- display: none;
- position: absolute;
- width: auto;
- top: 100%;
- z-index: 300;
- min-width: 240px;
- max-width: 500px;
- margin-top: 4px;
- margin-bottom: 24px;
- font-size: 0.875rem;
- font-weight: 400;
- padding: 8px 0;
- background-color: #fff;
- border: 1px solid #dcdcde;
- border-radius: 0.25rem;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-}
-.dropdown-menu ul {
- margin: 0;
- padding: 0;
-}
-.dropdown-menu li {
- display: block;
- text-align: left;
- list-style: none;
-}
-.dropdown-menu li > a,
-.dropdown-menu li > button {
- background: transparent;
- border: 0;
- border-radius: 0;
- box-shadow: none;
- display: block;
- font-weight: 400;
- position: relative;
- padding: 8px 12px;
- color: #333238;
- line-height: 16px;
- white-space: normal;
- overflow: hidden;
- text-align: left;
- width: 100%;
-}
-.dropdown-menu li > a:active,
-.dropdown-menu li > button:active {
- background-color: #ececef;
- color: #333238;
- 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;
- padding: 0;
- background-color: #dcdcde;
-}
-.dropdown-menu .badge.badge-pill + span:not(.badge):not(.badge-pill) {
- margin-right: 40px;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab li.dropdown {
- position: static;
- }
- .navbar-gitlab li.dropdown.user-counter {
- margin-left: 8px !important;
- }
- .navbar-gitlab li.dropdown.user-counter > a {
- padding: 0 4px !important;
- }
- header.navbar-gitlab .dropdown .dropdown-menu {
- width: 100%;
- min-width: 100%;
- }
-}
-input {
- border-radius: 0.25rem;
- color: #333238;
- background-color: #fff;
-}
-input[type="search"] {
- appearance: textfield;
-}
-.form-control {
- border-radius: 4px;
- padding: 6px 10px;
-}
-.form-control::placeholder {
- color: #89888d;
-}
-kbd {
- display: inline-block;
- padding: 3px 5px;
- font-size: 0.75rem;
- line-height: 10px;
- color: var(--gray-700, #535158);
- vertical-align: unset;
- background-color: var(--gray-10, #fbfafd);
- border-width: 1px;
- border-style: solid;
- border-color: var(--gray-100, #dcdcde) var(--gray-100, #dcdcde)
- var(--gray-200, #bfbfc3);
- border-image: none;
- border-radius: 3px;
- box-shadow: 0 -1px 0 var(--gray-200, #bfbfc3) inset;
-}
-.navbar-gitlab {
- padding: 0 16px;
- z-index: 1000;
- margin-bottom: 0;
- min-height: var(--header-height, 48px);
- border: 0;
- position: fixed;
- top: calc(var(--system-header-height) + var(--performance-bar-height));
- left: 0;
- right: 0;
- border-radius: 0;
-}
-.navbar-gitlab .close-icon {
- display: none;
-}
-.navbar-gitlab .header-content {
- width: 100%;
- display: flex;
- justify-content: space-between;
- position: relative;
- min-height: var(--header-height, 48px);
- padding-left: 0;
-}
-.navbar-gitlab .header-content .title {
- padding-right: 0;
- color: currentColor;
- display: flex;
- position: relative;
- margin: 0;
- font-size: 18px;
- vertical-align: top;
- white-space: nowrap;
-}
-.navbar-gitlab .header-content .title img {
- height: 24px;
-}
-.navbar-gitlab .header-content .title a:not(.canary-badge) {
- display: flex;
- align-items: center;
- padding: 2px 8px;
- margin: 4px 2px 4px -8px;
- border-radius: 4px;
-}
-.navbar-gitlab .header-content .title a:not(.canary-badge):active {
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 0 0 3px #63a6e9;
- outline: none;
-}
-.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
- margin: 0 2px;
-}
-.navbar-gitlab .header-search-form {
- min-width: 320px;
-}
-@media (min-width: 768px) and (max-width: 1199.98px) {
- .navbar-gitlab .header-search-form {
- min-width: 200px;
- }
-}
-.navbar-gitlab .header-search-form .keyboard-shortcut-helper {
- transform: translateY(calc(50% - 2px));
- box-shadow: none;
- border-color: transparent;
-}
-.navbar-gitlab .navbar-collapse {
- flex: 0 0 auto;
- border-top: 0;
- padding: 0;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .navbar-collapse {
- flex: 1 1 auto;
- }
-}
-.navbar-gitlab .navbar-collapse .nav {
- flex-wrap: nowrap;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .navbar-collapse .nav > li:not(.d-none) a {
- margin-left: 0;
- }
-}
-.navbar-gitlab .container-fluid {
- padding: 0;
-}
-.navbar-gitlab .container-fluid .user-counter svg {
- margin-right: 3px;
-}
-.navbar-gitlab .container-fluid .navbar-toggler {
- position: relative;
- right: -10px;
- border-radius: 0;
- min-width: 45px;
- padding: 0;
- margin: 8px 8px 8px 0;
- font-size: 14px;
- text-align: center;
- color: currentColor;
-}
-.navbar-gitlab .container-fluid .navbar-toggler.active {
- color: currentColor;
- background-color: transparent;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .container-fluid .navbar-nav {
- display: flex;
- padding-right: 10px;
- flex-direction: row;
- }
-}
-.navbar-gitlab
- .container-fluid
- .navbar-nav
- li
- .badge.badge-pill:not(.gl-badge) {
- box-shadow: none;
- font-weight: 600;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .container-fluid .nav > li.header-user {
- padding-left: 10px;
- }
-}
-.navbar-gitlab .container-fluid .nav > li > a {
- will-change: color;
- margin: 4px 0;
- padding: 6px 8px;
- height: 32px;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .container-fluid .nav > li > a {
- padding: 0;
- }
-}
-.navbar-gitlab .container-fluid .nav > li > a.header-user-dropdown-toggle {
- margin-left: 2px;
-}
-.navbar-gitlab
- .container-fluid
- .nav
- > li
- > a.header-user-dropdown-toggle
- .header-user-avatar {
- margin-right: 0;
-}
-.navbar-gitlab .container-fluid .nav > li .header-new-dropdown-toggle {
- margin-right: 0;
-}
-.navbar-sub-nav > li > a,
-.navbar-sub-nav > li > button,
-.navbar-nav > li > a,
-.navbar-nav > li > button {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 6px 8px;
- margin: 4px 2px;
- font-size: 12px;
- color: currentColor;
- border-radius: 4px;
- height: 32px;
- font-weight: 600;
-}
-.navbar-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,
-.navbar-nav > li > button {
- background: transparent;
- border: 0;
-}
-.navbar-sub-nav .dropdown-menu,
-.navbar-nav .dropdown-menu {
- position: absolute;
-}
-.navbar-sub-nav {
- display: flex;
- align-items: center;
- height: 100%;
- margin: 0 0 0 6px;
-}
-.caret-down,
-.btn .caret-down {
- top: 0;
- height: 11px;
- width: 11px;
- margin-left: 4px;
- fill: currentColor;
-}
-.header-user .dropdown-menu,
-.header-new .dropdown-menu {
- margin-top: 4px;
-}
-@media (max-width: 575.98px) {
- .navbar-gitlab .container-fluid {
- font-size: 18px;
- }
- .navbar-gitlab .container-fluid .navbar-nav {
- table-layout: fixed;
- width: 100%;
- margin: 0;
- text-align: right;
- }
- .navbar-gitlab .container-fluid .navbar-collapse {
- margin-left: -8px;
- margin-right: -10px;
- }
- .navbar-gitlab .container-fluid .navbar-collapse .nav > li:not(.d-none) {
- flex: 1;
- }
- .header-user-dropdown-toggle {
- text-align: center;
- }
- .header-user-avatar {
- float: none;
- }
-}
-.header-user-avatar {
- float: left;
- margin-right: 5px;
- border-radius: 50%;
- border: 1px solid #f2f2f4;
-}
-.notification-dot {
- background-color: #d99530;
- height: 12px;
- width: 12px;
- pointer-events: none;
- visibility: hidden;
- top: 3px;
-}
-.tanuki-logo .tanuki {
- fill: #e24329;
-}
-.tanuki-logo .left-cheek,
-.tanuki-logo .right-cheek {
- fill: #fc6d26;
-}
-.tanuki-logo .chin {
- fill: #fca326;
-}
-.context-header {
- position: relative;
- margin-right: 2px;
- width: 256px;
-}
-.context-header > a,
-.context-header > button {
- font-weight: 600;
- display: flex;
- width: 100%;
- align-items: center;
- padding: 10px 16px 10px 10px;
- color: #333238;
- background-color: transparent;
- border: 0;
- text-align: left;
-}
-.context-header .avatar-container {
- flex: 0 0 32px;
- background-color: #fff;
-}
-.context-header .sidebar-context-title {
- overflow: hidden;
- text-overflow: ellipsis;
- color: #333238;
-}
-@media (min-width: 768px) {
- .page-with-contextual-sidebar {
- padding-left: 56px;
- }
-}
-@media (min-width: 1200px) {
- .page-with-contextual-sidebar {
- padding-left: 256px;
- }
-}
-@media (min-width: 768px) {
- .page-with-icon-sidebar {
- padding-left: 56px;
- }
-}
-.nav-sidebar {
- position: fixed;
- bottom: var(--system-footer-height);
- left: 0;
- z-index: 600;
- width: 256px;
- top: calc(
- var(--header-height, 48px) +
- calc(var(--system-header-height) + var(--performance-bar-height)) +
- var(--top-bar-height)
- );
- background-color: #fbfafd;
- border-right: 1px solid #e9e9e9;
- transform: translate3d(0, 0, 0);
-}
-.nav-sidebar.sidebar-collapsed-desktop {
- width: 56px;
-}
-.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll {
- overflow-x: hidden;
-}
-.nav-sidebar.sidebar-collapsed-desktop .badge.badge-pill:not(.fly-out-badge),
-.nav-sidebar.sidebar-collapsed-desktop .nav-item-name,
-.nav-sidebar.sidebar-collapsed-desktop .collapse-text {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
-}
-.nav-sidebar.sidebar-collapsed-desktop .sidebar-top-level-items > li > a {
- min-height: unset;
-}
-.nav-sidebar.sidebar-collapsed-desktop .fly-out-top-item:not(.divider) {
- display: block !important;
-}
-.nav-sidebar.sidebar-collapsed-desktop .avatar-container {
- margin: 0 auto;
-}
-.nav-sidebar.sidebar-collapsed-desktop li.active:not(.fly-out-top-item) > a {
- background-color: rgba(41, 41, 97, 0.08);
-}
-.nav-sidebar a {
- text-decoration: none;
- color: #333238;
-}
-.nav-sidebar li {
- white-space: nowrap;
-}
-.nav-sidebar li .nav-item-name {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-.nav-sidebar li > a,
-.nav-sidebar li > .fly-out-top-item-container {
- height: 2rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- display: flex;
- align-items: center;
- border-radius: 0.25rem;
- width: auto;
- margin: 1px 8px;
-}
-.nav-sidebar li.active > a {
- font-weight: 600;
-}
-.nav-sidebar li.active:not(.fly-out-top-item) > a:not(.has-sub-items) {
- background-color: rgba(31, 30, 36, 0.08);
-}
-.nav-sidebar ul {
- padding-left: 0;
- list-style: none;
-}
-@media (max-width: 767.98px) {
- .nav-sidebar {
- left: -256px;
- }
-}
-.nav-sidebar .nav-icon-container {
- display: flex;
- margin-right: 8px;
-}
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item {
- display: none;
-}
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- a,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item.active
- a,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- .fly-out-top-item-container {
- margin-left: 0;
- margin-right: 0;
- padding-left: 1rem;
- padding-right: 1rem;
- cursor: default;
- pointer-events: none;
- font-size: 0.75rem;
- margin-top: -0.25rem;
- margin-bottom: -0.25rem;
- margin-top: 0;
- position: relative;
- color: #fff;
- background: var(--black, #000);
-}
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- a
- strong,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item.active
- a
- strong,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- .fly-out-top-item-container
- strong {
- font-weight: 400;
-}
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- a::before,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item.active
- a::before,
-.nav-sidebar
- a:not(.has-sub-items)
- + .sidebar-sub-level-items
- .fly-out-top-item
- .fly-out-top-item-container::before {
- position: absolute;
- content: "";
- display: block;
- top: 50%;
- left: -0.25rem;
- margin-top: -0.25rem;
- width: 0;
- height: 0;
- border-top: 0.25rem solid transparent;
- border-bottom: 0.25rem solid transparent;
- border-right: 0.25rem solid #000;
- border-right-color: var(--black, #000);
-}
-@media (min-width: 576px) {
- .nav-sidebar a.has-sub-items + .sidebar-sub-level-items {
- min-width: 150px;
- }
-}
-.nav-sidebar a.has-sub-items + .sidebar-sub-level-items .fly-out-top-item {
- display: none;
-}
-.nav-sidebar a.has-sub-items + .sidebar-sub-level-items .fly-out-top-item a,
-.nav-sidebar
- a.has-sub-items
- + .sidebar-sub-level-items
- .fly-out-top-item.active
- a,
-.nav-sidebar
- a.has-sub-items
- + .sidebar-sub-level-items
- .fly-out-top-item
- .fly-out-top-item-container {
- margin-left: 0;
- margin-right: 0;
- padding-left: 1rem;
- padding-right: 1rem;
- cursor: default;
- pointer-events: none;
- font-size: 0.75rem;
- margin-top: 0;
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
-}
-@media (min-width: 768px) and (max-width: 1199px) {
- .nav-sidebar:not(.sidebar-expanded-mobile) {
- width: 56px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll {
- overflow-x: hidden;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- .badge.badge-pill:not(.fly-out-badge),
- .nav-sidebar:not(.sidebar-expanded-mobile) .nav-item-name,
- .nav-sidebar:not(.sidebar-expanded-mobile) .collapse-text {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items > li > a {
- min-height: unset;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .fly-out-top-item:not(.divider) {
- display: block !important;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .avatar-container {
- margin: 0 auto;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- li.active:not(.fly-out-top-item)
- > a {
- background-color: rgba(41, 41, 97, 0.08);
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
- height: 60px;
- width: 56px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
- padding: 10px 4px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-context-title {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
- height: auto;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
- padding: 0.25rem;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- .sidebar-top-level-items
- > li
- .sidebar-sub-level-items:not(.flyout-list) {
- display: none;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .nav-icon-container {
- margin-right: 0;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button {
- width: 55px;
- padding: 0 21px;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- .toggle-sidebar-button
- .collapse-text {
- display: none;
- }
- .nav-sidebar:not(.sidebar-expanded-mobile)
- .toggle-sidebar-button
- .icon-chevron-double-lg-left {
- transform: rotate(180deg);
- margin: 0;
- }
-}
-.nav-sidebar-inner-scroll {
- height: 100%;
- width: 100%;
- overflow-x: hidden;
- overflow-y: auto;
-}
-.nav-sidebar-inner-scroll > div.context-header {
- margin-top: 0.25rem;
-}
-.nav-sidebar-inner-scroll > div.context-header a {
- height: 2rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- display: flex;
- align-items: center;
- border-radius: 0.25rem;
- width: auto;
- margin: 1px 8px;
- padding: 0.25rem;
- margin-bottom: 0.25rem;
- margin-top: 0.125rem;
- height: auto;
-}
-.nav-sidebar-inner-scroll > div.context-header a .avatar-container {
- font-weight: 400;
- flex: none;
-}
-.sidebar-top-level-items {
- margin-bottom: 60px;
-}
-.sidebar-top-level-items .context-header a {
- padding: 0.25rem;
- margin-bottom: 0.25rem;
- margin-top: 0.125rem;
- height: auto;
-}
-.sidebar-top-level-items .context-header a .avatar-container {
- font-weight: 400;
- flex: none;
-}
-.sidebar-top-level-items
- > li.active
- .sidebar-sub-level-items:not(.is-fly-out-only) {
- display: block;
-}
-.sidebar-top-level-items li > a.gl-link {
- color: #333238;
-}
-.sidebar-top-level-items li > a.gl-link:active {
- text-decoration: none;
-}
-.sidebar-sub-level-items {
- padding-top: 0;
- padding-bottom: 0;
- display: none;
-}
-.sidebar-sub-level-items:not(.fly-out-list) li > a {
- padding-left: 2.25rem;
-}
-.toggle-sidebar-button,
-.close-nav-button {
- height: 48px;
- padding: 0 16px;
- background-color: #fbfafd;
- border: 0;
- color: #737278;
- display: flex;
- align-items: center;
- background-color: #fbfafd;
- position: fixed;
- bottom: 0;
- width: 255px;
-}
-.toggle-sidebar-button .collapse-text,
-.toggle-sidebar-button .icon-chevron-double-lg-left,
-.close-nav-button .collapse-text,
-.close-nav-button .icon-chevron-double-lg-left {
- color: inherit;
-}
-.collapse-text {
- white-space: nowrap;
- overflow: hidden;
-}
-.sidebar-collapsed-desktop .context-header {
- height: 60px;
- width: 56px;
-}
-.sidebar-collapsed-desktop .context-header a {
- padding: 10px 4px;
-}
-.sidebar-collapsed-desktop .sidebar-context-title {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
-}
-.sidebar-collapsed-desktop .context-header {
- height: auto;
-}
-.sidebar-collapsed-desktop .context-header a {
- padding: 0.25rem;
-}
-.sidebar-collapsed-desktop
- .sidebar-top-level-items
- > li
- .sidebar-sub-level-items:not(.flyout-list) {
- display: none;
-}
-.sidebar-collapsed-desktop .nav-icon-container {
- margin-right: 0;
-}
-.sidebar-collapsed-desktop .toggle-sidebar-button {
- width: 55px;
- padding: 0 21px;
-}
-.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text {
- display: none;
-}
-.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-left {
- transform: rotate(180deg);
- margin: 0;
-}
-.close-nav-button {
- display: none;
-}
-@media (max-width: 767.98px) {
- .close-nav-button {
- display: flex;
- }
- .toggle-sidebar-button {
- display: none;
- }
-}
-.super-sidebar {
- display: flex;
- flex-direction: column;
- position: fixed;
- top: calc(
- var(--header-height, 48px) +
- calc(var(--system-header-height) + var(--performance-bar-height))
- );
- bottom: var(--system-footer-height);
- left: 0;
- background-color: var(--gray-10, #fbfafd);
- border-right: 1px solid rgba(31, 30, 36, 0.08);
- transform: translate3d(0, 0, 0);
- width: 256px;
- z-index: 600;
-}
-.super-sidebar.super-sidebar-loading {
- transform: translate3d(-100%, 0, 0);
-}
-@media (min-width: 1200px) {
- .super-sidebar.super-sidebar-loading {
- transform: translate3d(0, 0, 0);
- }
-}
-@media (prefers-reduced-motion: no-preference) {
-}
-.page-with-super-sidebar {
- padding-left: 0;
-}
-@media (prefers-reduced-motion: no-preference) {
-}
-@media (min-width: 1200px) {
- .page-with-super-sidebar {
- padding-left: 256px;
- }
-}
-.page-with-super-sidebar-collapsed .super-sidebar {
- transform: translate3d(-100%, 0, 0);
-}
-@media (min-width: 1200px) {
- .page-with-super-sidebar-collapsed {
- padding-left: 0;
- }
-}
-input::-moz-placeholder {
- color: #89888d;
- opacity: 1;
-}
-input::-ms-input-placeholder {
- color: #89888d;
-}
-input:-ms-input-placeholder {
- color: #89888d;
-}
-svg {
- fill: currentColor;
-}
-svg.s12 {
- width: 12px;
- height: 12px;
-}
-svg.s16 {
- width: 16px;
- height: 16px;
-}
-svg.s32 {
- width: 32px;
- height: 32px;
-}
-svg.s12 {
- vertical-align: -1px;
-}
-svg.s16 {
- vertical-align: -3px;
-}
-.avatar,
-.avatar-container {
- float: left;
- margin-right: 16px;
- border-radius: 50%;
-}
-.avatar.s16,
-.avatar-container.s16 {
- width: 16px;
- height: 16px;
- margin-right: 8px;
-}
-.avatar.s32,
-.avatar-container.s32 {
- width: 32px;
- height: 32px;
- margin-right: 8px;
-}
-.avatar {
- transition-property: none;
- width: 40px;
- height: 40px;
- padding: 0;
- background: #fefefe;
- overflow: hidden;
- box-shadow: inset 0 0 0 1px rgba(31, 30, 36, 0.1);
-}
-.avatar.avatar-tile {
- border-radius: 0;
- border: 0;
-}
-.identicon {
- text-align: center;
- vertical-align: top;
- color: #333238;
- background-color: #ececef;
-}
-.identicon.s16 {
- font-size: 10px;
- line-height: 16px;
-}
-.identicon.s32 {
- font-size: 14px;
- line-height: 32px;
-}
-.identicon.bg1 {
- background-color: #fcf1ef;
-}
-.identicon.bg2 {
- background-color: #f4f0ff;
-}
-.identicon.bg3 {
- background-color: #f1f1ff;
-}
-.identicon.bg4 {
- background-color: #e9f3fc;
-}
-.identicon.bg5 {
- background-color: #ecf4ee;
-}
-.identicon.bg6 {
- background-color: #fdf1dd;
-}
-.identicon.bg7 {
- background-color: #ececef;
-}
-.avatar-container {
- overflow: hidden;
- display: flex;
-}
-.avatar-container a {
- width: 100%;
- height: 100%;
- display: flex;
- text-decoration: none;
-}
-.avatar-container .avatar {
- border-radius: 0;
- border: 0;
- height: auto;
- width: 100%;
- margin: 0;
- align-self: center;
-}
-.rect-avatar {
- border-radius: 2px;
-}
-.rect-avatar.s16 {
- border-radius: 2px;
-}
-.rect-avatar.s16 .avatar {
- border-radius: 2px;
-}
-.rect-avatar.s32 {
- border-radius: 4px;
-}
-.rect-avatar.s32 .avatar {
- border-radius: 4px;
-}
-
-.tab-width-8 {
- tab-size: 8;
-}
-.gl-sr-only {
- border: 0;
- clip: rect(0, 0, 0, 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
-}
-.gl-border-none\! {
- border-style: none !important;
-}
-.gl-display-none {
- display: none;
-}
-.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;
- }
-}
-@media (min-width: 992px) {
- .gl-lg-display-block {
- display: block;
- }
-}
-.gl-align-items-center {
- align-items: center;
-}
-.gl-align-items-stretch {
- align-items: stretch;
-}
-.gl-flex-grow-0\! {
- flex-grow: 0 !important;
-}
-.gl-flex-grow-1 {
- flex-grow: 1;
-}
-.gl-flex-basis-half\! {
- flex-basis: 50% !important;
-}
-.gl-justify-content-end {
- justify-content: flex-end;
-}
-.gl-relative {
- position: relative;
-}
-.gl-absolute {
- position: absolute;
-}
-.gl-top-0 {
- top: 0;
-}
-.gl-right-3 {
- right: 0.5rem;
-}
-.gl-w-full {
- width: 100%;
-}
-.gl-px-3 {
- padding-left: 0.5rem;
- padding-right: 0.5rem;
-}
-.gl-pr-2 {
- padding-right: 0.25rem;
-}
-.gl-pt-0 {
- padding-top: 0;
-}
-.gl-mr-auto {
- margin-right: auto;
-}
-.gl-mr-3 {
- margin-right: 0.5rem;
-}
-.gl-ml-n2 {
- margin-left: -0.25rem;
-}
-.gl-ml-3 {
- margin-left: 0.5rem;
-}
-.gl-mx-0\! {
- margin-left: 0 !important;
- margin-right: 0 !important;
-}
-.gl-text-right {
- text-align: right;
-}
-.gl-white-space-nowrap {
- white-space: nowrap;
-}
-.gl-font-sm {
- font-size: 0.75rem;
-}
-.gl-font-weight-bold {
- font-weight: 600;
-}
-.gl-z-index-1 {
- z-index: 1;
-}
-
-@import "startup/cloaking";
-@include cloak-startup-scss(none);
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
deleted file mode 100644
index 32da8e1bb6b..00000000000
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ /dev/null
@@ -1,852 +0,0 @@
-// DO NOT EDIT! This is auto-generated from "yarn run generate:startup_css"
-// Please see the feedback issue for more details and help:
-// https://gitlab.com/gitlab-org/gitlab/-/issues/331812
-@charset "UTF-8";
-:root {
- --white: #fff;
-}
-*,
-*::before,
-*::after {
- box-sizing: border-box;
-}
-html {
- font-family: sans-serif;
- line-height: 1.15;
-}
-header {
- display: block;
-}
-body {
- margin: 0;
- font-family: var(--default-regular-font, "GitLab Sans"), -apple-system,
- BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell,
- "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
- "Segoe UI Symbol", "Noto Color Emoji";
- font-size: 1rem;
- font-weight: 400;
- line-height: 1.5;
- color: #333238;
- text-align: left;
- background-color: #fff;
-}
-hr {
- box-sizing: content-box;
- height: 0;
- overflow: visible;
-}
-h1,
-h3 {
- margin-top: 0;
- margin-bottom: 0.25rem;
-}
-p {
- margin-top: 0;
- margin-bottom: 1rem;
-}
-a {
- color: #1f75cb;
- text-decoration: none;
- background-color: transparent;
-}
-a:not([href]):not([class]) {
- color: inherit;
- text-decoration: none;
-}
-img {
- vertical-align: middle;
- border-style: none;
-}
-svg {
- overflow: hidden;
- vertical-align: middle;
-}
-label {
- display: inline-block;
- margin-bottom: 0.5rem;
-}
-button {
- border-radius: 0;
-}
-input,
-button {
- margin: 0;
- font-family: inherit;
- font-size: inherit;
- line-height: inherit;
-}
-button,
-input {
- overflow: visible;
-}
-button {
- text-transform: none;
-}
-button:not(:disabled),
-[type="submit"]:not(:disabled) {
- cursor: pointer;
-}
-button::-moz-focus-inner,
-[type="submit"]::-moz-focus-inner {
- padding: 0;
- border-style: none;
-}
-input[type="checkbox"] {
- box-sizing: border-box;
- padding: 0;
-}
-fieldset {
- min-width: 0;
- padding: 0;
- margin: 0;
- border: 0;
-}
-[hidden] {
- display: none !important;
-}
-h1,
-h3 {
- margin-bottom: 0.25rem;
- font-weight: 600;
- line-height: 1.2;
- color: #333238;
-}
-h1 {
- font-size: 2.1875rem;
-}
-h3 {
- font-size: 1.53125rem;
-}
-hr {
- margin-top: 0.5rem;
- margin-bottom: 0.5rem;
- border: 0;
- border-top: 1px solid rgba(0, 0, 0, 0.1);
-}
-.container {
- width: 100%;
- padding-right: 15px;
- padding-left: 15px;
- margin-right: auto;
- margin-left: auto;
-}
-@media (min-width: 576px) {
- .container {
- max-width: 540px;
- }
-}
-@media (min-width: 768px) {
- .container {
- max-width: 720px;
- }
-}
-@media (min-width: 992px) {
- .container {
- max-width: 960px;
- }
-}
-@media (min-width: 1200px) {
- .container {
- max-width: 1140px;
- }
-}
-.row {
- display: flex;
- flex-wrap: wrap;
- margin-right: -15px;
- margin-left: -15px;
-}
-.col-md-6,
-.col-sm-12 {
- position: relative;
- width: 100%;
- padding-right: 15px;
- padding-left: 15px;
-}
-.order-1 {
- order: 1;
-}
-.order-12 {
- order: 12;
-}
-@media (min-width: 576px) {
- .col-sm-12 {
- flex: 0 0 100%;
- max-width: 100%;
- }
- .order-sm-1 {
- order: 1;
- }
- .order-sm-12 {
- order: 12;
- }
-}
-@media (min-width: 768px) {
- .col-md-6 {
- flex: 0 0 50%;
- max-width: 50%;
- }
-}
-.form-control {
- display: block;
- width: 100%;
- height: 32px;
- padding: 0.375rem 0.75rem;
- font-size: 0.875rem;
- font-weight: 400;
- line-height: 1.5;
- color: #333238;
- background-color: #fff;
- background-clip: padding-box;
- border: 1px solid #89888d;
- border-radius: 0.25rem;
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.form-control::placeholder {
- color: #626168;
- opacity: 1;
-}
-.form-control:disabled {
- background-color: #fbfafd;
- opacity: 1;
-}
-.form-group {
- margin-bottom: 1rem;
-}
-.form-text {
- display: block;
- margin-top: 0.25rem;
-}
-.btn {
- display: inline-block;
- font-weight: 400;
- color: #333238;
- text-align: center;
- vertical-align: middle;
- user-select: none;
- background-color: transparent;
- border: 1px solid transparent;
- padding: 0.375rem 0.75rem;
- font-size: 1rem;
- line-height: 20px;
- border-radius: 0.25rem;
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.btn:disabled {
- opacity: 0.65;
-}
-.btn:not(:disabled):not(.disabled) {
- cursor: pointer;
-}
-fieldset:disabled a.btn {
- pointer-events: none;
-}
-.btn-block {
- display: block;
- width: 100%;
-}
-.btn-block + .btn-block {
- margin-top: 0.5rem;
-}
-input.btn-block[type="submit"] {
- width: 100%;
-}
-.custom-control {
- position: relative;
- z-index: 1;
- display: block;
- min-height: 1.5rem;
- padding-left: 1.5rem;
- print-color-adjust: exact;
-}
-.custom-control-input {
- position: absolute;
- left: 0;
- z-index: -1;
- width: 1rem;
- height: 1.25rem;
- opacity: 0;
-}
-.custom-control-input:checked ~ .custom-control-label::before {
- color: #fff;
- border-color: #007bff;
- background-color: #007bff;
-}
-.custom-control-input:not(:disabled):active ~ .custom-control-label::before {
- color: #fff;
- background-color: #b3d7ff;
- border-color: #b3d7ff;
-}
-.custom-control-input:disabled ~ .custom-control-label {
- color: #626168;
-}
-.custom-control-input:disabled ~ .custom-control-label::before {
- background-color: #fbfafd;
-}
-.custom-control-label {
- position: relative;
- margin-bottom: 0;
- vertical-align: top;
-}
-.custom-control-label::before {
- position: absolute;
- top: 0.25rem;
- left: -1.5rem;
- display: block;
- width: 1rem;
- height: 1rem;
- pointer-events: none;
- content: "";
- background-color: #fff;
- border: 1px solid #737278;
-}
-.custom-control-label::after {
- position: absolute;
- top: 0.25rem;
- left: -1.5rem;
- display: block;
- width: 1rem;
- height: 1rem;
- content: "";
- background: 50% / 50% 50% no-repeat;
-}
-.custom-checkbox .custom-control-label::before {
- border-radius: 0.25rem;
-}
-.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {
- background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e");
-}
-.custom-checkbox
- .custom-control-input:indeterminate
- ~ .custom-control-label::before {
- border-color: #007bff;
- background-color: #007bff;
-}
-.custom-checkbox
- .custom-control-input:indeterminate
- ~ .custom-control-label::after {
- background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e");
-}
-.custom-checkbox
- .custom-control-input:disabled:checked
- ~ .custom-control-label::before {
- background-color: rgba(0, 123, 255, 0.5);
-}
-.custom-checkbox
- .custom-control-input:disabled:indeterminate
- ~ .custom-control-label::before {
- background-color: rgba(0, 123, 255, 0.5);
-}
-@media (prefers-reduced-motion: reduce) {
-}
-.tab-content > .tab-pane {
- display: none;
-}
-.tab-content > .active {
- display: block;
-}
-.navbar {
- position: relative;
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
- padding: 0.25rem 0.5rem;
-}
-.navbar .container {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
-}
-.fixed-top {
- position: fixed;
- top: 0;
- right: 0;
- left: 0;
- z-index: 1030;
-}
-.mt-3 {
- margin-top: 1rem !important;
-}
-.mb-3 {
- margin-bottom: 1rem !important;
-}
-.text-nowrap {
- white-space: nowrap !important;
-}
-.font-weight-normal {
- font-weight: 400 !important;
-}
-.gl-form-input,
-.gl-form-input.form-control {
- background-color: #fff;
- font-family: var(--default-regular-font, "GitLab Sans"), -apple-system,
- BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell,
- "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
- "Segoe UI Symbol", "Noto Color Emoji";
- font-size: 0.875rem;
- line-height: 1rem;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- height: auto;
- color: #333238;
- box-shadow: inset 0 0 0 1px #89888d;
- border-style: none;
- appearance: none;
- -moz-appearance: none;
-}
-.gl-form-input:disabled,
-.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: #fbfafd;
- box-shadow: inset 0 0 0 1px #dcdcde;
-}
-.gl-form-input:disabled,
-.gl-form-input.form-control:disabled {
- cursor: not-allowed;
- color: #737278;
-}
-.gl-form-input::placeholder,
-.gl-form-input.form-control::placeholder {
- color: #89888d;
-}
-.gl-form-checkbox {
- font-size: 0.875rem;
- line-height: 1rem;
- color: #333238;
-}
-.gl-form-checkbox .custom-control-input:disabled,
-.gl-form-checkbox .custom-control-input:disabled ~ .custom-control-label {
- cursor: not-allowed;
- color: #89888d;
-}
-.gl-form-checkbox.custom-control {
- padding-left: 1rem;
-}
-.gl-form-checkbox.custom-control .custom-control-input ~ .custom-control-label {
- cursor: pointer;
- padding-left: 0.5rem;
- margin-bottom: 0.5rem;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input
- ~ .custom-control-label::before,
-.gl-form-checkbox.custom-control
- .custom-control-input
- ~ .custom-control-label::after {
- top: 0;
- left: -1rem;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input
- ~ .custom-control-label::before {
- background-color: #fff;
- border-color: #89888d;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input:checked
- ~ .custom-control-label::before {
- background-color: #1f75cb;
- border-color: #1f75cb;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input[type="checkbox"]:checked
- ~ .custom-control-label::after,
-.gl-form-checkbox.custom-control
- .custom-control-input[type="checkbox"]:indeterminate
- ~ .custom-control-label::after {
- background: none;
- background-color: #fff;
- mask-repeat: no-repeat;
- mask-position: center center;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input[type="checkbox"]:checked
- ~ .custom-control-label::after {
- mask-image: url('data:image/svg+xml,%3Csvg width="8" height="7" viewBox="0 0 8 7" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M1 3.05299L2.99123 5L7 1" stroke="white" stroke-width="2"/%3E%3C/svg%3E%0A');
-}
-.gl-form-checkbox.custom-control
- .custom-control-input[type="checkbox"]:indeterminate
- ~ .custom-control-label::after {
- mask-image: url('data:image/svg+xml,%3Csvg width="8" height="2" viewBox="0 0 8 2" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M0 1L8 1" stroke="white" stroke-width="2"/%3E%3C/svg%3E%0A');
-}
-.gl-form-checkbox.custom-control.custom-checkbox
- .custom-control-input:indeterminate
- ~ .custom-control-label::before {
- background-color: #1f75cb;
- border-color: #1f75cb;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input:disabled
- ~ .custom-control-label {
- cursor: not-allowed;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input:disabled
- ~ .custom-control-label::before {
- background-color: #ececef;
- border-color: #dcdcde;
- pointer-events: auto;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input:checked:disabled
- ~ .custom-control-label::before,
-.gl-form-checkbox.custom-control
- .custom-control-input[type="checkbox"]:indeterminate:disabled
- ~ .custom-control-label::before {
- background-color: #dcdcde;
- border-color: #dcdcde;
-}
-.gl-form-checkbox.custom-control
- .custom-control-input:checked:disabled
- ~ .custom-control-label::after,
-.gl-form-checkbox.custom-control
- .custom-control-input[type="checkbox"]:indeterminate:disabled
- ~ .custom-control-label::after {
- background-color: #737278;
-}
-.gl-button {
- display: inline-flex;
-}
-.gl-button:not(.btn-link):active {
- text-decoration: none;
-}
-.gl-button.gl-button,
-.gl-button.gl-button.btn-block {
- border-width: 0;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- background-color: transparent;
- line-height: 1rem;
- color: #333238;
- fill: currentColor;
- box-shadow: inset 0 0 0 1px #bfbfc3;
- justify-content: center;
- align-items: center;
- font-size: 0.875rem;
- border-radius: 0.25rem;
-}
-.gl-button.gl-button .gl-button-text,
-.gl-button.gl-button.btn-block .gl-button-text {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- padding-top: 1px;
- padding-bottom: 1px;
- margin-top: -1px;
- margin-bottom: -1px;
-}
-.gl-button.gl-button .gl-button-icon,
-.gl-button.gl-button.btn-block .gl-button-icon {
- height: 1rem;
- width: 1rem;
- flex-shrink: 0;
- margin-right: 0.25rem;
- top: auto;
-}
-.gl-button.gl-button.btn-default,
-.gl-button.gl-button.btn-block.btn-default {
- background-color: #fff;
-}
-.gl-button.gl-button.btn-default:active,
-.gl-button.gl-button.btn-default.active,
-.gl-button.gl-button.btn-block.btn-default:active,
-.gl-button.gl-button.btn-block.btn-default.active {
- box-shadow: inset 0 0 0 1px #626168, 0 0 0 1px #fff, 0 0 0 3px #428fdc;
- outline: none;
- background-color: #dcdcde;
-}
-.gl-button.gl-button.btn-confirm,
-.gl-button.gl-button.btn-block.btn-confirm {
- color: #fff;
-}
-.gl-button.gl-button.btn-confirm,
-.gl-button.gl-button.btn-block.btn-confirm {
- background-color: #1f75cb;
- box-shadow: inset 0 0 0 1px #1068bf;
-}
-.gl-button.gl-button.btn-confirm:active,
-.gl-button.gl-button.btn-confirm.active,
-.gl-button.gl-button.btn-block.btn-confirm:active,
-.gl-button.gl-button.btn-block.btn-confirm.active {
- box-shadow: inset 0 0 0 1px #033464, 0 0 0 1px #fff, 0 0 0 3px #428fdc;
- outline: none;
- background-color: #0b5cad;
-}
-body {
- font-size: 0.875rem;
-}
-button,
-[type="submit"] {
- cursor: pointer;
-}
-h1,
-h3 {
- margin-top: 20px;
- margin-bottom: 10px;
-}
-hr {
- overflow: hidden;
-}
-svg {
- vertical-align: baseline;
-}
-.form-control {
- font-size: 0.875rem;
-}
-.hidden {
- display: none !important;
- visibility: hidden !important;
-}
-html {
- overflow-y: scroll;
-}
-body.navless {
- background-color: #fff !important;
-}
-.container {
- padding-top: 0;
- z-index: 5;
-}
-.container .content {
- margin: 0;
-}
-@media (max-width: 575.98px) {
- .container .content {
- margin-top: 20px;
- }
-}
-.btn {
- border-radius: 4px;
- font-size: 0.875rem;
- font-weight: 400;
- padding: 6px 10px;
- background-color: #fff;
- border-color: #dcdcde;
- color: #333238;
- color: #333238;
- white-space: nowrap;
-}
-.btn:active {
- background-color: #ececef;
- box-shadow: none;
-}
-.btn:active,
-.btn.active {
- background-color: #e6e6ea;
- border-color: #dedee3;
- color: #333238;
-}
-.btn svg {
- height: 15px;
- width: 15px;
-}
-.btn svg:not(:last-child) {
- margin-right: 5px;
-}
-.btn-block {
- width: 100%;
- margin: 0;
-}
-.btn-block.btn {
- padding: 6px 0;
-}
-:root {
- --performance-bar-height: 0px;
- --system-header-height: 0px;
- --top-bar-height: 0px;
- --system-footer-height: 0px;
- --mr-review-bar-height: 0px;
- --breakpoint-xs: 0;
- --breakpoint-sm: 576px;
- --breakpoint-md: 768px;
- --breakpoint-lg: 992px;
- --breakpoint-xl: 1200px;
-}
-.tab-content {
- overflow: visible;
-}
-@media (max-width: 767.98px) {
- .tab-content {
- isolation: isolate;
- }
-}
-hr {
- margin: 1.5rem 0;
- border-top: 1px solid #ececef;
-}
-.flash-container {
- margin: 0;
- margin-bottom: 16px;
- font-size: 14px;
- position: relative;
- z-index: 1;
-}
-.flash-container.sticky {
- position: sticky;
- top: calc(
- var(--header-height, 48px) +
- calc(var(--system-header-height) + var(--performance-bar-height)) +
- var(--top-bar-height)
- );
- z-index: 251;
-}
-.flash-container.flash-container-page {
- margin-bottom: 0;
-}
-.flash-container:empty {
- margin: 0;
-}
-input {
- border-radius: 0.25rem;
- color: #333238;
- background-color: #fff;
-}
-label {
- font-weight: 600;
-}
-label.custom-control-label {
- font-weight: 400;
-}
-.form-control {
- border-radius: 4px;
- padding: 6px 10px;
-}
-.form-control::placeholder {
- color: #89888d;
-}
-.gl-show-field-errors .form-control:not(textarea) {
- height: 32px;
-}
-.navbar-empty {
- justify-content: center;
- height: var(--header-height, 48px);
- background: #fff;
- border-bottom: 1px solid #dcdcde;
-}
-.navbar-empty .tanuki-logo,
-.navbar-empty .brand-header-logo {
- max-height: 100%;
-}
-.tanuki-logo .tanuki {
- fill: #e24329;
-}
-.tanuki-logo .left-cheek,
-.tanuki-logo .right-cheek {
- fill: #fc6d26;
-}
-.tanuki-logo .chin {
- fill: #fca326;
-}
-input::-moz-placeholder {
- color: #89888d;
- opacity: 1;
-}
-input::-ms-input-placeholder {
- color: #89888d;
-}
-input:-ms-input-placeholder {
- color: #89888d;
-}
-svg {
- fill: currentColor;
-}
-
-.fixed-top {
- top: calc(var(--system-header-height) + var(--performance-bar-height));
-}
-.gl-display-flex {
- display: flex;
-}
-.gl-align-items-center {
- align-items: center;
-}
-.gl-flex-wrap {
- flex-wrap: wrap;
-}
-.gl-justify-content-space-between {
- justify-content: space-between;
-}
-.gl-align-self-end {
- align-self: flex-end;
-}
-.gl-w-10 {
- width: 3.5rem;
-}
-.gl-w-half {
- width: 50%;
-}
-.gl-w-full {
- width: 100%;
-}
-@media (max-width: 575.98px) {
- .gl-xs-w-full {
- width: 100%;
- }
-}
-.gl-h-full {
- height: 100%;
-}
-.gl-p-5 {
- padding: 1rem;
-}
-.gl-px-5 {
- padding-left: 1rem;
- padding-right: 1rem;
-}
-.gl-py-5 {
- padding-top: 1rem;
- padding-bottom: 1rem;
-}
-.gl-m-0 {
- margin: 0;
-}
-.gl-mt-3 {
- margin-top: 0.5rem;
-}
-.gl-mt-5 {
- margin-top: 1rem;
-}
-.gl-mr-auto {
- margin-right: auto;
-}
-.gl-mb-2 {
- margin-bottom: 0.25rem;
-}
-.gl-mb-3 {
- margin-bottom: 0.5rem;
-}
-.gl-ml-auto {
- margin-left: auto;
-}
-.gl-gap-5 {
- gap: 1rem;
-}
-@media (min-width: 576px) {
- .gl-sm-mt-0 {
- margin-top: 0;
- }
-}
-.gl-text-center {
- text-align: center;
-}
-.gl-text-right {
- text-align: right;
-}
-.gl-font-size-h2 {
- font-size: 1.1875rem;
-}
-.gl-font-weight-bold {
- font-weight: 600;
-}
-
-@import "startup/cloaking";
-@include cloak-startup-scss(none);
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index 73877c04c46..c0eced48171 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -105,7 +105,7 @@
--svg-status-bg: #{$white};
}
-body.gl-dark {
+:root.gl-dark {
// redefine some colors and values to prevent sourcegraph conflicts
color-scheme: dark;
--gray-10: #{$gray-10};
@@ -178,6 +178,10 @@ body.gl-dark {
}
}
+.gl-label-text-light .gl-label-close.gl-button:hover {
+ background-color: $gray-900;
+}
+
.gl-label-text-dark.gl-label-text-dark {
&,
.gl-label-close .gl-icon {
@@ -194,6 +198,10 @@ body.gl-dark {
}
}
+.gl-label-text-dark .gl-label-close.gl-button:hover {
+ background-color: $gray-10;
+}
+
// duplicated class as the original .atwho-view style is added later
.atwho-view.atwho-view {
background-color: $white;
@@ -231,7 +239,7 @@ aside.right-sidebar:not(.right-sidebar-merge-requests) {
border-left-color: $gray-50;
}
-body.gl-dark {
+:root.gl-dark {
@include gitlab-theme($gray-900, $gray-400, $gray-500, $gray-900, $white);
.terms {
diff --git a/app/assets/stylesheets/themes/theme_blue.scss b/app/assets/stylesheets/themes/theme_blue.scss
index 06f3e13e99e..749120a0ecb 100644
--- a/app/assets/stylesheets/themes/theme_blue.scss
+++ b/app/assets/stylesheets/themes/theme_blue.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-blue {
@include gitlab-theme(
$theme-blue-200,
diff --git a/app/assets/stylesheets/themes/theme_gray.scss b/app/assets/stylesheets/themes/theme_gray.scss
index 3112aaef227..70611e692cd 100644
--- a/app/assets/stylesheets/themes/theme_gray.scss
+++ b/app/assets/stylesheets/themes/theme_gray.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-gray {
@include gitlab-theme(
$gray-200,
diff --git a/app/assets/stylesheets/themes/theme_green.scss b/app/assets/stylesheets/themes/theme_green.scss
index c9ea1162206..ae969873692 100644
--- a/app/assets/stylesheets/themes/theme_green.scss
+++ b/app/assets/stylesheets/themes/theme_green.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-green {
@include gitlab-theme(
$theme-green-200,
diff --git a/app/assets/stylesheets/themes/theme_indigo.scss b/app/assets/stylesheets/themes/theme_indigo.scss
index 78ce96667d4..d7e8ddadf46 100644
--- a/app/assets/stylesheets/themes/theme_indigo.scss
+++ b/app/assets/stylesheets/themes/theme_indigo.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-indigo {
@include gitlab-theme(
$indigo-200,
diff --git a/app/assets/stylesheets/themes/theme_light_blue.scss b/app/assets/stylesheets/themes/theme_light_blue.scss
index 73fe072393f..430960f563f 100644
--- a/app/assets/stylesheets/themes/theme_light_blue.scss
+++ b/app/assets/stylesheets/themes/theme_light_blue.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-light-blue {
@include gitlab-theme(
$theme-light-blue-200,
diff --git a/app/assets/stylesheets/themes/theme_light_gray.scss b/app/assets/stylesheets/themes/theme_light_gray.scss
index e8357647f48..f63da3f22f1 100644
--- a/app/assets/stylesheets/themes/theme_light_gray.scss
+++ b/app/assets/stylesheets/themes/theme_light_gray.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-light-gray {
@include gitlab-theme(
$gray-500,
diff --git a/app/assets/stylesheets/themes/theme_light_green.scss b/app/assets/stylesheets/themes/theme_light_green.scss
index 6b058b2dd7b..05adc56c36a 100644
--- a/app/assets/stylesheets/themes/theme_light_green.scss
+++ b/app/assets/stylesheets/themes/theme_light_green.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-light-green {
@include gitlab-theme(
$theme-green-200,
diff --git a/app/assets/stylesheets/themes/theme_light_indigo.scss b/app/assets/stylesheets/themes/theme_light_indigo.scss
index ff12366466a..04bcfaf8366 100644
--- a/app/assets/stylesheets/themes/theme_light_indigo.scss
+++ b/app/assets/stylesheets/themes/theme_light_indigo.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-light-indigo {
@include gitlab-theme(
$indigo-200,
diff --git a/app/assets/stylesheets/themes/theme_light_red.scss b/app/assets/stylesheets/themes/theme_light_red.scss
index 3ae67309014..c4952b8e155 100644
--- a/app/assets/stylesheets/themes/theme_light_red.scss
+++ b/app/assets/stylesheets/themes/theme_light_red.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-light-red {
@include gitlab-theme(
$theme-light-red-200,
diff --git a/app/assets/stylesheets/themes/theme_red.scss b/app/assets/stylesheets/themes/theme_red.scss
index 82de30e8b0e..536963e12ef 100644
--- a/app/assets/stylesheets/themes/theme_red.scss
+++ b/app/assets/stylesheets/themes/theme_red.scss
@@ -1,6 +1,6 @@
@import './theme_helper';
-body {
+:root {
&.ui-red {
@include gitlab-theme(
$theme-red-200,
diff --git a/app/assets/stylesheets/tmp_utilities.scss b/app/assets/stylesheets/tmp_utilities.scss
new file mode 100644
index 00000000000..96464aa5a39
--- /dev/null
+++ b/app/assets/stylesheets/tmp_utilities.scss
@@ -0,0 +1,32 @@
+/**
+ * DISCLAIMER
+ * This is a temporary stylesheet meant to assist in migrating away from desktop-first responsive
+ * CSS utilities.
+ * DO NOT add utils in here unless you are actively taking part in in the migration.
+ * We needed this new file for temporary utils to be defined _after_ the main, non-responsive
+ * GitLab UI util.
+ * This file is scheduled to be removed by the end of 2023.
+ */
+ .gl-sm-w-25p {
+ @include gl-media-breakpoint-up(sm) {
+ width: 25%;
+ }
+}
+
+.gl-sm-w-30p {
+ @include gl-media-breakpoint-up(sm) {
+ width: 30%;
+ }
+}
+
+.gl-sm-w-40p {
+ @include gl-media-breakpoint-up(sm) {
+ width: 40%;
+ }
+}
+
+.gl-sm-w-75p {
+ @include gl-media-breakpoint-up(sm) {
+ width: 75%;
+ }
+}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 8fe45d4bb9d..347b8e20ab4 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -65,9 +65,6 @@
min-width: 0;
}
-// .gl-font-size-inherit will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1466
-.gl-font-size-inherit,
-.font-size-inherit { font-size: inherit; }
.gl-w-16 { width: px-to-rem($grid-size * 2); }
.gl-w-64 { width: px-to-rem($grid-size * 8); }
.gl-h-32 { height: px-to-rem($grid-size * 4); }
diff --git a/app/components/projects/ml/models_index_component.rb b/app/components/projects/ml/models_index_component.rb
index 57900165ad1..5754c2a1fa9 100644
--- a/app/components/projects/ml/models_index_component.rb
+++ b/app/components/projects/ml/models_index_component.rb
@@ -3,10 +3,11 @@
module Projects
module Ml
class ModelsIndexComponent < ViewComponent::Base
- attr_reader :paginator
+ attr_reader :paginator, :model_count
- def initialize(paginator:)
+ def initialize(paginator:, model_count:)
@paginator = paginator
+ @model_count = model_count
end
private
@@ -14,7 +15,8 @@ module Projects
def view_model
vm = {
models: models_view_model,
- page_info: page_info_view_model
+ page_info: page_info_view_model,
+ model_count: model_count
}
Gitlab::Json.generate(vm.deep_transform_keys { |k| k.to_s.camelize(:lower) })
@@ -26,7 +28,8 @@ module Projects
name: m.name,
version: m.latest_version_name,
version_count: m.version_count,
- path: m.latest_package_path
+ version_package_path: m.latest_package_path,
+ version_path: m.latest_version_path
}
end
end
diff --git a/app/components/projects/ml/show_ml_model_component.rb b/app/components/projects/ml/show_ml_model_component.rb
index 2fe2c7e7e9d..d349c0a22e9 100644
--- a/app/components/projects/ml/show_ml_model_component.rb
+++ b/app/components/projects/ml/show_ml_model_component.rb
@@ -16,11 +16,22 @@ module Projects
model: {
id: model.id,
name: model.name,
- path: model.path
+ path: model.path,
+ description: "This is a placeholder for the short description",
+ latest_version: latest_version_view_model,
+ version_count: model.version_count
}
}
- Gitlab::Json.generate(vm)
+ Gitlab::Json.generate(vm.deep_transform_keys { |k| k.to_s.camelize(:lower) })
+ end
+
+ def latest_version_view_model
+ return unless model.latest_version
+
+ {
+ version: model.latest_version.version
+ }
end
end
end
diff --git a/app/components/projects/ml/show_ml_model_version_component.html.haml b/app/components/projects/ml/show_ml_model_version_component.html.haml
new file mode 100644
index 00000000000..7410e648306
--- /dev/null
+++ b/app/components/projects/ml/show_ml_model_version_component.html.haml
@@ -0,0 +1 @@
+#js-mount-show-ml-model-version{ data: { view_model: view_model } }
diff --git a/app/components/projects/ml/show_ml_model_version_component.rb b/app/components/projects/ml/show_ml_model_version_component.rb
new file mode 100644
index 00000000000..ae81642a891
--- /dev/null
+++ b/app/components/projects/ml/show_ml_model_version_component.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Projects
+ module Ml
+ class ShowMlModelVersionComponent < ViewComponent::Base
+ attr_reader :model_version, :model
+
+ def initialize(model_version:)
+ @model_version = model_version.present
+ @model = model_version.model.present
+ end
+
+ private
+
+ def view_model
+ vm = {
+ model_version: {
+ id: model_version.id,
+ version: model_version.version,
+ path: model_version.path,
+ model: {
+ name: model.name,
+ path: model.path
+ }
+ }
+ }
+
+ Gitlab::Json.generate(vm.deep_transform_keys { |k| k.to_s.camelize(:lower) })
+ end
+ end
+ end
+end
diff --git a/app/controllers/acme_challenges_controller.rb b/app/controllers/acme_challenges_controller.rb
index a187e43b3df..4a7706db94e 100644
--- a/app/controllers/acme_challenges_controller.rb
+++ b/app/controllers/acme_challenges_controller.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-class AcmeChallengesController < BaseActionController
+# rubocop:disable Rails/ApplicationController
+class AcmeChallengesController < ActionController::Base
def show
if acme_order
render plain: acme_order.challenge_file_content, content_type: 'text/plain'
@@ -15,3 +16,4 @@ class AcmeChallengesController < BaseActionController
@acme_order ||= PagesDomainAcmeOrder.find_by_domain_and_token(params[:domain], params[:token])
end
end
+# rubocop:enable Rails/ApplicationController
diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb
index b48d6f4f7c2..d5c505ba1dd 100644
--- a/app/controllers/admin/abuse_reports_controller.rb
+++ b/app/controllers/admin/abuse_reports_controller.rb
@@ -7,6 +7,7 @@ class Admin::AbuseReportsController < Admin::ApplicationController
before_action :find_abuse_report, only: [:show, :moderate_user, :update, :destroy]
before_action only: :show do
push_frontend_feature_flag(:abuse_report_labels)
+ push_frontend_feature_flag(:abuse_report_notes)
end
def index
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index be1edeb0d37..8cf0ab60fd3 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -12,10 +12,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
before_action :set_application_setting, except: :integrations
before_action :disable_query_limiting, only: [:usage_data]
+ before_action :prerecorded_service_ping_data, only: [:metrics_and_profiling] # rubocop:disable Rails/LexicallyScopedActionFilter
before_action do
push_frontend_feature_flag(:ci_variables_pages, current_user)
- push_frontend_feature_flag(:ci_variable_drawer, current_user)
end
feature_category :not_owned, [ # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
@@ -30,7 +30,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
feature_category :source_code_management, [:repository, :clear_repository_check_states]
feature_category :continuous_integration, [:ci_cd, :reset_registration_token]
urgency :low, [:ci_cd, :reset_registration_token]
- feature_category :service_ping, [:usage_data, :service_usage_data]
+ feature_category :service_ping, [:usage_data]
feature_category :integrations, [:integrations, :slack_app_manifest_share, :slack_app_manifest_download]
feature_category :pages, [:lets_encrypt_terms_of_service]
feature_category :error_tracking, [:reset_error_tracking_access_token]
@@ -56,18 +56,16 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
@integrations = Integration.find_or_initialize_all_non_project_specific(Integration.for_instance).sort_by(&:title)
end
- def service_usage_data
- @service_ping_data_present = prerecorded_service_ping_data.present?
- end
-
def update
perform_update
end
def usage_data
+ return not_found unless prerecorded_service_ping_data.present?
+
respond_to do |format|
format.html do
- usage_data_json = Gitlab::Json.pretty_generate(service_ping_data)
+ usage_data_json = Gitlab::Json.pretty_generate(prerecorded_service_ping_data)
render html: Gitlab::Highlight.highlight('payload.json', usage_data_json, language: 'json')
end
@@ -75,7 +73,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
format.json do
Gitlab::UsageDataCounters::ServiceUsageDataCounter.count(:download_payload_click)
- render json: Gitlab::Json.dump(service_ping_data)
+ render json: Gitlab::Json.dump(prerecorded_service_ping_data)
end
end
end
@@ -243,12 +241,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
VALID_SETTING_PANELS
end
- def service_ping_data
- prerecorded_service_ping_data || Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values)
- end
-
def prerecorded_service_ping_data
- Rails.cache.fetch(Gitlab::Usage::ServicePingReport::CACHE_KEY) || ::RawUsageData.for_current_reporting_cycle.first&.payload
+ @service_ping_data ||= Rails.cache.fetch(Gitlab::Usage::ServicePingReport::CACHE_KEY) ||
+ ::RawUsageData.for_current_reporting_cycle.first&.payload
end
end
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index dab0f3e870a..a03e0c0807f 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -13,8 +13,7 @@ class Admin::DashboardController < Admin::ApplicationController
@projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@users = User.order_id_desc.limit(10)
@groups = Group.order_id_desc.with_route.limit(10)
- @notices = Gitlab::ConfigChecker::PumaRuggedChecker.check
- @notices += Gitlab::ConfigChecker::ExternalDatabaseChecker.check
+ @notices = Gitlab::ConfigChecker::ExternalDatabaseChecker.check
@redis_versions = Gitlab::Redis::ALL_CLASSES.map(&:version).uniq
end
diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb
index b27185a6add..d7ed6aa33ef 100644
--- a/app/controllers/admin/spam_logs_controller.rb
+++ b/app/controllers/admin/spam_logs_controller.rb
@@ -5,7 +5,9 @@ class Admin::SpamLogsController < Admin::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def index
- @spam_logs = SpamLog.includes(:user).order(id: :desc).page(params[:page]).without_count
+ @spam_logs = SpamLog.preload(user: [:trusted_with_spam_attribute])
+ .order(id: :desc)
+ .page(params[:page]).without_count
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 1f05e4e7b21..ee78d5a8c35 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -164,6 +164,26 @@ class Admin::UsersController < Admin::ApplicationController
end
end
+ def trust
+ result = Users::TrustService.new(current_user).execute(user)
+
+ if result[:status] == :success
+ redirect_back_or_admin_user(notice: _("Successfully trusted"))
+ else
+ redirect_back_or_admin_user(alert: _("Error occurred. User was not updated"))
+ end
+ end
+
+ def untrust
+ result = Users::UntrustService.new(current_user).execute(user)
+
+ if result[:status] == :success
+ redirect_back_or_admin_user(notice: _("Successfully untrusted"))
+ else
+ redirect_back_or_admin_user(alert: _("Error occurred. User was not updated"))
+ end
+ end
+
def confirm
if update_user(&:force_confirm)
redirect_back_or_admin_user(notice: _("Successfully confirmed"))
@@ -290,7 +310,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def users_with_included_associations(users)
- users.includes(:authorized_projects) # rubocop: disable CodeReuse/ActiveRecord
+ users.includes(:authorized_projects, :trusted_with_spam_attribute) # rubocop: disable CodeReuse/ActiveRecord
end
def admin_making_changes_for_another_user?
@@ -342,6 +362,7 @@ class Admin::UsersController < Admin::ApplicationController
:bio,
:can_create_group,
:color_scheme_id,
+ :discord,
:email,
:extern_uid,
:external,
@@ -350,6 +371,7 @@ class Admin::UsersController < Admin::ApplicationController
:hide_no_ssh_key,
:key_id,
:linkedin,
+ :mastodon,
:name,
:password_expires_at,
:projects_limit,
@@ -358,7 +380,6 @@ class Admin::UsersController < Admin::ApplicationController
:skype,
:theme_id,
:twitter,
- :discord,
:username,
:website_url,
:note,
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index f60da46826a..6739fc57a1f 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -3,7 +3,7 @@
require 'gon'
require 'fogbugz'
-class ApplicationController < BaseActionController
+class ApplicationController < ActionController::Base
include Gitlab::GonHelper
include Gitlab::NoCacheHeaders
include GitlabRoutingHelper
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index c9cb1ca14e2..1c2bd10bc81 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -3,16 +3,18 @@
class AutocompleteController < ApplicationController
include SearchRateLimitable
- skip_before_action :authenticate_user!, only: [:users, :award_emojis, :merge_request_target_branches]
+ skip_before_action :authenticate_user!, only: [
+ :users, :award_emojis, :merge_request_target_branches, :merge_request_source_branches
+ ]
before_action :check_search_rate_limit!, only: [:users, :projects]
feature_category :user_profile, [:users, :user]
feature_category :groups_and_projects, [:projects]
feature_category :team_planning, [:award_emojis]
- feature_category :code_review_workflow, [:merge_request_target_branches]
+ feature_category :code_review_workflow, [:merge_request_target_branches, :merge_request_source_branches]
feature_category :continuous_delivery, [:deploy_keys_with_owners]
- urgency :low, [:merge_request_target_branches, :deploy_keys_with_owners, :users]
+ urgency :low, [:merge_request_target_branches, :merge_request_source_branches, :deploy_keys_with_owners, :users]
urgency :low, [:award_emojis]
urgency :medium, [:projects]
@@ -62,14 +64,11 @@ class AutocompleteController < ApplicationController
end
def merge_request_target_branches
- if target_branch_params.present?
- merge_requests = MergeRequestsFinder.new(current_user, target_branch_params).execute
- target_branches = merge_requests.recent_target_branches
+ merge_request_branches(target: true)
+ end
- render json: target_branches.map { |target_branch| { title: target_branch } }
- else
- render json: { error: _('At least one of group_id or project_id must be specified') }, status: :bad_request
- end
+ def merge_request_source_branches
+ merge_request_branches(source: true)
end
def deploy_keys_with_owners
@@ -90,7 +89,7 @@ class AutocompleteController < ApplicationController
.execute
end
- def target_branch_params
+ def branch_params
params.permit(:group_id, :project_id).select { |_, v| v.present? }
end
@@ -98,6 +97,21 @@ class AutocompleteController < ApplicationController
def presented_suggested_users
[]
end
+
+ def merge_request_branches(source: false, target: false)
+ if branch_params.present?
+ merge_requests = MergeRequestsFinder.new(current_user, branch_params).execute
+
+ branches = []
+
+ branches.concat(merge_requests.recent_source_branches) if source
+ branches.concat(merge_requests.recent_target_branches) if target
+
+ render json: branches.map { |branch| { title: branch } }
+ else
+ render json: { error: _('At least one of group_id or project_id must be specified') }, status: :bad_request
+ end
+ end
end
AutocompleteController.prepend_mod_with('AutocompleteController')
diff --git a/app/controllers/base_action_controller.rb b/app/controllers/base_action_controller.rb
deleted file mode 100644
index af2c9e98778..00000000000
--- a/app/controllers/base_action_controller.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-# GitLab lightweight base action controller
-#
-# This class should be limited to content that
-# is desired/required for *all* controllers in
-# GitLab.
-#
-# Most controllers inherit from `ApplicationController`.
-# Some controllers don't want or need all of that
-# logic and instead inherit from `ActionController::Base`.
-# This makes it difficult to set security headers and
-# handle other critical logic across *all* controllers.
-#
-# Between this controller and `ApplicationController`
-# no controller should ever inherit directly from
-# `ActionController::Base`
-#
-# rubocop:disable Rails/ApplicationController
-# rubocop:disable Gitlab/NamespacedClass
-class BaseActionController < ActionController::Base
- before_action :security_headers
-
- private
-
- def security_headers
- headers['Cross-Origin-Opener-Policy'] = 'same-origin' if ::Feature.enabled?(:coop_header)
- end
-end
-# rubocop:enable Gitlab/NamespacedClass
-# rubocop:enable Rails/ApplicationController
diff --git a/app/controllers/chaos_controller.rb b/app/controllers/chaos_controller.rb
index b61a8c5ff12..7328b793b09 100644
--- a/app/controllers/chaos_controller.rb
+++ b/app/controllers/chaos_controller.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-class ChaosController < BaseActionController
+# rubocop:disable Rails/ApplicationController
+class ChaosController < ActionController::Base
before_action :validate_chaos_secret, unless: :development_or_test?
def leakmem
@@ -94,3 +95,4 @@ class ChaosController < BaseActionController
Rails.env.development? || Rails.env.test?
end
end
+# rubocop:enable Rails/ApplicationController
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index 27f1d1f5528..5009bf7ff0c 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -3,6 +3,7 @@
module CreatesCommit
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
+ include SafeFormatHelper
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil, target_project: nil)
@@ -31,10 +32,10 @@ module CreatesCommit
result = service.new(@project_to_commit_into, current_user, commit_params).execute
if result[:status] == :success
- update_flash_notice(success_notice)
-
success_path = final_success_path(success_path, target_project)
+ update_flash_notice(success_notice, success_path)
+
respond_to do |format|
format.html { redirect_to success_path }
format.json { render json: { message: _("success"), filePath: success_path } }
@@ -65,8 +66,13 @@ module CreatesCommit
private
- def update_flash_notice(success_notice)
- flash[:notice] = success_notice || _("Your changes have been successfully committed.")
+ def update_flash_notice(success_notice, success_path)
+ changes_link = ActionController::Base.helpers.link_to _('changes'), success_path, class: 'gl-link'
+
+ default_message = safe_format(_("Your %{changes_link} have been committed successfully."),
+ changes_link: changes_link)
+
+ flash[:notice] = success_notice || default_message
if create_merge_request?
flash[:notice] =
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 28e1056092d..cd2372825ac 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -147,6 +147,8 @@ module IssuableActions
finder = Issuable::DiscussionsListService.new(current_user, issuable, finder_params_for_issuable)
discussion_notes = finder.execute
+ yield discussion_notes if block_given?
+
if finder.paginator.present? && finder.paginator.has_next_page?
response.headers['X-Next-Page-Cursor'] = finder.paginator.cursor_for_next_page
end
diff --git a/app/controllers/concerns/render_access_tokens.rb b/app/controllers/concerns/render_access_tokens.rb
index b0bbad7e37f..43e4686e66f 100644
--- a/app/controllers/concerns/render_access_tokens.rb
+++ b/app/controllers/concerns/render_access_tokens.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module RenderAccessTokens
extend ActiveSupport::Concern
diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb
index c606ccf4a07..f8c3e125c3b 100644
--- a/app/controllers/concerns/wiki_actions.rb
+++ b/app/controllers/concerns/wiki_actions.rb
@@ -246,7 +246,7 @@ module WikiActions
@sidebar_page = wiki.find_sidebar(params[:version_id])
unless @sidebar_page # Fallback to default sidebar
- @sidebar_wiki_entries, @sidebar_limited = wiki.sidebar_entries
+ @sidebar_wiki_entries, @sidebar_limited = wiki.sidebar_entries(load_content: Feature.enabled?(:wiki_front_matter_title, container))
end
rescue ::Gitlab::Git::CommandTimedOut => e
@sidebar_error = e
@@ -326,7 +326,9 @@ module WikiActions
end
def load_content?
- return false if %w[history destroy diff show].include?(params[:action])
+ skip_actions = Feature.enabled?(:wiki_front_matter_title, container) ? %w[history destroy diff] : %w[history destroy diff show]
+
+ return false if skip_actions.include?(params[:action])
true
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 188a8540a58..a0997484c58 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -14,6 +14,7 @@ class DashboardController < Dashboard::ApplicationController
before_action only: :issues do
push_frontend_feature_flag(:frontend_caching)
+ push_frontend_feature_flag(:group_multi_select_tokens)
end
before_action only: :merge_requests do
diff --git a/app/controllers/explore/catalog_controller.rb b/app/controllers/explore/catalog_controller.rb
new file mode 100644
index 00000000000..3cd3771129e
--- /dev/null
+++ b/app/controllers/explore/catalog_controller.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Explore
+ class CatalogController < Explore::ApplicationController
+ feature_category :pipeline_composition
+ before_action :check_feature_flag
+
+ def show; end
+
+ def index
+ render 'show'
+ end
+
+ private
+
+ def check_feature_flag
+ render_404 unless Feature.enabled?(:global_ci_catalog, current_user)
+ end
+ end
+end
diff --git a/app/controllers/external_redirect/external_redirect_controller.rb b/app/controllers/external_redirect/external_redirect_controller.rb
new file mode 100644
index 00000000000..532196157b7
--- /dev/null
+++ b/app/controllers/external_redirect/external_redirect_controller.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module ExternalRedirect
+ class ExternalRedirectController < ApplicationController
+ feature_category :navigation
+ skip_before_action :authenticate_user!
+ before_action :check_url_param
+
+ def index
+ if known_url?
+ redirect_to url_param
+ else
+ render layout: 'fullscreen', locals: {
+ minimal: true,
+ url: url_param
+ }
+ end
+ end
+
+ private
+
+ def url_param
+ params['url']&.strip
+ end
+
+ def known_url?
+ uri_data = Addressable::URI.parse(url_param)
+
+ uri_data.site == Gitlab.config.gitlab.url
+ end
+
+ def check_url_param
+ render_404 unless ::Gitlab::UrlSanitizer.valid_web?(url_param)
+ end
+ end
+end
diff --git a/app/controllers/groups/settings/applications_controller.rb b/app/controllers/groups/settings/applications_controller.rb
index 3ae1ae824a0..5aea078db17 100644
--- a/app/controllers/groups/settings/applications_controller.rb
+++ b/app/controllers/groups/settings/applications_controller.rb
@@ -5,7 +5,7 @@ module Groups
class ApplicationsController < Groups::ApplicationController
include OauthApplications
- prepend_before_action :authorize_admin_group!
+ before_action :authorize_admin_group!
before_action :set_application, only: [:show, :edit, :update, :renew, :destroy]
before_action :load_scopes, only: [:index, :create, :edit, :update]
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index f50cdd2b1de..371db7b30b6 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -15,7 +15,6 @@ module Groups
before_action do
push_frontend_feature_flag(:ci_variables_pages, current_user)
- push_frontend_feature_flag(:ci_variable_drawer, current_user)
end
urgency :low
diff --git a/app/controllers/groups/work_items_controller.rb b/app/controllers/groups/work_items_controller.rb
index bd85f12119b..ece279da778 100644
--- a/app/controllers/groups/work_items_controller.rb
+++ b/app/controllers/groups/work_items_controller.rb
@@ -4,6 +4,13 @@ module Groups
class WorkItemsController < Groups::ApplicationController
feature_category :team_planning
+ before_action do
+ push_force_frontend_feature_flag(:work_items, group&.work_items_feature_flag_enabled?)
+ push_force_frontend_feature_flag(:work_items_mvc, group&.work_items_mvc_feature_flag_enabled?)
+ push_force_frontend_feature_flag(:work_items_mvc_2, group&.work_items_mvc_2_feature_flag_enabled?)
+ push_force_frontend_feature_flag(:linked_work_items, group&.linked_work_items_feature_flag_enabled?)
+ end
+
def index
not_found unless Feature.enabled?(:namespace_level_work_items, group)
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index edc590e1370..5b9b3b7de11 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -36,7 +36,11 @@ class GroupsController < Groups::ApplicationController
push_frontend_feature_flag(:or_issuable_queries, group)
push_frontend_feature_flag(:frontend_caching, group)
push_force_frontend_feature_flag(:work_items, group.work_items_feature_flag_enabled?)
+ push_force_frontend_feature_flag(:work_items_mvc, group.work_items_mvc_feature_flag_enabled?)
+ push_force_frontend_feature_flag(:work_items_mvc_2, group.work_items_mvc_2_feature_flag_enabled?)
+ push_force_frontend_feature_flag(:linked_work_items, group.linked_work_items_feature_flag_enabled?)
push_frontend_feature_flag(:issues_grid_view)
+ push_frontend_feature_flag(:group_multi_select_tokens, group)
end
before_action only: :merge_requests do
@@ -275,6 +279,7 @@ class GroupsController < Groups::ApplicationController
:avatar,
:description,
:emails_disabled,
+ :emails_enabled,
:show_diff_preview_in_email,
:mentions_disabled,
:lfs_enabled,
diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb
index 2b2db2f950c..1381999ab4c 100644
--- a/app/controllers/health_controller.rb
+++ b/app/controllers/health_controller.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-class HealthController < BaseActionController
+# rubocop:disable Rails/ApplicationController
+class HealthController < ActionController::Base
protect_from_forgery with: :exception, prepend: true
include RequiresAllowlistedMonitoringClient
@@ -39,3 +40,4 @@ class HealthController < BaseActionController
render json: result.json, status: result.http_status
end
end
+# rubocop:enable Rails/ApplicationController
diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb
index a8ec738caf4..bc425323d6f 100644
--- a/app/controllers/import/bulk_imports_controller.rb
+++ b/app/controllers/import/bulk_imports_controller.rb
@@ -6,6 +6,10 @@ class Import::BulkImportsController < ApplicationController
before_action :ensure_bulk_import_enabled
before_action :verify_blocked_uri, only: :status
+ before_action only: [:history] do
+ push_frontend_feature_flag(:bulk_import_details_page)
+ end
+
feature_category :importers
urgency :low
@@ -49,6 +53,10 @@ class Import::BulkImportsController < ApplicationController
end
end
+ def details
+ render_404 unless Feature.enabled?(:bulk_import_details_page)
+ end
+
def create
return render json: { success: false }, status: :too_many_requests if throttled_request?
return render json: { success: false }, status: :unprocessable_entity unless valid_create_params?
diff --git a/app/controllers/jira_connect/subscriptions_controller.rb b/app/controllers/jira_connect/subscriptions_controller.rb
index 773ef2bddca..17a79f83a78 100644
--- a/app/controllers/jira_connect/subscriptions_controller.rb
+++ b/app/controllers/jira_connect/subscriptions_controller.rb
@@ -48,7 +48,7 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
def destroy
subscription = current_jira_installation.subscriptions.find(params[:id])
- if !jira_user&.site_admin?
+ if !jira_user&.jira_admin?
render json: { error: 'forbidden' }, status: :forbidden
elsif subscription.destroy
render json: { success: true }
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 84ccfbc603a..83409c7e096 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -33,7 +33,7 @@ class JwtController < ApplicationController
@authentication_result = Gitlab::Auth::Result.new(nil, nil, :none, Gitlab::Auth.read_only_authentication_abilities)
authenticate_with_http_basic do |login, password|
- @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
+ @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, request: request)
if @authentication_result.failed?
log_authentication_failed(login, @authentication_result)
@@ -98,11 +98,7 @@ class JwtController < ApplicationController
return unless params[:scope].present?
scopes = Array(Rack::Utils.parse_query(request.query_string)['scope'])
- if Feature.enabled?(:jwt_auth_space_delimited_scopes, Feature.current_request)
- scopes.flat_map(&:split)
- else
- scopes
- end
+ scopes.flat_map(&:split)
end
def auth_user
diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb
index 61851fd1c60..9f41c092fa0 100644
--- a/app/controllers/metrics_controller.rb
+++ b/app/controllers/metrics_controller.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-class MetricsController < BaseActionController
+# rubocop:disable Rails/ApplicationController
+class MetricsController < ActionController::Base
include RequiresAllowlistedMonitoringClient
protect_from_forgery with: :exception, prepend: true
@@ -35,3 +36,4 @@ class MetricsController < BaseActionController
)
end
end
+# rubocop:enable Rails/ApplicationController
diff --git a/app/controllers/oauth/jira_dvcs/authorizations_controller.rb b/app/controllers/oauth/jira_dvcs/authorizations_controller.rb
deleted file mode 100644
index ba587944a36..00000000000
--- a/app/controllers/oauth/jira_dvcs/authorizations_controller.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-# This controller's role is to mimic and rewire the GitLab OAuth
-# flow routes for Jira DVCS integration.
-# See https://gitlab.com/gitlab-org/gitlab/issues/2381
-#
-class Oauth::JiraDvcs::AuthorizationsController < ApplicationController
- skip_before_action :authenticate_user!
- skip_before_action :verify_authenticity_token
-
- before_action :reversible_end_of_life!
- before_action :validate_redirect_uri, only: :new
-
- feature_category :integrations
-
- # 1. Rewire Jira OAuth initial request to our stablished OAuth authorization URL.
- def new
- session[:redirect_uri] = params['redirect_uri']
-
- redirect_to oauth_authorization_path(
- client_id: params['client_id'],
- response_type: 'code',
- scope: normalize_scope(params['scope']),
- redirect_uri: oauth_jira_dvcs_callback_url
- )
- end
-
- # 2. Handle the callback call as we were a Github Enterprise instance client.
- def callback
- # Handling URI query params concatenation.
- redirect_uri = URI.parse(session['redirect_uri'])
- new_query = URI.decode_www_form(String(redirect_uri.query)) << ['code', params[:code]]
- redirect_uri.query = URI.encode_www_form(new_query)
-
- redirect_to redirect_uri.to_s
- end
-
- # 3. Rewire and adjust access_token request accordingly.
- def access_token
- # We have to modify request.parameters because Doorkeeper::Server reads params from there
- request.parameters[:redirect_uri] = oauth_jira_dvcs_callback_url
-
- strategy = Doorkeeper::Server.new(self).token_request('authorization_code')
- response = strategy.authorize
-
- if response.status == :ok
- access_token, scope, token_type = response.body.values_at('access_token', 'scope', 'token_type')
-
- render body: "access_token=#{access_token}&scope=#{scope}&token_type=#{token_type}"
- else
- render status: response.status, body: response.body
- end
- rescue Doorkeeper::Errors::DoorkeeperError => e
- render status: :unauthorized, body: e.type
- end
-
- private
-
- # The endpoints in this controller have been deprecated since 15.1.
- #
- # Due to uncertainty about the impact of a full removal in 16.0, all endpoints return `404`
- # by default but we allow customers to toggle a flag to reverse this breaking change.
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/362168#note_1347692683.
- #
- # TODO Make the breaking change irreversible https://gitlab.com/gitlab-org/gitlab/-/issues/408148.
- def reversible_end_of_life!
- render_404 unless Feature.enabled?(:jira_dvcs_end_of_life_amnesty)
- end
-
- # When using the GitHub Enterprise connector in Jira we receive the "repo" scope,
- # this doesn't exist in GitLab but we can map it to our "api" scope.
- def normalize_scope(scope)
- scope == 'repo' ? 'api' : scope
- end
-
- def validate_redirect_uri
- client = Doorkeeper::OAuth::Client.find(params[:client_id])
- return render_404 unless client
-
- return true if Doorkeeper::OAuth::Helpers::URIChecker.valid_for_authorization?(
- params['redirect_uri'], client.redirect_uri
- )
-
- render_403
- end
-end
diff --git a/app/controllers/organizations/organizations_controller.rb b/app/controllers/organizations/organizations_controller.rb
index 88c6c9b3cef..3085f0c07d1 100644
--- a/app/controllers/organizations/organizations_controller.rb
+++ b/app/controllers/organizations/organizations_controller.rb
@@ -19,5 +19,9 @@ module Organizations
def groups_and_projects
authorize_read_organization!
end
+
+ def users
+ authorize_read_organization!
+ end
end
end
diff --git a/app/controllers/profiles/comment_templates_controller.rb b/app/controllers/profiles/comment_templates_controller.rb
index d6725c27f76..f7c1f8733de 100644
--- a/app/controllers/profiles/comment_templates_controller.rb
+++ b/app/controllers/profiles/comment_templates_controller.rb
@@ -5,8 +5,6 @@ module Profiles
feature_category :user_profile
before_action do
- render_404 unless Feature.enabled?(:saved_replies, current_user)
-
@hide_search_settings = true
end
end
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 931070ecdd4..7059e2a0371 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -48,6 +48,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:first_day_of_week,
:preferred_language,
:time_display_relative,
+ :time_display_format,
:show_whitespace_in_diffs,
:view_diffs_file_by_file,
:tab_width,
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index da15b393e6c..cb29f0f3539 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -111,6 +111,7 @@ class ProfilesController < Profiles::ApplicationController
[
:avatar,
:bio,
+ :discord,
:email,
:role,
:gitpod_enabled,
@@ -119,12 +120,12 @@ class ProfilesController < Profiles::ApplicationController
:hide_project_limit,
:linkedin,
:location,
+ :mastodon,
:name,
:public_email,
:commit_email,
:skype,
:twitter,
- :discord,
:username,
:website_url,
:organization,
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 30c6f4d865a..4bfee0c9c82 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -91,6 +91,19 @@ class Projects::ApplicationController < ApplicationController
def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user)
end
+
+ def set_is_ambiguous_ref
+ return @is_ambiguous_ref if defined? @is_ambiguous_ref
+
+ @is_ambiguous_ref = if Feature.enabled?(:ambiguous_ref_modal, @project)
+ ExtractsRef::RequestedRef
+ .new(@project.repository, ref_type: ref_type, ref: @ref)
+ .find
+ .fetch(:ambiguous, false)
+ else
+ false
+ end
+ end
end
Projects::ApplicationController.prepend_mod_with('Projects::ApplicationController')
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index 2828d17c36f..85bdeb07b00 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -62,7 +62,11 @@ class Projects::ArtifactsController < Projects::ApplicationController
conditionally_expand_blob(blob)
if blob.external_link?(build)
- redirect_to external_file_project_job_artifacts_path(@project, @build, path: params[:path])
+ if Gitlab::CurrentSettings.enable_artifact_external_redirect_warning_page
+ redirect_to external_file_project_job_artifacts_path(@project, @build, path: params[:path])
+ else
+ redirect_to blob.external_url(build)
+ end
else
respond_to do |format|
format.html do
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 015e56db012..7371902a6bd 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -31,6 +31,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy]
before_action :commit, except: [:new, :create]
+ before_action :set_is_ambiguous_ref, only: [:show]
before_action :check_for_ambiguous_ref, only: [:show]
before_action :blob, except: [:new, :create]
before_action :require_branch_head, only: [:edit, :update]
@@ -48,6 +49,7 @@ class Projects::BlobController < Projects::ApplicationController
urgency :low, [:create, :show, :edit, :update, :diff]
before_action do
+ push_frontend_feature_flag(:blob_blame_info, @project)
push_frontend_feature_flag(:highlight_js_worker, @project)
push_frontend_feature_flag(:explain_code_chat, current_user)
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index aabea122fb6..4b2749dc716 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -2,12 +2,18 @@
class Projects::EnvironmentsController < Projects::ApplicationController
MIN_SEARCH_LENGTH = 3
+ ACTIVE_STATES = %i[available stopping].freeze
+ SCOPES_TO_STATES = { "active" => ACTIVE_STATES, "stopped" => %i[stopped] }.freeze
include ProductAnalyticsTracking
include KasCookie
layout 'project'
+ before_action only: [:index] do
+ push_frontend_feature_flag(:k8s_watch_api, project)
+ end
+
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_stop_environment!, only: [:stop]
@@ -31,7 +37,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController
respond_to do |format|
format.html
format.json do
- @environments = search_environments.with_state(params[:scope] || :available)
+ states = SCOPES_TO_STATES.fetch(params[:scope], ACTIVE_STATES)
+ @environments = search_environments.with_state(states)
+
environments_count_by_state = search_environments.count_by_state
Gitlab::PollingInterval.set_header(response, interval: 3_000)
@@ -40,6 +48,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
review_app: serialize_review_app,
can_stop_stale_environments: can?(current_user, :stop_environment, @project),
available_count: environments_count_by_state[:available],
+ active_count: environments_count_by_state[:available] + environments_count_by_state[:stopping],
stopped_count: environments_count_by_state[:stopped]
}
end
@@ -54,14 +63,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController
respond_to do |format|
format.html
format.json do
+ states = SCOPES_TO_STATES.fetch(params[:scope], ACTIVE_STATES)
folder_environments = search_environments(type: params[:id])
- @environments = folder_environments.with_state(params[:scope] || :available)
+ @environments = folder_environments.with_state(states)
.order(:name)
render json: {
environments: serialize_environments(request, response),
available_count: folder_environments.available.count,
+ active_count: folder_environments.active.count,
stopped_count: folder_environments.stopped.count
}
end
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 60300f78bbb..5f8bf423219 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -9,30 +9,47 @@ class Projects::GroupLinksController < Projects::ApplicationController
feature_category :groups_and_projects
def update
- Projects::GroupLinks::UpdateService.new(group_link, current_user).execute(group_link_params)
+ result = Projects::GroupLinks::UpdateService.new(group_link, current_user).execute(group_link_params)
- if group_link.expires?
- render json: {
- expires_in: helpers.time_ago_with_tooltip(group_link.expires_at),
- expires_soon: group_link.expires_soon?
- }
- else
- render json: {}
+ if result.success?
+ if group_link.expires?
+ render json: {
+ expires_in: helpers.time_ago_with_tooltip(group_link.expires_at),
+ expires_soon: group_link.expires_soon?
+ }
+ else
+ render json: {}
+ end
+ elsif result.reason == :not_found
+ render json: { message: result.message }, status: :not_found
end
end
def destroy
- ::Projects::GroupLinks::DestroyService.new(project, current_user).execute(group_link)
-
- respond_to do |format|
- format.html do
- if can?(current_user, :admin_group, group_link.group)
- redirect_to group_path(group_link.group), status: :found
- elsif can?(current_user, :admin_project, group_link.project)
- redirect_to project_project_members_path(project), status: :found
+ result = ::Projects::GroupLinks::DestroyService.new(project, current_user).execute(group_link)
+
+ if result.success?
+ respond_to do |format|
+ format.html do
+ if can?(current_user, :admin_group, group_link.group)
+ redirect_to group_path(group_link.group), status: :found
+ elsif can?(current_user, :admin_project, group_link.project)
+ redirect_to project_project_members_path(project), status: :found
+ end
+ end
+ format.js { head :ok }
+ end
+ else
+ respond_to do |format|
+ format.html do
+ redirect_to project_project_members_path(project, tab: :groups), status: :found,
+ alert: _('The project-group link could not be removed.')
+ end
+
+ format.js do
+ render json: { message: result.message }, status: :not_found if result.reason == :not_found
end
end
- format.js { head :ok }
end
end
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
index bacf3192ee6..a3c1fd64a9d 100644
--- a/app/controllers/projects/incidents_controller.rb
+++ b/app/controllers/projects/incidents_controller.rb
@@ -12,7 +12,7 @@ class Projects::IncidentsController < Projects::ApplicationController
push_force_frontend_feature_flag(:work_items_mvc_2, @project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:moved_mr_sidebar, project)
push_force_frontend_feature_flag(:linked_work_items, @project&.linked_work_items_feature_flag_enabled?)
- push_frontend_feature_flag(:notifications_todos_buttons, project)
+ push_frontend_feature_flag(:notifications_todos_buttons, current_user)
end
feature_category :incident_management
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 4849cccac52..a6444dc038c 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -45,8 +45,6 @@ class Projects::IssuesController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:preserve_unchanged_markdown, project)
- push_frontend_feature_flag(:service_desk_new_note_email_native_attachments, project)
- push_frontend_feature_flag(:saved_replies, current_user)
push_frontend_feature_flag(:issues_grid_view)
push_frontend_feature_flag(:service_desk_ticket)
push_frontend_feature_flag(:issues_list_drawer, project)
@@ -60,17 +58,17 @@ class Projects::IssuesController < Projects::ApplicationController
before_action only: [:index, :service_desk] do
push_frontend_feature_flag(:or_issuable_queries, project)
push_frontend_feature_flag(:frontend_caching, project&.group)
+ push_frontend_feature_flag(:group_multi_select_tokens, project)
end
before_action only: :show do
- push_frontend_feature_flag(:issue_assignees_widget, project)
push_frontend_feature_flag(:work_items_mvc, project&.group)
push_force_frontend_feature_flag(:work_items_mvc, project&.work_items_mvc_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:epic_widget_edit_confirmation, project)
push_frontend_feature_flag(:moved_mr_sidebar, project)
push_force_frontend_feature_flag(:linked_work_items, project.linked_work_items_feature_flag_enabled?)
- push_frontend_feature_flag(:notifications_todos_buttons, project)
+ push_frontend_feature_flag(:notifications_todos_buttons, current_user)
end
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 802ffd99e41..d5a7f25d4ce 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -6,14 +6,16 @@ class Projects::JobsController < Projects::ApplicationController
include ContinueParams
include ProjectStatsRefreshConflictsGuard
- urgency :low, [:index, :show, :trace, :retry, :play, :cancel, :unschedule, :erase, :raw]
+ urgency :low, [:index, :show, :trace, :retry, :play, :cancel, :unschedule, :erase, :raw, :test_report_summary]
before_action :find_job_as_build, except: [:index, :play, :retry, :show]
before_action :find_job_as_processable, only: [:play, :retry, :show]
before_action :authorize_read_build_trace!, only: [:trace, :raw]
- before_action :authorize_read_build!
+ before_action :authorize_read_build!, except: [:test_report_summary]
+ before_action :authorize_read_build_report_results!, only: [:test_report_summary]
before_action :authorize_update_build!,
- except: [:index, :show, :raw, :trace, :erase, :cancel, :unschedule]
+ except: [:index, :show, :raw, :trace, :erase, :cancel, :unschedule, :test_report_summary]
+ before_action :authorize_cancel_build!, only: [:cancel]
before_action :authorize_erase_build!, only: [:erase]
before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize]
before_action :verify_api_request!, only: :terminal_websocket_authorize
@@ -153,6 +155,20 @@ class Projects::JobsController < Projects::ApplicationController
end
end
+ def test_report_summary
+ return not_found unless @build.report_results.present?
+
+ summary = Gitlab::Ci::Reports::TestReportSummary.new(@build.report_results)
+
+ respond_to do |format|
+ format.json do
+ render json: TestReportSummarySerializer
+ .new(project: project, current_user: @current_user)
+ .represent(summary)
+ end
+ end
+ end
+
def terminal
end
@@ -170,10 +186,18 @@ class Projects::JobsController < Projects::ApplicationController
attr_reader :build
+ def authorize_read_build_report_results!
+ return access_denied! unless can?(current_user, :read_build_report_results, build)
+ end
+
def authorize_update_build!
return access_denied! unless can?(current_user, :update_build, @build)
end
+ def authorize_cancel_build!
+ return access_denied! unless can?(current_user, :cancel_build, @build)
+ end
+
def authorize_erase_build!
return access_denied! unless can?(current_user, :erase_build, @build)
end
diff --git a/app/controllers/projects/merge_requests/drafts_controller.rb b/app/controllers/projects/merge_requests/drafts_controller.rb
index 74c495261a3..fb0073e0ad4 100644
--- a/app/controllers/projects/merge_requests/drafts_controller.rb
+++ b/app/controllers/projects/merge_requests/drafts_controller.rb
@@ -61,7 +61,9 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
merge_request_activity_counter.track_submit_review_comment(user: current_user)
end
- if Gitlab::Utils.to_boolean(approve_params[:approve])
+ if Feature.enabled?(:mr_request_changes, current_user) && reviewer_state_params[:reviewer_state]
+ update_reviewer_state
+ elsif Gitlab::Utils.to_boolean(approve_params[:approve])
unless merge_request.approved_by?(current_user)
success = ::MergeRequests::ApprovalService
.new(project: @project, current_user: current_user, params: approve_params)
@@ -144,6 +146,10 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
params.permit(:approve)
end
+ def reviewer_state_params
+ params.permit(:reviewer_state)
+ end
+
def prepare_notes_for_rendering(notes)
return [] unless notes
@@ -180,6 +186,18 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
def merge_request_activity_counter
Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter
end
+
+ def update_reviewer_state
+ if reviewer_state_params[:reviewer_state] === 'approved'
+ ::MergeRequests::ApprovalService
+ .new(project: @project, current_user: current_user, params: approve_params)
+ .execute(merge_request)
+ else
+ ::MergeRequests::UpdateReviewerStateService
+ .new(project: @project, current_user: current_user)
+ .execute(merge_request, reviewer_state_params[:reviewer_state])
+ end
+ end
end
Projects::MergeRequests::DraftsController.prepend_mod
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index ad7b7221e44..eb7505bd81f 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -11,6 +11,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include SourcegraphDecorator
include DiffHelper
include Gitlab::Cache::Helpers
+ include MergeRequestsHelper
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
skip_before_action :merge_request, only: [:index, :bulk_update, :export_csv]
@@ -37,15 +38,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action only: [:show, :diffs] do
push_frontend_feature_flag(:core_security_mr_widget_counts, project)
- push_frontend_feature_flag(:issue_assignees_widget, @project)
push_frontend_feature_flag(:moved_mr_sidebar, project)
push_frontend_feature_flag(:sast_reports_in_inline_diff, project)
push_frontend_feature_flag(:mr_experience_survey, project)
- push_frontend_feature_flag(:saved_replies, current_user)
push_force_frontend_feature_flag(:summarize_my_code_review, summarize_my_code_review_enabled?)
push_frontend_feature_flag(:ci_job_failures_in_mr, project)
push_frontend_feature_flag(:mr_pipelines_graphql, project)
- push_frontend_feature_flag(:notifications_todos_buttons, project)
+ push_frontend_feature_flag(:notifications_todos_buttons, current_user)
+ push_frontend_feature_flag(:widget_pipeline_pass_subscription_update, project)
+ push_frontend_feature_flag(:mr_request_changes, current_user)
end
before_action only: [:edit] do
@@ -159,7 +160,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
.represent(
@pipelines,
preload: true,
- disable_failed_builds: ::Feature.enabled?(:ci_fix_performance_pipelines_json_endpoint, @project)
+ disable_failed_builds: true
),
count: {
all: @pipelines.count
@@ -344,9 +345,16 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def discussions
- merge_request.discussions_diffs.load_highlight
+ if Feature.enabled?(:only_highlight_discussions_requested, project)
+ super do |discussion_notes|
+ note_ids = discussion_notes.flat_map { |x| x.notes.collect(&:id) }
+ merge_request.discussions_diffs.load_highlight(diff_note_ids: note_ids)
+ end
+ else
+ merge_request.discussions_diffs.load_highlight
- super
+ super
+ end
end
def export_csv
@@ -617,7 +625,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def endpoint_diff_batch_url(project, merge_request)
- per_page = current_user&.view_diffs_file_by_file ? '1' : '5'
+ per_page = current_user&.view_diffs_file_by_file ? '1' : DIFF_BATCH_ENDPOINT_PER_PAGE.to_s
params = request
.query_parameters
.merge(view: 'inline', diff_head: true, w: show_whitespace, page: '0', per_page: per_page)
diff --git a/app/controllers/projects/ml/model_versions_controller.rb b/app/controllers/projects/ml/model_versions_controller.rb
new file mode 100644
index 00000000000..bc69f5bf144
--- /dev/null
+++ b/app/controllers/projects/ml/model_versions_controller.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Projects
+ module Ml
+ class ModelVersionsController < ::Projects::ApplicationController
+ before_action :authorize_read_model_registry!
+ feature_category :mlops
+
+ def show
+ @model_version = ::Ml::ModelVersion.by_project_id_and_id(@project, params[:model_version_id])
+
+ return render_404 unless @model_version
+
+ @model = @model_version.model
+ end
+
+ private
+
+ def authorize_read_model_registry!
+ render_404 unless can?(current_user, :read_model_registry, @project)
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/ml/models_controller.rb b/app/controllers/projects/ml/models_controller.rb
index 4ff7d014723..68a8b7a1686 100644
--- a/app/controllers/projects/ml/models_controller.rb
+++ b/app/controllers/projects/ml/models_controller.rb
@@ -3,26 +3,45 @@
module Projects
module Ml
class ModelsController < ::Projects::ApplicationController
- before_action :check_feature_enabled
- before_action :set_model, only: [:show]
+ before_action :authorize_read_model_registry!
+ before_action :authorize_write_model_registry!, only: [:destroy]
+ before_action :set_model, only: [:show, :destroy]
feature_category :mlops
MAX_MODELS_PER_PAGE = 20
def index
- @paginator = ::Projects::Ml::ModelFinder.new(@project)
- .execute
- .keyset_paginate(cursor: params[:cursor], per_page: MAX_MODELS_PER_PAGE)
+ find_params = params
+ .transform_keys(&:underscore)
+ .permit(:name, :order_by, :sort)
+
+ finder = ::Projects::Ml::ModelFinder.new(@project, find_params)
+
+ @paginator = finder.execute.keyset_paginate(cursor: params[:cursor], per_page: MAX_MODELS_PER_PAGE)
+
+ @model_count = finder.count
end
def show; end
+ def destroy
+ @model.destroy!
+
+ redirect_to project_ml_models_path(@project),
+ status: :found,
+ notice: s_("MlExperimentTracking|Model removed")
+ end
+
private
- def check_feature_enabled
+ def authorize_read_model_registry!
render_404 unless can?(current_user, :read_model_registry, @project)
end
+ def authorize_write_model_registry!
+ render_404 unless can?(current_user, :write_model_registry, @project)
+ end
+
def set_model
@model = ::Ml::Model.by_project_id_and_id(@project, params[:model_id])
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 036ea45cc78..cd2db2dad2c 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -18,7 +18,8 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_read_build!, only: [:index, :show]
before_action :authorize_read_ci_cd_analytics!, only: [:charts]
before_action :authorize_create_pipeline!, only: [:new, :create]
- before_action :authorize_update_pipeline!, only: [:retry, :cancel]
+ before_action :authorize_update_pipeline!, only: [:retry]
+ before_action :authorize_cancel_pipeline!, only: [:cancel]
before_action :ensure_pipeline, only: [:show, :downloadable_artifacts]
before_action :reject_if_build_artifacts_size_refreshing!, only: [:destroy]
@@ -303,6 +304,10 @@ class Projects::PipelinesController < Projects::ApplicationController
return access_denied! unless can?(current_user, :update_pipeline, @pipeline)
end
+ def authorize_cancel_pipeline!
+ return access_denied! unless can?(current_user, :cancel_pipeline, @pipeline)
+ end
+
def limited_pipelines_count(project, scope = nil)
finder = Ci::PipelinesFinder.new(project, current_user, index_params.merge(scope: scope))
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index 79b5990abba..d0a80c6aa07 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -19,7 +19,8 @@ class Projects::RawController < Projects::ApplicationController
def show
@blob = @repository.blob_at(@ref, @path, limit: Gitlab::Git::Blob::LFS_POINTER_MAX_SIZE)
- send_blob(@repository, @blob, inline: (params[:inline] != 'false'), allow_caching: Guest.can?(:read_code, @project))
+ send_blob(@repository, @blob, inline: (params[:inline] != 'false'), allow_caching:
+::Users::Anonymous.can?(:read_code, @project))
end
private
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index 4a9282432fd..406e3bd62c2 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -48,7 +48,7 @@ class Projects::RepositoriesController < Projects::ApplicationController
expires_in(
cache_max_age(commit_id),
- public: Guest.can?(:download_code, project),
+ public: ::Users::Anonymous.can?(:download_code, project),
must_revalidate: true,
stale_if_error: 5.minutes,
stale_while_revalidate: 1.minute,
diff --git a/app/controllers/projects/service_desk_controller.rb b/app/controllers/projects/service_desk_controller.rb
index ca3cecf5949..70cb439c4f3 100644
--- a/app/controllers/projects/service_desk_controller.rb
+++ b/app/controllers/projects/service_desk_controller.rb
@@ -29,7 +29,7 @@ class Projects::ServiceDeskController < Projects::ApplicationController
end
def allowed_update_attributes
- %i[issue_template_key outgoing_name project_key]
+ %i[issue_template_key outgoing_name project_key add_external_participants_from_cc]
end
def service_desk_attributes
@@ -41,7 +41,8 @@ class Projects::ServiceDeskController < Projects::ApplicationController
issue_template_key: service_desk_settings&.issue_template_key,
template_file_missing: service_desk_settings&.issue_template_missing?,
outgoing_name: service_desk_settings&.outgoing_name,
- project_key: service_desk_settings&.project_key
+ project_key: service_desk_settings&.project_key,
+ add_external_participants_from_cc: service_desk_settings&.add_external_participants_from_cc
}
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 0845fbc9713..9a128adb926 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -14,7 +14,6 @@ module Projects
before_action do
push_frontend_feature_flag(:ci_variables_pages, current_user)
- push_frontend_feature_flag(:ci_variable_drawer, current_user)
end
helper_method :highlight_badge
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index 0371fb21ac8..cfcc27edf3e 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -12,12 +12,14 @@ class Projects::TreeController < Projects::ApplicationController
before_action :require_non_empty_project, except: [:new, :create]
before_action :assign_ref_vars
+ before_action :set_is_ambiguous_ref, only: [:show]
before_action :find_requested_ref, only: [:show]
before_action :assign_dir_vars, only: [:create_dir]
before_action :authorize_read_code!
before_action :authorize_edit_tree!, only: [:create_dir]
before_action do
+ push_frontend_feature_flag(:blob_blame_info, @project)
push_frontend_feature_flag(:highlight_js_worker, @project)
push_frontend_feature_flag(:explain_code_chat, current_user)
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
diff --git a/app/controllers/projects/work_items_controller.rb b/app/controllers/projects/work_items_controller.rb
index c3986be31b0..84cc1b16136 100644
--- a/app/controllers/projects/work_items_controller.rb
+++ b/app/controllers/projects/work_items_controller.rb
@@ -11,7 +11,6 @@ class Projects::WorkItemsController < Projects::ApplicationController
push_force_frontend_feature_flag(:work_items, project&.work_items_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc, project&.work_items_mvc_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
- push_force_frontend_feature_flag(:saved_replies, current_user)
push_force_frontend_feature_flag(:linked_work_items, project&.linked_work_items_feature_flag_enabled?)
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index fa26601204a..cee56dca538 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -29,7 +29,8 @@ class ProjectsController < Projects::ApplicationController
before_action :authorize_read_code!, only: [:refs]
# Authorize
- before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
+ before_action :authorize_admin_project_or_custom_permissions!, only: :edit
+ before_action :authorize_admin_project!, only: [:update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
before_action :authorize_archive_project!, only: [:archive, :unarchive]
before_action :event_filter, only: [:show, :activity]
@@ -37,11 +38,14 @@ class ProjectsController < Projects::ApplicationController
before_action :check_export_rate_limit!, only: [:export, :download_export, :generate_new_export]
before_action do
+ push_frontend_feature_flag(:blob_blame_info, @project)
push_frontend_feature_flag(:highlight_js_worker, @project)
push_frontend_feature_flag(:remove_monitor_metrics, @project)
push_frontend_feature_flag(:explain_code_chat, current_user)
push_frontend_feature_flag(:service_desk_custom_email, @project)
push_frontend_feature_flag(:issue_email_participants, @project)
+ # TODO: We need to remove the FF eventually when we rollout page_specific_styles
+ push_frontend_feature_flag(:page_specific_styles, current_user)
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?)
@@ -595,6 +599,11 @@ class ProjectsController < Projects::ApplicationController
def render_edit
render 'edit'
end
+
+ # Overridden in EE
+ def authorize_admin_project_or_custom_permissions!
+ authorize_admin_project!
+ end
end
ProjectsController.prepend_mod_with('ProjectsController')
diff --git a/app/controllers/repositories/git_http_client_controller.rb b/app/controllers/repositories/git_http_client_controller.rb
index a5ca17db113..e8da6ee986a 100644
--- a/app/controllers/repositories/git_http_client_controller.rb
+++ b/app/controllers/repositories/git_http_client_controller.rb
@@ -129,7 +129,7 @@ module Repositories
def handle_basic_authentication(login, password)
@authentication_result = Gitlab::Auth.find_for_git_client(
- login, password, project: project, ip: request.ip)
+ login, password, project: project, request: request)
@authentication_result.success?
end
@@ -142,7 +142,7 @@ module Repositories
Gitlab::ProtocolAccess.allowed?('http') &&
download_request? &&
container &&
- Guest.can?(repo_type.guest_read_ability, container)
+ ::Users::Anonymous.can?(repo_type.guest_read_ability, container)
end
def bypass_admin_mode!(&block)
diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb
index 4f228ced542..48edda13904 100644
--- a/app/controllers/repositories/git_http_controller.rb
+++ b/app/controllers/repositories/git_http_controller.rb
@@ -106,7 +106,8 @@ module Repositories
def access_actor
return user if user
- return :ci if ci?
+
+ :ci if ci?
end
def access_check
@@ -124,6 +125,13 @@ module Repositories
def log_user_activity
Users::ActivityService.new(author: user, project: project, namespace: project&.namespace).execute
end
+
+ def append_info_to_payload(payload)
+ super
+
+ payload[:metadata] ||= {}
+ payload[:metadata][:repository_storage] = project&.repository_storage
+ end
end
end
diff --git a/app/controllers/repositories/lfs_api_controller.rb b/app/controllers/repositories/lfs_api_controller.rb
index d9ca216b168..d9d3753a2ff 100644
--- a/app/controllers/repositories/lfs_api_controller.rb
+++ b/app/controllers/repositories/lfs_api_controller.rb
@@ -60,7 +60,7 @@ module Repositories
.for_oids(objects_oids)
.index_by(&:oid)
- guest_can_download = Guest.can?(:download_code, project)
+ guest_can_download = ::Users::Anonymous.can?(:download_code, project)
objects.each do |object|
if lfs_object = existing_oids[object[:oid]]
@@ -87,7 +87,7 @@ module Repositories
if existing_oids.include?(object[:oid])
object[:actions] = proxy_download_actions(object)
- if Guest.can?(:download_code, project)
+ if ::Users::Anonymous.can?(:download_code, project)
object[:authenticated] = true
end
else
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 7fff31c767f..b639a9dda3f 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -4,7 +4,6 @@ class SearchController < ApplicationController
include ControllerWithCrossProjectAccessCheck
include SearchHelper
include ProductAnalyticsTracking
- include ProductAnalyticsTracking
include SearchRateLimitable
RESCUE_FROM_TIMEOUT_ACTIONS = [:count, :show, :autocomplete, :aggregations].freeze
@@ -16,6 +15,12 @@ class SearchController < ApplicationController
action: 'executed',
destinations: [:redis_hll, :snowplow]
+ track_event :autocomplete,
+ name: 'i_search_total',
+ label: 'redis_hll_counters.search.search_total_unique_counts_monthly',
+ action: 'autocomplete',
+ destinations: [:redis_hll, :snowplow]
+
def self.search_rate_limited_endpoints
%i[show count autocomplete]
end
@@ -35,18 +40,6 @@ class SearchController < ApplicationController
update_scope_for_code_search
end
- before_action only: :show do
- push_frontend_feature_flag(:search_notes_hide_archived_projects, current_user)
- end
-
- before_action only: :show do
- push_frontend_feature_flag(:search_issues_hide_archived_projects, current_user)
- end
-
- before_action only: :show do
- push_frontend_feature_flag(:search_merge_requests_hide_archived_projects, current_user)
- end
-
rescue_from ActiveRecord::QueryCanceled, with: :render_timeout
layout 'search'
diff --git a/app/experiments/ios_specific_templates_experiment.rb b/app/experiments/ios_specific_templates_experiment.rb
deleted file mode 100644
index 5bd4a3d0287..00000000000
--- a/app/experiments/ios_specific_templates_experiment.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-class IosSpecificTemplatesExperiment < ApplicationExperiment
- control
-
- before_run(if: :skip_experiment) { throw(:abort) } # rubocop:disable Cop/BanCatchThrow
-
- private
-
- def skip_experiment
- actor_not_able_to_create_pipelines? ||
- project_targets_non_ios_platforms? ||
- project_has_gitlab_ci? ||
- project_has_pipelines?
- end
-
- def actor_not_able_to_create_pipelines?
- !context.actor.is_a?(User) || !context.actor.can?(:create_pipeline, context.project)
- end
-
- def project_targets_non_ios_platforms?
- context.project.project_setting.target_platforms.exclude?('ios')
- end
-
- def project_has_gitlab_ci?
- context.project.has_ci? && context.project.builds_enabled?
- end
-
- def project_has_pipelines?
- context.project.all_pipelines.count > 0
- end
-end
diff --git a/app/finders/ci/catalog/resources/versions_finder.rb b/app/finders/ci/catalog/resources/versions_finder.rb
new file mode 100644
index 00000000000..b37d4f0377a
--- /dev/null
+++ b/app/finders/ci/catalog/resources/versions_finder.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Ci
+ module Catalog
+ module Resources
+ class VersionsFinder
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(catalog_resources, current_user, params = {})
+ # The catalog resources should already have their project association preloaded
+ @catalog_resources = Array.wrap(catalog_resources)
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ return Ci::Catalog::Resources::Version.none if authorized_catalog_resources.empty?
+
+ versions = params[:latest] ? get_latest_versions : get_versions
+ versions = versions.preloaded
+ sort(versions)
+ end
+
+ private
+
+ DEFAULT_SORT = :released_at_desc
+
+ attr_reader :catalog_resources, :current_user, :params
+
+ def get_versions
+ Ci::Catalog::Resources::Version.for_catalog_resources(authorized_catalog_resources)
+ end
+
+ def get_latest_versions
+ Ci::Catalog::Resources::Version.latest_for_catalog_resources(authorized_catalog_resources)
+ end
+
+ def authorized_catalog_resources
+ # Preload project authorizations to avoid N+1 queries
+ projects = catalog_resources.map(&:project)
+ ActiveRecord::Associations::Preloader.new(records: projects, associations: :project_feature).call
+ Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects, current_user).execute
+
+ catalog_resources.select { |resource| authorized?(resource.project) }
+ end
+ strong_memoize_attr :authorized_catalog_resources
+
+ def sort(versions)
+ versions.order_by(params[:sort] || DEFAULT_SORT)
+ end
+
+ def authorized?(project)
+ Ability.allowed?(current_user, :read_release, project)
+ end
+ end
+ end
+ end
+end
diff --git a/app/finders/ci/runners_finder.rb b/app/finders/ci/runners_finder.rb
index 331f732bff7..945d332ff47 100644
--- a/app/finders/ci/runners_finder.rb
+++ b/app/finders/ci/runners_finder.rb
@@ -20,6 +20,8 @@ module Ci
filter_by_upgrade_status!
filter_by_runner_type!
filter_by_tag_list!
+ filter_by_creator_id!
+ filter_by_version_prefix!
sort!
request_tag_list!
@@ -113,6 +115,21 @@ module Ci
end
end
+ def filter_by_creator_id!
+ creator_id = @params[:creator_id]
+ @runners = @runners.with_creator_id(creator_id) if creator_id.present?
+ end
+
+ def filter_by_version_prefix!
+ return @runners unless @params[:version_prefix]
+
+ sanitized_prefix = @params[:version_prefix][/^[\d+.]+/]
+
+ return @runners unless sanitized_prefix
+
+ @runners = @runners.with_version_prefix(sanitized_prefix)
+ end
+
def sort!
@runners = @runners.order_by(sort_key)
end
diff --git a/app/finders/data_transfer/mocked_transfer_finder.rb b/app/finders/data_transfer/mocked_transfer_finder.rb
deleted file mode 100644
index 9c5551005ea..00000000000
--- a/app/finders/data_transfer/mocked_transfer_finder.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-# Mocked data for data transfer
-# Follow this epic for recent progress: https://gitlab.com/groups/gitlab-org/-/epics/9330
-module DataTransfer
- class MockedTransferFinder
- def execute
- start_date = Date.new(2023, 0o1, 0o1)
- date_for_index = ->(i) { (start_date + i.months).strftime('%Y-%m-%d') }
-
- 0.upto(11).map do |i|
- {
- date: date_for_index.call(i),
- repository_egress: rand(70000..550000),
- artifacts_egress: rand(70000..550000),
- packages_egress: rand(70000..550000),
- registry_egress: rand(70000..550000)
- }.tap do |hash|
- hash[:total_egress] = hash
- .slice(:repository_egress, :artifacts_egress, :packages_egress, :registry_egress)
- .values
- .sum
- end
- end
- end
- end
-end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 95b5b267089..b7de1c08f86 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -46,6 +46,7 @@ class MergeRequestsFinder < IssuableFinder
:merged_before,
:reviewer_id,
:reviewer_username,
+ :source_branch,
:target_branch,
:wip
]
@@ -73,7 +74,6 @@ class MergeRequestsFinder < IssuableFinder
items = by_deployments(items)
items = by_reviewer(items)
items = by_source_project_id(items)
- items = items.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417462")
by_approved(items)
end
@@ -82,7 +82,8 @@ class MergeRequestsFinder < IssuableFinder
items = super(items)
items = by_negated_reviewer(items)
items = by_negated_approved_by(items)
- by_negated_target_branch(items)
+ items = by_negated_target_branch(items)
+ by_negated_source_branch(items)
end
private
@@ -133,6 +134,12 @@ class MergeRequestsFinder < IssuableFinder
items.where.not(target_branch: not_params[:target_branch])
end
+
+ def by_negated_source_branch(items)
+ return items unless not_params[:source_branch]
+
+ items.where.not(source_branch: not_params[:source_branch])
+ end
# rubocop: enable CodeReuse/ActiveRecord
def by_negated_approved_by(items)
diff --git a/app/finders/organizations/user_organizations_finder.rb b/app/finders/organizations/user_organizations_finder.rb
new file mode 100644
index 00000000000..739940c44ca
--- /dev/null
+++ b/app/finders/organizations/user_organizations_finder.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Organizations
+ class UserOrganizationsFinder
+ def initialize(current_user, target_user, params = {})
+ @current_user = current_user
+ @target_user = target_user
+ @params = params
+ end
+
+ def execute
+ return Organizations::Organization.none unless can_read_user_organizations?
+ return Organizations::Organization.none if target_user.blank?
+
+ target_user.organizations
+ end
+
+ private
+
+ attr_reader :current_user, :target_user, :params
+
+ def can_read_user_organizations?
+ current_user&.can?(:read_user_organizations, target_user)
+ end
+ end
+end
diff --git a/app/finders/packages/packages_finder.rb b/app/finders/packages/packages_finder.rb
index 31fbbfb7937..8fe1a73a030 100644
--- a/app/finders/packages/packages_finder.rb
+++ b/app/finders/packages/packages_finder.rb
@@ -22,6 +22,7 @@ module Packages
packages = filter_by_package_type(packages)
packages = filter_by_package_name(packages)
packages = filter_by_status(packages)
+ packages = filter_by_package_version(packages)
order_packages(packages)
end
diff --git a/app/finders/packages/pypi/packages_finder.rb b/app/finders/packages/pypi/packages_finder.rb
index 17138134eb3..944824bee6e 100644
--- a/app/finders/packages/pypi/packages_finder.rb
+++ b/app/finders/packages/pypi/packages_finder.rb
@@ -3,6 +3,8 @@
module Packages
module Pypi
class PackagesFinder < ::Packages::GroupOrProjectPackageFinder
+ extend ::Gitlab::Utils::Override
+
def execute
return packages unless @params[:package_name]
@@ -14,6 +16,15 @@ module Packages
def packages
base.pypi.has_version
end
+
+ override :group_packages
+ def group_packages
+ packages_visible_to_user(
+ @current_user,
+ within_group: @project_or_group,
+ with_package_registry_enabled: true
+ )
+ end
end
end
end
diff --git a/app/finders/projects/ml/model_finder.rb b/app/finders/projects/ml/model_finder.rb
index 1e407ba4aa4..57e0620c7a7 100644
--- a/app/finders/projects/ml/model_finder.rb
+++ b/app/finders/projects/ml/model_finder.rb
@@ -3,16 +3,58 @@
module Projects
module Ml
class ModelFinder
- def initialize(project)
+ include Gitlab::Utils::StrongMemoize
+
+ VALID_ORDER_BY = %w[name created_at id].freeze
+ VALID_SORT = %w[asc desc].freeze
+
+ def initialize(project, params = {})
@project = project
+ @params = params
end
def execute
- ::Ml::Model
- .by_project(@project)
- .including_latest_version
- .with_version_count
+ relation
+ end
+
+ def count
+ relation.length
+ end
+
+ private
+
+ def relation
+ @models = ::Ml::Model
+ .by_project(project)
+ .including_latest_version
+ .including_project
+ .with_version_count
+
+ @models = by_name
+ ordered
+ end
+ strong_memoize_attr :relation
+
+ def by_name
+ return models unless params[:name].present?
+
+ models.by_name(params[:name])
+ end
+
+ def ordered
+ order_by = valid_or_default(params[:order_by]&.downcase, VALID_ORDER_BY, 'created_at')
+ sort = valid_or_default(params[:sort]&.downcase, VALID_SORT, 'desc')
+
+ models.order_by("#{order_by}_#{sort}").with_order_id_desc
end
+
+ def valid_or_default(value, valid_values, default)
+ return value if valid_values.include?(value)
+
+ default
+ end
+
+ attr_reader :params, :project, :models
end
end
end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 87edf36d1ce..1aa5245590e 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -28,6 +28,7 @@
# last_activity_before: datetime
# repository_storage: string
# not_aimed_for_deletion: boolean
+# full_paths: string[]
#
class ProjectsFinder < UnionFinder
include CustomAttributesFilter
@@ -76,8 +77,9 @@ class ProjectsFinder < UnionFinder
# EE would override this to add more filters
def filter_projects(collection)
- collection = collection.without_deleted
+ collection = by_deleted_status(collection)
collection = by_ids(collection)
+ collection = by_full_paths(collection)
collection = by_personal(collection)
collection = by_starred(collection)
collection = by_trending(collection)
@@ -153,6 +155,12 @@ class ProjectsFinder < UnionFinder
params[:min_access_level].present?
end
+ def by_deleted_status(items)
+ return items.without_deleted unless current_user&.can?(:admin_all_resources)
+
+ params[:include_pending_delete].present? ? items : items.without_deleted
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def by_ids(items)
items = items.where(id: project_ids_relation) if project_ids_relation
@@ -162,6 +170,10 @@ class ProjectsFinder < UnionFinder
end
# rubocop: enable CodeReuse/ActiveRecord
+ def by_full_paths(items)
+ params[:full_paths].present? ? items.where_full_path_in(params[:full_paths], use_includes: false) : items
+ end
+
def union(items)
find_union(items, Project).with_route
end
diff --git a/app/finders/user_group_notification_settings_finder.rb b/app/finders/user_group_notification_settings_finder.rb
index c6a1a6b36d1..8d06d3d18ca 100644
--- a/app/finders/user_group_notification_settings_finder.rb
+++ b/app/finders/user_group_notification_settings_finder.rb
@@ -11,11 +11,16 @@ class UserGroupNotificationSettingsFinder
@loaded_groups_with_ancestors = groups_with_ancestors.index_by(&:id)
@loaded_notification_settings = user.notification_settings_for_groups(groups_with_ancestors).preload_source_route.index_by(&:source_id)
- preload_emails_disabled
+ preload_emails_enabled
- groups.map do |group|
+ group_notifications = groups.map do |group|
find_notification_setting_for(group)
end
+
+ group_sources = group_notifications.map(&:source)
+ ActiveRecord::Associations::Preloader.new(records: group_sources, associations: :namespace_settings).call
+
+ group_notifications
end
private
@@ -45,18 +50,18 @@ class UserGroupNotificationSettingsFinder
parent_setting.level != NotificationSetting.levels[:global] || parent_setting.notification_email.present?
end
- # This method preloads the `emails_disabled` strong memoized method for the given groups.
+ # This method preloads the `emails_enabled` strong memoized method for the given groups.
#
- # For each group, look up the ancestor hierarchy and look for any group where emails_disabled is true.
+ # For each group, look up the ancestor hierarchy and look for any group where emails_enabled is false.
# The lookup is implemented with an EXISTS subquery, so we can look up the ancestor chain for each group individually.
# The query will return groups where at least one ancestor has the `emails_disabled` set to true.
#
# After the query, we set the instance variable.
- def preload_emails_disabled
+ def preload_emails_enabled
group_ids_with_disabled_email = Group.ids_with_disabled_email(groups.to_a)
groups.each do |group|
- group.emails_disabled_memoized = group_ids_with_disabled_email.include?(group.id) if group.parent_id
+ group.emails_enabled_memoized = group_ids_with_disabled_email.exclude?(group.id) if group.parent_id
end
end
end
diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb
index 994668b5f8f..8419f7d5eae 100644
--- a/app/graphql/mutations/base_mutation.rb
+++ b/app/graphql/mutations/base_mutation.rb
@@ -30,12 +30,6 @@ module Mutations
def ready?(**args)
raise_resource_not_available_error!(ERROR_MESSAGE) if read_only?
- missing_args = self.class.arguments.values
- .reject { |arg| arg.accepts?(args.fetch(arg.keyword, :not_given)) }
- .map(&:graphql_name)
-
- raise ArgumentError, "Arguments must be provided: #{missing_args.join(", ")}" if missing_args.any?
-
true
end
diff --git a/app/graphql/mutations/ci/catalog/resources/create.rb b/app/graphql/mutations/ci/catalog/resources/create.rb
new file mode 100644
index 00000000000..7f934e101c8
--- /dev/null
+++ b/app/graphql/mutations/ci/catalog/resources/create.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module Catalog
+ module Resources
+ class Create < BaseMutation
+ graphql_name 'CatalogResourcesCreate'
+
+ argument :project_path, GraphQL::Types::ID,
+ required: true,
+ description: 'Project to convert to a catalog resource.'
+
+ authorize :add_catalog_resource
+
+ def resolve(project_path:)
+ project = authorized_find!(project_path: project_path)
+ response = ::Ci::Catalog::Resources::CreateService.new(project, current_user).execute
+
+ errors = response.success? ? [] : [response.message]
+
+ {
+ errors: errors
+ }
+ end
+
+ private
+
+ def find_object(project_path:)
+ Project.find_by_full_path(project_path)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/catalog/resources/unpublish.rb b/app/graphql/mutations/ci/catalog/resources/unpublish.rb
new file mode 100644
index 00000000000..e45e9646147
--- /dev/null
+++ b/app/graphql/mutations/ci/catalog/resources/unpublish.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module Catalog
+ module Resources
+ class Unpublish < BaseMutation
+ graphql_name 'CatalogResourceUnpublish'
+
+ authorize :add_catalog_resource
+
+ argument :id, ::Types::GlobalIDType[::Ci::Catalog::Resource],
+ required: true,
+ description: 'Global ID of the catalog resource to unpublish.'
+
+ def resolve(id:)
+ catalog_resource = ::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(id))
+ authorize!(catalog_resource&.project)
+
+ catalog_resource.unpublish!
+
+ {
+ errors: []
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/job/cancel.rb b/app/graphql/mutations/ci/job/cancel.rb
index dc9f4d19779..44a7772019d 100644
--- a/app/graphql/mutations/ci/job/cancel.rb
+++ b/app/graphql/mutations/ci/job/cancel.rb
@@ -11,7 +11,7 @@ module Mutations
null: true,
description: 'Job after the mutation.'
- authorize :update_build
+ authorize :cancel_build
def resolve(id:)
job = authorized_find!(id: id)
diff --git a/app/graphql/mutations/ci/pipeline/cancel.rb b/app/graphql/mutations/ci/pipeline/cancel.rb
index 810f458fd75..1014462d0b1 100644
--- a/app/graphql/mutations/ci/pipeline/cancel.rb
+++ b/app/graphql/mutations/ci/pipeline/cancel.rb
@@ -6,7 +6,7 @@ module Mutations
class Cancel < Base
graphql_name 'PipelineCancel'
- authorize :update_pipeline
+ authorize :cancel_pipeline
def resolve(id:)
pipeline = authorized_find!(id: id)
diff --git a/app/graphql/mutations/commits/create.rb b/app/graphql/mutations/commits/create.rb
index 02e1e4c78bf..cbe2c49e950 100644
--- a/app/graphql/mutations/commits/create.rb
+++ b/app/graphql/mutations/commits/create.rb
@@ -64,7 +64,7 @@ module Mutations
result = ::Files::MultiService.new(project, current_user, attributes).execute
{
- content: actions.pluck(:content), # rubocop:disable CodeReuse/ActiveRecord because actions is an Array, not a Relation
+ content: actions.pluck(:content), # rubocop:disable CodeReuse/ActiveRecord -- Array#pluck
commit: (project.repository.commit(result[:result]) if result[:status] == :success),
commit_pipeline_path: UrlHelpers.new.graphql_etag_pipeline_sha_path(result[:result]),
errors: Array.wrap(result[:message])
diff --git a/app/graphql/mutations/container_registry/protection/rule/create.rb b/app/graphql/mutations/container_registry/protection/rule/create.rb
new file mode 100644
index 00000000000..cf8416480a2
--- /dev/null
+++ b/app/graphql/mutations/container_registry/protection/rule/create.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Mutations
+ module ContainerRegistry
+ module Protection
+ module Rule
+ class Create < ::Mutations::BaseMutation
+ graphql_name 'CreateContainerRegistryProtectionRule'
+ description 'Creates a protection rule to restrict access to a project\'s container registry. ' \
+ 'Available only when feature flag `container_registry_protected_containers` is enabled.'
+
+ include FindsProject
+
+ authorize :admin_container_image
+
+ argument :project_path,
+ GraphQL::Types::ID,
+ required: true,
+ description: 'Full path of the project where a protection rule is located.'
+
+ argument :container_path_pattern,
+ GraphQL::Types::String,
+ required: true,
+ description:
+ 'ContainerRegistryname protected by the protection rule. For example `@my-scope/my-container-*`. ' \
+ 'Wildcard character `*` allowed.'
+
+ argument :push_protected_up_to_access_level,
+ Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
+ required: true,
+ description:
+ 'Max GitLab access level to prevent from pushing container images to the container registry. ' \
+ 'For example `DEVELOPER`, `MAINTAINER`, `OWNER`.'
+
+ argument :delete_protected_up_to_access_level,
+ Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
+ required: true,
+ description:
+ 'Max GitLab access level to prevent from deleting container images in the container registry. ' \
+ 'For example `DEVELOPER`, `MAINTAINER`, `OWNER`.'
+
+ field :container_registry_protection_rule,
+ Types::ContainerRegistry::Protection::RuleType,
+ null: true,
+ description: 'Container registry protection rule after mutation.'
+
+ def resolve(project_path:, **kwargs)
+ project = authorized_find!(project_path)
+
+ if Feature.disabled?(:container_registry_protected_containers, project)
+ raise_resource_not_available_error!("'container_registry_protected_containers' feature flag is disabled")
+ end
+
+ response = ::ContainerRegistry::Protection::CreateRuleService.new(project, current_user, kwargs).execute
+
+ { container_registry_protection_rule: response.payload[:container_registry_protection_rule],
+ errors: response.errors }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/merge_requests/accept.rb b/app/graphql/mutations/merge_requests/accept.rb
index 220ebea22c7..604fdd49f45 100644
--- a/app/graphql/mutations/merge_requests/accept.rb
+++ b/app/graphql/mutations/merge_requests/accept.rb
@@ -9,6 +9,10 @@ module Mutations
Accepts a merge request.
When accepted, the source branch will be scheduled to merge into the target branch, either
immediately if possible, or using one of the automatic merge strategies.
+
+ [In GitLab 16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/421510), the merging happens asynchronously.
+ This results in `mergeRequest` and `state` not updating after a mutation request,
+ because the merging may not have happened yet.
DESC
NOT_MERGEABLE = 'This branch cannot be merged'
diff --git a/app/graphql/mutations/namespace/package_settings/update.rb b/app/graphql/mutations/namespace/package_settings/update.rb
index 4e71bed52c6..97c16ee79fe 100644
--- a/app/graphql/mutations/namespace/package_settings/update.rb
+++ b/app/graphql/mutations/namespace/package_settings/update.rb
@@ -8,8 +8,6 @@ module Mutations
include Mutations::ResolvesNamespace
- NUGET_DUPLICATES_FF_ERROR = '`nuget_duplicates_option` feature flag is disabled.'
-
description <<~DESC
These settings can be adjusted by the group Owner or Maintainer.
[Issue 370471](https://gitlab.com/gitlab-org/gitlab/-/issues/370471) proposes limiting
@@ -91,10 +89,6 @@ module Mutations
def resolve(namespace_path:, **args)
namespace = authorized_find!(namespace_path: namespace_path)
- if nuget_duplicate_settings_present?(args) && Feature.disabled?(:nuget_duplicates_option, namespace)
- raise_resource_not_available_error! NUGET_DUPLICATES_FF_ERROR
- end
-
result = ::Namespaces::PackageSettings::UpdateService
.new(container: namespace, current_user: current_user, params: args)
.execute
@@ -110,10 +104,6 @@ module Mutations
def find_object(namespace_path:)
resolve_namespace(full_path: namespace_path)
end
-
- def nuget_duplicate_settings_present?(args)
- args.key?(:nuget_duplicates_allowed) || args.key?(:nuget_duplicate_exception_regex)
- end
end
end
end
diff --git a/app/graphql/mutations/organizations/create.rb b/app/graphql/mutations/organizations/create.rb
new file mode 100644
index 00000000000..0d1b204a4c1
--- /dev/null
+++ b/app/graphql/mutations/organizations/create.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Organizations
+ class Create < BaseMutation
+ graphql_name 'OrganizationCreate'
+
+ authorize :create_organization
+
+ field :organization,
+ ::Types::Organizations::OrganizationType,
+ null: true,
+ description: 'Organization created.'
+
+ argument :name, GraphQL::Types::String,
+ required: true,
+ description: 'Name for the organization.'
+
+ argument :path, GraphQL::Types::String,
+ required: true,
+ description: 'Path for the organization.'
+
+ def resolve(args)
+ authorize!(:global)
+
+ result = ::Organizations::CreateService.new(
+ current_user: current_user,
+ params: args
+ ).execute
+
+ { organization: result.payload, errors: result.errors }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/packages/protection/rule/delete.rb b/app/graphql/mutations/packages/protection/rule/delete.rb
new file mode 100644
index 00000000000..bd0159d3c23
--- /dev/null
+++ b/app/graphql/mutations/packages/protection/rule/delete.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Packages
+ module Protection
+ module Rule
+ class Delete < ::Mutations::BaseMutation
+ graphql_name 'DeletePackagesProtectionRule'
+ description 'Deletes a protection rule for packages. ' \
+ 'Available only when feature flag `packages_protected_packages` is enabled.'
+
+ authorize :admin_package
+
+ argument :id,
+ ::Types::GlobalIDType[::Packages::Protection::Rule],
+ required: true,
+ description: 'Global ID of the package protection rule to delete.'
+
+ field :package_protection_rule,
+ Types::Packages::Protection::RuleType,
+ null: true,
+ description: 'Packages protection rule that was deleted successfully.'
+
+ def resolve(id:, **_kwargs)
+ if Feature.disabled?(:packages_protected_packages)
+ raise_resource_not_available_error!("'packages_protected_packages' feature flag is disabled")
+ end
+
+ package_protection_rule = authorized_find!(id: id)
+
+ response = ::Packages::Protection::DeleteRuleService.new(package_protection_rule,
+ current_user: current_user).execute
+
+ { package_protection_rule: response.payload[:package_protection_rule], errors: response.errors }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/saved_replies/base.rb b/app/graphql/mutations/saved_replies/base.rb
index 4923fcb7851..79761645eb7 100644
--- a/app/graphql/mutations/saved_replies/base.rb
+++ b/app/graphql/mutations/saved_replies/base.rb
@@ -23,10 +23,6 @@ module Mutations
end
end
- def feature_enabled?
- Feature.enabled?(:saved_replies, current_user)
- end
-
def find_object(id)
GitlabSchema.find_by_gid(id)
end
diff --git a/app/graphql/mutations/saved_replies/create.rb b/app/graphql/mutations/saved_replies/create.rb
index d97461a1c2a..25c02b79cb8 100644
--- a/app/graphql/mutations/saved_replies/create.rb
+++ b/app/graphql/mutations/saved_replies/create.rb
@@ -16,8 +16,6 @@ module Mutations
description: copy_field_description(Types::SavedReplyType, :content)
def resolve(name:, content:)
- raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless feature_enabled?
-
result = ::Users::SavedReplies::CreateService.new(current_user: current_user, name: name, content: content).execute
present_result(result)
end
diff --git a/app/graphql/mutations/saved_replies/destroy.rb b/app/graphql/mutations/saved_replies/destroy.rb
index 7cd0f21ad45..655ed9cb798 100644
--- a/app/graphql/mutations/saved_replies/destroy.rb
+++ b/app/graphql/mutations/saved_replies/destroy.rb
@@ -12,8 +12,6 @@ module Mutations
description: copy_field_description(Types::SavedReplyType, :id)
def resolve(id:)
- raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless feature_enabled?
-
saved_reply = authorized_find!(id)
result = ::Users::SavedReplies::DestroyService.new(saved_reply: saved_reply).execute
present_result(result)
diff --git a/app/graphql/mutations/saved_replies/update.rb b/app/graphql/mutations/saved_replies/update.rb
index d9368de7547..f5dc81614d2 100644
--- a/app/graphql/mutations/saved_replies/update.rb
+++ b/app/graphql/mutations/saved_replies/update.rb
@@ -20,8 +20,6 @@ module Mutations
description: copy_field_description(Types::SavedReplyType, :content)
def resolve(id:, name:, content:)
- raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless feature_enabled?
-
saved_reply = authorized_find!(id)
result = ::Users::SavedReplies::UpdateService.new(saved_reply: saved_reply, name: name, content: content).execute
present_result(result)
diff --git a/app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb
index 51a1afdd5ab..2d722b02bf1 100644
--- a/app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb
+++ b/app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from Resolvers::Analytics::CycleAnalytics::BaseCountResolver)
+# rubocop:disable Graphql/ResolverType -- inherited from Resolvers::Analytics::CycleAnalytics::BaseCountResolver
module Resolvers
module Analytics
module CycleAnalytics
diff --git a/app/graphql/resolvers/analytics/cycle_analytics/issue_count_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/issue_count_resolver.rb
index fd20800ee16..32b884df84f 100644
--- a/app/graphql/resolvers/analytics/cycle_analytics/issue_count_resolver.rb
+++ b/app/graphql/resolvers/analytics/cycle_analytics/issue_count_resolver.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from Resolvers::Analytics::CycleAnalytics::BaseIssueResolver)
+# rubocop:disable Graphql/ResolverType -- inherited from Resolvers::Analytics::CycleAnalytics::BaseIssueResolver
module Resolvers
module Analytics
module CycleAnalytics
diff --git a/app/graphql/resolvers/ci/catalog/resource_resolver.rb b/app/graphql/resolvers/ci/catalog/resource_resolver.rb
new file mode 100644
index 00000000000..4b722bd3ec7
--- /dev/null
+++ b/app/graphql/resolvers/ci/catalog/resource_resolver.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ module Catalog
+ class ResourceResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ authorize :read_code
+
+ type ::Types::Ci::Catalog::ResourceType, null: true
+
+ argument :id, ::Types::GlobalIDType[::Ci::Catalog::Resource],
+ required: false,
+ description: 'CI/CD Catalog resource global ID.'
+
+ argument :full_path, GraphQL::Types::ID,
+ required: false,
+ description: 'CI/CD Catalog resource full path.'
+
+ def ready?(**args)
+ unless args[:id].present? ^ args[:full_path].present?
+ raise Gitlab::Graphql::Errors::ArgumentError,
+ "Exactly one of 'id' or 'full_path' arguments is required."
+ end
+
+ super
+ end
+
+ def resolve(id: nil, full_path: nil)
+ if full_path.present?
+ project = Project.find_by_full_path(full_path)
+ authorize!(project)
+
+ raise_resource_not_available_error! unless project.catalog_resource
+
+ project.catalog_resource
+ else
+ catalog_resource = ::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(id))
+ authorize!(catalog_resource&.project)
+
+ catalog_resource
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/catalog/resources_resolver.rb b/app/graphql/resolvers/ci/catalog/resources_resolver.rb
new file mode 100644
index 00000000000..c6904dcd7f6
--- /dev/null
+++ b/app/graphql/resolvers/ci/catalog/resources_resolver.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ module Catalog
+ class ResourcesResolver < BaseResolver
+ include LooksAhead
+
+ type ::Types::Ci::Catalog::ResourceType.connection_type, null: true
+
+ argument :scope, ::Types::Ci::Catalog::ResourceScopeEnum,
+ required: false,
+ default_value: :all,
+ description: 'Scope of the returned catalog resources.'
+
+ argument :search, GraphQL::Types::String,
+ required: false,
+ description: 'Search term to filter the catalog resources by name or description.'
+
+ argument :sort, ::Types::Ci::Catalog::ResourceSortEnum,
+ required: false,
+ description: 'Sort catalog resources by given criteria.'
+
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/429636
+ argument :project_path, GraphQL::Types::ID,
+ required: false,
+ description: 'Project with the namespace catalog.'
+
+ def resolve_with_lookahead(scope:, project_path: nil, search: nil, sort: nil)
+ if project_path.present?
+ project = Project.find_by_full_path(project_path)
+
+ apply_lookahead(
+ ::Ci::Catalog::Listing
+ .new(context[:current_user])
+ .resources(namespace: project.root_namespace, sort: sort, search: search)
+ )
+ elsif scope == :all
+ apply_lookahead(::Ci::Catalog::Listing.new(context[:current_user]).resources(sort: sort, search: search))
+ end
+ end
+
+ private
+
+ def preloads
+ {
+ web_path: { project: { namespace: :route } },
+ readme_html: { project: :route }
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/catalog/versions_resolver.rb b/app/graphql/resolvers/ci/catalog/versions_resolver.rb
new file mode 100644
index 00000000000..046adeb7a67
--- /dev/null
+++ b/app/graphql/resolvers/ci/catalog/versions_resolver.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ module Catalog
+ class VersionsResolver < ::Resolvers::ReleasesResolver
+ type Types::ReleaseType.connection_type, null: true
+
+ # This allows a maximum of 1 call to the field that uses this resolver. If the
+ # field is evaluated on more than one node, it causes performance degradation.
+ extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1
+
+ private
+
+ def get_project
+ object.respond_to?(:project) ? object.project : object
+ end
+
+ # Override the aliased method in ReleasesResolver
+ alias_method :project, :get_project
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/runners_resolver.rb b/app/graphql/resolvers/ci/runners_resolver.rb
index 3289f1d0056..9121c413b1f 100644
--- a/app/graphql/resolvers/ci/runners_resolver.rb
+++ b/app/graphql/resolvers/ci/runners_resolver.rb
@@ -41,6 +41,17 @@ module Resolvers
required: false,
description: 'Filter by upgrade status.'
+ argument :creator_id, ::Types::GlobalIDType[::User].as('UserID'),
+ required: false,
+ description: 'Filter runners by creator ID.'
+
+ argument :version_prefix, GraphQL::Types::String,
+ required: false,
+ description: "Filter runners by version. Runners that contain runner managers with the version at " \
+ "the start of the search term are returned. For example, the search term '14.' returns " \
+ "runner managers with versions '14.11.1' and '14.2.3'.",
+ alpha: { milestone: '16.6' }
+
def resolve_with_lookahead(**args)
apply_lookahead(
::Ci::RunnersFinder
@@ -68,6 +79,9 @@ module Resolvers
upgrade_status: params[:upgrade_status],
search: params[:search],
sort: params[:sort]&.to_s,
+ creator_id:
+ params[:creator_id] ? ::GitlabSchema.parse_gid(params[:creator_id], expected_type: ::User).model_id : nil,
+ version_prefix: params[:version_prefix],
preload: false # we'll handle preloading ourselves
}.compact
.merge(parent_param)
diff --git a/app/graphql/resolvers/concerns/caching_array_resolver.rb b/app/graphql/resolvers/concerns/caching_array_resolver.rb
index 15bf9a90e46..f678e02533d 100644
--- a/app/graphql/resolvers/concerns/caching_array_resolver.rb
+++ b/app/graphql/resolvers/concerns/caching_array_resolver.rb
@@ -132,7 +132,7 @@ module CachingArrayResolver
model_class.arel_table[Arel.star]
end
- # rubocop: disable Graphql/Descriptions (false positive!)
+ # rubocop: disable Graphql/Descriptions -- false positive
def query_limit
field&.max_page_size.presence || context.schema.default_max_page_size
end
diff --git a/app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb b/app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb
index ecb105a64d0..1982b458143 100644
--- a/app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb
+++ b/app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb
@@ -17,7 +17,12 @@ module WorkItems
argument :state,
Types::IssuableStateEnum,
required: false,
- description: 'Current state of the work item.'
+ description: 'Current state of the work item.',
+ prepare: ->(state, _ctx) {
+ return state unless state == 'locked'
+
+ raise Gitlab::Graphql::Errors::ArgumentError, Types::IssuableStateEnum::INVALID_LOCKED_MESSAGE
+ }
argument :types,
[Types::IssueTypeEnum],
as: :issue_types,
diff --git a/app/graphql/resolvers/container_repository_tags_resolver.rb b/app/graphql/resolvers/container_repository_tags_resolver.rb
index 55a83dd49da..bc5006ae06c 100644
--- a/app/graphql/resolvers/container_repository_tags_resolver.rb
+++ b/app/graphql/resolvers/container_repository_tags_resolver.rb
@@ -14,21 +14,61 @@ module Resolvers
required: false,
default_value: nil
+ alias_method :container_repository, :object
+
def resolve(sort:, **filters)
- result = tags
+ if container_repository.migrated? && Feature.enabled?(:use_repository_list_tags_on_graphql, container_repository.project)
+ page_size = [filters[:first], filters[:last]].map(&:to_i).max
+
+ result = container_repository.tags_page(
+ before: filters[:before],
+ last: filters[:after],
+ sort: map_sort_field(sort),
+ name: filters[:name],
+ page_size: page_size
+ )
- if filters[:name]
- result = tags.filter do |tag|
- tag.name.include?(filters[:name])
+ Gitlab::Graphql::ExternallyPaginatedArray.new(
+ parse_pagination_cursor(result, :previous),
+ parse_pagination_cursor(result, :next),
+ *result[:tags]
+ )
+ else
+ result = tags
+
+ if filters[:name]
+ result = tags.filter do |tag|
+ tag.name.include?(filters[:name])
+ end
end
- end
- result = sort_tags(result, sort) if sort
- result
+ result = sort_tags(result, sort) if sort
+ result
+ end
end
private
+ def parse_pagination_cursor(result, direction)
+ pagination_uri = result.dig(:pagination, direction, :uri)
+
+ return unless pagination_uri
+
+ query_params = CGI.parse(pagination_uri.query)
+ key = direction == :previous ? 'before' : 'last'
+
+ query_params[key]&.first
+ end
+
+ def map_sort_field(sort)
+ return unless sort
+
+ sort_field, direction = sort.to_s.split('_')
+ return sort_field if direction == 'asc'
+
+ "-#{sort_field}"
+ end
+
def sort_tags(to_be_sorted, sort)
raise StandardError unless Types::ContainerRepositoryTagsSortEnum.enum.include?(sort)
@@ -41,7 +81,7 @@ module Resolvers
end
def tags
- object.tags
+ container_repository.tags
rescue Faraday::Error
raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, "Can't connect to the Container Registry. If this error persists, please review the troubleshooting documentation."
end
diff --git a/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb b/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb
index 83bb144017c..133b86623f1 100644
--- a/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb
+++ b/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb
@@ -16,16 +16,12 @@ module Resolvers
def resolve(**args)
return { egress_nodes: [] } unless Feature.enabled?(:data_transfer_monitoring, group)
- results = if Feature.enabled?(:data_transfer_monitoring_mock_data, group)
- ::DataTransfer::MockedTransferFinder.new.execute
- else
- ::DataTransfer::GroupDataTransferFinder.new(
- group: group,
- from: args[:from],
- to: args[:to],
- user: current_user
- ).execute.map(&:attributes)
- end
+ results = ::DataTransfer::GroupDataTransferFinder.new(
+ group: group,
+ from: args[:from],
+ to: args[:to],
+ user: current_user
+ ).execute.map(&:attributes)
{ egress_nodes: results.to_a }
end
diff --git a/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb b/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb
index c3296f7d4c3..d711f837251 100644
--- a/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb
+++ b/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb
@@ -16,16 +16,12 @@ module Resolvers
def resolve(**args)
return { egress_nodes: [] } unless Feature.enabled?(:data_transfer_monitoring, project.group)
- results = if Feature.enabled?(:data_transfer_monitoring_mock_data, project.group)
- ::DataTransfer::MockedTransferFinder.new.execute
- else
- ::DataTransfer::ProjectDataTransferFinder.new(
- project: project,
- from: args[:from],
- to: args[:to],
- user: current_user
- ).execute
- end
+ results = ::DataTransfer::ProjectDataTransferFinder.new(
+ project: project,
+ from: args[:from],
+ to: args[:to],
+ user: current_user
+ ).execute
{ egress_nodes: results }
end
diff --git a/app/graphql/resolvers/group_issues_resolver.rb b/app/graphql/resolvers/group_issues_resolver.rb
index 5e0fb27bafa..5a6a3d678b9 100644
--- a/app/graphql/resolvers/group_issues_resolver.rb
+++ b/app/graphql/resolvers/group_issues_resolver.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from Issues::BaseParentResolver)
+# rubocop:disable Graphql/ResolverType -- inherited from Issues::BaseParentResolver
module Resolvers
class GroupIssuesResolver < Issues::BaseParentResolver
def self.issuable_collection_name
diff --git a/app/graphql/resolvers/issues/base_parent_resolver.rb b/app/graphql/resolvers/issues/base_parent_resolver.rb
index 6308e56f049..78ef4132baf 100644
--- a/app/graphql/resolvers/issues/base_parent_resolver.rb
+++ b/app/graphql/resolvers/issues/base_parent_resolver.rb
@@ -7,8 +7,13 @@ module Resolvers
include ::Issues::SortArguments
argument :state, Types::IssuableStateEnum,
- required: false,
- description: 'Current state of this issue.'
+ required: false,
+ description: 'Current state of this issue.',
+ prepare: ->(state, _ctx) {
+ return state unless state == 'locked'
+
+ raise Gitlab::Graphql::Errors::ArgumentError, Types::IssuableStateEnum::INVALID_LOCKED_MESSAGE
+ }
# see app/graphql/types/issue_connection.rb
type 'Types::IssueConnection', null: true
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index 34f14eee0e5..bc0e7334303 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -14,7 +14,12 @@ module Resolvers
description: 'Whether to include issues from archived projects. Defaults to `false`.'
argument :state, Types::IssuableStateEnum,
required: false,
- description: 'Current state of this issue.'
+ description: 'Current state of this issue.',
+ prepare: ->(state, _ctx) {
+ return state unless state == 'locked'
+
+ raise Gitlab::Graphql::Errors::ArgumentError, Types::IssuableStateEnum::INVALID_LOCKED_MESSAGE
+ }
# see app/graphql/types/issue_connection.rb
type 'Types::IssueConnection', null: true
diff --git a/app/graphql/resolvers/namespaces/work_items_resolver.rb b/app/graphql/resolvers/namespaces/work_items_resolver.rb
index 6985a7a898a..671788668b1 100644
--- a/app/graphql/resolvers/namespaces/work_items_resolver.rb
+++ b/app/graphql/resolvers/namespaces/work_items_resolver.rb
@@ -2,7 +2,7 @@
module Resolvers
module Namespaces
- # rubocop:disable Graphql/ResolverType (inherited from Resolvers::WorkItemsResolver)
+ # rubocop:disable Graphql/ResolverType -- inherited from Resolvers::WorkItemsResolver
class WorkItemsResolver < ::Resolvers::WorkItemsResolver
def ready?(**args)
return false if Feature.disabled?(:namespace_level_work_items, resource_parent)
diff --git a/app/graphql/resolvers/packages_base_resolver.rb b/app/graphql/resolvers/packages_base_resolver.rb
index 7d153d16910..7e5d89a7897 100644
--- a/app/graphql/resolvers/packages_base_resolver.rb
+++ b/app/graphql/resolvers/packages_base_resolver.rb
@@ -19,6 +19,12 @@ module Resolvers
required: false,
default_value: nil
+ argument :package_version, GraphQL::Types::String,
+ description: 'Filter a package by version. If used in combination with `include_versionless`,
+ then no versionless packages are returned.',
+ required: false,
+ default_value: nil
+
argument :status, Types::Packages::PackageStatusEnum,
description: 'Filter a package by status.',
required: false,
diff --git a/app/graphql/resolvers/project_issues_resolver.rb b/app/graphql/resolvers/project_issues_resolver.rb
index f869d8f11c6..2bc610e8266 100644
--- a/app/graphql/resolvers/project_issues_resolver.rb
+++ b/app/graphql/resolvers/project_issues_resolver.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from Issues::BaseParentResolver)
+# rubocop:disable Graphql/ResolverType -- inherited from Issues::BaseParentResolver
module Resolvers
class ProjectIssuesResolver < Issues::BaseParentResolver
accept_release_tag
diff --git a/app/graphql/resolvers/project_members_resolver.rb b/app/graphql/resolvers/project_members_resolver.rb
index e889b47c000..a27183438cd 100644
--- a/app/graphql/resolvers/project_members_resolver.rb
+++ b/app/graphql/resolvers/project_members_resolver.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from MembersResolver)
+
+# rubocop:disable Graphql/ResolverType -- inherited from MembersResolver
module Resolvers
class ProjectMembersResolver < MembersResolver
@@ -17,3 +18,4 @@ module Resolvers
end
end
end
+# rubocop:enable Graphql/ResolverType
diff --git a/app/graphql/resolvers/project_milestones_resolver.rb b/app/graphql/resolvers/project_milestones_resolver.rb
index 567a55aa09b..cb4e9a5cdf7 100644
--- a/app/graphql/resolvers/project_milestones_resolver.rb
+++ b/app/graphql/resolvers/project_milestones_resolver.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from MilestonesResolver)
module Resolvers
class ProjectMilestonesResolver < MilestonesResolver
diff --git a/app/graphql/resolvers/projects/snippets_resolver.rb b/app/graphql/resolvers/projects/snippets_resolver.rb
index 448918be2f5..9ab9db21e89 100644
--- a/app/graphql/resolvers/projects/snippets_resolver.rb
+++ b/app/graphql/resolvers/projects/snippets_resolver.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from ResolvesSnippets)
+
+# rubocop:disable Graphql/ResolverType -- inherited from ResolvesSnippets
module Resolvers
module Projects
@@ -27,3 +28,4 @@ module Resolvers
end
end
end
+# rubocop:enable Graphql/ResolverType
diff --git a/app/graphql/resolvers/projects_resolver.rb b/app/graphql/resolvers/projects_resolver.rb
index 8dd409a8173..450caa9aff6 100644
--- a/app/graphql/resolvers/projects_resolver.rb
+++ b/app/graphql/resolvers/projects_resolver.rb
@@ -3,6 +3,7 @@
module Resolvers
class ProjectsResolver < BaseResolver
include ProjectSearchArguments
+ include LooksAhead
type Types::ProjectType.connection_type, null: true
@@ -10,6 +11,10 @@ module Resolvers
required: false,
description: 'Filter projects by IDs.'
+ argument :full_paths, [GraphQL::Types::String],
+ required: false,
+ description: 'Filter projects by full paths. You cannot provide more than 50 full paths.'
+
argument :sort, GraphQL::Types::String,
required: false,
description: "Sort order of results. Format: `<field_name>_<sort_direction>`, " \
@@ -23,19 +28,48 @@ module Resolvers
required: false,
description: "Return only projects with merge requests enabled."
- def resolve(**args)
- ProjectsFinder
+ def resolve_with_lookahead(**args)
+ validate_args!(args)
+
+ projects = ProjectsFinder
.new(current_user: current_user, params: finder_params(args), project_ids_relation: parse_gids(args[:ids]))
.execute
+
+ apply_lookahead(projects)
end
private
+ def validate_args!(args)
+ return unless args[:full_paths].present? && args[:full_paths].length > 50
+
+ raise Gitlab::Graphql::Errors::ArgumentError, 'You cannot provide more than 50 full_paths'
+ end
+
+ def unconditional_includes
+ [:creator, :group, :invited_groups, :project_setting]
+ end
+
+ def preloads
+ {
+ full_path: [:route],
+ topics: [:topics],
+ import_status: [:import_state],
+ service_desk_address: [:project_feature, :service_desk_setting],
+ jira_import_status: [:jira_imports],
+ container_repositories: [:container_repositories],
+ container_repositories_count: [:container_repositories],
+ web_url: { namespace: [:route] },
+ is_catalog_resource: [:catalog_resource]
+ }
+ end
+
def finder_params(args)
{
**project_finder_params(args),
with_issues_enabled: args[:with_issues_enabled],
- with_merge_requests_enabled: args[:with_merge_requests_enabled]
+ with_merge_requests_enabled: args[:with_merge_requests_enabled],
+ full_paths: args[:full_paths]
}
end
@@ -44,3 +78,5 @@ module Resolvers
end
end
end
+
+Resolvers::ProjectsResolver.prepend_mod_with('Resolvers::ProjectsResolver')
diff --git a/app/graphql/resolvers/saved_reply_resolver.rb b/app/graphql/resolvers/saved_reply_resolver.rb
index 96bbc139c96..1a5f2c9be78 100644
--- a/app/graphql/resolvers/saved_reply_resolver.rb
+++ b/app/graphql/resolvers/saved_reply_resolver.rb
@@ -11,8 +11,6 @@ module Resolvers
description: 'ID of a saved reply.'
def resolve(id:)
- return unless Feature.enabled?(:saved_replies, current_user)
-
saved_reply = ::Users::SavedReply.find_saved_reply(user_id: current_user.id, id: id.model_id)
return unless saved_reply
diff --git a/app/graphql/resolvers/snippets_resolver.rb b/app/graphql/resolvers/snippets_resolver.rb
index 90f5f2cb534..759cc61a8a7 100644
--- a/app/graphql/resolvers/snippets_resolver.rb
+++ b/app/graphql/resolvers/snippets_resolver.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from ResolvesSnippets)
+
+# rubocop:disable Graphql/ResolverType -- inherited from ResolvesSnippets
module Resolvers
class SnippetsResolver < BaseResolver
@@ -45,3 +46,4 @@ module Resolvers
end
end
end
+# rubocop:enable Graphql/ResolverType
diff --git a/app/graphql/resolvers/users/frecent_groups_resolver.rb b/app/graphql/resolvers/users/frecent_groups_resolver.rb
new file mode 100644
index 00000000000..2fc757e31ab
--- /dev/null
+++ b/app/graphql/resolvers/users/frecent_groups_resolver.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Users
+ class FrecentGroupsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type [Types::GroupType], null: true
+
+ def resolve
+ return unless current_user.present?
+
+ if Feature.disabled?(:frecent_namespaces_suggestions, current_user)
+ raise_resource_not_available_error!("'frecent_namespaces_suggestions' feature flag is disabled")
+ end
+
+ return unless Feature.enabled?(:frecent_namespaces_suggestions, current_user)
+
+ ::Users::GroupVisit.frecent_groups(user_id: current_user.id)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/users/frecent_projects_resolver.rb b/app/graphql/resolvers/users/frecent_projects_resolver.rb
new file mode 100644
index 00000000000..397d4ca0cfd
--- /dev/null
+++ b/app/graphql/resolvers/users/frecent_projects_resolver.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Users
+ class FrecentProjectsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type [Types::ProjectType], null: true
+
+ def resolve
+ return unless current_user.present?
+
+ if Feature.disabled?(:frecent_namespaces_suggestions, current_user)
+ raise_resource_not_available_error!("'frecent_namespaces_suggestions' feature flag is disabled")
+ end
+
+ ::Users::ProjectVisit.frecent_projects(user_id: current_user.id)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/users/organizations_resolver.rb b/app/graphql/resolvers/users/organizations_resolver.rb
new file mode 100644
index 00000000000..ffc1a141eb6
--- /dev/null
+++ b/app/graphql/resolvers/users/organizations_resolver.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Users
+ class OrganizationsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type Types::Organizations::OrganizationType.connection_type, null: true
+
+ authorize :read_user_organizations
+ authorizes_object!
+
+ def resolve(**args)
+ ::Organizations::UserOrganizationsFinder.new(current_user, object, args).execute
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/users/snippets_resolver.rb b/app/graphql/resolvers/users/snippets_resolver.rb
index 75bba8debab..ea5f6b7b8c9 100644
--- a/app/graphql/resolvers/users/snippets_resolver.rb
+++ b/app/graphql/resolvers/users/snippets_resolver.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from ResolvesSnippets)
+
+# rubocop:disable Graphql/ResolverType -- inherited from ResolvesSnippets
module Resolvers
module Users
@@ -27,3 +28,4 @@ module Resolvers
end
end
end
+# rubocop:enable Graphql/ResolverType
diff --git a/app/graphql/resolvers/work_items/linked_items_resolver.rb b/app/graphql/resolvers/work_items/linked_items_resolver.rb
index 108d5d41b62..f2ff1205d3a 100644
--- a/app/graphql/resolvers/work_items/linked_items_resolver.rb
+++ b/app/graphql/resolvers/work_items/linked_items_resolver.rb
@@ -3,6 +3,8 @@
module Resolvers
module WorkItems
class LinkedItemsResolver < BaseResolver
+ prepend ::WorkItems::LookAheadPreloads
+
alias_method :linked_items_widget, :object
argument :filter, Types::WorkItems::RelatedLinkTypeEnum,
@@ -13,30 +15,28 @@ module Resolvers
type Types::WorkItems::LinkedItemType.connection_type, null: true
- def resolve(filter: nil)
- related_work_items(filter).map do |related_work_item|
- {
- link_id: related_work_item.issue_link_id,
- link_type: related_work_item.issue_link_type,
- link_created_at: related_work_item.issue_link_created_at,
- link_updated_at: related_work_item.issue_link_updated_at,
- work_item: related_work_item
- }
- end
+ def resolve_with_lookahead(**args)
+ apply_lookahead(related_work_items(args))
end
private
- def related_work_items(type)
- return [] unless work_item.resource_parent.linked_work_items_feature_flag_enabled?
+ def related_work_items(args)
+ return WorkItem.none unless work_item.resource_parent.linked_work_items_feature_flag_enabled?
- work_item.linked_work_items(current_user, preload: { project: [:project_feature, :group] }, link_type: type)
+ offset_pagination(
+ work_item.linked_work_items(authorize: false, link_type: args[:filter])
+ )
end
def work_item
linked_items_widget.work_item
end
strong_memoize_attr :work_item
+
+ def node_selection(selection = lookahead)
+ super.selection(:work_item)
+ end
end
end
end
diff --git a/app/graphql/types/abuse_report_type.rb b/app/graphql/types/abuse_report_type.rb
index 012e709cdb5..2532530cfa9 100644
--- a/app/graphql/types/abuse_report_type.rb
+++ b/app/graphql/types/abuse_report_type.rb
@@ -3,9 +3,18 @@
module Types
class AbuseReportType < BaseObject
graphql_name 'AbuseReport'
+
+ implements Types::Notes::NoteableInterface
+
description 'An abuse report'
+
authorize :read_abuse_report
+ expose_permissions Types::PermissionTypes::AbuseReport
+
+ field :id, Types::GlobalIDType[::AbuseReport],
+ null: false, description: 'Global ID of the abuse report.'
+
field :labels, ::Types::LabelType.connection_type,
null: true, description: 'Labels of the abuse report.'
end
diff --git a/app/graphql/types/analytics/cycle_analytics/value_stream_type.rb b/app/graphql/types/analytics/cycle_analytics/value_stream_type.rb
new file mode 100644
index 00000000000..16ce9b82718
--- /dev/null
+++ b/app/graphql/types/analytics/cycle_analytics/value_stream_type.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Types
+ module Analytics
+ module CycleAnalytics
+ class ValueStreamType < BaseObject
+ graphql_name 'ValueStream'
+
+ authorize :read_cycle_analytics
+
+ field :id,
+ type: ::Types::GlobalIDType[::Analytics::CycleAnalytics::ValueStream],
+ null: false,
+ description: "ID of the value stream."
+
+ field :name,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Name of the value stream.'
+
+ field :namespace, Types::NamespaceType,
+ null: false,
+ description: 'Namespace the value stream belongs to.'
+
+ field :project, Types::ProjectType,
+ null: true,
+ description: 'Project the value stream belongs to, returns empty if it belongs to a group.',
+ alpha: { milestone: '15.6' }
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/base_argument.rb b/app/graphql/types/base_argument.rb
index cda7fa4a5df..3b4223c3ba1 100644
--- a/app/graphql/types/base_argument.rb
+++ b/app/graphql/types/base_argument.rb
@@ -9,29 +9,7 @@ module Types
def initialize(*args, **kwargs, &block)
@doc_reference = kwargs.delete(:see)
- # our custom addition `nullable` which allows us to declare
- # an argument that must be provided, even if its value is null.
- # When `required: true` then required arguments must not be null.
- @gl_required = !!kwargs[:required]
- @gl_nullable = kwargs[:required] == :nullable
-
- # Only valid if an argument is also required.
- if @gl_nullable
- # Since the framework asserts that "required" means "cannot be null"
- # we have to switch off "required" but still do the check in `ready?` behind the scenes
- kwargs[:required] = false
- end
-
super(*args, **kwargs, &block)
end
-
- def accepts?(value)
- # if the argument is declared as required, it must be included
- return false if @gl_required && value == :not_given
- # if the argument is declared as required, the value can only be null IF it is also nullable.
- return false if @gl_required && value.nil? && !@gl_nullable
-
- true
- end
end
end
diff --git a/app/graphql/types/base_input_object.rb b/app/graphql/types/base_input_object.rb
index 90a29b0cfb8..d14da9ac878 100644
--- a/app/graphql/types/base_input_object.rb
+++ b/app/graphql/types/base_input_object.rb
@@ -3,5 +3,7 @@
module Types
class BaseInputObject < GraphQL::Schema::InputObject
prepend Gitlab::Graphql::CopyFieldDescription
+
+ argument_class ::Types::BaseArgument
end
end
diff --git a/app/graphql/types/ci/catalog/resource_scope_enum.rb b/app/graphql/types/ci/catalog/resource_scope_enum.rb
new file mode 100644
index 00000000000..b825c3a7925
--- /dev/null
+++ b/app/graphql/types/ci/catalog/resource_scope_enum.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ module Catalog
+ class ResourceScopeEnum < BaseEnum
+ graphql_name 'CiCatalogResourceScope'
+ description 'Values for scoping catalog resources'
+
+ value 'ALL', 'All catalog resources visible to the current user.', value: :all
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/catalog/resource_sort_enum.rb b/app/graphql/types/ci/catalog/resource_sort_enum.rb
new file mode 100644
index 00000000000..bb0b5a6e0eb
--- /dev/null
+++ b/app/graphql/types/ci/catalog/resource_sort_enum.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ module Catalog
+ class ResourceSortEnum < BaseEnum
+ graphql_name 'CiCatalogResourceSort'
+ description 'Values for sorting catalog resources'
+
+ value 'NAME_ASC', 'Name by ascending order.', value: :name_asc
+ value 'NAME_DESC', 'Name by descending order.', value: :name_desc
+ value 'LATEST_RELEASED_AT_ASC', 'Latest release date by ascending order.', value: :latest_released_at_asc
+ value 'LATEST_RELEASED_AT_DESC', 'Latest release date by descending order.', value: :latest_released_at_desc
+ value 'CREATED_ASC', 'Created date by ascending order.', value: :created_at_asc
+ value 'CREATED_DESC', 'Created date by descending order.', value: :created_at_desc
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/catalog/resource_type.rb b/app/graphql/types/ci/catalog/resource_type.rb
new file mode 100644
index 00000000000..119313ae52b
--- /dev/null
+++ b/app/graphql/types/ci/catalog/resource_type.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ module Catalog
+ # rubocop: disable Graphql/AuthorizeTypes
+ class ResourceType < BaseObject
+ graphql_name 'CiCatalogResource'
+
+ connection_type_class Types::CountableConnectionType
+
+ field :open_issues_count, GraphQL::Types::Int, null: false,
+ description: 'Count of open issues that belong to the the catalog resource.',
+ alpha: { milestone: '16.3' }
+
+ field :open_merge_requests_count, GraphQL::Types::Int, null: false,
+ description: 'Count of open merge requests that belong to the the catalog resource.',
+ alpha: { milestone: '16.3' }
+
+ field :id, GraphQL::Types::ID, null: false, description: 'ID of the catalog resource.',
+ alpha: { milestone: '15.11' }
+
+ field :name, GraphQL::Types::String, null: true, description: 'Name of the catalog resource.',
+ alpha: { milestone: '15.11' }
+
+ field :description, GraphQL::Types::String, null: true, description: 'Description of the catalog resource.',
+ alpha: { milestone: '15.11' }
+
+ field :icon, GraphQL::Types::String, null: true, description: 'Icon for the catalog resource.',
+ method: :avatar_path, alpha: { milestone: '15.11' }
+
+ field :web_path, GraphQL::Types::String, null: true, description: 'Web path of the catalog resource.',
+ alpha: { milestone: '16.1' }
+
+ field :versions, Types::ReleaseType.connection_type, null: true,
+ description: 'Versions of the catalog resource. This field can only be ' \
+ 'resolved for one catalog resource in any single request.',
+ resolver: Resolvers::Ci::Catalog::VersionsResolver,
+ alpha: { milestone: '16.2' }
+
+ field :latest_version, Types::ReleaseType, null: true, description: 'Latest version of the catalog resource.',
+ alpha: { milestone: '16.1' }
+
+ field :latest_released_at, Types::TimeType, null: true,
+ description: "Release date of the catalog resource's latest version.",
+ alpha: { milestone: '16.5' }
+
+ field :star_count, GraphQL::Types::Int, null: false,
+ description: 'Number of times the catalog resource has been starred.',
+ alpha: { milestone: '16.1' }
+
+ field :forks_count, GraphQL::Types::Int, null: false, calls_gitaly: true,
+ description: 'Number of times the catalog resource has been forked.',
+ alpha: { milestone: '16.1' }
+
+ field :root_namespace, Types::NamespaceType, null: true,
+ description: 'Root namespace of the catalog resource.',
+ alpha: { milestone: '16.1' }
+
+ markdown_field :readme_html, null: false,
+ alpha: { milestone: '16.1' }
+
+ def open_issues_count
+ BatchLoader::GraphQL.wrap(object.project.open_issues_count)
+ end
+
+ def open_merge_requests_count
+ BatchLoader::GraphQL.wrap(object.project.open_merge_requests_count)
+ end
+
+ def web_path
+ ::Gitlab::Routing.url_helpers.project_path(object.project)
+ end
+
+ def latest_version
+ BatchLoader::GraphQL.for(object.project).batch do |projects, loader|
+ latest_releases = ReleasesFinder.new(projects, current_user, latest: true).execute
+
+ latest_releases.index_by(&:project).each do |project, latest_release|
+ loader.call(project, latest_release)
+ end
+ end
+ end
+
+ def forks_count
+ BatchLoader::GraphQL.wrap(object.forks_count)
+ end
+
+ def root_namespace
+ BatchLoader::GraphQL.for(object.project_id).batch do |project_ids, loader|
+ projects = Project.id_in(project_ids)
+
+ # This preloader uses traversal_ids to obtain Group-type root namespaces.
+ # It also preloads each project's immediate parent namespace, which effectively
+ # preloads the User-type root namespaces since they cannot be nested (parent == root).
+ Preloaders::ProjectRootAncestorPreloader.new(projects, :group).execute
+ root_namespaces = projects.map(&:root_ancestor)
+
+ # NamespaceType requires the `:read_namespace` ability. We must preload the policy for
+ # Group-type namespaces to avoid N+1 queries caused by the authorization requests.
+ group_root_namespaces = root_namespaces.select { |n| n.type == ::Group.sti_name }
+ Preloaders::GroupPolicyPreloader.new(group_root_namespaces, current_user).execute
+
+ # For User-type namespaces, the authorization request requires preloading the owner objects.
+ user_root_namespaces = root_namespaces.select { |n| n.type == ::Namespaces::UserNamespace.sti_name }
+ ActiveRecord::Associations::Preloader.new(records: user_root_namespaces, associations: :owner).call
+
+ projects.each { |project| loader.call(project.id, project.root_ancestor) }
+ end
+ end
+
+ def readme_html_resolver
+ markdown_context = context.to_h.dup.merge(project: object.project)
+ ::MarkupHelper.markdown(object.project.repository.readme&.data, markdown_context)
+ end
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+ end
+end
diff --git a/app/graphql/types/ci/pipeline_status_enum.rb b/app/graphql/types/ci/pipeline_status_enum.rb
index c8e031e18ea..17cf48bb5cf 100644
--- a/app/graphql/types/ci/pipeline_status_enum.rb
+++ b/app/graphql/types/ci/pipeline_status_enum.rb
@@ -7,6 +7,7 @@ module Types
created: 'Pipeline has been created.',
waiting_for_resource: 'A resource (for example, a runner) that the pipeline requires to run is unavailable.',
preparing: 'Pipeline is preparing to run.',
+ waiting_for_callback: 'Pipeline is waiting for an external action.',
pending: 'Pipeline has not started running yet.',
running: 'Pipeline is running.',
failed: 'At least one stage of the pipeline failed.',
diff --git a/app/graphql/types/container_registry/protection/rule_access_level_enum.rb b/app/graphql/types/container_registry/protection/rule_access_level_enum.rb
new file mode 100644
index 00000000000..31e8cbe2e49
--- /dev/null
+++ b/app/graphql/types/container_registry/protection/rule_access_level_enum.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Types
+ module ContainerRegistry
+ module Protection
+ class RuleAccessLevelEnum < BaseEnum
+ graphql_name 'ContainerRegistryProtectionRuleAccessLevel'
+ description 'Access level of a container registry protection rule resource'
+
+ ::ContainerRegistry::Protection::Rule.push_protected_up_to_access_levels.each_key do |access_level_key|
+ value access_level_key.upcase, value: access_level_key.to_s,
+ description: "#{access_level_key.capitalize} access."
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/container_registry/protection/rule_type.rb b/app/graphql/types/container_registry/protection/rule_type.rb
new file mode 100644
index 00000000000..387f0202d2d
--- /dev/null
+++ b/app/graphql/types/container_registry/protection/rule_type.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Types
+ module ContainerRegistry
+ module Protection
+ class RuleType < ::Types::BaseObject
+ graphql_name 'ContainerRegistryProtectionRule'
+ description 'A container registry protection rule designed to prevent users with a certain ' \
+ 'access level or lower from altering the container registry.'
+
+ authorize :admin_container_image
+
+ field :id,
+ ::Types::GlobalIDType[::ContainerRegistry::Protection::Rule],
+ null: false,
+ description: 'ID of the container registry protection rule.'
+
+ field :container_path_pattern,
+ GraphQL::Types::String,
+ null: false,
+ description:
+ 'Container repository path pattern protected by the protection rule. ' \
+ 'For example `@my-scope/my-container-*`. Wildcard character `*` allowed.'
+
+ field :push_protected_up_to_access_level,
+ Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
+ null: false,
+ description:
+ 'Max GitLab access level to prevent from pushing container images to the container registry. ' \
+ 'For example `DEVELOPER`, `MAINTAINER`, `OWNER`.'
+
+ field :delete_protected_up_to_access_level,
+ Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
+ null: false,
+ description:
+ 'Max GitLab access level to prevent from pushing container images to the container registry. ' \
+ 'For example `DEVELOPER`, `MAINTAINER`, `OWNER`.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/container_repository_details_type.rb b/app/graphql/types/container_repository_details_type.rb
index 1ee9e76a1c8..b043a7c9d8d 100644
--- a/app/graphql/types/container_repository_details_type.rb
+++ b/app/graphql/types/container_repository_details_type.rb
@@ -13,7 +13,8 @@ module Types
null: true,
description: 'Tags of the container repository.',
max_page_size: 20,
- resolver: Resolvers::ContainerRepositoryTagsResolver
+ resolver: Resolvers::ContainerRepositoryTagsResolver,
+ connection_extension: Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension
field :size,
GraphQL::Types::Float,
diff --git a/app/graphql/types/data_transfer/project_data_transfer_type.rb b/app/graphql/types/data_transfer/project_data_transfer_type.rb
index 36afa20194e..363b675209d 100644
--- a/app/graphql/types/data_transfer/project_data_transfer_type.rb
+++ b/app/graphql/types/data_transfer/project_data_transfer_type.rb
@@ -13,7 +13,6 @@ module Types
def total_egress(parent:)
return unless Feature.enabled?(:data_transfer_monitoring, parent.group)
- return 40_000_000 if Feature.enabled?(:data_transfer_monitoring_mock_data, parent.group)
object[:egress_nodes].sum('repository_egress + artifacts_egress + packages_egress + registry_egress')
end
diff --git a/app/graphql/types/group_member_type.rb b/app/graphql/types/group_member_type.rb
index 2745853c9bb..d494c55369d 100644
--- a/app/graphql/types/group_member_type.rb
+++ b/app/graphql/types/group_member_type.rb
@@ -11,11 +11,11 @@ module Types
implements MemberInterface
field :group, Types::GroupType, null: true,
- description: 'Group that a User is a member of.'
+ description: 'Group that a user is a member of.'
field :notification_email,
resolver: Resolvers::GroupMembers::NotificationEmailResolver,
- description: "Group notification email for User. Only available 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/issuable_state_enum.rb b/app/graphql/types/issuable_state_enum.rb
index 5a1b11b3bdc..8e3ed1d4bc8 100644
--- a/app/graphql/types/issuable_state_enum.rb
+++ b/app/graphql/types/issuable_state_enum.rb
@@ -1,10 +1,15 @@
# frozen_string_literal: true
+# DO NOT use this ENUM with issues. We need to define a new enum in places where we
+# need to filter by state. locked is not a valid state filter for issues. More info in
+# https://gitlab.com/gitlab-org/gitlab/-/issues/420667#note_1605900474
module Types
class IssuableStateEnum < BaseEnum
graphql_name 'IssuableState'
description 'State of a GitLab issue or merge request'
+ INVALID_LOCKED_MESSAGE = 'locked is not a valid state filter for issues.'
+
value 'opened', description: 'In open state.'
value 'closed', description: 'In closed state.'
value 'locked', description: 'Discussion has been locked.'
diff --git a/app/graphql/types/merge_request_review_state_enum.rb b/app/graphql/types/merge_request_review_state_enum.rb
index 45f97758425..c7c82de2906 100644
--- a/app/graphql/types/merge_request_review_state_enum.rb
+++ b/app/graphql/types/merge_request_review_state_enum.rb
@@ -5,7 +5,11 @@ module Types
graphql_name 'MergeRequestReviewState'
description 'State of a review of a GitLab merge request.'
- from_rails_enum(::MergeRequestReviewer.states,
- description: "The merge request is %{name}.")
+ value 'UNREVIEWED', value: 'unreviewed',
+ description: 'Awaiting review from merge request reviewer.'
+ value 'REVIEWED', value: 'reviewed',
+ description: 'Merge request reviewer has reviewed.'
+ value 'REQUESTED_CHANGES', value: 'requested_changes',
+ description: 'Merge request reviewer has requested changes.'
end
end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index e6625e44508..9dca82f1750 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -106,7 +106,8 @@ module Types
null: false,
description: 'Status of all mergeability checks of the merge request.',
method: :all_mergeability_checks_results,
- alpha: { milestone: '16.5' }
+ alpha: { milestone: '16.5' },
+ calls_gitaly: true
field :mergeable_discussions_state, GraphQL::Types::Boolean, null: true,
calls_gitaly: true,
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 3af7140aed3..e1bd1f603ad 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -106,6 +106,7 @@ module Types
mount_mutation Mutations::Notes::Update::ImageDiffNote
mount_mutation Mutations::Notes::RepositionImageDiffNote
mount_mutation Mutations::Notes::Destroy
+ mount_mutation Mutations::Organizations::Create, alpha: { milestone: '16.6' }
mount_mutation Mutations::Projects::SyncFork, calls_gitaly: true, alpha: { milestone: '15.9' }
mount_mutation Mutations::Releases::Create
mount_mutation Mutations::Releases::Update
@@ -134,33 +135,36 @@ module Types
mount_mutation Mutations::DesignManagement::Move
mount_mutation Mutations::DesignManagement::Update
mount_mutation Mutations::ContainerExpirationPolicies::Update
+ mount_mutation Mutations::ContainerRegistry::Protection::Rule::Create, alpha: { milestone: '16.6' }
mount_mutation Mutations::ContainerRepositories::Destroy
mount_mutation Mutations::ContainerRepositories::DestroyTags
+ mount_mutation Mutations::Ci::Catalog::Resources::Create, alpha: { milestone: '15.11' }
+ mount_mutation Mutations::Ci::Catalog::Resources::Unpublish, alpha: { milestone: '16.6' }
+ mount_mutation Mutations::Ci::Job::Cancel
+ mount_mutation Mutations::Ci::Job::Play
+ mount_mutation Mutations::Ci::Job::Retry
+ mount_mutation Mutations::Ci::Job::ArtifactsDestroy
+ mount_mutation Mutations::Ci::Job::Unschedule
+ mount_mutation Mutations::Ci::JobTokenScope::AddProject
+ mount_mutation Mutations::Ci::JobArtifact::BulkDestroy, alpha: { milestone: '15.10' }
+ mount_mutation Mutations::Ci::JobArtifact::Destroy
+ mount_mutation Mutations::Ci::JobTokenScope::RemoveProject
mount_mutation Mutations::Ci::Pipeline::Cancel
mount_mutation Mutations::Ci::Pipeline::Destroy
mount_mutation Mutations::Ci::Pipeline::Retry
+ mount_mutation Mutations::Ci::PipelineSchedule::Create
mount_mutation Mutations::Ci::PipelineSchedule::Delete
- mount_mutation Mutations::Ci::PipelineSchedule::TakeOwnership
mount_mutation Mutations::Ci::PipelineSchedule::Play
- mount_mutation Mutations::Ci::PipelineSchedule::Create
+ mount_mutation Mutations::Ci::PipelineSchedule::TakeOwnership
mount_mutation Mutations::Ci::PipelineSchedule::Update
mount_mutation Mutations::Ci::PipelineTrigger::Create, alpha: { milestone: '16.3' }
- mount_mutation Mutations::Ci::PipelineTrigger::Update, alpha: { milestone: '16.3' }
mount_mutation Mutations::Ci::PipelineTrigger::Delete, alpha: { milestone: '16.3' }
+ mount_mutation Mutations::Ci::PipelineTrigger::Update, alpha: { milestone: '16.3' }
mount_mutation Mutations::Ci::ProjectCiCdSettingsUpdate
- mount_mutation Mutations::Ci::Job::ArtifactsDestroy
- mount_mutation Mutations::Ci::Job::Play
- mount_mutation Mutations::Ci::Job::Retry
- mount_mutation Mutations::Ci::Job::Cancel
- mount_mutation Mutations::Ci::Job::Unschedule
- mount_mutation Mutations::Ci::JobArtifact::Destroy
- mount_mutation Mutations::Ci::JobArtifact::BulkDestroy, alpha: { milestone: '15.10' }
- mount_mutation Mutations::Ci::JobTokenScope::AddProject
- mount_mutation Mutations::Ci::JobTokenScope::RemoveProject
+ mount_mutation Mutations::Ci::Runner::BulkDelete, alpha: { milestone: '15.3' }
mount_mutation Mutations::Ci::Runner::Create, alpha: { milestone: '15.10' }
- mount_mutation Mutations::Ci::Runner::Update
mount_mutation Mutations::Ci::Runner::Delete
- mount_mutation Mutations::Ci::Runner::BulkDelete, alpha: { milestone: '15.3' }
+ mount_mutation Mutations::Ci::Runner::Update
mount_mutation Mutations::Ci::RunnersRegistrationToken::Reset
mount_mutation Mutations::Namespace::PackageSettings::Update
mount_mutation Mutations::Groups::Update
@@ -171,6 +175,7 @@ module Types
extensions: [::Gitlab::Graphql::Limit::FieldCallCount => { limit: 1 }]
mount_mutation Mutations::Packages::DestroyFile
mount_mutation Mutations::Packages::Protection::Rule::Create, alpha: { milestone: '16.5' }
+ mount_mutation Mutations::Packages::Protection::Rule::Delete, alpha: { milestone: '16.6' }
mount_mutation Mutations::Packages::DestroyFiles
mount_mutation Mutations::Packages::Cleanup::Policy::Update
mount_mutation Mutations::Echo
diff --git a/app/graphql/types/namespace/package_settings_type.rb b/app/graphql/types/namespace/package_settings_type.rb
index 61240243b1f..6c6144f2357 100644
--- a/app/graphql/types/namespace/package_settings_type.rb
+++ b/app/graphql/types/namespace/package_settings_type.rb
@@ -20,21 +20,18 @@ module Types
field :maven_duplicates_allowed, GraphQL::Types::Boolean,
null: false,
description: 'Indicates whether duplicate Maven packages are allowed for this namespace.'
- field :nuget_duplicate_exception_regex, Types::UntrustedRegexp,
- null: true,
- description: 'When nuget_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect. ' \
- 'Error is raised if `nuget_duplicates_option` feature flag is disabled.'
- field :nuget_duplicates_allowed, GraphQL::Types::Boolean,
- null: false,
- description: 'Indicates whether duplicate NuGet packages are allowed for this namespace. ' \
- 'Error is raised if `nuget_duplicates_option` feature flag is disabled.'
-
field :maven_package_requests_forwarding, GraphQL::Types::Boolean,
null: true,
description: 'Indicates whether Maven package forwarding is allowed for this namespace.'
field :npm_package_requests_forwarding, GraphQL::Types::Boolean,
null: true,
description: 'Indicates whether npm package forwarding is allowed for this namespace.'
+ field :nuget_duplicate_exception_regex, Types::UntrustedRegexp,
+ null: true,
+ description: 'When nuget_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect. '
+ field :nuget_duplicates_allowed, GraphQL::Types::Boolean,
+ null: false,
+ description: 'Indicates whether duplicate NuGet packages are allowed for this namespace. '
field :pypi_package_requests_forwarding, GraphQL::Types::Boolean,
null: true,
description: 'Indicates whether PyPI package forwarding is allowed for this namespace.'
diff --git a/app/graphql/types/notes/noteable_interface.rb b/app/graphql/types/notes/noteable_interface.rb
index 9971511d6ce..7c75f213e24 100644
--- a/app/graphql/types/notes/noteable_interface.rb
+++ b/app/graphql/types/notes/noteable_interface.rb
@@ -21,6 +21,8 @@ module Types
Types::DesignManagement::DesignType
when ::AlertManagement::Alert
Types::AlertManagement::AlertType
+ when AbuseReport
+ Types::AbuseReportType
else
raise "Unknown GraphQL type for #{object}"
end
diff --git a/app/graphql/types/organizations/organization_type.rb b/app/graphql/types/organizations/organization_type.rb
index cae0ef2232e..e7ba8de527c 100644
--- a/app/graphql/types/organizations/organization_type.rb
+++ b/app/graphql/types/organizations/organization_type.rb
@@ -33,6 +33,10 @@ module Types
null: false,
description: 'Path of the organization.',
alpha: { milestone: '16.4' }
+ field :web_url, GraphQL::Types::String,
+ null: false,
+ description: 'Web URL of the organization.',
+ alpha: { milestone: '16.6' }
end
end
end
diff --git a/app/graphql/types/organizations/organization_user_badge_type.rb b/app/graphql/types/organizations/organization_user_badge_type.rb
new file mode 100644
index 00000000000..f4e18676dd1
--- /dev/null
+++ b/app/graphql/types/organizations/organization_user_badge_type.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Types
+ module Organizations
+ # rubocop: disable Graphql/AuthorizeTypes -- Already authorized in parent OrganizationUserType.
+ class OrganizationUserBadgeType < BaseObject
+ graphql_name 'OrganizationUserBadge'
+ description 'An organization user badge.'
+
+ field :text,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Badge text.'
+
+ field :variant,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Badge variant.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/organizations/organization_user_type.rb b/app/graphql/types/organizations/organization_user_type.rb
index 41924586f38..ce036c7dd4a 100644
--- a/app/graphql/types/organizations/organization_user_type.rb
+++ b/app/graphql/types/organizations/organization_user_type.rb
@@ -13,7 +13,7 @@ module Types
alias_method :organization_user, :object
field :badges,
- [GraphQL::Types::String],
+ [::Types::Organizations::OrganizationUserBadgeType],
null: true,
description: 'Badges describing the user within the organization.',
alpha: { milestone: '16.4' }
@@ -29,7 +29,7 @@ module Types
alpha: { milestone: '16.4' }
def badges
- user_badges_in_admin_section(organization_user.user).pluck(:text) # rubocop:disable CodeReuse/ActiveRecord
+ user_badges_in_admin_section(organization_user.user)
end
end
end
diff --git a/app/graphql/types/packages/package_base_type.rb b/app/graphql/types/packages/package_base_type.rb
index aa580d48709..5102e4ebcd5 100644
--- a/app/graphql/types/packages/package_base_type.rb
+++ b/app/graphql/types/packages/package_base_type.rb
@@ -10,11 +10,19 @@ module Types
authorize :read_package
+ expose_permissions Types::PermissionTypes::Package
+
field :id, ::Types::GlobalIDType[::Packages::Package], null: false, description: 'ID of the package.'
field :_links, Types::Packages::PackageLinksType, null: false, method: :itself,
description: 'Map of links to perform actions on the package.'
- field :can_destroy, GraphQL::Types::Boolean, null: false, description: 'Whether the user can destroy the package.'
+ field :can_destroy, GraphQL::Types::Boolean,
+ null: false,
+ deprecated: {
+ reason: 'Superseded by `user_permissions` field. See `Types::PermissionTypes::Package` type',
+ milestone: '16.6'
+ },
+ description: 'Whether the user can destroy the package.'
field :created_at, Types::TimeType, null: false, description: 'Date of creation.'
field :metadata, Types::Packages::MetadataType,
null: true,
diff --git a/app/graphql/types/packages/protection/rule_type.rb b/app/graphql/types/packages/protection/rule_type.rb
index 1e969d39ce2..e2ea2d89d2d 100644
--- a/app/graphql/types/packages/protection/rule_type.rb
+++ b/app/graphql/types/packages/protection/rule_type.rb
@@ -10,6 +10,11 @@ module Types
authorize :admin_package
+ field :id,
+ ::Types::GlobalIDType[::Packages::Protection::Rule],
+ null: false,
+ description: 'ID of the package protection rule.'
+
field :package_name_pattern,
GraphQL::Types::String,
null: false,
diff --git a/app/graphql/types/packages/pypi/metadatum_type.rb b/app/graphql/types/packages/pypi/metadatum_type.rb
index 63452d8ab6e..8ccdb592c52 100644
--- a/app/graphql/types/packages/pypi/metadatum_type.rb
+++ b/app/graphql/types/packages/pypi/metadatum_type.rb
@@ -9,8 +9,17 @@ module Types
authorize :read_package
+ field :author_email, GraphQL::Types::String, null: true,
+ description: 'Author email address(es) in RFC-822 format.'
+ field :description, GraphQL::Types::String, null: true,
+ description: 'Longer description that can run to several paragraphs.'
+ field :description_content_type, GraphQL::Types::String, null: true,
+ description: 'Markup syntax used in the description field.'
field :id, ::Types::GlobalIDType[::Packages::Pypi::Metadatum], null: false, description: 'ID of the metadatum.'
+ field :keywords, GraphQL::Types::String, null: true, description: 'List of keywords, separated by commas.'
+ field :metadata_version, GraphQL::Types::String, null: true, description: 'Metadata version.'
field :required_python, GraphQL::Types::String, null: true, description: 'Required Python version of the Pypi package.'
+ field :summary, GraphQL::Types::String, null: true, description: 'One-line summary of the description.'
end
end
end
diff --git a/app/graphql/types/permission_types/abuse_report.rb b/app/graphql/types/permission_types/abuse_report.rb
new file mode 100644
index 00000000000..abd5d545d02
--- /dev/null
+++ b/app/graphql/types/permission_types/abuse_report.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ class AbuseReport < BasePermissionType
+ graphql_name 'AbuseReportPermissions'
+
+ abilities :read_abuse_report, :create_note
+ 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 d45c61f489b..3c0e68bdaf2 100644
--- a/app/graphql/types/permission_types/base_permission_type.rb
+++ b/app/graphql/types/permission_types/base_permission_type.rb
@@ -21,7 +21,7 @@ module Types
kword_args = kword_args.reverse_merge(
name: name,
type: GraphQL::Types::Boolean,
- description: "Indicates the user can perform `#{name}` on this resource",
+ description: "If `true`, the user can perform `#{name}` on this resource",
null: false)
field(**kword_args, &block) # rubocop:disable Graphql/Descriptions
diff --git a/app/graphql/types/permission_types/ci/job.rb b/app/graphql/types/permission_types/ci/job.rb
index c9a85317e67..35904fb1fc3 100644
--- a/app/graphql/types/permission_types/ci/job.rb
+++ b/app/graphql/types/permission_types/ci/job.rb
@@ -8,6 +8,7 @@ module Types
abilities :read_job_artifacts, :read_build
ability_field :update_build, calls_gitaly: true
+ ability_field :cancel_build, calls_gitaly: true
end
end
end
diff --git a/app/graphql/types/permission_types/ci/pipeline.rb b/app/graphql/types/permission_types/ci/pipeline.rb
index cfd68380005..94adbf7c59b 100644
--- a/app/graphql/types/permission_types/ci/pipeline.rb
+++ b/app/graphql/types/permission_types/ci/pipeline.rb
@@ -8,6 +8,7 @@ module Types
abilities :admin_pipeline, :destroy_pipeline
ability_field :update_pipeline, calls_gitaly: true
+ ability_field :cancel_pipeline, calls_gitaly: true
end
end
end
diff --git a/app/graphql/types/permission_types/package.rb b/app/graphql/types/permission_types/package.rb
new file mode 100644
index 00000000000..debde3a1a8e
--- /dev/null
+++ b/app/graphql/types/permission_types/package.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ class Package < BasePermissionType
+ graphql_name 'PackagePermissions'
+
+ ability_field :destroy_package,
+ description: 'If `true`, the user can perform `destroy_package` on this resource'
+ end
+ end
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 95caefc3825..ec87f133843 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -641,6 +641,12 @@ module Types
resolver: Resolvers::AutocompleteUsersResolver,
description: 'Search users for autocompletion'
+ field :detailed_import_status,
+ ::Types::Projects::DetailedImportStatusType,
+ null: true,
+ description: 'Detailed import status of the project.',
+ method: :import_state
+
def timelog_categories
object.project_namespace.timelog_categories if Feature.enabled?(:timelog_categories)
end
diff --git a/app/graphql/types/projects/detailed_import_status_type.rb b/app/graphql/types/projects/detailed_import_status_type.rb
new file mode 100644
index 00000000000..9cba176e097
--- /dev/null
+++ b/app/graphql/types/projects/detailed_import_status_type.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Types
+ module Projects
+ class DetailedImportStatusType < BaseObject
+ graphql_name 'DetailedImportStatus'
+ description 'Details of the import status of a project.'
+
+ authorize :read_project
+
+ field :id, ::Types::GlobalIDType[::ProjectImportState],
+ description: 'ID of the import state.'
+
+ field :status, GraphQL::Types::String,
+ description: 'Current status of the import.'
+
+ field :url, GraphQL::Types::String,
+ description: 'Import url.'
+
+ field :last_error, GraphQL::Types::String,
+ description: 'Last error of the import.',
+ null: true,
+ authorize: :read_import_error
+
+ field :last_update_at, Types::TimeType,
+ description: 'Time of the last update.'
+
+ field :last_update_started_at, Types::TimeType,
+ description: 'Time of the start of the last update.'
+
+ field :last_successful_update_at, Types::TimeType,
+ description: 'Time of the last successful update.'
+
+ def url
+ object.project.safe_import_url
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index d185007f05b..173e877d86c 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -21,6 +21,20 @@ module Types
required: true, description: 'Global ID of the CI stage.'
end
+ field :ci_catalog_resources,
+ ::Types::Ci::Catalog::ResourceType.connection_type,
+ null: true,
+ alpha: { milestone: '15.11' },
+ description: 'All CI/CD Catalog resources under a common namespace, visible to an authorized user',
+ resolver: ::Resolvers::Ci::Catalog::ResourcesResolver
+
+ field :ci_catalog_resource,
+ ::Types::Ci::Catalog::ResourceType,
+ null: true,
+ alpha: { milestone: '16.1' },
+ description: 'A single CI/CD Catalog resource visible to an authorized user',
+ resolver: ::Resolvers::Ci::Catalog::ResourceResolver
+
field :ci_variables,
Types::Ci::InstanceVariableType.connection_type,
null: true,
@@ -41,6 +55,14 @@ module Types
null: false,
description: 'Fields related to design management.'
field :echo, resolver: Resolvers::EchoResolver
+ field :frecent_groups, [Types::GroupType],
+ resolver: Resolvers::Users::FrecentGroupsResolver,
+ description: "A user's frecently visited groups. Requires the `frecent_namespaces_suggestions` feature flag to be enabled.",
+ alpha: { milestone: '16.6' }
+ field :frecent_projects, [Types::ProjectType],
+ resolver: Resolvers::Users::FrecentProjectsResolver,
+ description: "A user's frecently visited projects. Requires the `frecent_namespaces_suggestions` feature flag to be enabled.",
+ alpha: { milestone: '16.6' }
field :gitpod_enabled, GraphQL::Types::Boolean,
null: true,
description: "Whether Gitpod is enabled in application settings."
diff --git a/app/graphql/types/security/codequality_reports_comparer/degradation_type.rb b/app/graphql/types/security/codequality_reports_comparer/degradation_type.rb
index fb7d722069f..7dd47611a2e 100644
--- a/app/graphql/types/security/codequality_reports_comparer/degradation_type.rb
+++ b/app/graphql/types/security/codequality_reports_comparer/degradation_type.rb
@@ -3,10 +3,9 @@
module Types
module Security
module CodequalityReportsComparer
- # rubocop: disable Graphql/AuthorizeTypes (The resolver authorizes the request)
+ # rubocop: disable Graphql/AuthorizeTypes -- The resolver authorizes the request
class DegradationType < BaseObject
graphql_name 'CodequalityReportsComparerReportDegradation'
-
description 'Represents a degradation on the compared codequality report.'
field :description, GraphQL::Types::String,
diff --git a/app/graphql/types/security/codequality_reports_comparer/report_generation_status_enum.rb b/app/graphql/types/security/codequality_reports_comparer/report_generation_status_enum.rb
new file mode 100644
index 00000000000..dace3aec97c
--- /dev/null
+++ b/app/graphql/types/security/codequality_reports_comparer/report_generation_status_enum.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Types
+ module Security
+ module CodequalityReportsComparer
+ class ReportGenerationStatusEnum < BaseEnum
+ graphql_name 'CodequalityReportsComparerReportGenerationStatus'
+ description 'Represents the generation status of the compared codequality report.'
+
+ value 'PARSED', value: :parsed, description: 'Report was generated.'
+ value 'PARSING', value: :parsing, description: 'Report is being generated.'
+ value 'ERROR', value: :error, description: 'An error happened while generating the report.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/security/codequality_reports_comparer/report_type.rb b/app/graphql/types/security/codequality_reports_comparer/report_type.rb
index 8a41160141a..d20c9dd9ab6 100644
--- a/app/graphql/types/security/codequality_reports_comparer/report_type.rb
+++ b/app/graphql/types/security/codequality_reports_comparer/report_type.rb
@@ -3,7 +3,7 @@
module Types
module Security
module CodequalityReportsComparer
- # rubocop: disable Graphql/AuthorizeTypes (Parent node applies authorization)
+ # rubocop: disable Graphql/AuthorizeTypes -- Parent node applies authorization
class ReportType < BaseObject
graphql_name 'CodequalityReportsComparerReport'
diff --git a/app/graphql/types/security/codequality_reports_comparer/status_enum.rb b/app/graphql/types/security/codequality_reports_comparer/status_enum.rb
index 9cab2664db8..fdccfdc7e44 100644
--- a/app/graphql/types/security/codequality_reports_comparer/status_enum.rb
+++ b/app/graphql/types/security/codequality_reports_comparer/status_enum.rb
@@ -4,11 +4,11 @@ module Types
module Security
module CodequalityReportsComparer
class StatusEnum < BaseEnum
- graphql_name 'CodequalityReportsComparerReportStatus'
- description 'Report comparison status'
+ graphql_name 'CodequalityReportsComparerStatus'
+ description 'Represents the state of the code quality report.'
- value 'SUCCESS', value: 'success', description: 'Report successfully generated.'
- value 'FAILED', value: 'failed', description: 'Report failed to generate.'
+ value 'SUCCESS', value: 'success', description: 'No degradations found in the head pipeline report.'
+ value 'FAILED', value: 'failed', description: 'Report generated and there are new code quality degradations.'
value 'NOT_FOUND', value: 'not_found', description: 'Head report or base report not found.'
end
end
diff --git a/app/graphql/types/security/codequality_reports_comparer/summary_type.rb b/app/graphql/types/security/codequality_reports_comparer/summary_type.rb
index cd4a594c193..43037be5245 100644
--- a/app/graphql/types/security/codequality_reports_comparer/summary_type.rb
+++ b/app/graphql/types/security/codequality_reports_comparer/summary_type.rb
@@ -3,7 +3,7 @@
module Types
module Security
module CodequalityReportsComparer
- # rubocop: disable Graphql/AuthorizeTypes (The resolver authorizes the request)
+ # rubocop: disable Graphql/AuthorizeTypes -- The resolver authorizes the request
class SummaryType < BaseObject
graphql_name 'CodequalityReportsComparerReportSummary'
diff --git a/app/graphql/types/security/codequality_reports_comparer_type.rb b/app/graphql/types/security/codequality_reports_comparer_type.rb
index 8088bf84627..32fe8c12330 100644
--- a/app/graphql/types/security/codequality_reports_comparer_type.rb
+++ b/app/graphql/types/security/codequality_reports_comparer_type.rb
@@ -2,12 +2,17 @@
module Types
module Security
- # rubocop: disable Graphql/AuthorizeTypes (The resolver authorizes the request)
+ # rubocop: disable Graphql/AuthorizeTypes -- The resolver authorizes the request
class CodequalityReportsComparerType < BaseObject
graphql_name 'CodequalityReportsComparer'
description 'Represents reports comparison for code quality.'
+ field :status,
+ type: CodequalityReportsComparer::ReportGenerationStatusEnum,
+ null: true,
+ description: 'Compared codequality report generation status.'
+
field :report,
type: CodequalityReportsComparer::ReportType,
null: true,
diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb
index 47d486265b0..040711b5f58 100644
--- a/app/graphql/types/user_interface.rb
+++ b/app/graphql/types/user_interface.rb
@@ -71,6 +71,11 @@ module Types
type: GraphQL::Types::String,
null: false,
description: 'Web path of the user.'
+ field :organizations,
+ resolver: Resolvers::Users::OrganizationsResolver,
+ null: true,
+ alpha: { milestone: '16.6' },
+ description: 'Organizations where the user has access.'
field :group_memberships,
type: Types::GroupMemberType.connection_type,
null: true,
@@ -134,13 +139,11 @@ module Types
field :saved_replies,
Types::SavedReplyType.connection_type,
null: true,
- description: 'Saved replies authored by the user. ' \
- 'Will not return saved replies if `saved_replies` feature flag is disabled.'
+ description: 'Saved replies authored by the user.'
field :saved_reply,
resolver: Resolvers::SavedReplyResolver,
- description: 'Saved reply authored by the user. ' \
- 'Will not return saved reply if `saved_replies` feature flag is disabled.'
+ description: 'Saved reply authored by the user.'
field :gitpod_enabled, GraphQL::Types::Boolean, null: true,
description: 'Whether Gitpod is enabled at the user level.'
@@ -197,6 +200,11 @@ module Types
null: true,
description: 'Timestamp of when the user was created.'
+ field :last_activity_on,
+ type: Types::DateType,
+ null: true,
+ description: 'Date the user last performed any actions.'
+
field :pronouns,
type: ::GraphQL::Types::String,
null: true,
diff --git a/app/graphql/types/work_items/linked_item_type.rb b/app/graphql/types/work_items/linked_item_type.rb
index a4dbeed7480..1b989d78091 100644
--- a/app/graphql/types/work_items/linked_item_type.rb
+++ b/app/graphql/types/work_items/linked_item_type.rb
@@ -2,21 +2,29 @@
module Types
module WorkItems
- # rubocop:disable Graphql/AuthorizeTypes
class LinkedItemType < BaseObject
graphql_name 'LinkedWorkItemType'
+ authorize :read_work_item
+
field :link_created_at, Types::TimeType,
- description: 'Timestamp the link was created.', null: false
+ description: 'Timestamp the link was created.', null: false,
+ method: :issue_link_created_at
field :link_id, ::Types::GlobalIDType[::WorkItems::RelatedWorkItemLink],
- description: 'Global ID of the link.', null: false
+ description: 'Global ID of the link.', null: false,
+ method: :issue_link_id
field :link_type, GraphQL::Types::String,
- description: 'Type of link.', null: false
+ description: 'Type of link.', null: false,
+ method: :issue_link_type
field :link_updated_at, Types::TimeType,
- description: 'Timestamp the link was updated.', null: false
+ description: 'Timestamp the link was updated.', null: false,
+ method: :issue_link_updated_at
field :work_item, Types::WorkItemType,
- description: 'Linked work item.', null: false
+ description: 'Linked work item.', null: true
+
+ def work_item
+ object
+ end
end
- # rubocop:enable Graphql/AuthorizeTypes
end
end
diff --git a/app/graphql/types/work_items/widgets/linked_items_type.rb b/app/graphql/types/work_items/widgets/linked_items_type.rb
index 2611c2456c5..c541a12a050 100644
--- a/app/graphql/types/work_items/widgets/linked_items_type.rb
+++ b/app/graphql/types/work_items/widgets/linked_items_type.rb
@@ -13,6 +13,7 @@ module Types
field :linked_items, Types::WorkItems::LinkedItemType.connection_type,
null: true, complexity: 5,
alpha: { milestone: '16.3' },
+ extras: [:lookahead],
description: 'Linked items for the work item. Returns `null` ' \
'if `linked_work_items` feature flag is disabled.',
resolver: Resolvers::WorkItems::LinkedItemsResolver
diff --git a/app/helpers/admin/user_actions_helper.rb b/app/helpers/admin/user_actions_helper.rb
index 969c5d5a0b5..ba40b3c8a8d 100644
--- a/app/helpers/admin/user_actions_helper.rb
+++ b/app/helpers/admin/user_actions_helper.rb
@@ -16,6 +16,7 @@ module Admin
unlock_actions
delete_actions
ban_actions
+ trust_actions
@actions
end
@@ -66,5 +67,19 @@ module Admin
@actions << 'ban'
end
end
+
+ def trust_actions
+ return if @user.internal? ||
+ @user.blocked_pending_approval? ||
+ @user.banned? ||
+ @user.blocked? ||
+ @user.deactivated?
+
+ @actions << if @user.trusted?
+ 'untrust'
+ else
+ 'trust'
+ end
+ end
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 57937353955..8a0a46e6b25 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -318,7 +318,6 @@ module ApplicationHelper
class_names << 'with-header' if !show_super_sidebar? || !current_user
class_names << 'with-top-bar' if show_super_sidebar? && !@hide_top_bar_padding
class_names << system_message_class
- class_names << 'logged-out-marketing-header' if !current_user && ::Gitlab.com? && !show_super_sidebar?
class_names
end
@@ -371,6 +370,14 @@ module ApplicationHelper
"https://discord.com/users/#{user.discord}"
end
+ def mastodon_url(user)
+ return '' if user.mastodon.blank?
+
+ url = user.mastodon.match UserDetail::MASTODON_VALIDATION_REGEX
+
+ external_redirect_path(url: "https://#{url[2]}/@#{url[1]}")
+ end
+
def collapsed_sidebar?
cookies["sidebar_collapsed"] == "true"
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 58648a82487..0c6ab41004a 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -488,6 +488,7 @@ module ApplicationSettingsHelper
:sidekiq_job_limiter_compression_threshold_bytes,
:sidekiq_job_limiter_limit_bytes,
:suggest_pipeline_enabled,
+ :enable_artifact_external_redirect_warning_page,
:search_rate_limit,
:search_rate_limit_unauthenticated,
:search_rate_limit_allowlist_raw,
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index fc157df3891..e447940e2af 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -93,16 +93,11 @@ module AuthHelper
end
def saml_providers
- auth_providers.select do |provider|
- provider == :saml || auth_strategy_class(provider) == 'OmniAuth::Strategies::SAML'
+ providers = Gitlab.config.omniauth.providers.select do |provider|
+ provider.name == 'saml' || provider.dig('args', 'strategy_class') == 'OmniAuth::Strategies::SAML'
end
- end
-
- def auth_strategy_class(provider)
- config = Gitlab::Auth::OAuth::Provider.config_for(provider)
- return if config.nil? || config['args'].blank?
- config.args['strategy_class']
+ providers.map(&:name).map(&:to_sym)
end
def any_form_based_providers_enabled?
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 0d5b8755a37..8c199aefd81 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -300,7 +300,7 @@ module BlobHelper
end
def show_suggest_pipeline_creation_celebration?
- @blob.path == Gitlab::FileDetector::PATTERNS[:gitlab_ci] &&
+ Gitlab::FileDetector.type_of(@blob.path) == :gitlab_ci &&
@blob.auxiliary_viewer&.valid?(project: @project, sha: @commit.sha, user: current_user) &&
@project.uses_default_ci_config? &&
cookies[suggest_pipeline_commit_cookie_name].present?
diff --git a/app/helpers/ci/catalog/resources_helper.rb b/app/helpers/ci/catalog/resources_helper.rb
index bc77e0cd33a..8324da870d3 100644
--- a/app/helpers/ci/catalog/resources_helper.rb
+++ b/app/helpers/ci/catalog/resources_helper.rb
@@ -3,8 +3,8 @@
module Ci
module Catalog
module ResourcesHelper
- def can_add_catalog_resource?(_project)
- false
+ def can_add_catalog_resource?(project)
+ can?(current_user, :add_catalog_resource, project)
end
def can_view_namespace_catalog?(_project)
diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb
index 510c7cd5fb6..9c4ceaccff1 100644
--- a/app/helpers/ci/pipelines_helper.rb
+++ b/app/helpers/ci/pipelines_helper.rb
@@ -71,7 +71,7 @@ module Ci
def pipelines_list_data(project, list_url)
artifacts_endpoint_placeholder = ':pipeline_artifacts_id'
- data = {
+ {
endpoint: list_url,
project_id: project.id,
default_branch_name: project.default_branch,
@@ -89,15 +89,6 @@ module Ci
full_path: project.full_path,
visibility_pipeline_id_type: visibility_pipeline_id_type
}
-
- experiment(:ios_specific_templates, actor: current_user, project: project, sticky_to: project) do |e|
- e.candidate do
- data[:registration_token] = project.runners_token if can?(current_user, :register_project_runners, project)
- data[:ios_runners_available] = (project.shared_runners_available? && Gitlab.com?).to_s
- end
- end
-
- data
end
def visibility_pipeline_id_type
diff --git a/app/helpers/ci/status_helper.rb b/app/helpers/ci/status_helper.rb
index 86f48b51f76..21d982d42bc 100644
--- a/app/helpers/ci/status_helper.rb
+++ b/app/helpers/ci/status_helper.rb
@@ -15,50 +15,46 @@ module Ci
end
# rubocop:disable Metrics/CyclomaticComplexity
- def ci_icon_for_status(status, size: 16)
- if detailed_status?(status)
- return sprite_icon(status.icon, size: size)
- end
-
+ def ci_icon_for_status(status, size: 24)
icon_name =
- case status
- when 'success'
- 'status_success'
- when 'success-with-warnings'
- 'status_warning'
- when 'failed'
- 'status_failed'
- when 'pending'
- 'status_pending'
- when 'waiting_for_resource'
- 'status_pending'
- when 'preparing'
- 'status_preparing'
- when 'running'
- 'status_running'
- when 'play'
- 'play'
- when 'created'
- 'status_created'
- when 'skipped'
- 'status_skipped'
- when 'manual'
- 'status_manual'
- when 'scheduled'
- 'status_scheduled'
+ if detailed_status?(status)
+ status.icon
else
- 'status_canceled'
+ case status
+ when 'success'
+ 'status_success'
+ when 'success-with-warnings'
+ 'status_warning'
+ when 'failed'
+ 'status_failed'
+ when 'pending'
+ 'status_pending'
+ when 'waiting-for-resource'
+ 'status_pending'
+ when 'preparing'
+ 'status_preparing'
+ when 'running'
+ 'status_running'
+ when 'play'
+ 'play'
+ when 'created'
+ 'status_created'
+ when 'skipped'
+ 'status_skipped'
+ when 'manual'
+ 'status_manual'
+ when 'scheduled'
+ 'status_scheduled'
+ else
+ 'status_canceled'
+ end
end
- sprite_icon(icon_name, size: size)
- end
- # rubocop:enable Metrics/CyclomaticComplexity
-
- def ci_icon_class_for_status(status)
- group = detailed_status?(status) ? status.group : status.dasherize
+ icon_name = icon_name == 'play' ? icon_name : "#{icon_name}_borderless"
- "ci-status-icon-#{group}"
+ sprite_icon(icon_name, size: size, css_class: 'gl-icon')
end
+ # rubocop:enable Metrics/CyclomaticComplexity
def pipeline_status_cache_key(pipeline_status)
"pipeline-status/#{pipeline_status.sha}-#{pipeline_status.status}"
@@ -68,23 +64,35 @@ module Ci
project = commit.project
path = pipelines_project_commit_path(project, commit, ref: ref)
- render_status_with_link(
+ render_ci_icon(
status,
path,
tooltip_placement: tooltip_placement,
- icon_size: 16)
+ option_css_classes: 'gl-ml-3'
+ )
end
- def render_status_with_link(status, path = nil, type: _('pipeline'), tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16)
+ def render_ci_icon(
+ status,
+ path = nil,
+ tooltip_placement: 'left',
+ option_css_classes: '',
+ container: 'body',
+ show_status_text: false
+ )
variant = badge_variant(status)
- klass = "ci-status-link #{ci_icon_class_for_status(status)} d-inline-flex gl-line-height-1 #{cssclass}"
- title = "#{type.titleize}: #{ci_label_for_status(status)}"
- data = { toggle: 'tooltip', placement: tooltip_placement, container: container, testid: 'ci-status-badge-legacy' }
- badge_classes = 'gl-px-2 gl-ml-3'
+ badge_classes = "ci-icon ci-icon-variant-#{variant} gl-p-2 #{option_css_classes}"
+ title = "#{_('Pipeline')}: #{ci_label_for_status(status)}"
+ data = { toggle: 'tooltip', placement: tooltip_placement, container: container, testid: 'ci-icon' }
+
+ icon_wrapper_class = "js-ci-status-badge-legacy ci-icon-gl-icon-wrapper"
gl_badge_tag(variant: variant, size: :md, href: path, class: badge_classes, title: title, data: data) do
- content_tag :span, ci_icon_for_status(status, size: icon_size),
- class: klass
+ if show_status_text
+ content_tag(:span, ci_icon_for_status(status), { class: icon_wrapper_class }) + content_tag(:span, status.label, { class: 'gl-mx-2 gl-white-space-nowrap', data: { testid: 'ci-icon-text' } })
+ else
+ content_tag(:span, ci_icon_for_status(status), { class: icon_wrapper_class })
+ end
end
end
@@ -124,16 +132,18 @@ module Ci
case variant
when 'success'
:success
- when 'success-with-warnings', 'pending'
+ when 'success-with-warnings'
+ :warning
+ when 'pending'
+ :warning
+ when 'waiting-for-resource'
:warning
when 'failed'
:danger
when 'running'
:info
- when 'canceled', 'manual'
- :neutral
else
- :muted
+ :neutral
end
end
end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index 1989d6ab3d5..319cec6f140 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -38,7 +38,7 @@ module ClustersHelper
environment_scope: cluster.environment_scope,
base_domain: cluster.base_domain,
auto_devops_help_path: help_page_path('topics/autodevops/index'),
- external_endpoint_help_path: help_page_path('user/project/clusters/gitlab_managed_clusters.md', anchor: 'base-domain')
+ external_endpoint_help_path: help_page_path('user/project/clusters/gitlab_managed_clusters', anchor: 'base-domain')
}
end
diff --git a/app/helpers/colors_helper.rb b/app/helpers/colors_helper.rb
index 3cd7263c39e..34b18b80be4 100644
--- a/app/helpers/colors_helper.rb
+++ b/app/helpers/colors_helper.rb
@@ -10,16 +10,4 @@ module ColorsHelper
hex_color.length == 7 ? hex_color[1, 7].scan(/.{2}/).map(&:hex) : hex_color[1, 4].scan(/./).map { |v| (v * 2).hex }
end
-
- def rgb_array_to_hex_color(rgb_array)
- raise ArgumentError, "invalid RGB array `#{rgb_array}`" unless rgb_array_valid?(rgb_array)
-
- "##{rgb_array.map{ "%02x" % _1 }.join}"
- end
-
- private
-
- def rgb_array_valid?(rgb_array)
- rgb_array.is_a?(Array) && rgb_array.length == 3 && rgb_array.all?{ _1 >= 0 && _1 <= 255 }
- end
end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index cc91b70758f..b6e0b2d6b20 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -110,7 +110,7 @@ module DropdownsHelper
def dropdown_filter(placeholder, search_id: nil)
content_tag :div, class: "dropdown-input" do
- filter_output = search_field_tag search_id, nil, data: { qa_selector: "dropdown_input_field" }, id: nil, class: "dropdown-input-field", placeholder: placeholder, autocomplete: 'off'
+ filter_output = search_field_tag search_id, nil, data: { testid: "dropdown-input-field" }, id: nil, class: "dropdown-input-field", placeholder: placeholder, autocomplete: 'off'
filter_output << sprite_icon('search', css_class: 'dropdown-input-search')
filter_output << sprite_icon('close', size: 16, css_class: 'dropdown-input-clear js-dropdown-input-clear')
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
index 6e9379a5926..fa47a12a72c 100644
--- a/app/helpers/environment_helper.rb
+++ b/app/helpers/environment_helper.rb
@@ -9,15 +9,6 @@ module EnvironmentHelper
end
# rubocop: enable CodeReuse/ActiveRecord
- def environment_link_for_build(project, build)
- environment = environment_for_build(project, build)
- if environment
- link_to environment.name, project_environment_path(project, environment)
- else
- content_tag :span, build.expanded_environment_name
- end
- end
-
def deployment_path(deployment)
[deployment.project, deployment.deployable]
end
@@ -30,45 +21,6 @@ module EnvironmentHelper
link_to link_label, deployment_path(deployment)
end
- def last_deployment_link_for_environment_build(project, build)
- environment = environment_for_build(project, build)
- return unless environment
-
- deployment_link(environment.last_deployment)
- end
-
- def render_deployment_status(deployment)
- status = deployment.status
-
- status_text =
- case status
- when 'created'
- s_('Deployment|created')
- when 'running'
- s_('Deployment|running')
- when 'success'
- s_('Deployment|success')
- when 'failed'
- s_('Deployment|failed')
- when 'canceled'
- s_('Deployment|canceled')
- when 'skipped'
- s_('Deployment|skipped')
- when 'blocked'
- s_('Deployment|blocked')
- end
-
- ci_icon_utilities = "gl-display-inline-flex gl-align-items-center gl-line-height-0 gl-px-3 gl-py-2 gl-rounded-base"
- klass = "ci-status ci-#{status.dasherize} #{ci_icon_utilities}"
- text = "#{ci_icon_for_status(status)} <span class=\"gl-ml-2\">#{status_text}</span>".html_safe
-
- if deployment.deployable.instance_of?(::Ci::Build)
- link_to(text, deployment_path(deployment), class: klass)
- else
- content_tag(:span, text, class: klass)
- end
- end
-
def environments_detail_data(user, project, environment)
{
name: environment.name,
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 80a56493653..28bdd3e69b6 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -33,15 +33,6 @@ module EnvironmentsHelper
metrics_data
end
- def environment_logs_data(project, environment)
- {
- "environment_name": environment.name,
- "environments_path": api_v4_projects_environments_path(id: project.id),
- "environment_id": environment.id,
- "clusters_path": project_clusters_path(project, format: :json)
- }
- end
-
def can_destroy_environment?(environment)
can?(current_user, :destroy_environment, environment)
end
@@ -85,8 +76,8 @@ module EnvironmentsHelper
def static_metrics_data
{
- 'documentation_path' => help_page_path('administration/monitoring/prometheus/index.md'),
- 'add_dashboard_documentation_path' => help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'),
+ 'documentation_path' => help_page_path('administration/monitoring/prometheus/index'),
+ 'add_dashboard_documentation_path' => help_page_path('operations/metrics/dashboards/index', anchor: 'add-a-new-dashboard-to-your-project'),
'empty_getting_started_svg_path' => image_path('illustrations/monitoring/getting_started.svg'),
'empty_loading_svg_path' => image_path('illustrations/monitoring/loading.svg'),
'empty_no_data_svg_path' => image_path('illustrations/monitoring/no_data.svg'),
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 795d35ec81f..769af0d9ef9 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -13,7 +13,10 @@ module EventsHelper
'deleted' => 'remove',
'destroyed' => 'remove',
'imported' => 'import',
- 'joined' => 'users'
+ 'joined' => 'users',
+ 'approved' => 'check',
+ 'added' => 'upload',
+ 'removed' => 'remove'
}.freeze
def localized_action_name_map
@@ -70,7 +73,7 @@ module EventsHelper
if author
name = self_added ? _('You') : author.name
- link_to name, user_path(author.username), title: name
+ link_to name, user_path(author.username), title: name, data: { user_id: author.id, username: author.username }, class: 'js-user-link'
else
escape_once(event.author_name)
end
@@ -242,7 +245,7 @@ module EventsHelper
def event_wiki_title_html(event)
capture do
- concat content_tag(:span, _('wiki page'), class: "event-target-type gl-mr-2")
+ concat content_tag(:span, _('wiki page'), class: "event-target-type gl-mr-2 #{user_profile_activity_classes}")
concat link_to(
event.target_title,
event_wiki_page_target_url(event),
@@ -254,7 +257,7 @@ module EventsHelper
def event_design_title_html(event)
capture do
- concat content_tag(:span, _('design'), class: "event-target-type gl-mr-2")
+ concat content_tag(:span, _('design'), class: "event-target-type gl-mr-2 #{user_profile_activity_classes}")
concat link_to(
event.design.reference_link_text,
design_url(event.design),
@@ -271,7 +274,7 @@ module EventsHelper
def event_note_title_html(event)
if event.note_target
capture do
- concat content_tag(:span, event.note_target_type_name, class: "event-target-type gl-mr-2")
+ concat content_tag(:span, event.note_target_type_name, class: "event-target-type gl-mr-2 #{user_profile_activity_classes}")
concat link_to(event.note_target_reference, event_note_target_url(event), title: event.target_title, class: 'has-tooltip event-target-link gl-mr-2')
end
else
@@ -303,19 +306,16 @@ module EventsHelper
end
def icon_for_profile_event(event)
- if current_path?('users#show')
- content_tag :div, class: "system-note-image #{event.action_name.parameterize}-icon" do
- icon_for_event(event.action_name)
- end
- else
- content_tag :div, class: 'system-note-image user-avatar' do
- author_avatar(event, size: 32)
- end
- end
+ base_class = 'system-note-image'
+
+ classes = current_path?('users#activity') ? "#{event.action_name.parameterize}-icon gl-rounded-full gl-bg-gray-50 gl-line-height-0" : "user-avatar"
+ content = current_path?('users#activity') ? icon_for_event(event.action_name, size: 14) : author_avatar(event, size: 32)
+
+ tag.div(class: "#{base_class} #{classes}") { content }
end
def inline_event_icon(event)
- unless current_path?('users#show')
+ unless current_path?('users#activity')
content_tag :span, class: "system-note-image-inline d-none d-sm-flex gl-mr-2 #{event.action_name.parameterize}-icon align-self-center" do
next design_event_icon(event.action, size: 14) if event.design?
@@ -325,13 +325,19 @@ module EventsHelper
end
def event_user_info(event)
- content_tag(:div, class: "event-user-info") do
- concat content_tag(:span, link_to_author(event), class: "author-name")
- concat "&nbsp;".html_safe
- concat content_tag(:span, event.author.to_reference, class: "username")
+ return if current_path?('users#activity')
+
+ tag.div(class: 'event-user-info') do
+ concat tag.span(link_to_author(event), class: 'author-name')
+ concat '&nbsp;'.html_safe
+ concat tag.span(event.author.to_reference, class: 'username')
end
end
+ def user_profile_activity_classes
+ current_path?('users#activity') ? ' gl-font-weight-semibold gl-text-black-normal' : ''
+ end
+
private
def design_url(design, opts = {})
diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb
index ab72442857b..829e72d9055 100644
--- a/app/helpers/graph_helper.rb
+++ b/app/helpers/graph_helper.rb
@@ -4,12 +4,6 @@ module GraphHelper
def refs(repo, commit)
refs = [commit.ref_names(repo).join(' ')]
- # append note count
- unless Feature.enabled?(:disable_network_graph_notes_count, @project, type: :experiment)
- notes_count = @graph.notes[commit.id]
- refs << "[#{pluralize(notes_count, 'note')}]" if notes_count > 0
- end
-
refs.join
end
@@ -18,13 +12,6 @@ module GraphHelper
ids.zip(parent_spaces)
end
- def success_ratio(counts)
- return 100 if counts[:failed] == 0
-
- ratio = (counts[:success].to_f / (counts[:success] + counts[:failed])) * 100
- ratio.to_i
- end
-
def should_render_dora_charts
false
end
diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb
index 2582d6fcc34..f2d393f1f77 100644
--- a/app/helpers/ide_helper.rb
+++ b/app/helpers/ide_helper.rb
@@ -5,7 +5,7 @@ module IdeHelper
def ide_data(project:, fork_info:, params:)
base_data = {
'use-new-web-ide' => use_new_web_ide?.to_s,
- 'new-web-ide-help-page-path' => help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'),
+ 'new-web-ide-help-page-path' => help_page_path('user/project/web_ide/index', anchor: 'vscode-reimplementation'),
'sign-in-path' => new_session_path(current_user),
'user-preferences-path' => profile_preferences_path
}.merge(use_new_web_ide? ? new_ide_data(project: project) : legacy_ide_data(project: project))
@@ -71,16 +71,16 @@ module IdeHelper
'switch-editor-svg-path': image_path('illustrations/rocket-launch-md.svg'),
'promotion-svg-path': image_path('illustrations/web-ide_promotion.svg'),
'ci-help-page-path' => help_page_path('ci/quick_start/index'),
- 'web-ide-help-page-path' => help_page_path('user/project/web_ide/index.md'),
+ 'web-ide-help-page-path' => help_page_path('user/project/web_ide/index'),
'render-whitespace-in-code': current_user.render_whitespace_in_code.to_s,
'default-branch' => project && project.default_branch,
'project' => convert_to_project_entity_json(project),
'enable-environments-guidance' => enable_environments_guidance?(project).to_s,
'preview-markdown-path' => project && preview_markdown_path(project),
'web-terminal-svg-path' => image_path('illustrations/web-ide_promotion.svg'),
- 'web-terminal-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'interactive-web-terminals-for-the-web-ide'),
- 'web-terminal-config-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'web-ide-configuration-file'),
- 'web-terminal-runners-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'runner-configuration')
+ 'web-terminal-help-path' => help_page_path('user/project/web_ide/index', anchor: 'interactive-web-terminals-for-the-web-ide'),
+ 'web-terminal-config-help-path' => help_page_path('user/project/web_ide/index', anchor: 'web-ide-configuration-file'),
+ 'web-terminal-runners-help-path' => help_page_path('user/project/web_ide/index', anchor: 'runner-configuration')
}
end
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index e4c1d7932aa..600e5f06c61 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -30,22 +30,11 @@ module MembersHelper
"#{text} #{action} the #{member.source.human_name} #{source_text(member)}?"
end
- def remove_member_title(member)
- action = member.request? ? 'Deny access request' : 'Remove user'
-
- "#{action} from #{source_text(member)}"
- end
-
def leave_confirmation_message(member_source)
"Are you sure you want to leave the " \
"\"#{member_source.human_name}\" #{member_source.model_name.to_s.humanize(capitalize: false)}?"
end
- def filter_group_project_member_path(options = {})
- options = params.slice(:search, :sort).merge(options).permit!
- "#{request.path}?#{options.to_param}"
- end
-
def member_path(member)
if member.is_a?(GroupMember)
group_group_member_path(member.source, member)
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 131cd7cd969..1dc4c393bf2 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -3,6 +3,7 @@
module MergeRequestsHelper
include Gitlab::Utils::StrongMemoize
include CompareHelper
+ DIFF_BATCH_ENDPOINT_PER_PAGE = 5
def create_mr_button_from_event?(event)
create_mr_button?(from: event.branch_name, source_project: event.project)
@@ -176,7 +177,7 @@ module MergeRequestsHelper
end
def notifications_todos_buttons_enabled?
- Feature.enabled?(:notifications_todos_buttons, @project)
+ Feature.enabled?(:notifications_todos_buttons, current_user)
end
def diffs_tab_pane_data(project, merge_request, params)
@@ -187,7 +188,7 @@ module MergeRequestsHelper
endpoint_batch: diffs_batch_project_json_merge_request_path(project, merge_request, 'json', params),
endpoint_coverage: @coverage_path,
endpoint_diff_for_path: diff_for_path_namespace_project_merge_request_path(format: 'json', id: merge_request.iid, namespace_id: project.namespace.to_param, project_id: project.path),
- help_page_path: help_page_path('user/project/merge_requests/reviews/suggestions.md'),
+ help_page_path: help_page_path('user/project/merge_requests/reviews/suggestions'),
current_user_data: @current_user_data,
update_current_user_path: @update_current_user_path,
project_path: project_path(merge_request.project),
@@ -202,7 +203,8 @@ module MergeRequestsHelper
source_project_full_path: merge_request.source_project&.full_path,
is_forked: project.forked?.to_s,
new_comment_template_path: profile_comment_templates_path,
- iid: merge_request.iid
+ iid: merge_request.iid,
+ per_page: DIFF_BATCH_ENDPOINT_PER_PAGE
}
end
@@ -219,7 +221,7 @@ module MergeRequestsHelper
source_project_full_path: merge_request.source_project&.full_path,
source_project_default_url: merge_request.source_project && default_url_to_repo(merge_request.source_project),
target_branch: merge_request.target_branch,
- reviewing_docs_path: help_page_path('user/project/merge_requests/reviews/index.md', anchor: "checkout-merge-requests-locally-through-the-head-ref")
+ reviewing_docs_path: help_page_path('user/project/merge_requests/reviews/index', anchor: "checkout-merge-requests-locally-through-the-head-ref")
}
end
@@ -288,6 +290,7 @@ module MergeRequestsHelper
data = {
iid: @merge_request.iid,
projectPath: @project.full_path,
+ sourceProjectPath: @merge_request.source_project_path,
title: markdown_field(@merge_request, :title),
isFluidLayout: fluid_layout.to_s,
tabs: [
diff --git a/app/helpers/nav/new_dropdown_helper.rb b/app/helpers/nav/new_dropdown_helper.rb
index 5274ace3d8a..88e834b537a 100644
--- a/app/helpers/nav/new_dropdown_helper.rb
+++ b/app/helpers/nav/new_dropdown_helper.rb
@@ -132,6 +132,17 @@ module Nav
)
end
+ if Feature.enabled?(:ui_for_organizations, current_user) && current_user.can?(:create_organization)
+ menu_items.push(
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'general_new_organization',
+ title: s_('Organization|New organization'),
+ href: new_organization_path,
+ data: { track_action: 'click_link_new_organization_parent', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', testid: 'global_new_organization_link' }
+ )
+ )
+ end
+
if current_user.can?(:create_snippet)
menu_items.push(
::Gitlab::Nav::TopNavMenuItem.build(
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index d3707183964..0c61749701e 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -57,10 +57,6 @@ module NavHelper
end
end
- def nav_control_class
- "nav-control" if current_user
- end
-
def user_dropdown_class
class_names = []
class_names << 'header-user-dropdown-toggle'
@@ -82,23 +78,11 @@ module NavHelper
%w[system_info background_migrations background_jobs health_check]
end
- def admin_analytics_nav_links
- %w[dev_ops_report usage_trends]
- end
-
- def show_super_sidebar?(user = current_user)
- # The new sidebar is not enabled for anonymous use
- # Once we enable the new sidebar by default, this
- # should return true
- return Feature.enabled?(:super_sidebar_logged_out) unless user
-
- # Users who got the special `super_sidebar_nav_enrolled` enabled,
- # see the new nav as long as they don't explicitly opt-out via the toggle
- if user.use_new_navigation.nil? && Feature.enabled?(:super_sidebar_nav_enrolled, user)
- true
- else
- !!user.use_new_navigation
- end
+ def show_super_sidebar?(_user = current_user)
+ # The new navigation is now enabled for everyone.
+ # We are working on cleaning up the use of this helper and other related code.
+ # See https://gitlab.com/groups/gitlab-org/-/epics/11875
+ true
end
private
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index af8da86b391..75e89a7d7bc 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -71,16 +71,20 @@ module NotesHelper
def link_to_reply_discussion(discussion, line_type = nil)
return unless current_user
- data = {
- discussion_id: discussion.reply_id,
- discussion_project_id: discussion.project&.id,
- line_type: line_type
- }
-
- button_tag 'Reply...',
- class: 'btn btn-text-field js-discussion-reply-button',
- data: data,
- title: 'Add a reply'
+ content_tag(
+ :textarea,
+ rows: 1,
+ placeholder: _('Reply...'),
+ 'aria-label': _('Reply to comment'),
+ class: 'reply-placeholder-text-field js-discussion-reply-button',
+ data: {
+ discussion_id: discussion.reply_id,
+ discussion_project_id: discussion.project&.id,
+ line_type: line_type
+ }
+ ) do
+ # render empty textarea
+ end
end
def note_max_access_for_user(note)
diff --git a/app/helpers/operations_helper.rb b/app/helpers/operations_helper.rb
index 8528f5f04f7..d8b3cc3b36e 100644
--- a/app/helpers/operations_helper.rb
+++ b/app/helpers/operations_helper.rb
@@ -21,7 +21,7 @@ module OperationsHelper
'prometheus_authorization_key' => @project.alerting_setting&.token,
'prometheus_api_url' => prometheus_integration.api_url,
'prometheus_url' => notify_project_prometheus_alerts_url(@project, format: :json),
- 'alerts_setup_url' => help_page_path('operations/incident_management/integrations.md', anchor: 'configuration'),
+ 'alerts_setup_url' => help_page_path('operations/incident_management/integrations', anchor: 'configuration'),
'alerts_usage_url' => project_alert_management_index_path(@project),
'disabled' => disabled.to_s,
'project_path' => @project.full_path,
diff --git a/app/helpers/organizations/organization_helper.rb b/app/helpers/organizations/organization_helper.rb
index 5d89bb93000..61eb9b5c35f 100644
--- a/app/helpers/organizations/organization_helper.rb
+++ b/app/helpers/organizations/organization_helper.rb
@@ -23,6 +23,14 @@ module Organizations
}.to_json
end
+ def organization_settings_general_app_data(organization)
+ {
+ organization: organization.slice(:id, :name, :path),
+ organizations_path: organizations_path,
+ root_url: root_url
+ }.to_json
+ end
+
def organization_groups_and_projects_app_data
shared_groups_and_projects_app_data.to_json
end
@@ -34,6 +42,19 @@ module Organizations
}
end
+ def organization_user_app_data(organization)
+ {
+ organization_gid: organization.to_global_id
+ }
+ end
+
+ def home_organization_setting_app_data
+ {
+ # TODO: use real setting - https://gitlab.com/gitlab-org/gitlab/-/issues/428668
+ initial_selection: 1
+ }.to_json
+ end
+
private
def shared_groups_and_projects_app_data
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 656d35e927d..204e3b149b9 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -59,6 +59,10 @@ module PreferencesHelper
]
end
+ def time_display_format_choices
+ UserPreference.time_display_formats
+ end
+
def first_day_of_week_choices_with_default
first_day_of_week_choices.unshift([_('System default (%{default})') % { default: default_first_day_of_week }, nil])
end
@@ -122,8 +126,8 @@ module PreferencesHelper
def integration_views
[].tap do |views|
- views << { name: 'gitpod', message: gitpod_enable_description, message_url: gitpod_url_placeholder, help_link: help_page_path('integration/gitpod.md') } if Gitlab::CurrentSettings.gitpod_enabled
- views << { name: 'sourcegraph', message: sourcegraph_url_message, message_url: Gitlab::CurrentSettings.sourcegraph_url, help_link: help_page_path('user/profile/preferences.md', anchor: 'sourcegraph') } if Gitlab::Sourcegraph.feature_available? && Gitlab::CurrentSettings.sourcegraph_enabled
+ views << { name: 'gitpod', message: gitpod_enable_description, message_url: gitpod_url_placeholder, help_link: help_page_path('integration/gitpod') } if Gitlab::CurrentSettings.gitpod_enabled
+ views << { name: 'sourcegraph', message: sourcegraph_url_message, message_url: Gitlab::CurrentSettings.sourcegraph_url, help_link: help_page_path('user/profile/preferences', anchor: 'sourcegraph') } if Gitlab::Sourcegraph.feature_available? && Gitlab::CurrentSettings.sourcegraph_enabled
end
end
diff --git a/app/helpers/projects/pipeline_helper.rb b/app/helpers/projects/pipeline_helper.rb
index 0c3b7d26fe2..fc33e239451 100644
--- a/app/helpers/projects/pipeline_helper.rb
+++ b/app/helpers/projects/pipeline_helper.rb
@@ -37,9 +37,11 @@ module Projects
failure_reason: pipeline.failure_reason,
triggered_by_path: pipeline.child? ? pipeline_path(pipeline.triggered_by_pipeline) : '',
schedule: pipeline.schedule?.to_s,
+ trigger: pipeline.trigger?.to_s,
child: pipeline.child?.to_s,
latest: pipeline.latest?.to_s,
merge_train_pipeline: pipeline.merge_train_pipeline?.to_s,
+ merged_results_pipeline: (pipeline.merged_result_pipeline? && !pipeline.merge_train_pipeline?).to_s,
invalid: pipeline.has_yaml_errors?.to_s,
failed: pipeline.failure_reason?.to_s,
auto_devops: pipeline.auto_devops_source?.to_s,
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 04fe0a4450c..c3287d141f7 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -11,7 +11,7 @@ module ProjectsHelper
end
def link_to_project(project)
- link_to namespace_project_path(namespace_id: project.namespace, id: project), title: h(project.name), class: 'gl-link' do
+ link_to namespace_project_path(namespace_id: project.namespace, id: project), title: h(project.name), class: 'gl-link gl-text-truncate' do
title = content_tag(:span, project.name, class: 'project-name')
if project.namespace
@@ -187,7 +187,7 @@ module ProjectsHelper
end
def link_to_autodeploy_doc
- link_to _('About auto deploy'), help_page_path('topics/autodevops/stages.md', anchor: 'auto-deploy'), target: '_blank', rel: 'noopener'
+ link_to _('About auto deploy'), help_page_path('topics/autodevops/stages', anchor: 'auto-deploy'), target: '_blank', rel: 'noopener'
end
def autodeploy_flash_notice(branch_name)
@@ -200,6 +200,10 @@ module ProjectsHelper
.load_in_batch_for_projects(projects)
end
+ def load_catalog_resources(projects)
+ ActiveRecord::Associations::Preloader.new(records: projects, associations: :catalog_resource).call
+ end
+
def last_pipeline_from_status_cache(project)
if Feature.enabled?(:last_pipeline_from_pipeline_status, project)
pipeline_status = project.pipeline_status
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index 33ca5ad584e..f983812ad22 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -358,7 +358,9 @@ module SidebarsHelper
def context_switcher_links
links = [
({ title: s_('Navigation|Your work'), link: root_path, icon: 'work' } if current_user),
- { title: s_('Navigation|Explore'), link: explore_root_path, icon: 'compass' }
+ { title: s_('Navigation|Explore'), link: explore_root_path, icon: 'compass' },
+ ({ title: s_('Navigation|Profile'), link: profile_path, icon: 'profile' } if current_user),
+ ({ title: s_('Navigation|Preferences'), link: profile_preferences_path, icon: 'preferences' } if current_user)
]
# Usually, using current_user.admin? is discouraged because it does not
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 94445564c22..8b5c0707d08 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -263,36 +263,6 @@ module SortingHelper
sort_direction_button(url, reverse_sort, sort_value)
end
- def packages_sort_options_hash
- {
- sort_value_recently_created => sort_title_created_date,
- sort_value_oldest_created => sort_title_created_date,
- sort_value_name => sort_title_name,
- sort_value_name_desc => sort_title_name,
- sort_value_version_desc => sort_title_version,
- sort_value_version_asc => sort_title_version,
- sort_value_type_desc => sort_title_type,
- sort_value_type_asc => sort_title_type,
- sort_value_project_name_desc => sort_title_project_name,
- sort_value_project_name_asc => sort_title_project_name
- }
- end
-
- def packages_reverse_sort_order_hash
- {
- sort_value_recently_created => sort_value_oldest_created,
- sort_value_oldest_created => sort_value_recently_created,
- sort_value_name => sort_value_name_desc,
- sort_value_name_desc => sort_value_name,
- sort_value_version_desc => sort_value_version_asc,
- sort_value_version_asc => sort_value_version_desc,
- sort_value_type_desc => sort_value_type_asc,
- sort_value_type_asc => sort_value_type_desc,
- sort_value_project_name_desc => sort_value_project_name_asc,
- sort_value_project_name_asc => sort_value_project_name_desc
- }
- end
-
def forks_sort_direction_button(sort_value, without = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id])
reverse_sort = forks_reverse_sort_options_hash[sort_value]
url = page_filter_path(sort: reverse_sort, without: without)
diff --git a/app/helpers/users/callouts_helper.rb b/app/helpers/users/callouts_helper.rb
index 1b5d0b276a3..6f1d4db4349 100644
--- a/app/helpers/users/callouts_helper.rb
+++ b/app/helpers/users/callouts_helper.rb
@@ -15,7 +15,7 @@ module Users
REGISTRATION_ENABLED_CALLOUT_ALLOWED_CONTROLLER_PATHS = [/^root/, /^dashboard\S*/, /^admin\S*/].freeze
WEB_HOOK_DISABLED = 'web_hook_disabled'
BRANCH_RULES_INFO_CALLOUT = 'branch_rules_info_callout'
- NEW_NAVIGATION_CALLOUT = 'new_navigation_callout'
+ NEW_NAV_FOR_EVERYONE_CALLOUT = 'new_nav_for_everyone_callout'
def show_gke_cluster_integration_callout?(project)
active_nav_link?(controller: sidebar_operations_paths) &&
@@ -71,26 +71,16 @@ module Users
!user_dismissed?(MERGE_REQUEST_SETTINGS_MOVED_CALLOUT) && project.merge_requests_enabled?
end
- def show_pages_menu_callout?
- !user_dismissed?(PAGES_MOVED_CALLOUT)
- end
-
def show_branch_rules_info?
!user_dismissed?(BRANCH_RULES_INFO_CALLOUT)
end
- def show_new_navigation_callout?
- show_super_sidebar? &&
- !user_dismissed?(NEW_NAVIGATION_CALLOUT) &&
- # GitLab.com users created after the feature flag's full rollout (June 2nd 2023) don't need to see the callout.
- # Remove the gitlab_com_user_created_after_new_nav_rollout? method when the callout isn't needed anymore.
- !gitlab_com_user_created_after_new_nav_rollout?
- end
-
- def gitlab_com_user_created_after_new_nav_rollout?
- return true unless current_user
-
- Gitlab.com? && current_user.created_at >= Date.new(2023, 6, 2)
+ def show_new_nav_for_everyone_callout?
+ # The use_new_navigation user preference was controlled by the now removed "New navigation" toggle in the UI.
+ # We want to show this banner only to signed-in users who chose to disable the new nav (`false`).
+ # We don't want to show it for users who never touched the toggle and already had the new nav by default (`nil`)
+ user_had_new_nav_off = current_user && current_user.use_new_navigation == false
+ user_had_new_nav_off && !user_dismissed?(NEW_NAV_FOR_EVERYONE_CALLOUT)
end
private
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index a892b6e6ac6..84a809bc510 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -80,10 +80,6 @@ module UsersHelper
current_user&.max_member_access_for_project(project.id) || Gitlab::Access::NO_ACCESS
end
- def max_project_member_access_cache_key(project)
- "access:#{max_project_member_access(project)}"
- end
-
def user_status(user)
return unless user
@@ -262,7 +258,9 @@ module UsersHelper
delete_with_contributions: admin_user_path(:id, hard_delete: true),
admin_user: admin_user_path(:id),
ban: ban_admin_user_path(:id),
- unban: unban_admin_user_path(:id)
+ unban: unban_admin_user_path(:id),
+ trust: trust_admin_user_path(:id),
+ untrust: untrust_admin_user_path(:id)
}
end
@@ -334,27 +332,6 @@ module UsersHelper
end
end
- def user_table_headers
- [
- {
- section_class_name: 'section-40',
- header_text: _('Name')
- },
- {
- section_class_name: 'section-10',
- header_text: _('Projects')
- },
- {
- section_class_name: 'section-15',
- header_text: _('Created on')
- },
- {
- section_class_name: 'section-15',
- header_text: _('Last activity')
- }
- ]
- end
-
# the keys should match the user model defined roles in app/models/user.rb
def localized_user_roles
{
@@ -370,10 +347,6 @@ module UsersHelper
}.with_indifferent_access.freeze
end
- def saved_replies_enabled?
- Feature.enabled?(:saved_replies, current_user)
- end
-
def preload_project_associations(_)
# Overridden in EE
end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index 68b15f7e042..cddfc48c649 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -76,16 +76,6 @@ module VisibilityLevelHelper
end
end
- def visibility_level_options(form_model)
- available_visibility_levels(form_model).map do |level|
- {
- level: level,
- label: visibility_level_label(level),
- description: visibility_level_description(level, form_model)
- }
- end
- end
-
def snippets_selected_visibility_level(visibility_levels, selected)
visibility_levels.find { |level| level == selected } || visibility_levels.min
end
diff --git a/app/helpers/vite_helper.rb b/app/helpers/vite_helper.rb
index 4d1085a5169..5096d3649b7 100644
--- a/app/helpers/vite_helper.rb
+++ b/app/helpers/vite_helper.rb
@@ -1,22 +1,6 @@
# frozen_string_literal: true
module ViteHelper
- def universal_javascript_include_tag(*args)
- if vite_enabled
- vite_javascript_tag(*args)
- else
- javascript_include_tag(*args)
- end
- end
-
- def universal_asset_path(*args)
- if vite_enabled
- vite_asset_path(*args)
- else
- asset_path(*args)
- end
- end
-
private
def vite_enabled
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
index bd63381e9d1..eda789d5e55 100644
--- a/app/helpers/wiki_helper.rb
+++ b/app/helpers/wiki_helper.rb
@@ -68,14 +68,6 @@ module WikiHelper
render Pajamas::ButtonComponent.new(href: wiki_path(wiki, **link_options), icon: "sort-#{icon_class}", button_options: { class: link_class, title: title })
end
- def wiki_sort_title(key)
- if key == Wiki::CREATED_AT_ORDER
- s_("Wiki|Created date")
- else
- s_("Wiki|Title")
- end
- end
-
def wiki_empty_state_messages(wiki)
case wiki.container
when Project
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 52a16475c07..f859294960c 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -70,7 +70,7 @@ module Emails
setup_issue_mail(issue_id, recipient_id)
@label_names = label_names
- @labels_url = project_labels_url(@project)
+ @labels_url = project_labels_url(@project, subscribed: true)
mail_answer_thread(
@issue,
issue_thread_options(
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index cd7869123f3..5e82a3e8dcf 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -65,7 +65,7 @@ module Emails
setup_merge_request_mail(merge_request_id, recipient_id)
@label_names = label_names
- @labels_url = project_labels_url(@project)
+ @labels_url = project_labels_url(@project, subscribed: true)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, reason))
end
diff --git a/app/mailers/emails/service_desk.rb b/app/mailers/emails/service_desk.rb
index f6595a91bee..f67c2636fc6 100644
--- a/app/mailers/emails/service_desk.rb
+++ b/app/mailers/emails/service_desk.rb
@@ -227,8 +227,6 @@ module Emails
# Filepaths we should replace in markdown content
@uploads_as_attachments = []
- return unless Feature.enabled?(:service_desk_new_note_email_native_attachments, @note.project)
-
uploaders = find_uploaders_for(@note)
return if uploaders.nil?
return if uploaders.sum(&:size) > EMAIL_ATTACHMENTS_SIZE_LIMIT
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index 872dedf07b1..de6b644c536 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -139,11 +139,11 @@ class AbuseReport < ApplicationRecord
def reported_content
case report_type
when :issue
- project.issues.iid_in(route_hash[:id]).pick(:description_html)
+ reported_project.issues.iid_in(route_hash[:id]).pick(:description_html)
when :merge_request
- project.merge_requests.iid_in(route_hash[:id]).pick(:description_html)
+ reported_project.merge_requests.iid_in(route_hash[:id]).pick(:description_html)
when :comment
- project.notes.id_in(note_id_from_url).pick(:note_html)
+ reported_project.notes.id_in(note_id_from_url).pick(:note_html)
end
end
@@ -157,13 +157,19 @@ class AbuseReport < ApplicationRecord
user.abuse_reports.open.by_category(category).id_not_in(id).includes(:reporter)
end
+ # createNote mutation calls noteable.project,
+ # which in case of abuse reports is nil
+ def project
+ nil
+ end
+
private
- def project
+ def reported_project
Project.find_by_full_path(route_hash.values_at(:namespace_id, :project_id).join('/'))
end
- def group
+ def reported_group
Group.find_by_full_path(route_hash[:group_id])
end
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
index e42f9eeef23..9756e1b7dd3 100644
--- a/app/models/active_session.rb
+++ b/app/models/active_session.rb
@@ -84,7 +84,7 @@ class ActiveSession
)
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.pipelined do |pipeline|
+ Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
pipeline.setex(
key_name(user.id, session_private_id),
expiry,
@@ -135,9 +135,15 @@ class ActiveSession
redis.srem(lookup_key_name(user.id), session_ids)
+ session_keys = rack_session_keys(session_ids)
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.del(key_names)
- redis.del(rack_session_keys(session_ids))
+ if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_unlink(key_names, redis)
+ Gitlab::Redis::ClusterUtil.batch_unlink(session_keys, redis)
+ else
+ redis.del(key_names)
+ redis.del(session_keys)
+ end
end
end
@@ -206,7 +212,13 @@ class ActiveSession
session_keys.each_slice(SESSION_BATCH_SIZE).flat_map do |session_keys_batch|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.mget(session_keys_batch).compact.map do |raw_session|
+ raw_sessions = if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_get(session_keys_batch, redis)
+ else
+ redis.mget(session_keys_batch)
+ end
+
+ raw_sessions.compact.map do |raw_session|
load_raw_session(raw_session)
end
end
@@ -249,7 +261,13 @@ class ActiveSession
found = Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
- session_ids.zip(redis.mget(entry_keys)).to_h
+ entries = if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_get(entry_keys, redis)
+ else
+ redis.mget(entry_keys)
+ end
+
+ session_ids.zip(entries).to_h
end
found.compact!
@@ -258,7 +276,13 @@ class ActiveSession
fallbacks = Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
entry_keys = missing.map { |session_id| key_name_v1(user_id, session_id) }
- missing.zip(redis.mget(entry_keys)).to_h
+ entries = if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_get(entry_keys, redis)
+ else
+ redis.mget(entry_keys)
+ end
+
+ missing.zip(entries).to_h
end
fallbacks.merge(found.compact)
diff --git a/app/models/activity_pub.rb b/app/models/activity_pub.rb
new file mode 100644
index 00000000000..9131d8be776
--- /dev/null
+++ b/app/models/activity_pub.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ def self.table_name_prefix
+ "activity_pub_"
+ end
+end
diff --git a/app/models/activity_pub/releases_subscription.rb b/app/models/activity_pub/releases_subscription.rb
new file mode 100644
index 00000000000..a6304f1fc35
--- /dev/null
+++ b/app/models/activity_pub/releases_subscription.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ class ReleasesSubscription < ApplicationRecord
+ belongs_to :project, optional: false
+
+ enum :status, [:requested, :accepted], default: :requested
+
+ attribute :payload, Gitlab::Database::Type::JsonPgSafe.new
+
+ validates :payload, json_schema: { filename: 'activity_pub_follow_payload' }, allow_blank: true
+ validates :subscriber_url, presence: true, uniqueness: { case_sensitive: false, scope: :project_id },
+ public_url: true
+ validates :subscriber_inbox_url, uniqueness: { case_sensitive: false, scope: :project_id },
+ public_url: { allow_nil: true }
+ validates :shared_inbox_url, public_url: { allow_nil: true }
+
+ def self.find_by_subscriber_url(subscriber_url)
+ find_by('LOWER(subscriber_url) = ?', subscriber_url.downcase)
+ end
+ end
+end
diff --git a/app/models/ai/service_access_token.rb b/app/models/ai/service_access_token.rb
index b8a2a271976..46dfbe9078c 100644
--- a/app/models/ai/service_access_token.rb
+++ b/app/models/ai/service_access_token.rb
@@ -2,11 +2,13 @@
module Ai
class ServiceAccessToken < ApplicationRecord
+ include IgnorableColumns
self.table_name = 'service_access_tokens'
+ ignore_column :category, remove_with: '16.8', remove_after: '2024-01-22'
+
scope :expired, -> { where('expires_at < :now', now: Time.current) }
scope :active, -> { where('expires_at > :now', now: Time.current) }
- scope :for_category, ->(category) { where(category: category) }
attr_encrypted :token,
mode: :per_attribute_iv,
@@ -16,11 +18,5 @@ module Ai
encode_iv: false
validates :token, :expires_at, presence: true
-
- enum category: {
- code_suggestions: 1
- }
-
- validates :category, presence: true
end
end
diff --git a/app/models/analytics/cycle_analytics/value_stream.rb b/app/models/analytics/cycle_analytics/value_stream.rb
index 7f8c6eef704..d884932072b 100644
--- a/app/models/analytics/cycle_analytics/value_stream.rb
+++ b/app/models/analytics/cycle_analytics/value_stream.rb
@@ -36,6 +36,12 @@ module Analytics
new(name: Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME, namespace: namespace)
end
+ def project
+ return unless namespace.is_a?(::Namespaces::ProjectNamespace)
+
+ namespace.project
+ end
+
private
def max_value_streams_count
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 824a2bd9fa4..8d4f50de75e 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -30,7 +30,9 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
jitsu_project_xid
jitsu_administrator_email
], remove_with: '16.5', remove_after: '2023-09-22'
- ignore_columns %i[encrypted_ai_access_token encrypted_ai_access_token_iv], remove_with: '16.6', remove_after: '2023-10-22'
+ ignore_columns %i[encrypted_ai_access_token encrypted_ai_access_token_iv], remove_with: '16.10', remove_after: '2024-03-22'
+
+ ignore_columns %i[repository_storages], remove_with: '16.8', remove_after: '2023-12-21'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
@@ -91,7 +93,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_denylist, Array # rubocop:disable Cop/ActiveRecordSerialize
- serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize
# See https://gitlab.com/gitlab-org/gitlab/-/issues/300916
serialize :asset_proxy_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize
@@ -303,8 +304,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
- validates :repository_storages, presence: true
- validate :check_repository_storages
validate :check_repository_storages_weighted
validates :auto_devops_domain,
@@ -488,7 +487,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
validates :invisible_captcha_enabled,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
- validates :invitation_flow_enforcement, :can_create_group, :user_defaults_to_private_profile,
+ validates :invitation_flow_enforcement, :can_create_group, :allow_project_creation_for_guest_and_below, :user_defaults_to_private_profile,
allow_nil: false,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 1bd15a56de5..00b093c8ac3 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -57,6 +57,7 @@ module ApplicationSettingImplementation
default_artifacts_expire_in: '30 days',
default_branch_name: nil,
default_branch_protection: Settings.gitlab['default_branch_protection'],
+ default_branch_protection_defaults: Settings.gitlab['default_branch_protection_defaults'],
default_ci_config_path: nil,
default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_project_creation: Settings.gitlab['default_project_creation'],
@@ -158,7 +159,6 @@ module ApplicationSettingImplementation
recaptcha_enabled: false,
repository_checks_enabled: true,
repository_storages_weighted: { 'default' => 100 },
- repository_storages: ['default'],
require_admin_approval_after_user_signup: true,
require_two_factor_authentication: false,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
@@ -433,10 +433,6 @@ module ApplicationSettingImplementation
read_attribute(:asset_proxy_whitelist)
end
- def repository_storages
- Array(read_attribute(:repository_storages))
- end
-
def commit_email_hostname
super.presence || self.class.default_commit_email_hostname
end
@@ -644,12 +640,6 @@ module ApplicationSettingImplementation
self.uuid = SecureRandom.uuid
end
- def check_repository_storages
- invalid = repository_storages - Gitlab.config.repositories.storages.keys
- errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless
- invalid.empty?
- end
-
def coerce_repository_storages_weighted
repository_storages_weighted.transform_values!(&:to_i)
end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index 437118c36e8..a075c2f7e4f 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -124,6 +124,10 @@ class BulkImports::Entity < ApplicationRecord
entity_type.pluralize
end
+ def portable_class
+ entity_type.classify.constantize
+ end
+
def base_resource_url_path
"/#{pluralized_name}/#{encoded_source_full_path}"
end
diff --git a/app/models/bulk_imports/failure.rb b/app/models/bulk_imports/failure.rb
index 44d16618c77..8a6077b523c 100644
--- a/app/models/bulk_imports/failure.rb
+++ b/app/models/bulk_imports/failure.rb
@@ -15,6 +15,10 @@ class BulkImports::Failure < ApplicationRecord
pipeline_relation || default_relation
end
+ def exception_message=(message)
+ super(::Projects::ImportErrorFilter.filter_message(message).truncate(255))
+ end
+
private
def pipeline_relation
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index d0ccf5c543a..cf6401dc1da 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -114,7 +114,7 @@ module Ci
project = options&.dig(:trigger, :project)
next unless project
- scoped_variables.to_runner_variables.yield_self do |all_variables|
+ scoped_variables.to_runner_variables.then do |all_variables|
::ExpandVariables.expand(project, all_variables)
end
end
@@ -199,7 +199,7 @@ module Ci
branch = options&.dig(:trigger, :branch)
return unless branch
- scoped_variables.to_runner_variables.yield_self do |all_variables|
+ scoped_variables.to_runner_variables.then do |all_variables|
::ExpandVariables.expand(branch, all_variables)
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index d2cf9058976..0bb93a68470 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -392,8 +392,8 @@ module Ci
name == 'pages'
end
- # overridden on EE
- def pages_path_prefix; end
+ # Overriden on EE
+ def pages; end
def runnable?
true
@@ -729,7 +729,7 @@ module Ci
end
def artifacts_expired?
- artifacts_expire_at && artifacts_expire_at < Time.current
+ artifacts_expire_at&.past?
end
def artifacts_expire_in
@@ -745,7 +745,7 @@ module Ci
def has_expired_locked_archive_artifacts?
locked_artifacts? &&
- artifacts_expire_at.present? && artifacts_expire_at < Time.current
+ artifacts_expire_at&.past?
end
def has_expiring_archive_artifacts?
@@ -921,13 +921,25 @@ module Ci
# Consider this object to have a structural integrity problems
def doom!
transaction do
- update_columns(status: :failed, failure_reason: :data_integrity_failure)
+ now = Time.current
+ attributes = {
+ status: :failed,
+ failure_reason: :data_integrity_failure,
+ updated_at: now
+ }
+ attributes[:finished_at] = now unless finished_at.present?
+
+ update_columns(attributes)
all_queuing_entries.delete_all
all_runtime_metadata.delete_all
end
deployment&.sync_status_with(self)
+ ::Gitlab::Ci::Pipeline::Metrics
+ .job_failure_reason_counter
+ .increment(reason: :data_integrity_failure)
+
Gitlab::AppLogger.info(
message: 'Build doomed',
class: self.class.name,
diff --git a/app/models/ci/build_trace_chunks/redis_base.rb b/app/models/ci/build_trace_chunks/redis_base.rb
index 3b7a844d122..5f6b5c30a6a 100644
--- a/app/models/ci/build_trace_chunks/redis_base.rb
+++ b/app/models/ci/build_trace_chunks/redis_base.rb
@@ -71,7 +71,11 @@ module Ci
with_redis do |redis|
# https://gitlab.com/gitlab-org/gitlab/-/issues/224171
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.del(keys)
+ if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_unlink(keys, redis)
+ else
+ redis.del(keys)
+ end
end
end
end
diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb
index c5ad3d19425..525cb08f2ca 100644
--- a/app/models/ci/build_trace_metadata.rb
+++ b/app/models/ci/build_trace_metadata.rb
@@ -33,7 +33,7 @@ module Ci
return false unless archival_attempts_available?
return true unless last_archival_attempt_at
- last_archival_attempt_at + backoff < Time.current
+ (last_archival_attempt_at + backoff).past?
end
def archival_attempts_available?
diff --git a/app/models/ci/catalog/components_project.rb b/app/models/ci/catalog/components_project.rb
index 2bc33a6f050..02593d41bc2 100644
--- a/app/models/ci/catalog/components_project.rb
+++ b/app/models/ci/catalog/components_project.rb
@@ -9,7 +9,8 @@ module Ci
TEMPLATE_FILE = 'template.yml'
TEMPLATES_DIR = 'templates'
- TEMPLATE_PATH_REGEX = '^templates\/\w+\-?\w+(?:\/template)?\.yml$'
+ TEMPLATE_PATH_REGEX = '^templates\/[\w-]+(?:\/template)?\.yml$'
+ COMPONENTS_LIMIT = 10
ComponentData = Struct.new(:content, :path, keyword_init: true)
@@ -18,8 +19,8 @@ module Ci
@sha = sha
end
- def fetch_component_paths(sha)
- project.repository.search_files_by_regexp(TEMPLATE_PATH_REGEX, sha)
+ def fetch_component_paths(sha, limit: COMPONENTS_LIMIT)
+ project.repository.search_files_by_regexp(TEMPLATE_PATH_REGEX, sha, limit: limit)
end
def extract_component_name(path)
diff --git a/app/models/ci/catalog/listing.rb b/app/models/ci/catalog/listing.rb
index c3b18af8c3f..51bd85016a5 100644
--- a/app/models/ci/catalog/listing.rb
+++ b/app/models/ci/catalog/listing.rb
@@ -3,42 +3,53 @@
module Ci
module Catalog
class Listing
- # This class is the SSoT to displaying the list of resources in the
- # CI/CD Catalog given a namespace as a scope.
+ # This class is the SSoT to displaying the list of resources in the CI/CD Catalog.
# This model is not directly backed by a table and joins catalog resources
# with projects to return relevant data.
- def initialize(namespace, current_user)
- raise ArgumentError, 'Namespace is not a root namespace' unless namespace.root?
- @namespace = namespace
+ MIN_SEARCH_LENGTH = 3
+
+ def initialize(current_user)
@current_user = current_user
end
- def resources(sort: nil)
+ def resources(namespace: nil, sort: nil, search: nil)
+ relation = all_resources
+ relation = by_namespace(relation, namespace)
+ relation = by_search(relation, search)
+
case sort.to_s
- when 'name_desc' then all_resources.order_by_name_desc
- when 'name_asc' then all_resources.order_by_name_asc
- when 'latest_released_at_desc' then all_resources.order_by_latest_released_at_desc
- when 'latest_released_at_asc' then all_resources.order_by_latest_released_at_asc
+ when 'name_desc' then relation.order_by_name_desc
+ when 'name_asc' then relation.order_by_name_asc
+ when 'latest_released_at_desc' then relation.order_by_latest_released_at_desc
+ when 'latest_released_at_asc' then relation.order_by_latest_released_at_asc
+ when 'created_at_asc' then relation.order_by_created_at_asc
else
- all_resources.order_by_created_at_desc
+ relation.order_by_created_at_desc
end
end
private
- attr_reader :namespace, :current_user
+ attr_reader :current_user
def all_resources
- Ci::Catalog::Resource
- .joins(:project).includes(:project)
- .merge(projects_in_namespace_visible_to_user)
+ Ci::Catalog::Resource.joins(:project).includes(:project)
+ .merge(Project.public_or_visible_to_user(current_user))
+ end
+
+ def by_namespace(relation, namespace)
+ return relation unless namespace
+ raise ArgumentError, 'Namespace is not a root namespace' unless namespace.root?
+
+ relation.merge(Project.in_namespace(namespace.self_and_descendant_ids))
end
- def projects_in_namespace_visible_to_user
- Project
- .in_namespace(namespace.self_and_descendant_ids)
- .public_or_visible_to_user(current_user, ::Gitlab::Access::DEVELOPER)
+ def by_search(relation, search)
+ return relation unless search
+ return relation.none if search.length < MIN_SEARCH_LENGTH
+
+ relation.search(search)
end
end
end
diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb
index 8ffc0292a69..f947c5158cf 100644
--- a/app/models/ci/catalog/resource.rb
+++ b/app/models/ci/catalog/resource.rb
@@ -8,29 +8,55 @@ module Ci
# dependency on the Project model and its need to join with that table
# in order to generate the CI/CD catalog.
class Resource < ::ApplicationRecord
+ include Gitlab::SQL::Pattern
+
self.table_name = 'catalog_resources'
belongs_to :project
- has_many :components, class_name: 'Ci::Catalog::Resources::Component', inverse_of: :catalog_resource
- has_many :versions, class_name: 'Ci::Catalog::Resources::Version', inverse_of: :catalog_resource
+ has_many :components, class_name: 'Ci::Catalog::Resources::Component', foreign_key: :catalog_resource_id,
+ inverse_of: :catalog_resource
+ has_many :versions, class_name: 'Ci::Catalog::Resources::Version', foreign_key: :catalog_resource_id,
+ inverse_of: :catalog_resource
scope :for_projects, ->(project_ids) { where(project_id: project_ids) }
+ scope :search, ->(query) { fuzzy_search(query, [:name, :description], use_minimum_char_limit: false) }
+
scope :order_by_created_at_desc, -> { reorder(created_at: :desc) }
- scope :order_by_name_desc, -> { joins(:project).merge(Project.sorted_by_name_desc) }
- scope :order_by_name_asc, -> { joins(:project).merge(Project.sorted_by_name_asc) }
+ scope :order_by_created_at_asc, -> { reorder(created_at: :asc) }
+ scope :order_by_name_desc, -> { reorder(arel_table[:name].desc.nulls_last) }
+ scope :order_by_name_asc, -> { reorder(arel_table[:name].asc.nulls_last) }
scope :order_by_latest_released_at_desc, -> { reorder(arel_table[:latest_released_at].desc.nulls_last) }
scope :order_by_latest_released_at_asc, -> { reorder(arel_table[:latest_released_at].asc.nulls_last) }
- delegate :avatar_path, :description, :name, :star_count, :forks_count, to: :project
+ delegate :avatar_path, :star_count, :forks_count, to: :project
enum state: { draft: 0, published: 1 }
- def versions
- project.releases.order_released_desc
+ before_create :sync_with_project
+
+ def unpublish!
+ update!(state: :draft)
+ end
+
+ def publish!
+ update!(state: :published)
+ end
+
+ def sync_with_project!
+ sync_with_project
+ save!
end
- def latest_version
- project.releases.latest
+ private
+
+ # These columns are denormalized from the `projects` table. We first sync these
+ # columns when the catalog resource record is created. Then any updates to the
+ # `projects` columns will be synced to the `catalog_resources` table by a worker
+ # (to be implemented in https://gitlab.com/gitlab-org/gitlab/-/issues/429376.)
+ def sync_with_project
+ self.name = project.name
+ self.description = project.description
+ self.visibility_level = project.visibility_level
end
end
end
diff --git a/app/models/ci/catalog/resources/component.rb b/app/models/ci/catalog/resources/component.rb
index 7b95c14ba7e..07d5404981b 100644
--- a/app/models/ci/catalog/resources/component.rb
+++ b/app/models/ci/catalog/resources/component.rb
@@ -6,6 +6,8 @@ module Ci
# This class represents a CI/CD Catalog resource component.
# The data will be used as metadata of a component.
class Component < ::ApplicationRecord
+ include BulkInsertSafe
+
self.table_name = 'catalog_resource_components'
belongs_to :project, inverse_of: :ci_components
diff --git a/app/models/ci/catalog/resources/version.rb b/app/models/ci/catalog/resources/version.rb
index 68f60e6a965..bd0ebc77a6d 100644
--- a/app/models/ci/catalog/resources/version.rb
+++ b/app/models/ci/catalog/resources/version.rb
@@ -6,6 +6,8 @@ module Ci
# This class represents a CI/CD Catalog resource version.
# Only versions which contain valid CI components are included in this table.
class Version < ::ApplicationRecord
+ include BulkInsertableAssociations
+
self.table_name = 'catalog_resource_versions'
belongs_to :release, inverse_of: :catalog_resource_version
@@ -14,6 +16,100 @@ module Ci
has_many :components, class_name: 'Ci::Catalog::Resources::Component', inverse_of: :version
validates :release, :catalog_resource, :project, presence: true
+
+ scope :for_catalog_resources, ->(catalog_resources) { where(catalog_resource_id: catalog_resources) }
+ scope :preloaded, -> { includes(:catalog_resource, project: [:route, { namespace: :route }], release: :author) }
+
+ scope :order_by_created_at_asc, -> { reorder(created_at: :asc) }
+ scope :order_by_created_at_desc, -> { reorder(created_at: :desc) }
+ # After we denormalize the `released_at` column, we won't need to use `joins(:release)` and keyset_order_*
+ scope :order_by_released_at_asc, -> { joins(:release).keyset_order_by_released_at_asc }
+ scope :order_by_released_at_desc, -> { joins(:release).keyset_order_by_released_at_desc }
+
+ delegate :name, :description, :tag, :sha, :released_at, :author_id, to: :release
+
+ class << self
+ # In the future, we should support semantic versioning.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/427286
+ def latest
+ order_by_released_at_desc.first
+ end
+
+ # This query uses LATERAL JOIN to find the latest version for each catalog resource. To avoid
+ # joining the `catalog_resources` table, we build an in-memory table using the resource ids.
+ # Example:
+ # SELECT ...
+ # FROM (VALUES (CATALOG_RESOURCE_ID_1),(CATALOG_RESOURCE_ID_2)) catalog_resources (id)
+ # INNER JOIN LATERAL (...)
+ def latest_for_catalog_resources(catalog_resources)
+ return none if catalog_resources.empty?
+
+ catalog_resources_table = Ci::Catalog::Resource.arel_table
+ catalog_resources_id_list = catalog_resources.map { |resource| "(#{resource.id})" }.join(',')
+
+ # We need to use an alias for the `releases` table here so that it does not
+ # conflict with `joins(:release)` in the `order_by_released_at_*` scope.
+ join_query = Ci::Catalog::Resources::Version
+ .where(catalog_resources_table[:id].eq(arel_table[:catalog_resource_id]))
+ .joins("INNER JOIN releases AS rel ON rel.id = #{table_name}.release_id")
+ .order(Arel.sql('rel.released_at DESC'))
+ .limit(1)
+
+ Ci::Catalog::Resources::Version
+ .from("(VALUES #{catalog_resources_id_list}) #{catalog_resources_table.name} (id)")
+ .joins("INNER JOIN LATERAL (#{join_query.to_sql}) #{table_name} ON TRUE")
+ end
+
+ def keyset_order_by_released_at_asc
+ keyset_order = Gitlab::Pagination::Keyset::Order.build([
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: :released_at,
+ column_expression: Release.arel_table[:released_at],
+ order_expression: Release.arel_table[:released_at].asc,
+ nullable: :not_nullable,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: :id,
+ order_expression: Release.arel_table[:id].asc,
+ nullable: :not_nullable,
+ distinct: true
+ )
+ ])
+
+ reorder(keyset_order)
+ end
+
+ def keyset_order_by_released_at_desc
+ keyset_order = Gitlab::Pagination::Keyset::Order.build([
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: :released_at,
+ column_expression: Release.arel_table[:released_at],
+ order_expression: Release.arel_table[:released_at].desc,
+ nullable: :not_nullable,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: :id,
+ order_expression: Release.arel_table[:id].desc,
+ nullable: :not_nullable,
+ distinct: true
+ )
+ ])
+
+ reorder(keyset_order)
+ end
+
+ def order_by(order)
+ case order.to_s
+ when 'created_asc' then order_by_created_at_asc
+ when 'created_desc' then order_by_created_at_desc
+ when 'released_at_asc' then order_by_released_at_asc
+ else
+ order_by_released_at_desc
+ end
+ end
+ end
end
end
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 2a346f97958..fe4437a4ad6 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -306,7 +306,7 @@ module Ci
end
def expired?
- expire_at.present? && expire_at < Time.current
+ expire_at.present? && expire_at.past?
end
def expiring?
diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb
index f389c642fd8..17809ba20d3 100644
--- a/app/models/ci/job_token/scope.rb
+++ b/app/models/ci/job_token/scope.rb
@@ -54,6 +54,11 @@ module Ci
# if the setting is disabled any project is considered to be in scope.
return true unless current_project.ci_outbound_job_token_scope_enabled?
+ if !accessed_project.private? &&
+ Feature.enabled?(:restrict_ci_job_token_for_public_and_internal_projects, accessed_project)
+ return true
+ end
+
outbound_allowlist.includes?(accessed_project)
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 0a876d26cc9..cf3efc5998f 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -30,9 +30,11 @@ module Ci
PROJECT_ROUTE_AND_NAMESPACE_ROUTE = {
project: [:project_feature, :route, { namespace: :route }]
}.freeze
- CONFIG_EXTENSION = '.gitlab-ci.yml'
- DEFAULT_CONFIG_PATH = CONFIG_EXTENSION
+
+ DEFAULT_CONFIG_PATH = '.gitlab-ci.yml'
+
CANCELABLE_STATUSES = (Ci::HasStatus::CANCELABLE_STATUSES + ['manual']).freeze
+ UNLOCKABLE_STATUSES = (Ci::Pipeline.completed_statuses + [:manual]).freeze
paginates_per 15
@@ -189,6 +191,7 @@ module Ci
# this is needed to ensure tests to be covered
transition [:running] => :running
+ transition [:waiting_for_callback] => :waiting_for_callback
end
event :request_resource do
@@ -203,6 +206,10 @@ module Ci
transition any - [:running] => :running
end
+ event :wait_for_callback do
+ transition any - [:waiting_for_callback] => :waiting_for_callback
+ end
+
event :skip do
transition any - [:skipped] => :skipped
end
@@ -266,6 +273,32 @@ module Ci
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end
+ after_transition any => UNLOCKABLE_STATUSES do |pipeline|
+ # This is a temporary flag that we added just in case we need to totally
+ # stop unlocking pipelines due to unexpected issues during rollout.
+ next if Feature.enabled?(:ci_stop_unlock_pipelines, pipeline.project)
+
+ next unless Feature.enabled?(:ci_unlock_non_successful_pipelines, pipeline.project)
+
+ pipeline.run_after_commit do
+ Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(pipeline.ci_ref_id)
+ end
+ end
+
+ # TODO: Remove this block once we've completed roll-out of ci_unlock_non_successful_pipelines
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/428408
+ after_transition any => :success do |pipeline|
+ # This is a temporary flag that we added just in case we need to totally
+ # stop unlocking pipelines due to unexpected issues during rollout.
+ next if Feature.enabled?(:ci_stop_unlock_pipelines, pipeline.project)
+
+ next unless Feature.disabled?(:ci_unlock_non_successful_pipelines, pipeline.project)
+
+ pipeline.run_after_commit do
+ Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(pipeline.ci_ref_id)
+ end
+ end
+
after_transition [:created, :waiting_for_resource, :preparing, :pending, :running] => :success do |pipeline|
# We wait a little bit to ensure that all Ci::BuildFinishedWorkers finish first
# because this is where some metrics like code coverage is parsed and stored
@@ -380,7 +413,7 @@ module Ci
pipeline.run_after_commit do
next if pipeline.child?
- next unless project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true)
+ next unless Feature.enabled?(:widget_pipeline_pass_subscription_update, project) || project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true)
pipeline.all_merge_requests.opened.each do |merge_request|
GraphqlTriggers.merge_request_merge_status_updated(merge_request)
@@ -389,6 +422,7 @@ module Ci
end
end
+ scope :with_unlockable_status, -> { with_status(*UNLOCKABLE_STATUSES) }
scope :internal, -> { where(source: internal_sources) }
scope :no_child, -> { where.not(source: :parent_pipeline) }
scope :ci_sources, -> { where(source: Enums::Ci::Pipeline.ci_sources.values) }
@@ -554,7 +588,7 @@ module Ci
end
def self.bridgeable_statuses
- ::Ci::Pipeline::AVAILABLE_STATUSES - %w[created waiting_for_resource preparing pending]
+ ::Ci::Pipeline::AVAILABLE_STATUSES - %w[created waiting_for_resource waiting_for_callback preparing pending]
end
def self.auto_devops_pipelines_completed_total
@@ -850,6 +884,7 @@ module Ci
when 'created' then nil
when 'waiting_for_resource' then request_resource
when 'preparing' then prepare
+ when 'waiting_for_callback' then wait_for_callback
when 'pending' then enqueue
when 'running' then run
when 'success' then succeed
@@ -1366,11 +1401,6 @@ module Ci
merge_request.merge_request_diff_for(merge_request_diff_sha)
end
- def reduced_build_attributes_list_for_rules?
- ::Feature.enabled?(:reduced_build_attributes_list_for_rules, project)
- end
- strong_memoize_attr :reduced_build_attributes_list_for_rules?
-
private
def add_message(severity, content)
diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb
index 8655e8eb9b8..e8ce58f2de5 100644
--- a/app/models/ci/ref.rb
+++ b/app/models/ci/ref.rb
@@ -30,15 +30,6 @@ module Ci
state :fixed, value: 3
state :broken, value: 4
state :still_failing, value: 5
-
- after_transition any => [:fixed, :success] do |ci_ref|
- # Do not try to unlock if no artifacts are locked
- next unless ci_ref.artifacts_locked?
-
- ci_ref.run_after_commit do
- Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(ci_ref.id)
- end
- end
end
class << self
@@ -75,5 +66,13 @@ module Ci
self.status_name
end
end
+
+ def last_successful_ci_source_pipeline
+ pipelines.ci_sources.success.order(id: :desc).first
+ end
+
+ def last_unlockable_ci_source_pipeline
+ pipelines.ci_sources.with_unlockable_status.order(id: :desc).first
+ end
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 91c919dc662..9c30beeeb59 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -123,6 +123,8 @@ module Ci
joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: group_id })
}
+ scope :with_creator_id, -> (value) { where(creator_id: value) }
+
scope :belonging_to_group_or_project_descendants, -> (group_id) {
group_ids = Ci::NamespaceMirror.by_group_and_descendants(group_id).select(:namespace_id)
project_ids = Ci::ProjectMirror.by_namespace_id(group_ids).select(:project_id)
@@ -217,6 +219,8 @@ module Ci
validate :any_project, if: :project_type?
validate :exactly_one_group, if: :group_type?
+ scope :with_version_prefix, ->(value) { joins(:runner_managers).merge(RunnerManager.with_version_prefix(value)) }
+
acts_as_taggable
after_destroy :cleanup_runner_queue
diff --git a/app/models/ci/runner_manager.rb b/app/models/ci/runner_manager.rb
index 7d8fc097f51..e6576859827 100644
--- a/app/models/ci/runner_manager.rb
+++ b/app/models/ci/runner_manager.rb
@@ -62,6 +62,16 @@ module Ci
scope :order_id_desc, -> { order(id: :desc) }
+ scope :with_version_prefix, ->(value) do
+ regex = version_regex_expression_for_version(value)
+ value += '.' if regex.end_with?('\.') && !value.end_with?('.')
+ substring = Arel::Nodes::NamedFunction.new('substring', [
+ Ci::RunnerManager.arel_table[:version],
+ Arel.sql("'#{regex}'::text")
+ ])
+ where(substring.eq(sanitize_sql_like(value)))
+ end
+
scope :with_upgrade_status, ->(upgrade_status) do
joins(:runner_version).where(runner_version: { status: upgrade_status })
end
@@ -137,5 +147,16 @@ module Ci
Ci::Runners::ProcessRunnerVersionUpdateWorker.perform_async(new_version)
end
+
+ def self.version_regex_expression_for_version(version)
+ case version
+ when /\d+\.\d+\.\d+/
+ '^\d+\.\d+\.\d+'
+ when /\d+\.\d+(\.)?/
+ '^\d+\.\d+\.'
+ else
+ '^\d+\.'
+ end
+ end
end
end
diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb
index 5b6946b04fd..475d57ee4c8 100644
--- a/app/models/ci/sources/pipeline.rb
+++ b/app/models/ci/sources/pipeline.rb
@@ -12,7 +12,7 @@ module Ci
:pipeline_id_convert_to_bigint, :source_pipeline_id_convert_to_bigint
], remove_with: '16.6', remove_after: '2023-10-22'
- columns_changing_default :partition_id
+ columns_changing_default :partition_id, :source_partition_id
self.table_name = "ci_sources_pipelines"
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 3a498972153..3d2df9a45ef 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -78,6 +78,10 @@ module Ci
transition any - [:running] => :running
end
+ event :wait_for_callback do
+ transition any - [:waiting_for_callback] => :waiting_for_callback
+ end
+
event :skip do
transition any - [:skipped] => :skipped
end
@@ -109,6 +113,7 @@ module Ci
when 'created' then nil
when 'waiting_for_resource' then request_resource
when 'preparing' then prepare
+ when 'waiting_for_callback' then wait_for_callback
when 'pending' then enqueue
when 'running' then run
when 'success' then succeed
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 39e12b53f21..886e6e9fbd7 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -372,9 +372,7 @@ class Commit
strong_memoize(:raw_signature_type) do
next unless @raw.instance_of?(Gitlab::Git::Commit)
- if raw_commit_from_rugged? && gpg_commit.signature_text.present?
- :PGP
- elsif defined? @raw.raw_commit.signature_type
+ if defined? @raw.raw_commit.signature_type
@raw.raw_commit.signature_type
end
end
@@ -397,10 +395,6 @@ class Commit
end
end
- def raw_commit_from_rugged?
- @raw.raw_commit.is_a?(Rugged::Commit)
- end
-
def gpg_commit
@gpg_commit ||= Gitlab::Gpg::Commit.new(self)
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 3761aa81bf7..9f77bd8ebe2 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -8,20 +8,24 @@ class CommitStatus < Ci::ApplicationRecord
include Presentable
include BulkInsertableAssociations
include TaggableQueries
-
- def self.switch_table_names
- if Gitlab::Utils.to_boolean(ENV['USE_CI_BUILDS_ROUTING_TABLE'])
- :p_ci_builds
- else
- :ci_builds
- end
- end
-
- self.table_name = self.switch_table_names
+ include IgnorableColumns
+
+ ignore_columns %i[
+ auto_canceled_by_id_convert_to_bigint
+ commit_id_convert_to_bigint
+ erased_by_id_convert_to_bigint
+ project_id_convert_to_bigint
+ runner_id_convert_to_bigint
+ trigger_request_id_convert_to_bigint
+ upstream_pipeline_id_convert_to_bigint
+ user_id_convert_to_bigint
+ ], remove_with: '17.0', remove_after: '2024-04-22'
+
+ self.table_name = :p_ci_builds
self.sequence_name = :ci_builds_id_seq
self.primary_key = :id
- partitionable scope: :pipeline
+ partitionable scope: :pipeline, partitioned: true
belongs_to :user
belongs_to :project
@@ -155,15 +159,15 @@ class CommitStatus < Ci::ApplicationRecord
end
event :drop do
- transition [:created, :waiting_for_resource, :preparing, :pending, :running, :manual, :scheduled] => :failed
+ transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running, :manual, :scheduled] => :failed
end
event :success do
- transition [:created, :waiting_for_resource, :preparing, :pending, :running] => :success
+ transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running] => :success
end
event :cancel do
- transition [:created, :waiting_for_resource, :preparing, :pending, :running, :manual, :scheduled] => :canceled
+ transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running, :manual, :scheduled] => :canceled
end
before_transition [:created, :waiting_for_resource, :preparing, :skipped, :manual, :scheduled] => :pending do |commit_status|
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 1d9cf5729cd..dfcc905b3c3 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module Analytics
module CycleAnalytics
module StageEventModel
diff --git a/app/models/concerns/can_move_repository_storage.rb b/app/models/concerns/can_move_repository_storage.rb
index 1132e4e79ac..1646ed3dc7c 100644
--- a/app/models/concerns/can_move_repository_storage.rb
+++ b/app/models/concerns/can_move_repository_storage.rb
@@ -9,6 +9,9 @@ module CanMoveRepositoryStorage
# progress beforehand. Setting a repository read-only will fail if it is
# already in that state.
#
+ # It is assumed that `with_lock` is used here to ensure that no race condition
+ # appears between reading and writing the read-only column.
+ #
# @return nil. Failures will raise an exception
def set_repository_read_only!(skip_git_transfer_check: false)
with_lock do
@@ -16,10 +19,10 @@ module CanMoveRepositoryStorage
!skip_git_transfer_check && git_transfer_in_progress?
raise RepositoryReadOnlyError, _('Repository already read-only') if
- _safe_read_repository_read_only_column
+ safe_read_repository_read_only_column
raise ActiveRecord::RecordNotSaved, _('Database update failed') unless
- _update_repository_read_only_column(true)
+ update_repository_read_only_column(true)
nil
end
@@ -28,12 +31,8 @@ module CanMoveRepositoryStorage
# Set repository as writable again. Unlike setting it read-only, this will
# succeed if the repository is already writable.
def set_repository_writable!
- with_lock do
- raise ActiveRecord::RecordNotSaved, _('Database update failed') unless
- _update_repository_read_only_column(false)
-
- nil
- end
+ raise ActiveRecord::RecordNotSaved, _('Database update failed') unless
+ update_repository_read_only_column(false)
end
def git_transfer_in_progress?
@@ -49,13 +48,13 @@ module CanMoveRepositoryStorage
# Not all resources that can move repositories have the `repository_read_only`
# in their table, for example groups. We need these methods to override the
# behavior in those classes in order to access the column.
- def _safe_read_repository_read_only_column
+ def safe_read_repository_read_only_column
# This was added originally this way because of
# https://gitlab.com/gitlab-org/gitlab/-/commit/43f9b98302d3985312c9f8b66018e2835d8293d2
self.class.where(id: id).pick(:repository_read_only)
end
- def _update_repository_read_only_column(value)
+ def update_repository_read_only_column(value)
update_column(:repository_read_only, value)
end
end
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index 2971ecb04b8..fb2b12e5f00 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -6,19 +6,20 @@ module Ci
DEFAULT_STATUS = 'created'
BLOCKED_STATUS = %w[manual scheduled].freeze
- AVAILABLE_STATUSES = %w[created waiting_for_resource preparing pending running success failed canceled skipped manual scheduled].freeze
+ AVAILABLE_STATUSES = %w[created waiting_for_resource preparing waiting_for_callback pending running success failed canceled skipped manual scheduled].freeze
STARTED_STATUSES = %w[running success failed].freeze
- ACTIVE_STATUSES = %w[waiting_for_resource preparing pending running].freeze
+ ACTIVE_STATUSES = %w[waiting_for_resource preparing waiting_for_callback pending running].freeze
COMPLETED_STATUSES = %w[success failed canceled skipped].freeze
STOPPED_STATUSES = COMPLETED_STATUSES + BLOCKED_STATUS
- ORDERED_STATUSES = %w[failed preparing pending running waiting_for_resource manual scheduled canceled success skipped created].freeze
+ ORDERED_STATUSES = %w[failed preparing pending running waiting_for_callback waiting_for_resource manual scheduled canceled success skipped created].freeze
PASSED_WITH_WARNINGS_STATUSES = %w[failed canceled].to_set.freeze
IGNORED_STATUSES = %w[manual].to_set.freeze
ALIVE_STATUSES = (ACTIVE_STATUSES + ['created']).freeze
CANCELABLE_STATUSES = (ALIVE_STATUSES + ['scheduled']).freeze
STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
failed: 4, canceled: 5, skipped: 6, manual: 7,
- scheduled: 8, preparing: 9, waiting_for_resource: 10 }.freeze
+ scheduled: 8, preparing: 9, waiting_for_resource: 10,
+ waiting_for_callback: 11 }.freeze
UnknownStatusError = Class.new(StandardError)
@@ -58,6 +59,7 @@ module Ci
state :created, value: 'created'
state :waiting_for_resource, value: 'waiting_for_resource'
state :preparing, value: 'preparing'
+ state :waiting_for_callback, value: 'waiting_for_callback'
state :pending, value: 'pending'
state :running, value: 'running'
state :failed, value: 'failed'
@@ -72,6 +74,7 @@ module Ci
scope :waiting_for_resource, -> { with_status(:waiting_for_resource) }
scope :preparing, -> { with_status(:preparing) }
scope :relevant, -> { without_status(:created) }
+ scope :waiting_for_callback, -> { with_status(:waiting_for_callback) }
scope :running, -> { with_status(:running) }
scope :pending, -> { with_status(:pending) }
scope :success, -> { with_status(:success) }
diff --git a/app/models/concerns/commit_signature.rb b/app/models/concerns/commit_signature.rb
index 5bdf6bb31bf..201994cb321 100644
--- a/app/models/concerns/commit_signature.rb
+++ b/app/models/concerns/commit_signature.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module CommitSignature
extend ActiveSupport::Concern
diff --git a/app/models/concerns/diff_positionable_note.rb b/app/models/concerns/diff_positionable_note.rb
index 2f64129b65f..e799127d69a 100644
--- a/app/models/concerns/diff_positionable_note.rb
+++ b/app/models/concerns/diff_positionable_note.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module DiffPositionableNote
extend ActiveSupport::Concern
diff --git a/app/models/concerns/enums/package_metadata.rb b/app/models/concerns/enums/package_metadata.rb
index 3f107987ef6..352eb41829b 100644
--- a/app/models/concerns/enums/package_metadata.rb
+++ b/app/models/concerns/enums/package_metadata.rb
@@ -14,7 +14,8 @@ module Enums
apk: 9,
rpm: 10,
deb: 11,
- cbl_mariner: 12
+ 'cbl-mariner': 12,
+ wolfi: 13
}.with_indifferent_access.freeze
ADVISORY_SOURCES = {
diff --git a/app/models/concerns/enums/sbom.rb b/app/models/concerns/enums/sbom.rb
index 59aafc32d94..af8e37b4248 100644
--- a/app/models/concerns/enums/sbom.rb
+++ b/app/models/concerns/enums/sbom.rb
@@ -18,7 +18,8 @@ module Enums
apk: 9,
rpm: 10,
deb: 11,
- cbl_mariner: 12
+ 'cbl-mariner': 12,
+ wolfi: 13
}.with_indifferent_access.freeze
def self.component_types
diff --git a/app/models/concerns/merge_request_reviewer_state.rb b/app/models/concerns/merge_request_reviewer_state.rb
index 412b1da55da..e4ee6e7e58e 100644
--- a/app/models/concerns/merge_request_reviewer_state.rb
+++ b/app/models/concerns/merge_request_reviewer_state.rb
@@ -6,7 +6,8 @@ module MergeRequestReviewerState
included do
enum state: {
unreviewed: 0,
- reviewed: 1
+ reviewed: 1,
+ requested_changes: 2
}
validates :state,
diff --git a/app/models/concerns/repository_storage_movable.rb b/app/models/concerns/repository_storage_movable.rb
index 77edabb9706..b1dbebff4fb 100644
--- a/app/models/concerns/repository_storage_movable.rb
+++ b/app/models/concerns/repository_storage_movable.rb
@@ -6,6 +6,9 @@ module RepositoryStorageMovable
included do
scope :order_created_at_desc, -> { order(created_at: :desc) }
+ scope :scheduled_or_started, -> do
+ where(state: [state_machine.states[:scheduled].value, state_machine.states[:started].value])
+ end
validates :container, presence: true
validates :state, presence: true
@@ -43,6 +46,8 @@ module RepositoryStorageMovable
transition replicated: :cleanup_failed
end
+ # An after_transition can't affect the success of the transition.
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45160#note_431071664
around_transition initial: :scheduled do |storage_move, block|
block.call
@@ -61,13 +66,9 @@ module RepositoryStorageMovable
true
end
- before_transition started: :replicated do |storage_move|
+ after_transition started: :replicated do |storage_move|
storage_move.container.set_repository_writable!
- storage_move.update_repository_storage(storage_move.destination_storage_name)
- end
-
- after_transition started: :replicated do |storage_move|
# We have several scripts in place that replicate some statistics information
# to other databases. Some of them depend on the updated_at column
# to identify the models they need to extract.
@@ -83,6 +84,13 @@ module RepositoryStorageMovable
storage_move.container.set_repository_writable!
end
+ # This callback ensures the repository is set to writable in the event of
+ # a connection error during the :started -> :replicated transition
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/427254#note_1636072125
+ before_transition replicated: :cleanup_failed do |storage_move|
+ storage_move.container.set_repository_writable!
+ end
+
state :initial, value: 1
state :scheduled, value: 2
state :started, value: 3
@@ -93,15 +101,6 @@ module RepositoryStorageMovable
end
end
- # Projects, snippets, and group wikis has different db structure. In projects,
- # we need to update some columns in this step, but we don't with the other resources.
- #
- # Therefore, we create this No-op method for snippets and wikis and let project
- # overwrite it in their implementation.
- def update_repository_storage(new_storage)
- # No-op
- end
-
def schedule_repository_storage_update_worker
raise NotImplementedError
end
diff --git a/app/models/concerns/restricted_signup.rb b/app/models/concerns/restricted_signup.rb
index 6af9ede5e8b..87b62214529 100644
--- a/app/models/concerns/restricted_signup.rb
+++ b/app/models/concerns/restricted_signup.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module RestrictedSignup
extend ActiveSupport::Concern
diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb
index d0085b60d98..b25ee434484 100644
--- a/app/models/concerns/token_authenticatable_strategies/base.rb
+++ b/app/models/concerns/token_authenticatable_strategies/base.rb
@@ -65,7 +65,7 @@ module TokenAuthenticatableStrategies
return false unless expirable? && token_expiration_enforced?
exp = expires_at(instance)
- !!exp && Time.current > exp
+ !!exp && exp.past?
end
def expirable?
diff --git a/app/models/concerns/use_sql_function_for_primary_key_lookups.rb b/app/models/concerns/use_sql_function_for_primary_key_lookups.rb
new file mode 100644
index 00000000000..c3ca3cfc038
--- /dev/null
+++ b/app/models/concerns/use_sql_function_for_primary_key_lookups.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module UseSqlFunctionForPrimaryKeyLookups
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def find(*args)
+ return super unless Feature.enabled?(:use_sql_functions_for_primary_key_lookups, Feature.current_request)
+ return super unless args.one?
+ return super if block_given? || primary_key.nil? || scope_attributes?
+
+ return_array = false
+ id = args.first
+
+ if id.is_a?(Array)
+ return super if id.many?
+
+ return_array = true
+
+ id = id.first
+ end
+
+ return super if id.nil? || (id.is_a?(String) && !id.number?)
+
+ from_clause = "find_#{table_name}_by_id(?) #{quoted_table_name}"
+ filter_empty_row = "#{quoted_table_name}.#{connection.quote_column_name(primary_key)} IS NOT NULL"
+ query = from(from_clause).where(filter_empty_row).limit(1).to_sql
+ # Using find_by_sql so we get query cache working
+ record = find_by_sql([query, id]).first
+
+ unless record
+ message = "Couldn't find #{name} with '#{primary_key}'=#{id}"
+ raise(ActiveRecord::RecordNotFound.new(message, name, primary_key, id))
+ end
+
+ return_array ? [record] : record
+ end
+ end
+end
diff --git a/app/models/concerns/users/visitable.rb b/app/models/concerns/users/visitable.rb
index cb8e5fdc682..029d60d61ee 100644
--- a/app/models/concerns/users/visitable.rb
+++ b/app/models/concerns/users/visitable.rb
@@ -13,6 +13,45 @@ module Users
time = time.to_datetime
where(entity_id: entity_id, user_id: user_id, visited_at: (time - 15.minutes)..(time + 15.minutes))
end
+
+ scope :for_user, ->(user_id) { where(user_id: user_id) }
+
+ scope :recently_visited, -> do
+ where('visited_at > ?', 3.months.ago)
+ .where('visited_at <= ?', Time.current)
+ end
+
+ def self.grouped_by_week_start_and_entity_for_user(user_id:)
+ recently_visited
+ .for_user(user_id)
+ .group(:week_start, :entity_id)
+ .select(
+ :entity_id,
+ "COUNT(entity_id) AS week_count",
+ "DATE_TRUNC('week', visited_at)::date AS week_start",
+ "DENSE_RANK() OVER (ORDER BY DATE_TRUNC('week', visited_at)::date)"
+ )
+ end
+
+ def self.frecent_visits_scores(user_id:, limit:)
+ ranked_entity_visits_query = grouped_by_week_start_and_entity_for_user(user_id: user_id).to_sql
+ sql = <<~SQL
+ SELECT
+ entity_id,
+ SUM(week_count * dense_rank) AS score
+ FROM
+ (#{ranked_entity_visits_query}) as ranked_entity_visits
+ GROUP BY
+ entity_id
+ ORDER BY
+ score DESC
+ LIMIT #{limit}
+ SQL
+
+ ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
+ connection.execute(sql).to_a
+ end
+ end
end
end
end
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 6a52f6a0112..15ed517dc12 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -482,6 +482,24 @@ class ContainerRepository < ApplicationRecord
raise 'too many pages requested' if page_count >= MAX_TAGS_PAGES
end
+ def tags_page(before: nil, last: nil, sort: nil, name: nil, page_size: 100)
+ raise ArgumentError, 'not a migrated repository' unless migrated?
+
+ page = gitlab_api_client.tags(
+ self.path,
+ page_size: page_size,
+ before: before,
+ last: last,
+ sort: sort,
+ name: name
+ )
+
+ {
+ tags: transform_tags_page(page[:response_body]),
+ pagination: page[:pagination]
+ }
+ end
+
def tags_count
return 0 unless manifest && manifest['tags']
@@ -505,15 +523,11 @@ class ContainerRepository < ApplicationRecord
digests = tags.map { |tag| tag.digest }.compact.to_set
- digests.map { |digest| delete_tag_by_digest(digest) }.all?
- end
-
- def delete_tag_by_digest(digest)
- client.delete_repository_tag_by_digest(self.path, digest)
+ digests.map { |digest| delete_tag(digest) }.all?
end
- def delete_tag_by_name(name)
- client.delete_repository_tag_by_name(self.path, name)
+ def delete_tag(name_or_digest)
+ client.delete_repository_tag_by_digest(self.path, name_or_digest)
end
def start_expiration_policy!
@@ -640,6 +654,9 @@ class ContainerRepository < ApplicationRecord
tag = ContainerRegistry::Tag.new(self, raw_tag['name'])
tag.force_created_at_from_iso8601(raw_tag['created_at'])
tag.updated_at = raw_tag['updated_at']
+ tag.total_size = raw_tag['size_bytes']
+ tag.manifest_digest = raw_tag['digest']
+ tag.revision = raw_tag['config_digest'].to_s.split(':')[1]
tag
end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 0bdce18bab5..f0093445ba8 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -8,12 +8,15 @@ class Deployment < ApplicationRecord
include Importable
include Gitlab::Utils::StrongMemoize
include FastDestroyAll
+ include IgnorableColumns
StatusUpdateError = Class.new(StandardError)
StatusSyncError = Class.new(StandardError)
ARCHIVABLE_OFFSET = 50_000
+ ignore_column :cluster_id, remove_with: '16.8', remove_after: '2023-12-21'
+
belongs_to :project, optional: false
belongs_to :environment, optional: false
belongs_to :user
diff --git a/app/models/environment.rb b/app/models/environment.rb
index efdcf7174aa..4f76fae24eb 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -8,6 +8,8 @@ class Environment < ApplicationRecord
include NullifyIfBlank
include FromUnion
+ LONG_STOP = 1.week
+
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 55.seconds
self.reactive_cache_hard_limit = 10.megabytes
@@ -89,6 +91,7 @@ class Environment < ApplicationRecord
delegate :auto_rollback_enabled?, to: :project
scope :available, -> { with_state(:available) }
+ scope :active, -> { with_state(:available, :stopping) }
scope :stopped, -> { with_state(:stopped) }
scope :order_by_last_deployed_at, -> do
@@ -104,6 +107,7 @@ class Environment < ApplicationRecord
scope :preload_project, -> { preload(:project) }
scope :auto_stoppable, -> (limit) { available.where('auto_stop_at < ?', Time.zone.now).limit(limit) }
scope :auto_deletable, -> (limit) { stopped.where('auto_delete_at < ?', Time.zone.now).limit(limit) }
+ scope :long_stopping, -> { with_state(:stopping).where('updated_at < ?', LONG_STOP.ago) }
scope :deployed_and_updated_before, -> (project_id, before) do
# this query joins deployments and filters out any environment that has recent deployments
@@ -322,6 +326,10 @@ class Environment < ApplicationRecord
last_deployment.try(:created_at)
end
+ def long_stopping?
+ stopping? && self.updated_at < LONG_STOP.ago
+ end
+
def ref_path
"refs/#{Repository::REF_ENVIRONMENTS}/#{slug}"
end
diff --git a/app/models/group.rb b/app/models/group.rb
index c83dd24e98e..51c26767569 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -300,14 +300,15 @@ class Group < Namespace
groups.drop(1).each { |group| group.root_ancestor = root }
end
- # Returns the ids of the passed group models where the `emails_disabled`
- # column is set to true anywhere in the ancestor hierarchy.
+ # Returns the ids of the passed group models where the `emails_enabled`
+ # column is set to false anywhere in the ancestor hierarchy.
def ids_with_disabled_email(groups)
inner_groups = Group.where('id = namespaces_with_emails_disabled.id')
inner_query = inner_groups
.self_and_ancestors
- .where(emails_disabled: true)
+ .joins(:namespace_settings)
+ .where(namespace_settings: { emails_enabled: false })
.select('1')
.limit(1)
@@ -593,40 +594,13 @@ class Group < Namespace
end
def authorizable_members_with_parents
- source_ids =
- if has_parent?
- self_and_ancestors.reorder(nil).select(:id)
- else
- id
- end
-
- group_hierarchy_members = GroupMember.where(source_id: source_ids).select(*GroupMember.cached_column_list)
-
- GroupMember.from_union([group_hierarchy_members,
- members_from_self_and_ancestor_group_shares]).authorizable
+ Members::MembersWithParents.new(self).all_members.authorizable
end
def members_with_parents(only_active_users: true)
- # Avoids an unnecessary SELECT when the group has no parents
- source_ids =
- if has_parent?
- self_and_ancestors.reorder(nil).select(:id)
- else
- id
- end
-
- group_hierarchy_members = GroupMember.non_minimal_access
- .where(source_id: source_ids)
- .select(*GroupMember.cached_column_list)
-
- group_hierarchy_members = if only_active_users
- group_hierarchy_members.active_without_invites_and_requests
- else
- group_hierarchy_members.without_invites_and_requests
- end
-
- GroupMember.from_union([group_hierarchy_members,
- members_from_self_and_ancestor_group_shares])
+ Members::MembersWithParents
+ .new(self)
+ .members(active_users: only_active_users)
end
def members_from_self_and_ancestors_with_effective_access_level
@@ -671,15 +645,6 @@ class Group < Namespace
members.count
end
- # Returns all users that are members of projects
- # belonging to the current group or sub-groups
- def project_users_with_descendants
- User
- .joins(projects: :group)
- .where(namespaces: { id: self_and_descendants.select(:id) })
- .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455")
- end
-
# Return the highest access level for a user
#
# A special case is handled here when the user is a GitLab admin
@@ -996,48 +961,6 @@ class Group < Namespace
errors.add(:require_two_factor_authentication, _('is forbidden by a top-level group'))
end
- def members_from_self_and_ancestor_group_shares
- group_group_link_table = GroupGroupLink.arel_table
- group_member_table = GroupMember.arel_table
-
- source_ids =
- if has_parent?
- self_and_ancestors.reorder(nil).select(:id)
- else
- id
- end
-
- group_group_links_query = GroupGroupLink.where(shared_group_id: source_ids)
- cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
- cte_alias = cte.table.alias(GroupGroupLink.table_name)
-
- # Instead of members.access_level, we need to maximize that access_level at
- # the respective group_group_links.group_access.
- member_columns = GroupMember.attribute_names.map do |column_name|
- if column_name == 'access_level'
- smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]], 'access_level')
- else
- group_member_table[column_name]
- end
- end
-
- GroupMember
- .with(cte.to_arel)
- .select(*member_columns)
- .from([group_member_table, cte.alias_to(group_group_link_table)])
- .where(group_member_table[:requested_at].eq(nil))
- .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
- .where(group_member_table[:source_type].eq('Namespace'))
- .where(group_member_table[:state].eq(::Member::STATE_ACTIVE))
- .non_minimal_access
- end
-
- def smallest_value_arel(args, column_alias)
- Arel::Nodes::As.new(
- Arel::Nodes::NamedFunction.new('LEAST', args),
- Arel::Nodes::SqlLiteral.new(column_alias))
- end
-
def runners_token_prefix
RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
end
diff --git a/app/models/guest.rb b/app/models/guest.rb
deleted file mode 100644
index 9c8097e1ac8..00000000000
--- a/app/models/guest.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class Guest
- class << self
- def can?(action, subject = :global)
- Ability.allowed?(nil, action, subject)
- end
- end
-end
diff --git a/app/models/integration.rb b/app/models/integration.rb
index b4408301c6d..7c14c1b1716 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -237,6 +237,18 @@ class Integration < ApplicationRecord
end
private_class_method :boolean_accessor
+ def self.title
+ raise NotImplementedError
+ end
+
+ def self.description
+ raise NotImplementedError
+ end
+
+ def self.help
+ # no-op
+ end
+
def self.to_param
raise NotImplementedError
end
@@ -447,19 +459,18 @@ class Integration < ApplicationRecord
end
def title
- # implement inside child
+ self.class.title
end
def description
- # implement inside child
+ self.class.description
end
def help
- # implement inside child
+ self.class.help
end
def to_param
- # implement inside child
self.class.to_param
end
@@ -588,7 +599,7 @@ class Integration < ApplicationRecord
return if ::Gitlab::SilentMode.enabled?
return unless supported_events.include?(data[:object_kind])
- Integrations::ExecuteWorker.perform_async(id, data)
+ Integrations::ExecuteWorker.perform_async(id, data.deep_stringify_keys)
end
# override if needed
diff --git a/app/models/integrations/apple_app_store.rb b/app/models/integrations/apple_app_store.rb
index ef12fc6bf6f..f8fddf8a457 100644
--- a/app/models/integrations/apple_app_store.rb
+++ b/app/models/integrations/apple_app_store.rb
@@ -37,15 +37,15 @@ module Integrations
title: -> { s_('AppleAppStore|Protected branches and tags only') },
checkbox_label: -> { s_('AppleAppStore|Only set variables on protected branches and tags') }
- def title
+ def self.title
'Apple App Store Connect'
end
- def description
+ def self.description
s_('AppleAppStore|Use GitLab to build and release an app in the Apple App Store.')
end
- def help
+ def self.help
variable_list = [
'<code>APP_STORE_CONNECT_API_KEY_ISSUER_ID</code>',
'<code>APP_STORE_CONNECT_API_KEY_KEY_ID</code>',
diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb
index 77555996cd9..39407acd6c9 100644
--- a/app/models/integrations/asana.rb
+++ b/app/models/integrations/asana.rb
@@ -20,15 +20,15 @@ module Integrations
title: -> { s_('Integrations|Restrict to branch (optional)') },
help: -> { s_('AsanaService|Comma-separated list of branches to be automatically inspected. Leave blank to include all branches.') }
- def title
+ def self.title
'Asana'
end
- def description
+ def self.description
s_('AsanaService|Add commit messages as comments to Asana tasks.')
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/asana'), target: '_blank', rel: 'noopener noreferrer'
s_('Add commit messages as comments to Asana tasks. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/assembla.rb b/app/models/integrations/assembla.rb
index 1d3616b4c3b..bbdd0e183f2 100644
--- a/app/models/integrations/assembla.rb
+++ b/app/models/integrations/assembla.rb
@@ -15,11 +15,11 @@ module Integrations
exposes_secrets: true,
placeholder: ''
- def title
+ def self.title
'Assembla'
end
- def description
+ def self.description
_('Manage projects.')
end
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index 9f15532a0b0..9fe73f86be3 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -38,15 +38,15 @@ module Integrations
attr_accessor :response
- def title
+ def self.title
s_('BambooService|Atlassian Bamboo')
end
- def description
+ def self.description
s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo.')
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'),
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index b75801335bd..167bc210349 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -136,10 +136,6 @@ module Integrations
raise NotImplementedError
end
- def help
- raise NotImplementedError
- end
-
# With some integrations the webhook is already tied to a specific channel,
# for others the channels are configurable for each event.
def configurable_channels?
diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb
index 09a0c9ba361..33dd9d9d387 100644
--- a/app/models/integrations/base_slack_notification.rb
+++ b/app/models/integrations/base_slack_notification.rb
@@ -36,7 +36,7 @@ module Integrations
true
end
- def help
+ def self.help
# noop
end
diff --git a/app/models/integrations/bugzilla.rb b/app/models/integrations/bugzilla.rb
index 74e282f6848..3ca348e42a1 100644
--- a/app/models/integrations/bugzilla.rb
+++ b/app/models/integrations/bugzilla.rb
@@ -6,15 +6,15 @@ module Integrations
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
- def title
+ def self.title
'Bugzilla'
end
- def description
+ def self.description
s_("IssueTracker|Use Bugzilla as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bugzilla'), target: '_blank', rel: 'noopener noreferrer'
s_("IssueTracker|Use Bugzilla as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb
index 82a5142e8c2..aab0cdf2134 100644
--- a/app/models/integrations/buildkite.rb
+++ b/app/models/integrations/buildkite.rb
@@ -75,20 +75,20 @@ module Integrations
"#{project_url}/builds?commit=#{sha}"
end
- def title
+ def self.title
'Buildkite'
end
- def description
+ def self.description
'Run CI/CD pipelines with Buildkite.'
end
- def self.to_param
- 'buildkite'
+ def self.help
+ s_('ProjectService|Run CI/CD pipelines with Buildkite.')
end
- def help
- s_('ProjectService|Run CI/CD pipelines with Buildkite.')
+ def self.to_param
+ 'buildkite'
end
def calculate_reactive_cache(sha, ref)
diff --git a/app/models/integrations/campfire.rb b/app/models/integrations/campfire.rb
index 8b5797a9d24..18268ed18f4 100644
--- a/app/models/integrations/campfire.rb
+++ b/app/models/integrations/campfire.rb
@@ -36,15 +36,15 @@ module Integrations
placeholder: '123456',
help: -> { s_('CampfireService|From the end of the room URL.') }
- def title
+ def self.title
'Campfire'
end
- def description
+ def self.description
'Send notifications about push events to Campfire chat rooms.'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('api/integrations', anchor: 'campfire'),
diff --git a/app/models/integrations/clickup.rb b/app/models/integrations/clickup.rb
index 7cc05d41e14..25287b53300 100644
--- a/app/models/integrations/clickup.rb
+++ b/app/models/integrations/clickup.rb
@@ -10,15 +10,15 @@ module Integrations
@reference_pattern ||= /((#|CU-)(?<issue>[a-z0-9]+)|(?<issue>[A-Z0-9_]{2,10}-\d+))\b/
end
- def title
+ def self.title
'ClickUp'
end
- def description
+ def self.description
s_("IssueTracker|Use Clickup as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/clickup'),
target: '_blank',
diff --git a/app/models/integrations/confluence.rb b/app/models/integrations/confluence.rb
index eda8c37fc72..f97f1fd25c9 100644
--- a/app/models/integrations/confluence.rb
+++ b/app/models/integrations/confluence.rb
@@ -22,11 +22,11 @@ module Integrations
'confluence'
end
- def title
+ def self.title
s_('ConfluenceService|Confluence Workspace')
end
- def description
+ def self.description
s_('ConfluenceService|Link to a Confluence Workspace from the sidebar.')
end
diff --git a/app/models/integrations/custom_issue_tracker.rb b/app/models/integrations/custom_issue_tracker.rb
index 3770e813eaa..fe0d01d60bd 100644
--- a/app/models/integrations/custom_issue_tracker.rb
+++ b/app/models/integrations/custom_issue_tracker.rb
@@ -6,15 +6,15 @@ module Integrations
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
- def title
+ def self.title
s_('IssueTracker|Custom issue tracker')
end
- def description
+ def self.description
s_("IssueTracker|Use a custom issue tracker as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/custom_issue_tracker'), target: '_blank', rel: 'noopener noreferrer'
s_('IssueTracker|Use a custom issue tracker that is not in the integration list. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
index b1f1361afcd..5682fc2b139 100644
--- a/app/models/integrations/datadog.rb
+++ b/app/models/integrations/datadog.rb
@@ -117,15 +117,15 @@ module Integrations
# archive_trace is opt-in but we handle it with a more detailed field below
end
- def title
+ def self.title
'Datadog'
end
- def description
+ def self.description
s_('DatadogIntegration|Trace your GitLab pipelines with Datadog.')
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
s_('DatadogIntegration|How do I set up this integration?'),
Rails.application.routes.url_helpers.help_page_url('integration/datadog'),
diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb
index 33b2b52fa62..7ce597389f0 100644
--- a/app/models/integrations/discord.rb
+++ b/app/models/integrations/discord.rb
@@ -21,23 +21,23 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
s_("DiscordService|Discord Notifications")
end
- def description
+ def self.description
s_("DiscordService|Send notifications about project events to a Discord channel.")
end
- def self.to_param
- "discord"
- end
-
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/discord_notifications'), target: '_blank', rel: 'noopener noreferrer'
s_('Send notifications about project events to a Discord channel. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
+ def self.to_param
+ "discord"
+ end
+
def default_channel_placeholder
s_('DiscordService|Override the default webhook (e.g. https://discord.com/api/webhooks/…)')
end
diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb
index f6a12c4bb1a..b59e504c98f 100644
--- a/app/models/integrations/drone_ci.rb
+++ b/app/models/integrations/drone_ci.rb
@@ -87,20 +87,20 @@ module Integrations
"gitlab/#{project.full_path}/redirect/commits/#{sha}?branch=#{Addressable::URI.encode_component(ref.to_s)}")
end
- def title
+ def self.title
'Drone'
end
- def description
+ def self.description
s_('ProjectService|Run CI/CD pipelines with Drone.')
end
- def self.to_param
- 'drone_ci'
+ def self.help
+ s_('ProjectService|Run CI/CD pipelines with Drone.')
end
- def help
- s_('ProjectService|Run CI/CD pipelines with Drone.')
+ def self.to_param
+ 'drone_ci'
end
override :hook_url
diff --git a/app/models/integrations/emails_on_push.rb b/app/models/integrations/emails_on_push.rb
index 144d1a07b04..77be8f5db45 100644
--- a/app/models/integrations/emails_on_push.rb
+++ b/app/models/integrations/emails_on_push.rb
@@ -39,11 +39,11 @@ module Integrations
recipients.split.grep(Devise.email_regexp).uniq(&:downcase)
end
- def title
+ def self.title
s_('EmailsOnPushService|Emails on push')
end
- def description
+ def self.description
s_('EmailsOnPushService|Email the commits and diff of each push to a list of recipients.')
end
diff --git a/app/models/integrations/ewm.rb b/app/models/integrations/ewm.rb
index 003c896704a..9d6f4c2a56c 100644
--- a/app/models/integrations/ewm.rb
+++ b/app/models/integrations/ewm.rb
@@ -10,15 +10,15 @@ module Integrations
@reference_pattern ||= %r{(?<issue>\b(bug|task|work item|workitem|rtcwi|defect)\b\s+\d+)}i
end
- def title
+ def self.title
'EWM'
end
- def description
+ def self.description
s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/ewm'), target: '_blank', rel: 'noopener noreferrer'
s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/external_wiki.rb b/app/models/integrations/external_wiki.rb
index acacab2528e..7408f86d231 100644
--- a/app/models/integrations/external_wiki.rb
+++ b/app/models/integrations/external_wiki.rb
@@ -11,24 +11,24 @@ module Integrations
help: -> { s_('ExternalWikiService|Enter the URL to the external wiki.') },
required: true
- def title
+ def self.title
s_('ExternalWikiService|External wiki')
end
- def description
+ def self.description
s_('ExternalWikiService|Link to an external wiki from the sidebar.')
end
- def self.to_param
- 'external_wiki'
- end
-
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/wiki/index', anchor: 'link-an-external-wiki'), target: '_blank', rel: 'noopener noreferrer'
s_('Link an external wiki from the project\'s sidebar. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
+ def self.to_param
+ 'external_wiki'
+ end
+
def sections
[
{
diff --git a/app/models/integrations/gitlab_slack_application.rb b/app/models/integrations/gitlab_slack_application.rb
index 2d520eaf7e7..d008a28a226 100644
--- a/app/models/integrations/gitlab_slack_application.rb
+++ b/app/models/integrations/gitlab_slack_application.rb
@@ -26,11 +26,11 @@ module Integrations
update(active: !!slack_integration)
end
- def title
+ def self.title
s_('Integrations|GitLab for Slack app')
end
- def description
+ def self.description
s_('Integrations|Enable slash commands and notifications for a Slack workspace.')
end
diff --git a/app/models/integrations/google_play.rb b/app/models/integrations/google_play.rb
index 5389e8dfa81..746f68fdc4c 100644
--- a/app/models/integrations/google_play.rb
+++ b/app/models/integrations/google_play.rb
@@ -32,15 +32,15 @@ module Integrations
title: -> { s_('GooglePlayStore|Protected branches and tags only') },
checkbox_label: -> { s_('GooglePlayStore|Only set variables on protected branches and tags') }
- def title
+ def self.title
s_('GooglePlay|Google Play')
end
- def description
+ def self.description
s_('GooglePlay|Use GitLab to build and release an app in Google Play.')
end
- def help
+ def self.help
variable_list = [
'<code>SUPPLY_PACKAGE_NAME</code>',
'<code>SUPPLY_JSON_KEY_DATA</code>'
diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb
index 6e4753470a3..6a9d603e6e5 100644
--- a/app/models/integrations/hangouts_chat.rb
+++ b/app/models/integrations/hangouts_chat.rb
@@ -17,11 +17,11 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
'Google Chat'
end
- def description
+ def self.description
'Send notifications from GitLab to a room in Google Chat.'
end
@@ -29,7 +29,7 @@ module Integrations
'hangouts_chat'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(_('How do I set up a Google Chat webhook?'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/hangouts_chat'),
target: '_blank', rel: 'noopener noreferrer')
diff --git a/app/models/integrations/harbor.rb b/app/models/integrations/harbor.rb
index 559e48afd10..cc570e49e36 100644
--- a/app/models/integrations/harbor.rb
+++ b/app/models/integrations/harbor.rb
@@ -32,34 +32,32 @@ module Integrations
non_empty_password_help: -> { s_('HarborIntegration|Leave blank to use your current password.') },
required: true
- def title
+ def self.title
'Harbor'
end
- def description
+ def self.description
s_("HarborIntegration|Use Harbor as this project's container registry.")
end
- def help
+ def self.help
s_("HarborIntegration|After the Harbor integration is activated, global variables `$HARBOR_USERNAME`, `$HARBOR_HOST`, `$HARBOR_OCI`, `$HARBOR_PASSWORD`, `$HARBOR_URL` and `$HARBOR_PROJECT` will be created for CI/CD use.")
end
+ def self.to_param
+ name.demodulize.downcase
+ end
+
def hostname
Gitlab::Utils.parse_url(url).hostname
end
- class << self
- def to_param
- name.demodulize.downcase
- end
-
- def supported_events
- []
- end
+ def self.supported_events
+ []
+ end
- def supported_event_actions
- []
- end
+ def self.supported_event_actions
+ []
end
def test(*_args)
diff --git a/app/models/integrations/irker.rb b/app/models/integrations/irker.rb
index a54946f074a..a1ce0877957 100644
--- a/app/models/integrations/irker.rb
+++ b/app/models/integrations/irker.rb
@@ -53,14 +53,31 @@ module Integrations
# in the UI or API.
prop_accessor :channels
- def title
+ def self.title
s_('IrkerService|irker (IRC gateway)')
end
- def description
+ def self.description
s_('IrkerService|Send update messages to an irker server.')
end
+ def self.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'
+ )
+
+ 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
+
def self.to_param
'irker'
end
@@ -85,23 +102,6 @@ module Integrations
}
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'
- )
-
- 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
def get_channels
diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb
index 0683c8408bc..a2f5667eaee 100644
--- a/app/models/integrations/jenkins.rb
+++ b/app/models/integrations/jenkins.rb
@@ -69,15 +69,15 @@ module Integrations
%w[push merge_request tag_push]
end
- def title
+ def self.title
'Jenkins'
end
- def description
+ def self.description
s_('Run CI/CD pipelines with Jenkins.')
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('integration/jenkins'), target: '_blank', rel: 'noopener noreferrer'
s_('Run CI/CD pipelines with Jenkins when you push to a repository, or when a merge request is created, updated, or merged. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index f6e99454cb1..22367ee336d 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -184,16 +184,24 @@ module Integrations
options
end
- def client
- @client ||= JIRA::Client.new(options).tap do |client|
+ def client(additional_options = {})
+ JIRA::Client.new(options.merge(additional_options)).tap do |client|
# Replaces JIRA default http client with our implementation
client.request_client = Gitlab::Jira::HttpClient.new(client.options)
end
end
- def help
+ def self.title
+ 'Jira'
+ end
+
+ def self.description
+ s_("JiraService|Use Jira as this project's issue tracker.")
+ end
+
+ def self.help
jira_doc_link_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe,
- url: help_page_path('integration/jira/index'))
+ url: Gitlab::Routing.url_helpers.help_page_path('integration/jira/index'))
format(
s_("JiraService|You must configure Jira before enabling this integration. " \
"%{jira_doc_link_start}Learn more.%{link_end}"),
@@ -201,14 +209,6 @@ module Integrations
link_end: '</a>'.html_safe)
end
- def title
- 'Jira'
- end
-
- def description
- s_("JiraService|Use Jira as this project's issue tracker.")
- end
-
def self.to_param
'jira'
end
diff --git a/app/models/integrations/mattermost.rb b/app/models/integrations/mattermost.rb
index 7e391b11d82..361ff4afce8 100644
--- a/app/models/integrations/mattermost.rb
+++ b/app/models/integrations/mattermost.rb
@@ -5,11 +5,11 @@ module Integrations
include SlackMattermostNotifier
include SlackMattermostFields
- def title
+ def self.title
_('Mattermost notifications')
end
- def description
+ def self.description
s_('Send notifications about project events to Mattermost channels.')
end
@@ -17,7 +17,7 @@ module Integrations
'mattermost'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/mattermost'), target: '_blank', rel: 'noopener noreferrer'
s_('Send notifications about project events to Mattermost channels. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/mattermost_slash_commands.rb b/app/models/integrations/mattermost_slash_commands.rb
index 73cddd163e0..9554dec4168 100644
--- a/app/models/integrations/mattermost_slash_commands.rb
+++ b/app/models/integrations/mattermost_slash_commands.rb
@@ -14,11 +14,11 @@ module Integrations
false
end
- def title
+ def self.title
s_('Integrations|Mattermost slash commands')
end
- def description
+ def self.description
s_('Integrations|Perform common tasks with slash commands.')
end
diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb
index 208172d6303..3a7c848d411 100644
--- a/app/models/integrations/microsoft_teams.rb
+++ b/app/models/integrations/microsoft_teams.rb
@@ -18,11 +18,11 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
'Microsoft Teams notifications'
end
- def description
+ def self.description
'Send notifications about project events to Microsoft Teams.'
end
@@ -30,7 +30,7 @@ module Integrations
'microsoft_teams'
end
- def help
+ def self.help
'<p>Use this service to send notifications about events in GitLab projects to your Microsoft Teams channels. <a href="https://docs.gitlab.com/ee/user/project/integrations/microsoft_teams.html" target="_blank" rel="noopener noreferrer">How do I configure this integration?</a></p>'
end
diff --git a/app/models/integrations/mock_ci.rb b/app/models/integrations/mock_ci.rb
index 2d8e26d409f..9c129ca727c 100644
--- a/app/models/integrations/mock_ci.rb
+++ b/app/models/integrations/mock_ci.rb
@@ -14,11 +14,11 @@ module Integrations
validates :mock_service_url, presence: true, public_url: true, if: :activated?
- def title
+ def self.title
'MockCI'
end
- def description
+ def self.description
'Mock an external CI'
end
diff --git a/app/models/integrations/mock_monitoring.rb b/app/models/integrations/mock_monitoring.rb
index 72bb292edaa..9e474078b28 100644
--- a/app/models/integrations/mock_monitoring.rb
+++ b/app/models/integrations/mock_monitoring.rb
@@ -2,11 +2,11 @@
module Integrations
class MockMonitoring < BaseMonitoring
- def title
+ def self.title
'Mock monitoring'
end
- def description
+ def self.description
'Mock monitoring service'
end
diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb
index c0acb6c87b4..f027afe0381 100644
--- a/app/models/integrations/packagist.rb
+++ b/app/models/integrations/packagist.rb
@@ -29,11 +29,11 @@ module Integrations
validates :username, presence: true, if: :activated?
validates :token, presence: true, if: :activated?
- def title
+ def self.title
'Packagist'
end
- def description
+ def self.description
s_('Integrations|Keep your PHP dependencies updated on Packagist.')
end
diff --git a/app/models/integrations/pipelines_email.rb b/app/models/integrations/pipelines_email.rb
index 01efbc3e4a4..c7a93d48825 100644
--- a/app/models/integrations/pipelines_email.rb
+++ b/app/models/integrations/pipelines_email.rb
@@ -44,11 +44,11 @@ module Integrations
end
end
- def title
+ def self.title
_('Pipeline status emails')
end
- def description
+ def self.description
_('Email the pipeline status to a list of recipients.')
end
diff --git a/app/models/integrations/pivotaltracker.rb b/app/models/integrations/pivotaltracker.rb
index b3cbc988dd6..97e6e3e09d1 100644
--- a/app/models/integrations/pivotaltracker.rb
+++ b/app/models/integrations/pivotaltracker.rb
@@ -20,15 +20,15 @@ module Integrations
'automatically inspect. Leave blank to include all branches.')
end
- def title
+ def self.title
'Pivotal Tracker'
end
- def description
+ def self.description
s_('PivotalTrackerService|Add commit messages as comments to Pivotal Tracker stories.')
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/pivotal_tracker'), target: '_blank', rel: 'noopener noreferrer'
s_('Add commit messages as comments to Pivotal Tracker stories. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb
index ff8d07a1b4c..de923bbbdd5 100644
--- a/app/models/integrations/prometheus.rb
+++ b/app/models/integrations/prometheus.rb
@@ -51,11 +51,11 @@ module Integrations
false
end
- def title
+ def self.title
'Prometheus'
end
- def description
+ def self.description
s_('PrometheusService|Monitor application health with Prometheus metrics and dashboards')
end
diff --git a/app/models/integrations/pumble.rb b/app/models/integrations/pumble.rb
index 09e011023ed..36ff5189b0f 100644
--- a/app/models/integrations/pumble.rb
+++ b/app/models/integrations/pumble.rb
@@ -18,11 +18,11 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
'Pumble'
end
- def description
+ def self.description
s_("PumbleIntegration|Send notifications about project events to Pumble.")
end
@@ -30,7 +30,7 @@ module Integrations
'pumble'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/pumble'),
diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb
index 2feae29f627..b2c4e06e71f 100644
--- a/app/models/integrations/pushover.rb
+++ b/app/models/integrations/pushover.rb
@@ -71,11 +71,11 @@ module Integrations
]
end
- def title
+ def self.title
'Pushover'
end
- def description
+ def self.description
s_('PushoverService|Get real-time notifications on your device.')
end
diff --git a/app/models/integrations/redmine.rb b/app/models/integrations/redmine.rb
index bc2a64b0848..11eda7c69f7 100644
--- a/app/models/integrations/redmine.rb
+++ b/app/models/integrations/redmine.rb
@@ -6,15 +6,15 @@ module Integrations
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
- def title
+ def self.title
'Redmine'
end
- def description
+ def self.description
s_("IssueTracker|Use Redmine as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/redmine'), target: '_blank', rel: 'noopener noreferrer'
s_('IssueTracker|Use Redmine as the issue tracker. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/shimo.rb b/app/models/integrations/shimo.rb
index 227fdca5c91..1d004356469 100644
--- a/app/models/integrations/shimo.rb
+++ b/app/models/integrations/shimo.rb
@@ -16,11 +16,11 @@ module Integrations
valid? && activated?
end
- def title
+ def self.title
s_('Shimo|Shimo')
end
- def description
+ def self.description
s_('Shimo|Link to a Shimo Workspace from the sidebar.')
end
diff --git a/app/models/integrations/slack.rb b/app/models/integrations/slack.rb
index f70376e2f0d..9f9614a84fd 100644
--- a/app/models/integrations/slack.rb
+++ b/app/models/integrations/slack.rb
@@ -5,11 +5,11 @@ module Integrations
include SlackMattermostNotifier
include SlackMattermostFields
- def title
+ def self.title
'Slack notifications'
end
- def description
+ def self.description
'Send notifications about project events to Slack.'
end
diff --git a/app/models/integrations/slack_slash_commands.rb b/app/models/integrations/slack_slash_commands.rb
index b209f37ee7c..c5ea6f22951 100644
--- a/app/models/integrations/slack_slash_commands.rb
+++ b/app/models/integrations/slack_slash_commands.rb
@@ -10,11 +10,11 @@ module Integrations
non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
placeholder: ''
- def title
+ def self.title
'Slack slash commands'
end
- def description
+ def self.description
"Perform common operations in Slack."
end
diff --git a/app/models/integrations/squash_tm.rb b/app/models/integrations/squash_tm.rb
index bf3f391564f..1b4ab152b1d 100644
--- a/app/models/integrations/squash_tm.rb
+++ b/app/models/integrations/squash_tm.rb
@@ -22,15 +22,15 @@ module Integrations
validates :token, length: { maximum: 255 }, allow_blank: true
end
- def title
+ def self.title
'Squash TM'
end
- def description
+ def self.description
s_("SquashTmIntegration|Update Squash TM requirements when GitLab issues are modified.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/squash_tm'),
diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb
index 575c3b8a334..913242ef9ac 100644
--- a/app/models/integrations/teamcity.rb
+++ b/app/models/integrations/teamcity.rb
@@ -47,15 +47,15 @@ module Integrations
end
end
- def title
+ def self.title
'JetBrains TeamCity'
end
- def description
+ def self.description
s_('ProjectService|Run CI/CD pipelines with JetBrains TeamCity.')
end
- def help
+ def self.help
s_('To run CI/CD pipelines with JetBrains TeamCity, input the GitLab project details in the TeamCity project Version Control Settings.')
end
diff --git a/app/models/integrations/telegram.rb b/app/models/integrations/telegram.rb
index 71fe6f8d6ef..8eb1a7ad0ea 100644
--- a/app/models/integrations/telegram.rb
+++ b/app/models/integrations/telegram.rb
@@ -38,11 +38,11 @@ module Integrations
before_validation :set_webhook
- def title
+ def self.title
'Telegram'
end
- def description
+ def self.description
s_("TelegramIntegration|Send notifications about project events to Telegram.")
end
@@ -50,7 +50,7 @@ module Integrations
'telegram'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/telegram'),
diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb
index 3b4bcfa28d3..6ee95c1173b 100644
--- a/app/models/integrations/unify_circuit.rb
+++ b/app/models/integrations/unify_circuit.rb
@@ -17,11 +17,11 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
'Unify Circuit'
end
- def description
+ def self.description
s_('Integrations|Send notifications about project events to Unify Circuit.')
end
@@ -29,7 +29,7 @@ module Integrations
'unify_circuit'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/unify_circuit'), target: '_blank', rel: 'noopener noreferrer'
s_('Integrations|Send notifications about project events to a Unify Circuit conversation. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb
index 3ef8ab39352..5f8cc195544 100644
--- a/app/models/integrations/webex_teams.rb
+++ b/app/models/integrations/webex_teams.rb
@@ -17,11 +17,11 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
s_("WebexTeamsService|Webex Teams")
end
- def description
+ def self.description
s_("WebexTeamsService|Send notifications about project events to Webex Teams.")
end
@@ -29,7 +29,7 @@ module Integrations
'webex_teams'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/webex_teams'), target: '_blank', rel: 'noopener noreferrer'
s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/youtrack.rb b/app/models/integrations/youtrack.rb
index 15246a37aa7..932e588a829 100644
--- a/app/models/integrations/youtrack.rb
+++ b/app/models/integrations/youtrack.rb
@@ -14,15 +14,15 @@ module Integrations
@reference_pattern = /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)#{regex_suffix if only_long}/
end
- def title
+ def self.title
'YouTrack'
end
- def description
+ def self.description
s_("IssueTracker|Use YouTrack as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/youtrack'), target: '_blank', rel: 'noopener noreferrer'
s_("IssueTracker|Use YouTrack as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/zentao.rb b/app/models/integrations/zentao.rb
index 58ec4abf30c..2aec0c1e871 100644
--- a/app/models/integrations/zentao.rb
+++ b/app/models/integrations/zentao.rb
@@ -57,18 +57,18 @@ module Integrations
data_fields.api_url ||= issues_tracker['api_url']
end
- def title
+ def self.title
'ZenTao'
end
- def description
+ def self.description
s_("ZentaoIntegration|Use ZenTao as this project's issue tracker.")
end
- def help
+ def self.help
s_("ZentaoIntegration|Before you enable this integration, you must configure ZenTao. For more details, read the %{link_start}ZenTao integration documentation%{link_end}.") % {
link_start: '<a href="%{url}" target="_blank" rel="noopener noreferrer">'
- .html_safe % { url: help_page_url('user/project/integrations/zentao') },
+ .html_safe % { url: Rails.application.routes.url_helpers.help_page_url('user/project/integrations/zentao') },
link_end: '</a>'.html_safe
}
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 77e283044ea..9690e16fd7d 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -135,11 +135,12 @@ class Member < ApplicationRecord
.reorder(nil)
end
- scope :without_invites_and_requests, -> do
- active_state
- .non_request
- .non_invite
- .non_minimal_access
+ scope :without_invites_and_requests, ->(minimal_access: false) do
+ result = active_state.non_request.non_invite
+
+ result = result.non_minimal_access unless minimal_access
+
+ result
end
scope :invite, -> { where.not(invite_token: nil) }
diff --git a/app/models/members/members/members_with_parents.rb b/app/models/members/members/members_with_parents.rb
new file mode 100644
index 00000000000..61ce99e1f3e
--- /dev/null
+++ b/app/models/members/members/members_with_parents.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+module Members
+ class MembersWithParents
+ attr_reader :group
+
+ def initialize(group)
+ @group = group
+ end
+
+ # Returns all members for group and parents, with no filters
+ def all_members
+ GroupMember.from_union([
+ members_from_self_and_ancestors,
+ members_from_self_and_ancestor_group_shares
+ ])
+ end
+
+ # Returns members based on filter options:
+ #
+ # - `active_users`. DEPRECATED. If true, returns only members for active users
+ # - `minimal_access`. Used only in EE (GitLab Premium). If true, returns
+ # members which has minimal access. If false (default), does not return
+ # members with minimal access
+ #
+ # NOTE : this method does not return pending invites, nor requests.
+ def members(active_users: false, minimal_access: false)
+ raise ArgumentError, 'active_users: is deprecated' if active_users && minimal_access
+
+ group_hierarchy_members = members_from_self_and_ancestors
+
+ group_hierarchy_members =
+ if active_users
+ group_hierarchy_members.active_without_invites_and_requests
+ else
+ filter_invites_and_requests(group_hierarchy_members, minimal_access)
+ end
+
+ GroupMember.from_union([
+ group_hierarchy_members,
+ members_from_self_and_ancestor_group_shares
+ ])
+ end
+
+ private
+
+ # NOTE: minimal access is Premium, so in FOSS we will not include minimal access member
+ def filter_invites_and_requests(members, _minimal_access)
+ members.without_invites_and_requests(minimal_access: false)
+ end
+
+ def source_ids
+ # Avoids an unnecessary SELECT when the group has no parents
+ @source_ids ||=
+ if group.has_parent?
+ group.self_and_ancestors.reorder(nil).select(:id)
+ else
+ group.id
+ end
+ end
+
+ def members_from_self_and_ancestors
+ GroupMember
+ .with_source_id(source_ids)
+ .select(*GroupMember.cached_column_list)
+ end
+
+ def members_from_self_and_ancestor_group_shares
+ group_group_link_table = GroupGroupLink.arel_table
+ group_member_table = GroupMember.arel_table
+
+ group_group_links_query = GroupGroupLink.where(shared_group_id: source_ids)
+ cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
+ cte_alias = cte.table.alias(GroupGroupLink.table_name)
+
+ # Instead of members.access_level, we need to maximize that access_level at
+ # the respective group_group_links.group_access.
+ member_columns = GroupMember.attribute_names.map do |column_name|
+ if column_name == 'access_level'
+ smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]], 'access_level')
+ else
+ group_member_table[column_name]
+ end
+ end
+
+ GroupMember
+ .with(cte.to_arel)
+ .select(*member_columns)
+ .from([group_member_table, cte.alias_to(group_group_link_table)])
+ .where(group_member_table[:requested_at].eq(nil))
+ .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
+ .where(group_member_table[:source_type].eq('Namespace'))
+ .where(group_member_table[:state].eq(::Member::STATE_ACTIVE))
+ .non_minimal_access
+ end
+
+ def smallest_value_arel(args, column_alias)
+ Arel::Nodes::As.new(
+ Arel::Nodes::NamedFunction.new('LEAST', args),
+ Arel::Nodes::SqlLiteral.new(column_alias))
+ end
+ end
+end
+
+Members::MembersWithParents.prepend_mod
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index d07e4f9e298..5e5f9ab7385 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -48,6 +48,12 @@ class ProjectMember < Member
end
end
+ def permissible_access_level_roles_for_project_access_token(current_user, project)
+ permissible_access_level_roles(current_user, project).filter do |_, value|
+ value <= project.project_authorizations.find_by(user: current_user).access_level
+ end
+ end
+
def access_level_roles
Gitlab::Access.options
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index d9726e76c4b..524a9b8074b 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -384,7 +384,6 @@ class MergeRequest < ApplicationRecord
}
scope :with_csv_entity_associations, -> { preload(:assignees, :approved_by_users, :author, :milestone, metrics: [:merged_by]) }
- scope :with_jira_integration_associations, -> { preload_routables.preload(:metrics, :assignees, :author) }
scope :recently_unprepared, -> { where(prepared_at: nil).where(created_at: 2.hours.ago..).order(:created_at, :id) } # id is the tie-breaker
scope :by_target_branch_wildcard, ->(wildcard_branch_name) do
@@ -530,6 +529,14 @@ class MergeRequest < ApplicationRecord
.pluck(:target_branch)
end
+ def self.recent_source_branches(limit: 100)
+ group(:source_branch)
+ .select(:source_branch)
+ .reorder(arel_table[:updated_at].maximum.desc)
+ .limit(limit)
+ .pluck(:source_branch)
+ end
+
def self.sort_by_attribute(method, excluded_labels: [])
case method.to_s
when 'merged_at', 'merged_at_asc' then order_merged_at_asc
@@ -1235,17 +1242,14 @@ class MergeRequest < ApplicationRecord
}
end
- def mergeable?(
- skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, check_mergeability_retry_lease: false,
- skip_draft_check: false, skip_rebase_check: false, skip_blocked_check: false)
-
- return false unless mergeable_state?(
- skip_ci_check: skip_ci_check,
- skip_discussions_check: skip_discussions_check,
- skip_draft_check: skip_draft_check,
- skip_approved_check: skip_approved_check,
- skip_blocked_check: skip_blocked_check
- )
+ # mergeable_state_check_params allows a hash of merge checks to skip or not
+ # skip_ci_check
+ # skip_discussions_check
+ # skip_draft_check
+ # skip_approved_check
+ # skip_blocked_check
+ def mergeable?(check_mergeability_retry_lease: false, skip_rebase_check: false, **mergeable_state_check_params)
+ return false unless mergeable_state?(**mergeable_state_check_params)
check_mergeability(sync_retry_lease: check_mergeability_retry_lease)
mergeable_git_state?(skip_rebase_check: skip_rebase_check)
@@ -1275,18 +1279,16 @@ class MergeRequest < ApplicationRecord
mergeable_state_checks + mergeable_git_state_checks
end
- def mergeable_state?(
- skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false,
- skip_draft_check: false, skip_blocked_check: false)
+ # mergeable_state_check_params allows a hash of merge checks to skip or not
+ # skip_ci_check
+ # skip_discussions_check
+ # skip_draft_check
+ # skip_approved_check
+ # skip_blocked_check
+ def mergeable_state?(**mergeable_state_check_params)
additional_checks = execute_merge_checks(
self.class.mergeable_state_checks,
- params: {
- skip_ci_check: skip_ci_check,
- skip_discussions_check: skip_discussions_check,
- skip_approved_check: skip_approved_check,
- skip_draft_check: skip_draft_check,
- skip_blocked_check: skip_blocked_check
- }
+ params: mergeable_state_check_params
)
additional_checks.success?
end
@@ -1386,7 +1388,7 @@ class MergeRequest < ApplicationRecord
end
def mergeable_discussions_state?
- return true unless project.only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: true)
+ return true unless only_allow_merge_if_all_discussions_are_resolved?
unresolved_notes.none?(&:to_be_resolved?)
end
@@ -1566,8 +1568,16 @@ class MergeRequest < ApplicationRecord
access.can_push_to_branch?(target_branch)
end
+ def only_allow_merge_if_pipeline_succeeds?
+ project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true)
+ end
+
+ def only_allow_merge_if_all_discussions_are_resolved?
+ project.only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: true)
+ end
+
def mergeable_ci_state?
- return true unless project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true)
+ return true unless only_allow_merge_if_pipeline_succeeds?
return false unless actual_head_pipeline
return true if project.allow_merge_on_skipped_pipeline?(inherit_group_setting: true) && actual_head_pipeline.skipped?
diff --git a/app/models/merge_request_context_commit_diff_file.rb b/app/models/merge_request_context_commit_diff_file.rb
index fdf57068928..2fb995ee512 100644
--- a/app/models/merge_request_context_commit_diff_file.rb
+++ b/app/models/merge_request_context_commit_diff_file.rb
@@ -10,7 +10,6 @@ class MergeRequestContextCommitDiffFile < ApplicationRecord
belongs_to :merge_request_context_commit, inverse_of: :diff_files
sha_attribute :sha
- alias_attribute :id, :sha
# create MergeRequestContextCommitDiffFile by given diff file record(s)
def self.bulk_insert(*args)
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index fc08dd4d9c8..790520c4123 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -6,13 +6,8 @@ class MergeRequestDiffCommit < ApplicationRecord
include BulkInsertSafe
include ShaAttribute
include CachedCommit
- include IgnorableColumns
include FromUnion
- ignore_column %i[author_name author_email committer_name committer_email],
- remove_with: '14.6',
- remove_after: '2021-11-22'
-
belongs_to :merge_request_diff
# This relation is called `commit_author` and not `author`, as the project
@@ -33,7 +28,6 @@ class MergeRequestDiffCommit < ApplicationRecord
belongs_to :committer, class_name: 'MergeRequest::DiffCommitUser'
sha_attribute :sha
- alias_attribute :id, :sha
attribute :trailers, :ind_jsonb
validates :trailers, json_schema: { filename: 'git_trailers' }
@@ -129,4 +123,8 @@ class MergeRequestDiffCommit < ApplicationRecord
def committer_email
committer&.email
end
+
+ def to_hash
+ super.merge({ 'id' => sha })
+ end
end
diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb
index 6f4728a1d98..70eaab8c0ab 100644
--- a/app/models/ml/candidate.rb
+++ b/app/models/ml/candidate.rb
@@ -12,12 +12,14 @@ module Ml
validates :eid, :experiment, presence: true
validates :status, inclusion: { in: statuses.keys }
+ validates :model_version_id, uniqueness: { allow_nil: true }
belongs_to :experiment, class_name: 'Ml::Experiment'
belongs_to :user
belongs_to :package, class_name: 'Packages::Package'
belongs_to :project
belongs_to :ci_build, class_name: 'Ci::Build', optional: true
+ belongs_to :model_version, class_name: 'Ml::ModelVersion', optional: true, inverse_of: :candidate
has_many :metrics, class_name: 'Ml::CandidateMetric'
has_many :params, class_name: 'Ml::CandidateParam'
has_many :metadata, class_name: 'Ml::CandidateMetadata'
diff --git a/app/models/ml/model.rb b/app/models/ml/model.rb
index 27f03ed5857..b6f7e9a0639 100644
--- a/app/models/ml/model.rb
+++ b/app/models/ml/model.rb
@@ -3,6 +3,7 @@
module Ml
class Model < ApplicationRecord
include Presentable
+ include Sortable
validates :project, :default_experiment, presence: true
validates :name,
@@ -15,15 +16,19 @@ module Ml
has_one :default_experiment, class_name: 'Ml::Experiment'
belongs_to :project
+ belongs_to :user
has_many :versions, class_name: 'Ml::ModelVersion'
+ has_many :metadata, class_name: 'Ml::ModelMetadata'
has_one :latest_version, -> { latest_by_model }, class_name: 'Ml::ModelVersion', inverse_of: :model
scope :including_latest_version, -> { includes(:latest_version) }
+ scope :including_project, -> { includes(:project) }
scope :with_version_count, -> {
left_outer_joins(:versions)
.select("ml_models.*, count(ml_model_versions.id) as version_count")
.group(:id)
}
+ scope :by_name, ->(name) { where("ml_models.name LIKE ?", "%#{sanitize_sql_like(name)}%") } # rubocop:disable GitlabSecurity/SqlInjection
scope :by_project, ->(project) { where(project_id: project.id) }
def valid_default_experiment?
@@ -33,13 +38,12 @@ module Ml
errors.add(:default_experiment) unless default_experiment.project_id == project_id
end
- def self.find_or_create(project, name, experiment)
- create_with(default_experiment: experiment)
- .find_or_create_by(project: project, name: name)
- end
-
def self.by_project_id_and_id(project_id, id)
find_by(project_id: project_id, id: id)
end
+
+ def self.by_project_id_and_name(project_id, name)
+ find_by(project_id: project_id, name: name)
+ end
end
end
diff --git a/app/models/ml/model_metadata.rb b/app/models/ml/model_metadata.rb
new file mode 100644
index 00000000000..9c4273c629c
--- /dev/null
+++ b/app/models/ml/model_metadata.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ml
+ class ModelMetadata < ApplicationRecord
+ validates :name,
+ length: { maximum: 250 },
+ presence: true,
+ uniqueness: { scope: :model, message: ->(metadata, _) { "'#{metadata.name}' already taken" } }
+ validates :value, length: { maximum: 5000 }, presence: true
+
+ belongs_to :model, class_name: 'Ml::Model', optional: false
+ end
+end
diff --git a/app/models/ml/model_version.rb b/app/models/ml/model_version.rb
index e7fcde2cb5c..58da57f27d6 100644
--- a/app/models/ml/model_version.rb
+++ b/app/models/ml/model_version.rb
@@ -2,6 +2,8 @@
module Ml
class ModelVersion < ApplicationRecord
+ include Presentable
+
validates :project, :model, presence: true
validates :version,
@@ -10,11 +12,15 @@ module Ml
presence: true,
length: { maximum: 255 }
+ validates :description,
+ length: { maximum: 500 }
+
validate :valid_model?, :valid_package?
belongs_to :model, class_name: 'Ml::Model'
belongs_to :project
belongs_to :package, class_name: 'Packages::MlModel::Package', optional: true
+ has_one :candidate, class_name: 'Ml::Candidate'
delegate :name, to: :model
@@ -22,8 +28,17 @@ module Ml
scope :latest_by_model, -> { order_by_model_id_id_desc.select('DISTINCT ON (model_id) *') }
class << self
- def find_or_create!(model, version, package)
- create_with(package: package).find_or_create_by!(project: model.project, model: model, version: version)
+ def find_or_create!(model, version, package, description)
+ create_with(package: package, description: description)
+ .find_or_create_by!(project: model.project, model: model, version: version)
+ end
+
+ def by_project_id_and_id(project_id, id)
+ find_by(project_id: project_id, id: id)
+ end
+
+ def by_project_id_name_and_version(project_id, name, version)
+ joins(:model).find_by(model: { name: name, project_id: project_id }, project_id: project_id, version: version)
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 733b89fcaf2..cd54ac1b24a 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -18,6 +18,7 @@ class Namespace < ApplicationRecord
include Referable
include CrossDatabaseIgnoredTables
include IgnorableColumns
+ include UseSqlFunctionForPrimaryKeyLookups
ignore_column :unlock_membership_to_ldap, remove_with: '16.7', remove_after: '2023-11-16'
@@ -138,6 +139,8 @@ class Namespace < ApplicationRecord
to: :namespace_settings
delegate :runner_registration_enabled, :runner_registration_enabled?, :runner_registration_enabled=,
to: :namespace_settings
+ delegate :emails_enabled, :emails_enabled=,
+ to: :namespace_settings, allow_nil: true
delegate :allow_runner_registration_token,
:allow_runner_registration_token=,
to: :namespace_settings
@@ -204,7 +207,7 @@ class Namespace < ApplicationRecord
# Make sure that the name is same as strong_memoize name in root_ancestor
# method
- attr_writer :root_ancestor, :emails_disabled_memoized
+ attr_writer :root_ancestor, :emails_enabled_memoized
class << self
def sti_class_for(type_name)
@@ -299,6 +302,14 @@ class Namespace < ApplicationRecord
super || Gitlab::CurrentSettings.default_branch_protection
end
+ def default_branch_protection_settings
+ settings = default_branch_protection_defaults
+
+ return settings unless settings.blank?
+
+ Gitlab::CurrentSettings.default_branch_protection_defaults
+ end
+
def visibility_level_field
:visibility_level
end
@@ -382,17 +393,16 @@ class Namespace < ApplicationRecord
# any ancestor can disable emails for all descendants
def emails_disabled?
- strong_memoize(:emails_disabled_memoized) do
- if parent_id
- self_and_ancestors.where(emails_disabled: true).exists?
- else
- !!emails_disabled
- end
- end
+ !emails_enabled?
end
def emails_enabled?
- !emails_disabled?
+ # If no namespace_settings, we can assume it has not changed from enabled
+ return true unless namespace_settings
+
+ strong_memoize(:emails_enabled_memoized) do
+ namespace_settings.emails_enabled?
+ end
end
def lfs_enabled?
@@ -626,8 +636,7 @@ class Namespace < ApplicationRecord
:route,
:project_setting,
:project_feature,
- pages_metadatum: :pages_deployment
- )
+ :active_pages_deployments)
end
private
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 3befcdeaec5..13d2c5a62e2 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -63,6 +63,12 @@ class NamespaceSetting < ApplicationRecord
namespace.root_ancestor.prevent_sharing_groups_outside_hierarchy
end
+ def emails_enabled?
+ return emails_enabled unless namespace.has_parent?
+
+ all_ancestors_have_emails_enabled?
+ end
+
def show_diff_preview_in_email?
return show_diff_preview_in_email unless namespace.has_parent?
@@ -89,6 +95,10 @@ class NamespaceSetting < ApplicationRecord
private
+ def all_ancestors_have_emails_enabled?
+ self.class.where(namespace_id: namespace.self_and_ancestors, emails_enabled: false).none?
+ end
+
def all_ancestors_allow_diff_preview_in_email?
!self.class.where(namespace_id: namespace.self_and_ancestors, show_diff_preview_in_email: false).exists?
end
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 0f410d4810d..f60e7682418 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -2,7 +2,7 @@
module Network
class Graph
- attr_reader :days, :commits, :map, :notes, :repo
+ attr_reader :days, :commits, :map, :repo
def self.max_count
@max_count ||= 650
@@ -17,28 +17,10 @@ module Network
@commits = collect_commits
@days = index_commits
- @notes = collect_notes
end
protected
- def collect_notes
- return {} if Feature.enabled?(:disable_network_graph_notes_count, @project, type: :experiment)
-
- h = Hash.new(0)
-
- @project
- .notes
- .where(noteable_type: 'Commit')
- .group('notes.commit_id')
- .select('notes.commit_id, count(notes.id) as note_count')
- .each do |item|
- h[item.commit_id] = item.note_count.to_i
- end
-
- h
- end
-
# Get commits from repository
#
def collect_commits
diff --git a/app/models/note.rb b/app/models/note.rb
index eae7a40fb4e..6f4a56dd3cc 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -383,7 +383,11 @@ class Note < ApplicationRecord
end
def for_project_noteable?
- !(for_personal_snippet? || for_abuse_report?)
+ !(for_personal_snippet? || for_abuse_report? || group_level_issue?)
+ end
+
+ def group_level_issue?
+ (for_issue? || for_work_item?) && noteable&.project_id.blank?
end
def for_design?
diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb
index 893b08d7872..157b851e009 100644
--- a/app/models/organizations/organization.rb
+++ b/app/models/organizations/organization.rb
@@ -42,6 +42,10 @@ module Organizations
organization_users.exists?(user: user)
end
+ def web_url(only_path: nil)
+ Gitlab::UrlBuilder.build(self, only_path: only_path)
+ end
+
private
def check_if_default_organization
diff --git a/app/models/packages/npm/metadata_cache.rb b/app/models/packages/npm/metadata_cache.rb
index 02efeda69cb..b6ab2a88a98 100644
--- a/app/models/packages/npm/metadata_cache.rb
+++ b/app/models/packages/npm/metadata_cache.rb
@@ -5,6 +5,9 @@ module Packages
class MetadataCache < ApplicationRecord
include FileStoreMounter
include Packages::Downloadable
+ include Packages::Destructible
+
+ enum status: { default: 0, processing: 1, error: 3 }
belongs_to :project, inverse_of: :npm_metadata_caches
@@ -18,6 +21,9 @@ module Packages
before_validation :set_object_storage_key
attr_readonly :object_storage_key
+ scope :stale, -> { where(project_id: nil) }
+ scope :pending_destruction, -> { stale.default }
+
def self.find_or_build(package_name:, project_id:)
find_or_initialize_by(
package_name: package_name,
diff --git a/app/models/packages/nuget/symbol.rb b/app/models/packages/nuget/symbol.rb
index 643b5552d84..3315f11b974 100644
--- a/app/models/packages/nuget/symbol.rb
+++ b/app/models/packages/nuget/symbol.rb
@@ -4,6 +4,7 @@ module Packages
module Nuget
class Symbol < ApplicationRecord
include FileStoreMounter
+ include ShaAttribute
belongs_to :package, -> { where(package_type: :nuget) }, inverse_of: :nuget_symbols
@@ -13,6 +14,8 @@ module Packages
validates :signature, uniqueness: { scope: :file_path }
validates :object_storage_key, uniqueness: true
+ sha256_attribute :file_sha256
+
mount_file_store_uploader SymbolUploader
before_validation :set_object_storage_key, on: :create
diff --git a/app/models/packages/protection/rule.rb b/app/models/packages/protection/rule.rb
index 582b51475c2..f13bcc6e32e 100644
--- a/app/models/packages/protection/rule.rb
+++ b/app/models/packages/protection/rule.rb
@@ -12,6 +12,12 @@ module Packages
validates :package_name_pattern, presence: true, uniqueness: { scope: [:project_id, :package_type] },
length: { maximum: 255 }
+ validates :package_name_pattern,
+ format: {
+ with: Gitlab::Regex.protection_rules_npm_package_name_pattern_regex,
+ message: ->(_object, _data) { _('should be a valid NPM package name with optional wildcard characters.') }
+ },
+ if: :npm?
validates :package_type, presence: true
validates :push_protected_up_to_access_level, presence: true
@@ -20,7 +26,7 @@ module Packages
scope :for_package_name, ->(package_name) {
return none if package_name.blank?
- where(":package_name ILIKE package_name_pattern_ilike_query", package_name: package_name)
+ where(':package_name ILIKE package_name_pattern_ilike_query', package_name: package_name)
}
def self.push_protected_from?(access_level:, package_name:, package_type:)
diff --git a/app/models/packages/pypi/metadatum.rb b/app/models/packages/pypi/metadatum.rb
index ff247fedb59..f7360409507 100644
--- a/app/models/packages/pypi/metadatum.rb
+++ b/app/models/packages/pypi/metadatum.rb
@@ -3,10 +3,24 @@
class Packages::Pypi::Metadatum < ApplicationRecord
self.primary_key = :package_id
+ MAX_REQUIRED_PYTHON_LENGTH = 255
+ MAX_KEYWORDS_LENGTH = 255
+ MAX_METADATA_VERSION_LENGTH = 16
+ MAX_AUTHOR_EMAIL_LENGTH = 2048
+ MAX_SUMMARY_LENGTH = 255
+ MAX_DESCRIPTION_LENGTH = 4000
+ MAX_DESCRIPTION_CONTENT_TYPE = 128
+
belongs_to :package, -> { where(package_type: :pypi) }, inverse_of: :pypi_metadatum
validates :package, presence: true
- validates :required_python, length: { maximum: 255 }, allow_nil: false
+ validates :required_python, length: { maximum: MAX_REQUIRED_PYTHON_LENGTH }, allow_nil: false
+ validates :keywords, length: { maximum: MAX_KEYWORDS_LENGTH }, allow_nil: true
+ validates :metadata_version, length: { maximum: MAX_METADATA_VERSION_LENGTH }, allow_nil: true
+ validates :author_email, length: { maximum: MAX_AUTHOR_EMAIL_LENGTH }, allow_nil: true
+ validates :summary, length: { maximum: MAX_SUMMARY_LENGTH }, allow_nil: true
+ validates :description, length: { maximum: MAX_DESCRIPTION_LENGTH }, allow_nil: true
+ validates :description_content_type, length: { maximum: MAX_DESCRIPTION_CONTENT_TYPE }, allow_nil: true
validate :pypi_package_type
diff --git a/app/models/packages/tag.rb b/app/models/packages/tag.rb
index 9c17a147bf4..0df64bfba54 100644
--- a/app/models/packages/tag.rb
+++ b/app/models/packages/tag.rb
@@ -1,9 +1,12 @@
# frozen_string_literal: true
class Packages::Tag < ApplicationRecord
belongs_to :package, inverse_of: :tags
+ belongs_to :project
validates :package, :name, presence: true
+ before_save :ensure_project_id
+
FOR_PACKAGES_TAGS_LIMIT = 200
NUGET_TAGS_SEPARATOR = ' ' # https://docs.microsoft.com/en-us/nuget/reference/nuspec#tags
@@ -15,4 +18,8 @@ class Packages::Tag < ApplicationRecord
.order(updated_at: :desc)
.limit(FOR_PACKAGES_TAGS_LIMIT)
end
+
+ def ensure_project_id
+ self.project_id ||= package.project_id
+ end
end
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index 8a02415aef4..e5e23c3bb84 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -4,8 +4,6 @@ module Pages
class LookupPath
include Gitlab::Utils::StrongMemoize
- LegacyStorageDisabledError = Class.new(::StandardError)
-
def initialize(project, trim_prefix: nil, domain: nil)
@project = project
@domain = domain
@@ -15,6 +13,7 @@ module Pages
def project_id
project.id
end
+ strong_memoize_attr :project_id
def access_control
project.private_pages?
@@ -76,8 +75,15 @@ module Pages
attr_reader :project, :trim_prefix, :domain
+ # project.active_pages_deployments is already loaded from the database,
+ # so selecting from the array to avoid N+1
+ # this will change with when serving multiple versions on
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133261
def deployment
- project.pages_metadatum.pages_deployment
+ project
+ .active_pages_deployments
+ .to_a
+ .find { |deployment| deployment.path_prefix.blank? }
end
strong_memoize_attr :deployment
diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb
index f05ed2aac6e..2aa36a94171 100644
--- a/app/models/pages_deployment.rb
+++ b/app/models/pages_deployment.rb
@@ -17,7 +17,8 @@ class PagesDeployment < ApplicationRecord
scope :with_files_stored_locally, -> { where(file_store: ::ObjectStorage::Store::LOCAL) }
scope :with_files_stored_remotely, -> { where(file_store: ::ObjectStorage::Store::REMOTE) }
scope :project_id_in, ->(ids) { where(project_id: ids) }
- scope :active, -> { where(deleted_at: nil) }
+ scope :with_path_prefix, ->(prefix) { where("COALESCE(path_prefix, '') = ?", prefix.to_s) }
+ scope :active, -> { where(deleted_at: nil).order(created_at: :desc) }
scope :deactivated, -> { where('deleted_at < ?', Time.now.utc) }
validates :file, presence: true
@@ -33,11 +34,23 @@ class PagesDeployment < ApplicationRecord
skip_callback :save, :after, :store_file!
after_commit :store_file_after_commit!, on: [:create, :update]
+ def self.latest_pipeline_id
+ Ci::Build.id_in(pluck(:ci_build_id)).maximum(:commit_id)
+ end
+
+ def self.deactivate_all(project)
+ now = Time.now.utc
+ active
+ .project_id_in(project.id)
+ .update_all(updated_at: now, deleted_at: now)
+ end
+
def self.deactivate_deployments_older_than(deployment, time: nil)
now = Time.now.utc
active
.older_than(deployment.id)
- .where(project_id: deployment.project_id, path_prefix: deployment.path_prefix)
+ .project_id_in(deployment.project_id)
+ .with_path_prefix(deployment.path_prefix)
.update_all(updated_at: now, deleted_at: time || now)
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index b86bc761cc1..cabd3924fd6 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -11,6 +11,8 @@ class PagesDomain < ApplicationRecord
MAX_CERTIFICATE_KEY_LENGTH = 8192
+ X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN = 19
+
enum certificate_source: { user_provided: 0, gitlab_provided: 1 }, _prefix: :certificate
enum scope: { instance: 0, group: 1, project: 2 }, _prefix: :scope, _default: :project
enum usage: { pages: 0, serverless: 1 }, _prefix: :usage, _default: :pages
@@ -122,15 +124,23 @@ class PagesDomain < ApplicationRecord
x509.check_private_key(pkey)
end
- def has_intermediates?
+ def has_valid_intermediates?
return false unless x509
- # self-signed certificates doesn't have the certificate chain
+ # self-signed certificates don't have the certificate chain
return true if x509.verify(x509.public_key)
store = OpenSSL::X509::Store.new
store.set_default_paths
+ store.verify_callback = ->(is_valid, store_ctx) {
+ # allow self signed certs, see https://gitlab.com/gitlab-org/gitlab/-/issues/356447
+ return true if store_ctx.error == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN
+
+ self.errors.add(:certificate, store_ctx.error_string) unless is_valid
+ is_valid
+ }
+
store.verify(x509, untrusted_ca_certs_bundle)
rescue OpenSSL::X509::StoreError
false
@@ -230,9 +240,7 @@ class PagesDomain < ApplicationRecord
end
def pages_deployed?
- return false unless project
-
- project.pages_metadatum&.deployed?
+ project&.pages_deployed?
end
private
@@ -260,9 +268,7 @@ class PagesDomain < ApplicationRecord
end
def validate_intermediates
- unless has_intermediates?
- self.errors.add(:certificate, 'misses intermediates')
- end
+ self.errors.add(:certificate, 'misses intermediates') unless has_valid_intermediates?
end
def validate_pages_domain
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 4dfe7252a0c..f2fbb5b989e 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -44,8 +44,9 @@ class PersonalAccessToken < ApplicationRecord
scope :last_used_after, -> (date) { where("last_used_at >= ?", date) }
validates :scopes, presence: true
+ validates :expires_at, presence: true, on: :create, unless: :allow_expires_at_to_be_empty?
+
validate :validate_scopes
- validates :expires_at, presence: true, on: :create
validate :expires_at_before_instance_max_expiry_date, on: :create
def revoke!
@@ -97,6 +98,10 @@ class PersonalAccessToken < ApplicationRecord
self.class.token_prefix
end
+ def allow_expires_at_to_be_empty?
+ false
+ end
+
def expires_at_before_instance_max_expiry_date
return unless expires_at
diff --git a/app/models/project.rb b/app/models/project.rb
index fd226d23e77..0d103094aec 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -45,6 +45,7 @@ class Project < ApplicationRecord
include UpdatedAtFilterable
include IgnorableColumns
include CrossDatabaseIgnoredTables
+ include UseSqlFunctionForPrimaryKeyLookups
ignore_column :emails_disabled, remove_with: '16.3', remove_after: '2023-08-22'
@@ -140,8 +141,14 @@ class Project < ApplicationRecord
after_create -> { create_or_load_association(:pages_metadatum) }
after_create :set_timestamps_for_create
after_create :check_repository_absence!
+
+ # TODO: Remove this callback after background syncing is implemented. See https://gitlab.com/gitlab-org/gitlab/-/issues/429376.
+ after_update :update_catalog_resource,
+ if: -> { (saved_change_to_name? || saved_change_to_description? || saved_change_to_visibility_level?) && catalog_resource }
+
before_destroy :remove_private_deploy_keys
after_destroy :remove_exports
+
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? }
@@ -457,8 +464,10 @@ class Project < ApplicationRecord
# GitLab Pages
has_many :pages_domains
has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project
- # we need to clean up files, not only remove records
- has_many :pages_deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ # rubocop:disable Cop/ActiveRecordDependent -- we need to clean up files, not only remove records
+ has_many :pages_deployments, dependent: :destroy, inverse_of: :project
+ # rubocop:enable Cop/ActiveRecordDependent
+ has_many :active_pages_deployments, -> { active }, class_name: 'PagesDeployment', inverse_of: :project
# Can be too many records. We need to implement delete_all in batches.
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/228637
@@ -497,7 +506,7 @@ class Project < ApplicationRecord
delegate :merge_requests_access_level, :forking_access_level, :issues_access_level, :wiki_access_level, :snippets_access_level, :builds_access_level, :repository_access_level, :package_registry_access_level, :pages_access_level, :metrics_dashboard_access_level, :analytics_access_level, :operations_access_level, :security_and_compliance_access_level, :container_registry_access_level, :environments_access_level, :feature_flags_access_level, :monitor_access_level, :releases_access_level, :infrastructure_access_level, :model_experiments_access_level, to: :project_feature, allow_nil: true
delegate :name, to: :owner, allow_nil: true, prefix: true
- delegate :log_jira_dvcs_integration_usage, :jira_dvcs_server_last_sync_at, :jira_dvcs_cloud_last_sync_at, to: :feature_usage
+ delegate :jira_dvcs_server_last_sync_at, :jira_dvcs_cloud_last_sync_at, to: :feature_usage
delegate :last_pipeline, to: :commit, allow_nil: true
with_options to: :team do
@@ -620,42 +629,6 @@ class Project < ApplicationRecord
.or(arel_table[:storage_version].eq(nil)))
end
- scope :sorted_by_name_desc, -> {
- keyset_order = Gitlab::Pagination::Keyset::Order.build([
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: :name,
- column_expression: Project.arel_table[:name],
- order_expression: Project.arel_table[:name].desc,
- distinct: false,
- nullable: :nulls_last
- ),
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: :id,
- order_expression: Project.arel_table[:id].desc
- )
- ])
-
- reorder(keyset_order)
- }
-
- scope :sorted_by_name_asc, -> {
- keyset_order = Gitlab::Pagination::Keyset::Order.build([
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: :name,
- column_expression: Project.arel_table[:name],
- order_expression: Project.arel_table[:name].asc,
- distinct: false,
- nullable: :nulls_last
- ),
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: :id,
- order_expression: Project.arel_table[:id].asc
- )
- ])
-
- reorder(keyset_order)
- }
-
scope :sorted_by_updated_asc, -> { reorder(self.arel_table['updated_at'].asc) }
scope :sorted_by_updated_desc, -> { reorder(self.arel_table['updated_at'].desc) }
scope :sorted_by_stars_desc, -> { reorder(self.arel_table['star_count'].desc) }
@@ -769,7 +742,7 @@ class Project < ApplicationRecord
end
scope :with_pages_deployed, -> do
- joins(:pages_metadatum).merge(ProjectPagesMetadatum.deployed)
+ where_exists(PagesDeployment.active.where('pages_deployments.project_id = projects.id'))
end
scope :pages_metadata_not_migrated, -> do
@@ -1476,12 +1449,10 @@ class Project < ApplicationRecord
end
def build_or_assign_import_data(data: nil, credentials: nil)
- return if data.nil? && credentials.nil?
-
project_import_data = import_data || build_import_data
- project_import_data.merge_data(data.to_h)
- project_import_data.merge_credentials(credentials.to_h)
+ project_import_data.merge_data(data.to_h) if data
+ project_import_data.merge_credentials(credentials.to_h) if credentials
project_import_data
end
@@ -1564,9 +1535,9 @@ class Project < ApplicationRecord
limit = creator.projects_limit
error =
if limit == 0
- _('Personal project creation is not allowed. Please contact your administrator with questions')
+ _('You cannot create projects in your personal namespace. Contact your GitLab administrator.')
else
- _('Your project limit is %{limit} projects! Please contact your administrator to increase it')
+ _("You've reached your limit of %{limit} projects created. Contact your GitLab administrator.")
end
self.errors.add(:limit_reached, error % { limit: limit })
@@ -2236,11 +2207,11 @@ class Project < ApplicationRecord
end
def pages_deployed?
- pages_metadatum&.deployed?
+ active_pages_deployments.exists?
end
def pages_show_onboarding?
- !(pages_metadatum&.onboarding_complete || pages_metadatum&.deployed)
+ !(pages_metadatum&.onboarding_complete || pages_deployed?)
end
def remove_private_deploy_keys
@@ -2262,27 +2233,6 @@ class Project < ApplicationRecord
ensure_pages_metadatum.update!(onboarding_complete: true)
end
- def mark_pages_as_deployed
- ensure_pages_metadatum.update!(deployed: true)
- end
-
- def mark_pages_as_not_deployed
- ensure_pages_metadatum.update!(deployed: false)
- end
-
- def update_pages_deployment!(deployment)
- ensure_pages_metadatum.update!(pages_deployment: deployment)
- end
-
- def set_first_pages_deployment!(deployment)
- ensure_pages_metadatum
-
- # where().update_all to perform update in the single transaction with check for null
- ProjectPagesMetadatum
- .where(project_id: id, pages_deployment_id: nil)
- .update_all(deployed: deployment.present?, pages_deployment_id: deployment&.id)
- end
-
def set_full_path(gl_full_path: full_path)
# We'd need to keep track of project full path otherwise directory tree
# created with hashed storage enabled cannot be usefully imported using
@@ -2875,7 +2825,7 @@ class Project < ApplicationRecord
end
def uses_default_ci_config?
- ci_config_path.blank? || ci_config_path == Gitlab::FileDetector::PATTERNS[:gitlab_ci]
+ ci_config_path.blank? || Gitlab::FileDetector.type_of(ci_config_path) == :gitlab_ci
end
def limited_protected_branches(limit)
@@ -3026,7 +2976,7 @@ class Project < ApplicationRecord
end
def ci_config_for(sha)
- repository.gitlab_ci_yml_for(sha, ci_config_path_or_default)
+ repository.blob_data_at(sha, ci_config_path_or_default)
end
def enabled_group_deploy_keys
@@ -3530,6 +3480,10 @@ class Project < ApplicationRecord
pool_repository_shard == repository_storage
end
+
+ def update_catalog_resource
+ catalog_resource.sync_with_project!
+ end
end
Project.prepend_mod_with('Project')
diff --git a/app/models/project_feature_usage.rb b/app/models/project_feature_usage.rb
index dba81a6cb60..5e47ec6310d 100644
--- a/app/models/project_feature_usage.rb
+++ b/app/models/project_feature_usage.rb
@@ -19,19 +19,6 @@ class ProjectFeatureUsage < ApplicationRecord
end
end
- def log_jira_dvcs_integration_usage(cloud: true)
- ::Gitlab::Database::LoadBalancing::Session.without_sticky_writes do
- integration_field = self.class.jira_dvcs_integration_field(cloud: cloud)
-
- # The feature usage is used only once later to query the feature usage in a
- # long date range. Therefore, we just need to update the timestamp once per
- # day
- break if persisted? && updated_today?(integration_field)
-
- persist_jira_dvcs_usage(integration_field)
- end
- end
-
private
def updated_today?(integration_field)
diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb
index eca2e5a740e..87cff4f2715 100644
--- a/app/models/project_pages_metadatum.rb
+++ b/app/models/project_pages_metadatum.rb
@@ -10,7 +10,4 @@ class ProjectPagesMetadatum < ApplicationRecord
belongs_to :project, inverse_of: :pages_metadatum
belongs_to :pages_deployment
-
- scope :deployed, -> { where(deployed: true) }
- scope :with_project_route_and_deployment, -> { preload(:pages_deployment, project: [:namespace, :route]) }
end
diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb
index ffb08e10f1f..7a80ad33d68 100644
--- a/app/models/project_snippet.rb
+++ b/app/models/project_snippet.rb
@@ -5,4 +5,6 @@ class ProjectSnippet < Snippet
validates :project, presence: true
validates :secret, inclusion: { in: [false] }
+
+ scope :by_project, ->(project) { where(project: project) }
end
diff --git a/app/models/projects/repository_storage_move.rb b/app/models/projects/repository_storage_move.rb
index f4411e0b4fd..e2c6d1853a9 100644
--- a/app/models/projects/repository_storage_move.rb
+++ b/app/models/projects/repository_storage_move.rb
@@ -14,11 +14,6 @@ module Projects
alias_attribute :project, :container
scope :with_projects, -> { includes(container: :route) }
- override :update_repository_storage
- def update_repository_storage(new_storage)
- container.update_column(:repository_storage, new_storage)
- end
-
override :schedule_repository_storage_update_worker
def schedule_repository_storage_update_worker
Projects::UpdateRepositoryStorageWorker.perform_async(
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index aebce59a040..40a1a4392dd 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -5,6 +5,7 @@ class ProtectedBranch < ApplicationRecord
include Gitlab::SQL::Pattern
include FromUnion
include EachBatch
+ include Presentable
belongs_to :group, foreign_key: :namespace_id, touch: true, inverse_of: :protected_branches
diff --git a/app/models/repository.rb b/app/models/repository.rb
index e565de9c4ba..e639a389e0a 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1102,10 +1102,6 @@ class Repository
blob_data_at(sha, '.gitlab/route-map.yml')
end
- def gitlab_ci_yml_for(sha, path = '.gitlab-ci.yml')
- blob_data_at(sha, path)
- end
-
def lfsconfig_for(sha)
blob_data_at(sha, '.lfsconfig')
end
@@ -1245,6 +1241,10 @@ class Repository
def get_patch_id(old_revision, new_revision)
raw_repository.get_patch_id(old_revision, new_revision)
rescue Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository => e
+ # This is expected when there are no differences between the old_revision and the new_revision.
+ # It's not ideal, but is simpler to handle this here than making breaking changes to gitaly.
+ return if e.message.match?(/no difference between old and new revision./)
+
Gitlab::ErrorTracking.track_exception(
e,
project_id: project.id,
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index d5c839724d4..ad1ce740c89 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -112,7 +112,7 @@ class ResourceLabelEvent < ResourceEvent
end
def resource_parent
- issuable.project || issuable.group
+ issuable.try(:resource_parent) || issuable.project || issuable.group
end
def discussion_id_key
diff --git a/app/models/service_desk/custom_email_credential.rb b/app/models/service_desk/custom_email_credential.rb
index 5986ac8a43f..82bda673491 100644
--- a/app/models/service_desk/custom_email_credential.rb
+++ b/app/models/service_desk/custom_email_credential.rb
@@ -2,6 +2,14 @@
module ServiceDesk
class CustomEmailCredential < ApplicationRecord
+ # Used to explicitly set the SMTP AUTH method.
+ # If nil Net::SMTP will choose one of methods listed by the SMTP server.
+ enum smtp_authentication: {
+ plain: 0,
+ login: 1,
+ cram_md5: 2
+ }
+
attr_encrypted :smtp_username,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
@@ -44,7 +52,8 @@ module ServiceDesk
password: smtp_password,
address: smtp_address,
domain: Mail::Address.new(service_desk_setting.custom_email).domain,
- port: smtp_port || 587
+ port: smtp_port || 587,
+ authentication: smtp_authentication
}
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 78b0c0849e3..3e075fdaa9e 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -77,6 +77,7 @@ class Snippet < ApplicationRecord
scope :inc_relations_for_view, -> { includes(author: :status) }
scope :inc_statistics, -> { includes(:statistics) }
scope :with_statistics, -> { joins(:statistics) }
+ scope :with_repository_storage_moves, -> { joins(:repository_storage_moves) }
scope :inc_projects_namespace_route, -> { includes(project: [:route, :namespace]) }
scope :without_created_by_banned_user, -> do
diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb
index a262802c8af..6b2fa99d547 100644
--- a/app/models/snippet_repository.rb
+++ b/app/models/snippet_repository.rb
@@ -31,6 +31,11 @@ class SnippetRepository < ApplicationRecord
options[:actions] = transform_file_entries(files)
+ # The Gitaly calls perform HTTP requests for permissions check
+ # Stick to the primary in order to make those requests aware that
+ # primary database must be used to fetch the data
+ self.class.sticking.stick(:user, user.id)
+
capture_git_error { repository.commit_files(user, **options) }
ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
diff --git a/app/models/system/broadcast_message.rb b/app/models/system/broadcast_message.rb
index 06f0115ade6..d959a6339a4 100644
--- a/app/models/system/broadcast_message.rb
+++ b/app/models/system/broadcast_message.rb
@@ -117,7 +117,7 @@ module System
end
def ended?
- ends_at < Time.current
+ ends_at.past?
end
def now?
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index dc93decce5e..8624a1a9463 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -4,6 +4,7 @@ class SystemNoteMetadata < ApplicationRecord
include Importable
include IgnorableColumns
+ ignore_column :id_convert_to_bigint, remove_with: '16.9', remove_after: '2024-01-13'
ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
# These notes's action text might contain a reference that is external.
diff --git a/app/models/upload.rb b/app/models/upload.rb
index 59ce9a1f37a..745a6174931 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -174,7 +174,7 @@ class Upload < ApplicationRecord
end
def update_project_statistics
- ProjectCacheWorker.perform_async(model_id, [], [:uploads_size])
+ ProjectCacheWorker.perform_async(model_id, [], ['uploads_size'])
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 4034677509f..25f22563136 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -32,6 +32,7 @@ class User < MainClusterwide::ApplicationRecord
include EachBatch
include CrossDatabaseIgnoredTables
include IgnorableColumns
+ include UseSqlFunctionForPrimaryKeyLookups
ignore_column %i[
email_opted_in
@@ -48,7 +49,7 @@ class User < MainClusterwide::ApplicationRecord
# Associations with dependent: option
cross_database_ignore_tables(
- %w[namespaces projects project_authorizations issues merge_requests merge_requests issues issues merge_requests],
+ %w[namespaces projects project_authorizations issues merge_requests merge_requests issues issues merge_requests events],
url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424285',
on: :destroy
)
@@ -390,6 +391,7 @@ class User < MainClusterwide::ApplicationRecord
:first_day_of_week, :first_day_of_week=,
:timezone, :timezone=,
:time_display_relative, :time_display_relative=,
+ :time_display_format, :time_display_format=,
:show_whitespace_in_diffs, :show_whitespace_in_diffs=,
:view_diffs_file_by_file, :view_diffs_file_by_file=,
:pass_user_identities_to_ci_jwt, :pass_user_identities_to_ci_jwt=,
@@ -417,6 +419,7 @@ class User < MainClusterwide::ApplicationRecord
delegate :pronouns, :pronouns=, to: :user_detail, allow_nil: true
delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true
delegate :registration_objective, :registration_objective=, to: :user_detail, allow_nil: true
+ delegate :mastodon, :mastodon=, to: :user_detail, allow_nil: true
delegate :linkedin, :linkedin=, to: :user_detail, allow_nil: true
delegate :twitter, :twitter=, to: :user_detail, allow_nil: true
delegate :skype, :skype=, to: :user_detail, allow_nil: true
@@ -425,6 +428,7 @@ class User < MainClusterwide::ApplicationRecord
delegate :organization, :organization=, to: :user_detail, allow_nil: true
delegate :discord, :discord=, to: :user_detail, allow_nil: true
delegate :email_reset_offered_at, :email_reset_offered_at=, to: :user_detail, allow_nil: true
+ delegate :project_authorizations_recalculated_at, :project_authorizations_recalculated_at=, to: :user_detail, allow_nil: true
accepts_nested_attributes_for :user_preference, update_only: true
accepts_nested_attributes_for :user_detail, update_only: true
@@ -600,6 +604,12 @@ class User < MainClusterwide::ApplicationRecord
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) }
+ scope :trusted, -> do
+ where('EXISTS (?)', ::UserCustomAttribute
+ .select(1)
+ .where('user_id = users.id')
+ .trusted_with_spam)
+ end
strip_attributes! :name
@@ -768,6 +778,8 @@ class User < MainClusterwide::ApplicationRecord
external
when 'deactivated'
deactivated
+ when "trusted"
+ trusted
else
active_without_ghosts
end
@@ -791,9 +803,9 @@ class User < MainClusterwide::ApplicationRecord
order = <<~SQL
CASE
- WHEN LOWER(users.name) = :query THEN 0
+ WHEN LOWER(users.public_email) = :query THEN 0
WHEN LOWER(users.username) = :query THEN 1
- WHEN LOWER(users.public_email) = :query THEN 2
+ WHEN LOWER(users.name) = :query THEN 2
ELSE 3
END
SQL
@@ -1081,7 +1093,7 @@ class User < MainClusterwide::ApplicationRecord
def otp_secret_expired?
return true unless otp_secret_expires_at
- otp_secret_expires_at < Time.current
+ otp_secret_expires_at.past?
end
def update_otp_secret!
@@ -1446,7 +1458,7 @@ class User < MainClusterwide::ApplicationRecord
if !Gitlab.config.ldap.enabled
false
elsif ldap_user?
- !last_credential_check_at || (last_credential_check_at + ldap_sync_time) < Time.current
+ !last_credential_check_at || (last_credential_check_at + ldap_sync_time).past?
else
false
end
@@ -2087,7 +2099,7 @@ class User < MainClusterwide::ApplicationRecord
end
def password_expired?
- !!(password_expires_at && password_expires_at < Time.current)
+ !!(password_expires_at && password_expires_at.past?)
end
def password_expired_if_applicable?
diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb
index 728c1f4844a..5a592b425df 100644
--- a/app/models/user_custom_attribute.rb
+++ b/app/models/user_custom_attribute.rb
@@ -20,6 +20,7 @@ class UserCustomAttribute < ApplicationRecord
TRUSTED_BY = 'trusted_by'
AUTO_BANNED_BY = 'auto_banned_by'
IDENTITY_VERIFICATION_PHONE_EXEMPT = 'identity_verification_phone_exempt'
+ IDENTITY_VERIFICATION_EXEMPT = 'identity_verification_exempt'
class << self
def upsert_custom_attributes(custom_attributes)
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index 9ac814eebda..bbb08ed5774 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -17,10 +17,24 @@ class UserDetail < MainClusterwide::ApplicationRecord
DEFAULT_FIELD_LENGTH = 500
+ MASTODON_VALIDATION_REGEX = /
+ \A # beginning of string
+ @?\b # optional leading at
+ ([\w\d.%+-]+) # character group to pick up words in user portion of username
+ @ # separator between user and host
+ ( # beginning of charagter group for host portion
+ [\w\d.-]+ # character group to pick up words in host portion of username
+ \.\w{2,} # pick up tld of host domain, 2 chars or more
+ )\b # end of character group to pick up words in host portion of username
+ \z # end of string
+ /x
+
validates :discord, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validate :discord_format
validates :linkedin, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validates :location, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
+ validates :mastodon, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
+ validate :mastodon_format
validates :organization, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validates :skype, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validates :twitter, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
@@ -32,7 +46,7 @@ class UserDetail < MainClusterwide::ApplicationRecord
enum registration_objective: REGISTRATION_OBJECTIVE_PAIRS, _suffix: true
def sanitize_attrs
- %i[discord linkedin skype twitter website_url].each do |attr|
+ %i[discord linkedin mastodon skype twitter website_url].each do |attr|
value = self[attr]
self[attr] = Sanitize.clean(value) if value.present?
end
@@ -49,6 +63,7 @@ class UserDetail < MainClusterwide::ApplicationRecord
self.discord = '' if discord.nil?
self.linkedin = '' if linkedin.nil?
self.location = '' if location.nil?
+ self.mastodon = '' if mastodon.nil?
self.organization = '' if organization.nil?
self.skype = '' if skype.nil?
self.twitter = '' if twitter.nil?
@@ -62,4 +77,10 @@ def discord_format
errors.add(:discord, _('must contain only a discord user ID.'))
end
+def mastodon_format
+ return if mastodon.blank? || mastodon =~ UserDetail::MASTODON_VALIDATION_REGEX
+
+ errors.add(:mastodon, _('must contain only a mastodon username.'))
+end
+
UserDetail.prepend_mod_with('UserDetail')
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 8fc9f4617d0..59cfe9a8426 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -7,6 +7,7 @@ class UserPreference < MainClusterwide::ApplicationRecord
# enum options with same name for multiple fields, also it creates
# extra methods that aren't really needed here.
NOTES_FILTERS = { all_notes: 0, only_comments: 1, only_activity: 2 }.freeze
+ TIME_DISPLAY_FORMATS = { system: 0, non_iso_format: 1, iso_format: 2 }.freeze
belongs_to :user
@@ -27,12 +28,15 @@ class UserPreference < MainClusterwide::ApplicationRecord
validates :pinned_nav_items, json_schema: { filename: 'pinned_nav_items' }
+ validates :time_display_format, inclusion: { in: TIME_DISPLAY_FORMATS.values }, presence: true
+
ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22'
# 2023-06-22 is after 16.1 release and during 16.2 release https://docs.gitlab.com/ee/development/database/avoiding_downtime_in_migrations.html#ignoring-the-column-release-m
ignore_columns :use_legacy_web_ide, remove_with: '16.2', remove_after: '2023-06-22'
attribute :tab_width, default: -> { Gitlab::TabWidth::DEFAULT }
attribute :time_display_relative, default: true
+ attribute :time_display_format, default: 0
attribute :render_whitespace_in_code, default: false
attribute :project_shortcut_buttons, default: true
attribute :keyboard_shortcuts_enabled, default: true
@@ -80,6 +84,16 @@ class UserPreference < MainClusterwide::ApplicationRecord
end
end
+ class << self
+ def time_display_formats
+ {
+ s_('Time Display|System') => TIME_DISPLAY_FORMATS[:system],
+ s_('Time Display|12-hour: 2:34 PM') => TIME_DISPLAY_FORMATS[:non_iso_format],
+ s_('Time Display|24-hour: 14:34') => TIME_DISPLAY_FORMATS[:iso_format]
+ }
+ end
+ end
+
def time_display_relative
value = read_attribute(:time_display_relative)
return value unless value.nil?
diff --git a/app/models/users/anonymous.rb b/app/models/users/anonymous.rb
new file mode 100644
index 00000000000..b4a182ba203
--- /dev/null
+++ b/app/models/users/anonymous.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Users
+ class Anonymous
+ class << self
+ def can?(action, subject = :global)
+ Ability.allowed?(nil, action, subject)
+ end
+ end
+ end
+end
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index 60dd89c3ee7..a9880e56e8c 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -65,18 +65,19 @@ module Users
# 62, removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131314
# 63 and 64 were removed with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120233
branch_rules_info_callout: 65,
- create_runner_workflow_banner: 66,
+ # 66 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135470/
# 67 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121920
project_repository_limit_alert_warning_threshold: 68, # EE-only
project_repository_limit_alert_alert_threshold: 69, # EE-only
project_repository_limit_alert_error_threshold: 70, # EE-only
- new_navigation_callout: 71,
+ # 71 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134432
# 72 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129022
namespace_over_storage_users_combined_alert: 73, # EE-only
# 74 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132751
vsd_feedback_banner: 75, # EE-only
security_policy_protected_branch_modification: 76, # EE-only
- vulnerability_report_grouping: 77 # EE-only
+ vulnerability_report_grouping: 77, # EE-only
+ new_nav_for_everyone_callout: 78
}
validates :feature_name,
diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb
index 276d549006f..6d0a22c8b0a 100644
--- a/app/models/users/credit_card_validation.rb
+++ b/app/models/users/credit_card_validation.rb
@@ -2,10 +2,16 @@
module Users
class CreditCardValidation < ApplicationRecord
+ include IgnorableColumns
+
RELEASE_DAY = Date.new(2021, 5, 17)
self.table_name = 'user_credit_card_validations'
+ ignore_columns %i[last_digits network holder_name expiration_date], remove_with: '16.8', remove_after: '2023-12-22'
+
+ attr_accessor :last_digits, :network, :holder_name, :expiration_date
+
belongs_to :user
belongs_to :banned_user, class_name: '::Users::BannedUser', foreign_key: :user_id,
inverse_of: :credit_card_validation
diff --git a/app/models/users/group_visit.rb b/app/models/users/group_visit.rb
index 0bcfda049fc..d7c76e2ee2c 100644
--- a/app/models/users/group_visit.rb
+++ b/app/models/users/group_visit.rb
@@ -13,5 +13,12 @@ module Users
validates :entity_id, presence: true
validates :user_id, presence: true
validates :visited_at, presence: true
+
+ MAX_FRECENT_ITEMS = 3
+
+ def self.frecent_groups(user_id:)
+ ids = frecent_visits_scores(user_id: user_id, limit: MAX_FRECENT_ITEMS).pluck("entity_id")
+ Group.find(ids)
+ end
end
end
diff --git a/app/models/users/phone_number_validation.rb b/app/models/users/phone_number_validation.rb
index e033445d76b..2256eb8ddc4 100644
--- a/app/models/users/phone_number_validation.rb
+++ b/app/models/users/phone_number_validation.rb
@@ -41,6 +41,10 @@ module Users
).exists?
end
+ def self.by_reference_id(ref_id)
+ find_by(telesign_reference_xid: ref_id)
+ end
+
def validated?
validated_at.present?
end
diff --git a/app/models/users/project_visit.rb b/app/models/users/project_visit.rb
index 1d076e0be56..9ff3d8d2c91 100644
--- a/app/models/users/project_visit.rb
+++ b/app/models/users/project_visit.rb
@@ -13,5 +13,12 @@ module Users
validates :entity_id, presence: true
validates :user_id, presence: true
validates :visited_at, presence: true
+
+ MAX_FRECENT_ITEMS = 5
+
+ def self.frecent_projects(user_id:)
+ ids = frecent_visits_scores(user_id: user_id, limit: MAX_FRECENT_ITEMS).pluck("entity_id")
+ Project.find(ids)
+ end
end
end
diff --git a/app/models/vs_code/settings/vs_code_setting.rb b/app/models/vs_code/settings/vs_code_setting.rb
index e55d958d2b4..1401ce82045 100644
--- a/app/models/vs_code/settings/vs_code_setting.rb
+++ b/app/models/vs_code/settings/vs_code_setting.rb
@@ -5,7 +5,9 @@ module VsCode
class VsCodeSetting < ApplicationRecord
belongs_to :user, inverse_of: :vscode_settings
- validates :setting_type, presence: true
+ validates :setting_type, presence: true,
+ inclusion: { in: SETTINGS_TYPES },
+ uniqueness: { scope: :user_id }
validates :content, presence: true
scope :by_setting_type, ->(setting_type) { where(setting_type: setting_type) }
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 2eed693ca76..3dd8f334a68 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -80,6 +80,7 @@ class WikiPage
alias_method :to_param, :slug
def human_title
+ return front_matter_title if Feature.enabled?(:wiki_front_matter_title, container) && front_matter_title.present?
return 'Home' if title == Wiki::HOMEPAGE
title
@@ -95,6 +96,10 @@ class WikiPage
attributes[:title] = new_title
end
+ def front_matter_title
+ front_matter[:title]
+ end
+
def raw_content
attributes[:content] ||= page&.text_data
end
@@ -320,7 +325,7 @@ class WikiPage
def serialize_front_matter(hash)
return '' unless hash.present?
- YAML.dump(hash.transform_keys(&:to_s)) + "---\n"
+ YAML.dump(hash.to_h.transform_keys(&:to_s)) + "---\n"
end
def update_front_matter(attrs)
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 0761a213532..a62d77939bf 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -73,6 +73,19 @@ class WorkItem < Issue
includes(:parent_link).order(keyset_order)
end
+ def linked_items_keyset_order
+ ::Gitlab::Pagination::Keyset::Order.build(
+ [
+ ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'issue_link_id',
+ column_expression: IssueLink.arel_table[:id],
+ order_expression: IssueLink.arel_table[:id].asc,
+ nullable: :not_nullable,
+ distinct: true
+ )
+ ])
+ end
+
override :related_link_class
def related_link_class
WorkItems::RelatedWorkItemLink
@@ -150,7 +163,9 @@ class WorkItem < Issue
def linked_work_items(current_user = nil, authorize: true, preload: nil, link_type: nil)
return [] if new_record?
- linked_work_items = linked_work_items_query(link_type).preload(preload).reorder('issue_link_id')
+ linked_work_items = linked_work_items_query(link_type)
+ .preload(preload)
+ .reorder(self.class.linked_items_keyset_order)
return linked_work_items unless authorize
cross_project_filter = ->(work_items) { work_items.where(project: project) }
diff --git a/app/policies/abuse_report_policy.rb b/app/policies/abuse_report_policy.rb
index f1f994e6a42..043dbd0cb89 100644
--- a/app/policies/abuse_report_policy.rb
+++ b/app/policies/abuse_report_policy.rb
@@ -3,5 +3,6 @@
class AbuseReportPolicy < ::BasePolicy
rule { admin }.policy do
enable :read_abuse_report
+ enable :create_note
end
end
diff --git a/app/policies/analytics/cycle_analytics/value_stream_policy.rb b/app/policies/analytics/cycle_analytics/value_stream_policy.rb
new file mode 100644
index 00000000000..7e236f94e91
--- /dev/null
+++ b/app/policies/analytics/cycle_analytics/value_stream_policy.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ class ValueStreamPolicy < ::BasePolicy
+ delegate { subject.namespace }
+ end
+ end
+end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 1ec2495a661..462afbaa475 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -37,7 +37,7 @@ class BasePolicy < DeclarativePolicy::Base
desc "User is security policy bot"
with_options scope: :user, score: 0
- condition(:security_policy_bot) { @user&.security_policy_bot? }
+ condition(:security_policy_bot) { false }
desc "User is automation bot"
with_options scope: :user, score: 0
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index bce7ceafe17..71ea42e1f23 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -81,6 +81,7 @@ module Ci
end
rule { ~can?(:jailbreak) & (archived | protected_ref) }.policy do
+ prevent :cancel_build
prevent :update_build
prevent :erase_build
end
@@ -88,6 +89,7 @@ module Ci
rule { can?(:admin_build) | (can?(:update_build) & owner_of_job & unprotected_ref) }.enable :erase_build
rule { can?(:public_access) & branch_allows_collaboration }.policy do
+ enable :cancel_build
enable :update_build
enable :update_commit_status
end
diff --git a/app/policies/ci/deployable_policy.rb b/app/policies/ci/deployable_policy.rb
index f0105b001f2..e83bdd5361a 100644
--- a/app/policies/ci/deployable_policy.rb
+++ b/app/policies/ci/deployable_policy.rb
@@ -11,7 +11,10 @@ module Ci
@subject.outdated_deployment?
end
- rule { outdated_deployment }.prevent :update_build
+ rule { outdated_deployment }.policy do
+ prevent :cancel_build
+ prevent :update_build
+ end
end
end
end
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index 1d60b1e79de..c01162a86df 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -27,10 +27,14 @@ module Ci
prevent :read_pipeline
end
- rule { protected_ref }.prevent :update_pipeline
+ rule { protected_ref }.policy do
+ prevent :update_pipeline
+ prevent :cancel_pipeline
+ end
rule { can?(:public_access) & branch_allows_collaboration }.policy do
enable :update_pipeline
+ enable :cancel_pipeline
end
rule { can?(:owner_access) }.policy do
diff --git a/app/policies/concerns/policy_actor.rb b/app/policies/concerns/policy_actor.rb
index e000f1514e5..8fa09683b06 100644
--- a/app/policies/concerns/policy_actor.rb
+++ b/app/policies/concerns/policy_actor.rb
@@ -53,10 +53,6 @@ module PolicyActor
false
end
- def security_policy_bot?
- false
- end
-
def automation_bot?
false
end
diff --git a/app/policies/container_registry/protection/rule_policy.rb b/app/policies/container_registry/protection/rule_policy.rb
new file mode 100644
index 00000000000..4dc8dba3276
--- /dev/null
+++ b/app/policies/container_registry/protection/rule_policy.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module ContainerRegistry
+ module Protection
+ class RulePolicy < BasePolicy
+ delegate { @subject.project }
+ end
+ end
+end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 7594360a91c..175f86c9673 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -63,10 +63,6 @@ class GlobalPolicy < BasePolicy
prevent :access_git
end
- rule { security_policy_bot }.policy do
- enable :access_git
- end
-
rule { project_bot | service_account }.policy do
prevent :log_in
prevent :receive_notifications
diff --git a/app/policies/group_group_link_policy.rb b/app/policies/group_group_link_policy.rb
new file mode 100644
index 00000000000..0108f0b7fca
--- /dev/null
+++ b/app/policies/group_group_link_policy.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class GroupGroupLinkPolicy < ::BasePolicy # rubocop:disable Gitlab/NamespacedClass
+ condition(:can_read_shared_with_group) { can?(:read_group, @subject.shared_with_group) }
+ condition(:group_member) { @subject.shared_group.member?(@user) }
+
+ rule { can_read_shared_with_group | group_member }.enable :read_shared_with_group
+end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 2ab59f5a34d..ca170133105 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -121,6 +121,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :upload_file
enable :guest_access
enable :read_release
+ enable :award_emoji
end
rule { admin }.policy do
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index 6114785a851..683c53d8d78 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -57,7 +57,10 @@ class IssuePolicy < IssuablePolicy
prevent :read_issue
end
- rule { ~can?(:read_issue) }.prevent :create_note
+ rule { ~can?(:read_issue) }.policy do
+ prevent :create_note
+ prevent :read_note
+ end
rule { locked }.policy do
prevent :reopen_issue
diff --git a/app/policies/namespaces/group_project_namespace_shared_policy.rb b/app/policies/namespaces/group_project_namespace_shared_policy.rb
index b24cb5be607..81bb5d6289e 100644
--- a/app/policies/namespaces/group_project_namespace_shared_policy.rb
+++ b/app/policies/namespaces/group_project_namespace_shared_policy.rb
@@ -22,6 +22,7 @@ module Namespaces
enable :create_work_item
enable :read_work_item
enable :read_issue
+ enable :read_note
enable :read_namespace
enable :read_namespace_via_membership
end
diff --git a/app/policies/project_group_link_policy.rb b/app/policies/project_group_link_policy.rb
index 00bb246d70b..7ad2985ecc5 100644
--- a/app/policies/project_group_link_policy.rb
+++ b/app/policies/project_group_link_policy.rb
@@ -2,9 +2,13 @@
class ProjectGroupLinkPolicy < BasePolicy # rubocop:disable Gitlab/NamespacedClass
condition(:group_owner_or_project_admin) { group_owner? || project_admin? }
+ condition(:can_read_group) { can?(:read_group, @subject.group) }
+ condition(:project_member) { @subject.project.member?(@user) }
rule { group_owner_or_project_admin }.enable :admin_project_group_link
+ rule { can_read_group | project_member }.enable :read_shared_with_group
+
private
def group_owner?
diff --git a/app/policies/project_import_state_policy.rb b/app/policies/project_import_state_policy.rb
new file mode 100644
index 00000000000..c2cd03337b7
--- /dev/null
+++ b/app/policies/project_import_state_policy.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ProjectImportStatePolicy < ::BasePolicy # rubocop:disable Gitlab/NamespacedClass -- required by DeclarativePolicy lookup logic
+ delegate { @subject.project }
+end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 20f88577d67..bbb0e3df500 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -38,9 +38,6 @@ class ProjectPolicy < BasePolicy
desc "User is a project bot"
condition(:project_bot) { user.project_bot? && team_member? }
- desc "User is a security policy bot on the project"
- condition(:security_policy_bot) { user&.security_policy_bot? && team_member? }
-
desc "Project is public"
condition(:public_project, scope: :subject, score: 0) { project.public? }
@@ -136,6 +133,29 @@ class ProjectPolicy < BasePolicy
!@user&.from_ci_job_token? || @user.ci_job_token_scope.accessible?(project)
end
+ desc "If the user is via CI job token and project container registry visibility allows access"
+ condition(:job_token_container_registry) { job_token_access_allowed_to?(:container_registry) }
+
+ desc "If the user is via CI job token and project package registry visibility allows access"
+ condition(:job_token_package_registry) { job_token_access_allowed_to?(:package_registry) }
+
+ desc "If the user is via CI job token and project ci/cd visibility allows access"
+ condition(:job_token_builds) { job_token_access_allowed_to?(:builds) }
+
+ desc "If the user is via CI job token and project releases visibility allows access"
+ condition(:job_token_releases) { job_token_access_allowed_to?(:releases) }
+
+ desc "If the user is via CI job token and project environment visibility allows access"
+ condition(:job_token_environments) { job_token_access_allowed_to?(:environments) }
+
+ desc "If the project is either public or internal"
+ condition(:public_or_internal) do
+ project.public? || project.internal?
+ end
+
+ with_scope :subject
+ condition(:restrict_job_token_enabled) { Feature.enabled?(:restrict_ci_job_token_for_public_and_internal_projects, @subject) }
+
with_scope :subject
condition(:forking_allowed) do
@subject.feature_available?(:forking, @user)
@@ -303,6 +323,8 @@ class ProjectPolicy < BasePolicy
enable :set_show_diff_preview_in_email
enable :set_warn_about_potentially_unwanted_characters
enable :manage_owners
+
+ enable :add_catalog_resource
end
rule { can?(:guest_access) }.policy do
@@ -469,6 +491,7 @@ class ProjectPolicy < BasePolicy
enable :update_commit_status
enable :create_build
enable :update_build
+ enable :cancel_build
enable :read_resource_group
enable :update_resource_group
enable :create_merge_request_from
@@ -512,6 +535,7 @@ class ProjectPolicy < BasePolicy
rule { can?(:developer_access) & user_confirmed? }.policy do
enable :create_pipeline
enable :update_pipeline
+ enable :cancel_pipeline
enable :create_pipeline_schedule
end
@@ -640,6 +664,7 @@ class ProjectPolicy < BasePolicy
rule { builds_disabled | repository_disabled }.policy do
prevent(*create_read_update_admin_destroy(:build))
+ prevent :cancel_build
prevent(*create_read_update_admin_destroy(:pipeline_schedule))
prevent(*create_read_update_admin_destroy(:environment))
prevent(*create_read_update_admin_destroy(:deployment))
@@ -652,6 +677,7 @@ class ProjectPolicy < BasePolicy
# - We prevent the user from accessing Pipelines
rule { (builds_disabled & ~internal_builds_disabled) | repository_disabled }.policy do
prevent(*create_read_update_admin_destroy(:pipeline))
+ prevent :cancel_pipeline
prevent(*create_read_update_admin_destroy(:commit_status))
end
@@ -679,8 +705,42 @@ class ProjectPolicy < BasePolicy
enable :read_project_for_iids
end
+ # If the project is private
rule { ~public_project & ~internal_access & ~project_allowed_for_job_token }.prevent_all
+ # If this project is public or internal we want to prevent all aside from a few public policies
+ rule { public_or_internal & ~project_allowed_for_job_token & restrict_job_token_enabled }.policy do
+ prevent :guest_access
+ prevent :public_access
+ prevent :public_user_access
+ prevent :reporter_access
+ prevent :developer_access
+ prevent :maintainer_access
+ prevent :owner_access
+ end
+
+ rule { public_or_internal & job_token_container_registry & restrict_job_token_enabled }.policy do
+ enable :build_read_container_image
+ enable :read_container_image
+ end
+
+ rule { public_or_internal & job_token_package_registry & restrict_job_token_enabled }.policy do
+ enable :read_package
+ enable :read_project
+ end
+
+ rule { public_or_internal & job_token_builds & restrict_job_token_enabled }.policy do
+ enable :read_commit_status # this is additionally needed to download artifacts
+ end
+
+ rule { public_or_internal & job_token_releases & restrict_job_token_enabled }.policy do
+ enable :read_release
+ end
+
+ rule { public_or_internal & job_token_environments & restrict_job_token_enabled }.policy do
+ enable :read_environment
+ end
+
rule { can?(:public_access) }.policy do
enable :read_package
enable :read_project
@@ -908,14 +968,14 @@ class ProjectPolicy < BasePolicy
enable :read_namespace_catalog
end
- rule { can?(:owner_access) & namespace_catalog_available }.policy do
- enable :add_catalog_resource
- end
-
rule { model_registry_enabled }.policy do
enable :read_model_registry
end
+ rule { can?(:reporter_access) & model_registry_enabled }.policy do
+ enable :write_model_registry
+ end
+
rule { model_experiments_enabled }.policy do
enable :read_model_experiments
end
@@ -1007,6 +1067,20 @@ class ProjectPolicy < BasePolicy
end
end
+ def job_token_access_allowed_to?(feature)
+ return false unless @user&.from_ci_job_token?
+ return false unless project.project_feature
+
+ case project.project_feature.access_level(feature)
+ when ProjectFeature::DISABLED
+ false
+ when ProjectFeature::PRIVATE
+ @user.ci_job_token_scope.accessible?(project)
+ else
+ true
+ end
+ end
+
def resource_access_token_feature_available?
true
end
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 2fd198b8cf4..04fbc8467c9 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -29,6 +29,7 @@ class UserPolicy < BasePolicy
enable :read_user_personal_access_tokens
enable :read_group_count
enable :read_user_groups
+ enable :read_user_organizations
enable :read_saved_replies
enable :read_user_email_address
enable :admin_user_email_address
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index ec1dc96c2e3..5765d08dfb3 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -61,7 +61,7 @@ module Clusters
'clusters-path': clusterable.index_path,
'dashboard-endpoint': clusterable.metrics_dashboard_path(cluster),
'documentation-path': help_page_path('user/infrastructure/clusters/manage/clusters_health'),
- 'add-dashboard-documentation-path': help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'),
+ 'add-dashboard-documentation-path': help_page_path('operations/metrics/dashboards/index', anchor: 'add-a-new-dashboard-to-your-project'),
'empty-getting-started-svg-path': image_path('illustrations/monitoring/getting_started.svg'),
'empty-loading-svg-path': image_path('illustrations/monitoring/loading.svg'),
'empty-no-data-svg-path': image_path('illustrations/monitoring/no_data.svg'),
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
index f6720546fab..0858fad1e1a 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
CALLOUT_FAILURE_MESSAGES = {
unknown_failure: 'There is an unknown failure, please try again',
diff --git a/app/presenters/member_presenter.rb b/app/presenters/member_presenter.rb
index 4cdaca3c39e..7acaa704368 100644
--- a/app/presenters/member_presenter.rb
+++ b/app/presenters/member_presenter.rb
@@ -15,6 +15,10 @@ class MemberPresenter < Gitlab::View::Presenter::Delegated
end
end
+ def valid_member_roles
+ []
+ end
+
def can_resend_invite?
invite? &&
can?(current_user, admin_member_permission, source)
@@ -37,6 +41,11 @@ class MemberPresenter < Gitlab::View::Presenter::Delegated
false
end
+ # This functionality is only available in EE.
+ def custom_permissions
+ []
+ end
+
def last_owner?
raise NotImplementedError
end
diff --git a/app/presenters/ml/model_presenter.rb b/app/presenters/ml/model_presenter.rb
index 388e2b73bc1..24d30af1d4e 100644
--- a/app/presenters/ml/model_presenter.rb
+++ b/app/presenters/ml/model_presenter.rb
@@ -5,17 +5,31 @@ module Ml
presents ::Ml::Model, as: :model
def latest_version_name
- model.latest_version&.version
+ latest_version&.version
+ end
+
+ def version_count
+ return model.version_count if model.respond_to?(:version_count)
+
+ model.versions.size
end
def latest_package_path
- return unless model.latest_version&.package_id.present?
+ latest_version&.package_path
+ end
- Gitlab::Routing.url_helpers.project_package_path(model.project, model.latest_version.package_id)
+ def latest_version_path
+ latest_version&.path
end
def path
- Gitlab::Routing.url_helpers.project_ml_model_path(model.project, model.id)
+ project_ml_model_path(model.project, model.id)
+ end
+
+ private
+
+ def latest_version
+ model.latest_version&.present
end
end
end
diff --git a/app/presenters/ml/model_version_presenter.rb b/app/presenters/ml/model_version_presenter.rb
new file mode 100644
index 00000000000..210b213ca2a
--- /dev/null
+++ b/app/presenters/ml/model_version_presenter.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Ml
+ class ModelVersionPresenter < Gitlab::View::Presenter::Delegated
+ presents ::Ml::ModelVersion, as: :model_version
+
+ def display_name
+ "#{model_version.model.name} / #{model_version.version}"
+ end
+
+ def path
+ project_ml_model_version_path(
+ model_version.model.project,
+ model_version.model,
+ model_version
+ )
+ end
+
+ def package_path
+ return unless model_version.package_id.present?
+
+ project_package_path(model_version.project, model_version.package_id)
+ end
+ end
+end
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 4533ef3633d..c983d8623d2 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -11,6 +11,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
include ChecksCollaboration
include Gitlab::Utils::StrongMemoize
include Gitlab::Experiment::Dsl
+ include SafeFormatHelper
delegator_override_with GitlabRoutingHelper # TODO: Remove `GitlabRoutingHelper` inclusion as it's duplicate
delegator_override_with Gitlab::Utils::StrongMemoize # This module inclusion is expected. See https://gitlab.com/gitlab-org/gitlab/-/issues/352884.
@@ -163,14 +164,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
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} 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
- },
+ statistic_icon('disk') + storage_anchor_text,
can_show_quota ? project_usage_quotas_path(project) : nil
)
end
@@ -439,6 +436,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
count_of_extra_topics_not_shown > 0
end
+ def has_review_app?
+ !project.environments_for_scope('review/*').empty?
+ end
+
def can_setup_review_app?
strong_memoize(:can_setup_review_app) do
(can_instantiate_cluster? && all_clusters_empty?) || cicd_missing?
@@ -528,6 +529,16 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def project_create_wiki_path
"#{wiki_path(project.wiki)}?view=create"
end
+
+ def storage_anchor_text
+ safe_format(
+ _('%{strong_start}%{human_size}%{strong_end} Project Storage'), {
+ human_size: storage_counter(statistics.storage_size),
+ strong_start: '<strong class="project-stat-value">'.html_safe,
+ strong_end: '</strong>'.html_safe
+ }
+ )
+ end
end
ProjectPresenter.prepend_mod_with('ProjectPresenter')
diff --git a/app/presenters/projects/security/configuration_presenter.rb b/app/presenters/projects/security/configuration_presenter.rb
index f248652befc..a0d731f0ccf 100644
--- a/app/presenters/projects/security/configuration_presenter.rb
+++ b/app/presenters/projects/security/configuration_presenter.rb
@@ -55,8 +55,8 @@ module Projects
def gitlab_ci_history_path
return '' if project.empty_repo?
- gitlab_ci = ::Gitlab::FileDetector::PATTERNS[:gitlab_ci]
- ::Gitlab::Routing.url_helpers.project_blame_path(project, File.join(project.default_branch_or_main, gitlab_ci))
+ ::Gitlab::Routing.url_helpers.project_blame_path(
+ project, File.join(project.default_branch_or_main, project.ci_config_path_or_default))
end
def features
diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb
index 43164cca9c9..da087ce6858 100644
--- a/app/presenters/user_presenter.rb
+++ b/app/presenters/user_presenter.rb
@@ -21,7 +21,6 @@ class UserPresenter < Gitlab::View::Presenter::Delegated
delegator_override :saved_replies
def saved_replies
- return ::Users::SavedReply.none unless Feature.enabled?(:saved_replies, current_user)
return ::Users::SavedReply.none unless current_user.can?(:read_saved_replies, user)
user.saved_replies
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 9aee031328b..35063ceeb06 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -14,7 +14,7 @@ class BuildDetailsEntity < Ci::JobEntity
expose :deployment_status, if: -> (*) { build.deployment_job? } do
expose :deployment_status, as: :status
expose :persisted_environment, as: :environment do |build, options|
- options.merge(deployment_details: false).yield_self do |opts|
+ options.merge(deployment_details: false).then do |opts|
EnvironmentEntity.represent(build.persisted_environment, opts)
end
end
diff --git a/app/serializers/ci/job_entity.rb b/app/serializers/ci/job_entity.rb
index 813938c2a18..828a9eb33a5 100644
--- a/app/serializers/ci/job_entity.rb
+++ b/app/serializers/ci/job_entity.rb
@@ -53,7 +53,7 @@ module Ci
alias_method :job, :object
def cancelable?
- job.cancelable? && can?(request.current_user, :update_build, job)
+ job.cancelable? && can?(request.current_user, :cancel_build, job)
end
def retryable?
diff --git a/app/serializers/ci/pipeline_entity.rb b/app/serializers/ci/pipeline_entity.rb
index 832ca619edc..4ff56c67d13 100644
--- a/app/serializers/ci/pipeline_entity.rb
+++ b/app/serializers/ci/pipeline_entity.rb
@@ -106,7 +106,7 @@ class Ci::PipelineEntity < Grape::Entity
end
def can_cancel?
- can?(request.current_user, :update_pipeline, pipeline) &&
+ can?(request.current_user, :cancel_pipeline, pipeline) &&
pipeline.cancelable?
end
diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb
index 7cd913d057e..851d7a95d40 100644
--- a/app/serializers/deployment_entity.rb
+++ b/app/serializers/deployment_entity.rb
@@ -28,7 +28,7 @@ class DeploymentEntity < Grape::Entity
expose :deployed_by, as: :user, using: UserEntity
expose :deployable, if: -> (deployment) { deployment.deployable.present? } do |deployment, opts|
- deployment.deployable.yield_self do |deployable|
+ deployment.deployable.then do |deployable|
if include_details?
Ci::JobEntity.represent(deployable, opts)
elsif can_read_deployables?
diff --git a/app/serializers/group_link/group_group_link_entity.rb b/app/serializers/group_link/group_group_link_entity.rb
index d5d7eea74ea..f855d89f593 100644
--- a/app/serializers/group_link/group_group_link_entity.rb
+++ b/app/serializers/group_link/group_group_link_entity.rb
@@ -4,7 +4,7 @@ module GroupLink
class GroupGroupLinkEntity < GroupLink::GroupLinkEntity
include RequestAwareEntity
- expose :source do |group_link|
+ expose :source, if: ->(group_link) { can_read_shared_group?(group_link) } do |group_link|
GroupEntity.represent(group_link.shared_from, only: [:id, :full_name, :web_url])
end
diff --git a/app/serializers/group_link/group_link_entity.rb b/app/serializers/group_link/group_link_entity.rb
index 4cc7e9f3c8c..66645e736a9 100644
--- a/app/serializers/group_link/group_link_entity.rb
+++ b/app/serializers/group_link/group_link_entity.rb
@@ -19,16 +19,28 @@ module GroupLink
group_link.class.access_options
end
+ expose :is_shared_with_group_private do |group_link|
+ !can_read_shared_group?(group_link)
+ end
+
expose :shared_with_group do
- expose :avatar_url do |group_link|
+ expose :avatar_url, if: ->(group_link) { can_read_shared_group?(group_link) } do |group_link|
group_link.shared_with_group.avatar_url(only_path: false, size: Member::AVATAR_SIZE)
end
- expose :web_url do |group_link|
+ expose :web_url, if: ->(group_link) { can_read_shared_group?(group_link) } do |group_link|
group_link.shared_with_group.web_url
end
- expose :shared_with_group, merge: true, using: GroupBasicEntity
+ # We have to expose shared_with_group.id because we use this to get distinct
+ # with ancestors
+ expose :shared_with_group, merge: true do |group_link|
+ if can_read_shared_group?(group_link)
+ GroupBasicEntity.represent(group_link.shared_with_group)
+ else
+ GroupBasicEntity.represent(group_link.shared_with_group, only: [:id])
+ end
+ end
end
expose :can_update do |group_link, options|
@@ -45,6 +57,10 @@ module GroupLink
private
+ def can_read_shared_group?(group_link)
+ can?(current_user, :read_shared_with_group, group_link)
+ end
+
def current_user
options[:current_user]
end
diff --git a/app/serializers/group_link/project_group_link_entity.rb b/app/serializers/group_link/project_group_link_entity.rb
index d246bff1c58..fbad69bf2c5 100644
--- a/app/serializers/group_link/project_group_link_entity.rb
+++ b/app/serializers/group_link/project_group_link_entity.rb
@@ -4,7 +4,7 @@ module GroupLink
class ProjectGroupLinkEntity < GroupLink::GroupLinkEntity
include RequestAwareEntity
- expose :source do |group_link|
+ expose :source, if: ->(group_link) { can_read_shared_group?(group_link) } do |group_link|
ProjectEntity.represent(group_link.shared_from, only: [:id, :full_name])
end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 657af578c7f..9a55e761bf0 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -73,11 +73,11 @@ class IssueEntity < IssuableEntity
end
expose :confidential_issues_docs_path, if: -> (issue) { issue.confidential? } do |issue|
- help_page_path('user/project/issues/confidential_issues.md')
+ help_page_path('user/project/issues/confidential_issues')
end
expose :locked_discussion_docs_path, if: -> (issue) { issue.discussion_locked? } do |issue|
- help_page_path('user/discussions/index.md', anchor: 'prevent-comments-by-locking-an-issue')
+ help_page_path('user/discussions/index', anchor: 'prevent-comments-by-locking-an-issue')
end
expose :is_project_archived do |issue|
@@ -85,7 +85,7 @@ class IssueEntity < IssuableEntity
end
expose :archived_project_docs_path, if: -> (issue) { issue.project.archived? } do |issue|
- help_page_path('user/project/settings/index.md', anchor: 'archive-a-project')
+ help_page_path('user/project/settings/index', anchor: 'archive-a-project')
end
expose :issue_email_participants do |issue|
diff --git a/app/serializers/member_entity.rb b/app/serializers/member_entity.rb
index 8e5d352e413..a710df9ce5b 100644
--- a/app/serializers/member_entity.rb
+++ b/app/serializers/member_entity.rb
@@ -32,8 +32,11 @@ class MemberEntity < Grape::Entity
expose :access_level do
expose :human_access, as: :string_value
expose :access_level, as: :integer_value
+ expose :member_role_id
end
+ expose :custom_permissions
+
expose :source do |member|
GroupEntity.represent(member.source, only: [:id, :full_name, :web_url])
end
@@ -42,6 +45,8 @@ class MemberEntity < Grape::Entity
expose :valid_level_roles, as: :valid_roles
+ expose :valid_member_roles, as: :custom_roles
+
expose :user, if: -> (member) { member.user.present? } do |member, options|
MemberUserEntity.represent(member.user, options)
end
diff --git a/app/serializers/merge_request_noteable_entity.rb b/app/serializers/merge_request_noteable_entity.rb
index aac90c20b53..04b801e29ad 100644
--- a/app/serializers/merge_request_noteable_entity.rb
+++ b/app/serializers/merge_request_noteable_entity.rb
@@ -49,14 +49,10 @@ class MergeRequestNoteableEntity < IssuableEntity
expose :can_update do |merge_request|
can?(current_user, :update_merge_request, merge_request)
end
-
- expose :can_approve do |merge_request|
- merge_request.eligible_for_approval_by?(current_user)
- end
end
expose :locked_discussion_docs_path, if: -> (merge_request) { merge_request.discussion_locked? } do |merge_request|
- help_page_path('user/discussions/index.md', anchor: 'prevent-comments-by-locking-an-issue')
+ help_page_path('user/discussions/index', anchor: 'prevent-comments-by-locking-an-issue')
end
expose :is_project_archived do |merge_request|
@@ -66,7 +62,7 @@ class MergeRequestNoteableEntity < IssuableEntity
expose :project_id
expose :archived_project_docs_path, if: -> (merge_request) { merge_request.project.archived? } do |merge_request|
- help_page_path('user/project/settings/index.md', anchor: 'archive-a-project')
+ help_page_path('user/project/settings/index', anchor: 'archive-a-project')
end
private
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index cf984207ad1..95072ae815e 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -48,15 +48,15 @@ class MergeRequestWidgetEntity < Grape::Entity
end
expose :conflicts_docs_path do |merge_request|
- help_page_path('user/project/merge_requests/conflicts.md')
+ help_page_path('user/project/merge_requests/conflicts')
end
expose :reviewing_and_managing_merge_requests_docs_path do |merge_request|
- help_page_path('user/project/merge_requests/reviews/index.md', anchor: "checkout-merge-requests-locally-through-the-head-ref")
+ help_page_path('user/project/merge_requests/reviews/index', anchor: "checkout-merge-requests-locally-through-the-head-ref")
end
expose :merge_request_pipelines_docs_path do |merge_request|
- help_page_path('ci/pipelines/merge_request_pipelines.md')
+ help_page_path('ci/pipelines/merge_request_pipelines')
end
expose :ci_environments_status_path do |merge_request|
@@ -129,7 +129,7 @@ class MergeRequestWidgetEntity < Grape::Entity
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')
+ help_page_path('user/application_security/index', anchor: 'view-security-scan-information-in-merge-requests')
end
expose :enabled_reports do |merge_request|
diff --git a/app/serializers/review_app_setup_entity.rb b/app/serializers/review_app_setup_entity.rb
index 3a21fe24d9e..1fde31bc847 100644
--- a/app/serializers/review_app_setup_entity.rb
+++ b/app/serializers/review_app_setup_entity.rb
@@ -13,6 +13,8 @@ class ReviewAppSetupEntity < Grape::Entity
YAML.safe_load(File.read(Rails.root.join('lib', 'gitlab', 'ci', 'snippets', 'review_app_default.yml'))).to_s
end
+ expose :has_review_app?, as: :has_review_app
+
private
def current_user
diff --git a/app/services/activity_pub/accept_follow_service.rb b/app/services/activity_pub/accept_follow_service.rb
new file mode 100644
index 00000000000..0ec440fa972
--- /dev/null
+++ b/app/services/activity_pub/accept_follow_service.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ class AcceptFollowService
+ MissingInboxURLError = Class.new(StandardError)
+
+ attr_reader :subscription, :actor
+
+ def initialize(subscription, actor)
+ @subscription = subscription
+ @actor = actor
+ end
+
+ def execute
+ return if subscription.accepted?
+ raise MissingInboxURLError unless subscription.subscriber_inbox_url.present?
+
+ upload_accept_activity
+ subscription.accepted!
+ end
+
+ private
+
+ def upload_accept_activity
+ body = Gitlab::Json::LimitedEncoder.encode(payload, limit: 1.megabyte)
+
+ begin
+ Gitlab::HTTP.post(subscription.subscriber_inbox_url, body: body, headers: headers)
+ rescue StandardError => e
+ raise ThirdPartyError, e.message
+ end
+ end
+
+ def payload
+ follow = subscription.payload.dup
+ follow.delete('@context')
+
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: "#{actor}#follow/#{subscription.id}/accept",
+ type: 'Accept',
+ actor: actor,
+ object: follow
+ }
+ end
+
+ def headers
+ {
+ 'User-Agent' => "GitLab/#{Gitlab::VERSION}",
+ 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
+ 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
+ }
+ end
+ end
+end
diff --git a/app/services/activity_pub/inbox_resolver_service.rb b/app/services/activity_pub/inbox_resolver_service.rb
new file mode 100644
index 00000000000..c2bd2112b16
--- /dev/null
+++ b/app/services/activity_pub/inbox_resolver_service.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ class InboxResolverService
+ attr_reader :subscription
+
+ def initialize(subscription)
+ @subscription = subscription
+ end
+
+ def execute
+ profile = subscriber_profile
+ unless profile.has_key?('inbox') && profile['inbox'].is_a?(String)
+ raise ThirdPartyError, 'Inbox parameter absent or invalid'
+ end
+
+ subscription.subscriber_inbox_url = profile['inbox']
+ subscription.shared_inbox_url = profile.dig('entrypoints', 'sharedInbox')
+ subscription.save!
+ end
+
+ private
+
+ def subscriber_profile
+ raw_data = download_subscriber_profile
+
+ begin
+ profile = Gitlab::Json.parse(raw_data)
+ rescue JSON::ParserError => e
+ raise ThirdPartyError, e.message
+ end
+
+ profile
+ end
+
+ def download_subscriber_profile
+ begin
+ response = Gitlab::HTTP.get(subscription.subscriber_url,
+ headers: {
+ 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
+ }
+ )
+ rescue StandardError => e
+ raise ThirdPartyError, e.message
+ end
+
+ response.body
+ end
+ end
+end
diff --git a/app/services/activity_pub/third_party_error.rb b/app/services/activity_pub/third_party_error.rb
new file mode 100644
index 00000000000..473a67984a4
--- /dev/null
+++ b/app/services/activity_pub/third_party_error.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ ThirdPartyError = Class.new(StandardError)
+end
diff --git a/app/services/admin/plan_limits/update_service.rb b/app/services/admin/plan_limits/update_service.rb
index 24ce3c4095f..7412f9852d1 100644
--- a/app/services/admin/plan_limits/update_service.rb
+++ b/app/services/admin/plan_limits/update_service.rb
@@ -51,35 +51,63 @@ module Admin
def validate_notification_limit
return unless parsed_params.include?(:notification_limit)
- return if notification_limit >= storage_size_limit && notification_limit <= enforcement_limit
+ return if unlimited_value?(:notification_limit)
- plan_limits.errors.add(:notification_limit, "must be greater than or equal to " \
- "storage_size_limit (Dashboard limit): #{storage_size_limit} " \
- "and less than or equal to enforcement_limit: #{enforcement_limit}")
+ if storage_size_limit > 0 && notification_limit < storage_size_limit
+ plan_limits.errors.add(
+ :notification_limit, "must be greater than or equal to the dashboard limit (#{storage_size_limit})"
+ )
+ end
+
+ return unless enforcement_limit > 0 && notification_limit > enforcement_limit
+
+ plan_limits.errors.add(
+ :notification_limit, "must be less than or equal to the enforcement limit (#{enforcement_limit})"
+ )
end
def validate_enforcement_limit
return unless parsed_params.include?(:enforcement_limit)
- return if enforcement_limit >= storage_size_limit && enforcement_limit >= notification_limit
+ return if unlimited_value?(:enforcement_limit)
+
+ if storage_size_limit > 0 && enforcement_limit < storage_size_limit
+ plan_limits.errors.add(
+ :enforcement_limit, "must be greater than or equal to the dashboard limit (#{storage_size_limit})"
+ )
+ end
+
+ return unless notification_limit > 0 && enforcement_limit < notification_limit
- plan_limits.errors.add(:enforcement_limit, "must be greater than or equal to " \
- "storage_size_limit (Dashboard limit): #{storage_size_limit} and " \
- "greater than or equal to notification_limit: #{notification_limit}")
+ plan_limits.errors.add(
+ :enforcement_limit, "must be greater than or equal to the notification limit (#{notification_limit})"
+ )
end
def validate_storage_size_limit
return unless parsed_params.include?(:storage_size_limit)
- return if storage_size_limit <= enforcement_limit && storage_size_limit <= notification_limit
+ return if unlimited_value?(:storage_size_limit)
- plan_limits.errors.add(:storage_size_limit, "(Dashboard limit) must be less than or equal to " \
- "enforcement_limit: #{enforcement_limit} " \
- "and notification_limit: #{notification_limit}")
+ if enforcement_limit > 0 && storage_size_limit > enforcement_limit
+ plan_limits.errors.add(
+ :dashboard_limit, "must be less than or equal to the enforcement limit (#{enforcement_limit})"
+ )
+ end
+
+ return unless notification_limit > 0 && storage_size_limit > notification_limit
+
+ plan_limits.errors.add(
+ :dashboard_limit, "must be less than or equal to the notification limit (#{notification_limit})"
+ )
end
# Overridden in EE
def parsed_params
params
end
+
+ def unlimited_value?(limit)
+ parsed_params[limit] == 0
+ end
end
end
end
diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb
index d0fde43138a..467a4ed2621 100644
--- a/app/services/auto_merge/base_service.rb
+++ b/app/services/auto_merge/base_service.rb
@@ -61,15 +61,19 @@ module AutoMerge
merge_request.can_be_merged_by?(current_user) &&
merge_request.open? &&
!merge_request.broken? &&
- (skip_draft_check(merge_request) || !merge_request.draft?) &&
- (skip_discussions_check(merge_request) || merge_request.mergeable_discussions_state?) &&
- (skip_blocked_check(merge_request) || !merge_request.merge_blocked_by_other_mrs?) &&
+ overrideable_available_for_checks(merge_request) &&
yield
end
end
private
+ def overrideable_available_for_checks(merge_request)
+ !merge_request.draft? &&
+ merge_request.mergeable_discussions_state? &&
+ !merge_request.merge_blocked_by_other_mrs?
+ end
+
# Overridden in child classes
def notify(merge_request)
end
@@ -109,20 +113,5 @@ module AutoMerge
def track_exception(error, merge_request)
Gitlab::ErrorTracking.track_exception(error, merge_request_id: merge_request&.id)
end
-
- # Will skip the draft check or not when checking if strategy is available
- def skip_draft_check(merge_request)
- false
- end
-
- # Will skip the blocked check or not when checking if strategy is available
- def skip_blocked_check(merge_request)
- false
- end
-
- # Will skip the discussions check or not when checking if strategy is available
- def skip_discussions_check(merge_request)
- false
- end
end
end
diff --git a/app/services/boards/lists/move_service.rb b/app/services/boards/lists/move_service.rb
index 4bb7b4dbc6d..4715f1276e3 100644
--- a/app/services/boards/lists/move_service.rb
+++ b/app/services/boards/lists/move_service.rb
@@ -22,8 +22,11 @@ module Boards
attr_reader :board, :old_position, :new_position
def valid_move?
- new_position.present? && new_position != old_position &&
- new_position >= 0 && new_position <= board.lists.movable.last.position
+ new_position.present? && new_position != old_position && new_position.between?(0, max_position)
+ end
+
+ def max_position
+ board.lists.movable.maximum(:position)
end
def reorder_intermediate_lists
diff --git a/app/services/bulk_imports/batched_relation_export_service.rb b/app/services/bulk_imports/batched_relation_export_service.rb
index 778510f2e35..c7c01c80fbf 100644
--- a/app/services/bulk_imports/batched_relation_export_service.rb
+++ b/app/services/bulk_imports/batched_relation_export_service.rb
@@ -26,8 +26,6 @@ module BulkImports
start_export!
export.batches.destroy_all # rubocop: disable Cop/DestroyAll
enqueue_batch_exports
- rescue StandardError => e
- fail_export!(e)
ensure
FinishBatchedRelationExportWorker.perform_async(export.id)
end
@@ -81,11 +79,5 @@ module BulkImports
def find_or_create_batch(batch_number)
export.batches.find_or_create_by!(batch_number: batch_number) # rubocop:disable CodeReuse/ActiveRecord
end
-
- def fail_export!(exception)
- Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name)
-
- export.update!(status_event: 'fail_op', error: exception.message.truncate(255))
- end
end
end
diff --git a/app/services/bulk_imports/file_download_service.rb b/app/services/bulk_imports/file_download_service.rb
index 1f2437d783d..cc2d544198b 100644
--- a/app/services/bulk_imports/file_download_service.rb
+++ b/app/services/bulk_imports/file_download_service.rb
@@ -83,7 +83,7 @@ module BulkImports
end
def raise_error(message)
- logger.warn(message: message, response_headers: response_headers, importer: 'gitlab_migration')
+ logger.warn(message: message, response_headers: response_headers)
raise ServiceError, message
end
@@ -112,7 +112,7 @@ module BulkImports
end
def logger
- @logger ||= Gitlab::Import::Logger.build
+ @logger ||= Logger.build
end
def validate_url
diff --git a/app/services/bulk_imports/process_service.rb b/app/services/bulk_imports/process_service.rb
index 14c5545cfd5..7a6a883f1a9 100644
--- a/app/services/bulk_imports/process_service.rb
+++ b/app/services/bulk_imports/process_service.rb
@@ -20,10 +20,6 @@ module BulkImports
process_bulk_import
re_enqueue
- rescue StandardError => e
- Gitlab::ErrorTracking.track_exception(e, bulk_import_id: bulk_import.id)
-
- bulk_import.fail_op
end
private
@@ -114,16 +110,15 @@ module BulkImports
bulk_import_id: entity.bulk_import_id,
bulk_import_entity_type: entity.source_type,
source_full_path: entity.source_full_path,
- pipeline_name: pipeline[:pipeline],
+ pipeline_class: pipeline[:pipeline],
minimum_source_version: minimum_version,
maximum_source_version: maximum_version,
- source_version: entity.source_version.to_s,
- importer: 'gitlab_migration'
+ source_version: entity.source_version.to_s
)
end
def logger
- @logger ||= Gitlab::Import::Logger.build
+ @logger ||= Logger.build
end
end
end
diff --git a/app/services/bulk_imports/relation_batch_export_service.rb b/app/services/bulk_imports/relation_batch_export_service.rb
index c7164d7c304..3f98d49aa1b 100644
--- a/app/services/bulk_imports/relation_batch_export_service.rb
+++ b/app/services/bulk_imports/relation_batch_export_service.rb
@@ -4,9 +4,9 @@ module BulkImports
class RelationBatchExportService
include Gitlab::ImportExport::CommandLineUtil
- def initialize(user_id, batch_id)
- @user = User.find(user_id)
- @batch = BulkImports::ExportBatch.find(batch_id)
+ def initialize(user, batch)
+ @user = user
+ @batch = batch
@config = FileTransfer.config_for(portable)
end
@@ -19,8 +19,6 @@ module BulkImports
upload_compressed_file
finish_batch!
- rescue StandardError => e
- fail_batch!(e)
ensure
FileUtils.remove_entry(export_path)
end
@@ -72,12 +70,6 @@ module BulkImports
batch.update!(status_event: 'finish', objects_count: exported_objects_count, error: nil)
end
- def fail_batch!(exception)
- Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name)
-
- batch.update!(status_event: 'fail_op', error: exception.message.truncate(255))
- end
-
def exported_filepath
File.join(export_path, exported_filename)
end
diff --git a/app/services/bulk_imports/relation_export_service.rb b/app/services/bulk_imports/relation_export_service.rb
index 91640496440..6db5ef3e9ec 100644
--- a/app/services/bulk_imports/relation_export_service.rb
+++ b/app/services/bulk_imports/relation_export_service.rb
@@ -42,8 +42,6 @@ module BulkImports
yield export
finish_export!(export)
- rescue StandardError => e
- fail_export!(export, e)
end
def export_service
@@ -87,12 +85,6 @@ module BulkImports
export.update!(status_event: 'finish', batched: false, error: nil)
end
- def fail_export!(export, exception)
- Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name)
-
- export&.update(status_event: 'fail_op', error: exception.class, batched: false)
- end
-
def exported_filepath
File.join(export_path, export_service.exported_filename)
end
diff --git a/app/services/ci/build_cancel_service.rb b/app/services/ci/build_cancel_service.rb
index a23418ed738..834d4febd10 100644
--- a/app/services/ci/build_cancel_service.rb
+++ b/app/services/ci/build_cancel_service.rb
@@ -21,7 +21,7 @@ module Ci
attr_reader :build, :user
def allowed?
- user.can?(:update_build, build)
+ user.can?(:cancel_build, build)
end
def forbidden
diff --git a/app/services/ci/cancel_pipeline_service.rb b/app/services/ci/cancel_pipeline_service.rb
index b5c8c00273e..38053b13921 100644
--- a/app/services/ci/cancel_pipeline_service.rb
+++ b/app/services/ci/cancel_pipeline_service.rb
@@ -8,27 +8,23 @@ module Ci
##
# @cascade_to_children - if true cancels all related child pipelines for parent child pipelines
- # @auto_canceled_by_pipeline_id - store the pipeline_id of the pipeline that triggered cancellation
+ # @auto_canceled_by_pipeline - store the pipeline_id of the pipeline that triggered cancellation
# @execute_async - if true cancel the children asyncronously
def initialize(
pipeline:,
current_user:,
cascade_to_children: true,
- auto_canceled_by_pipeline_id: nil,
+ auto_canceled_by_pipeline: nil,
execute_async: true)
@pipeline = pipeline
@current_user = current_user
@cascade_to_children = cascade_to_children
- @auto_canceled_by_pipeline_id = auto_canceled_by_pipeline_id
+ @auto_canceled_by_pipeline = auto_canceled_by_pipeline
@execute_async = execute_async
end
def execute
- unless can?(current_user, :update_pipeline, pipeline)
- return ServiceResponse.error(
- message: 'Insufficient permissions to cancel the pipeline',
- reason: :insufficient_permissions)
- end
+ return permission_error_response unless can?(current_user, :cancel_pipeline, pipeline)
force_execute
end
@@ -45,7 +41,7 @@ module Ci
log_pipeline_being_canceled
- pipeline.update_column(:auto_canceled_by_id, @auto_canceled_by_pipeline_id) if @auto_canceled_by_pipeline_id
+ pipeline.update_column(:auto_canceled_by_id, @auto_canceled_by_pipeline.id) if @auto_canceled_by_pipeline
cancel_jobs(pipeline.cancelable_statuses)
return ServiceResponse.success unless cascade_to_children?
@@ -65,7 +61,7 @@ module Ci
Gitlab::AppJsonLogger.info(
event: 'pipeline_cancel_running',
pipeline_id: pipeline.id,
- auto_canceled_by_pipeline_id: @auto_canceled_by_pipeline_id,
+ auto_canceled_by_pipeline_id: @auto_canceled_by_pipeline&.id,
cascade_to_children: cascade_to_children?,
execute_async: execute_async?,
**Gitlab::ApplicationContext.current
@@ -89,21 +85,34 @@ module Ci
relation = CommitStatus.id_in(batch)
Preloaders::CommitStatusPreloader.new(relation).execute(preloaded_relations)
- relation.each do |job|
- job.auto_canceled_by_id = @auto_canceled_by_pipeline_id if @auto_canceled_by_pipeline_id
- job.cancel
- end
+ relation.each { |job| cancel_job(job) }
end
end
end
+ def cancel_job(job)
+ if @auto_canceled_by_pipeline
+ job.auto_canceled_by_id = @auto_canceled_by_pipeline.id
+ job.auto_canceled_by_partition_id = @auto_canceled_by_pipeline.partition_id
+ end
+
+ job.cancel
+ end
+
+ def permission_error_response
+ ServiceResponse.error(
+ message: 'Insufficient permissions to cancel the pipeline',
+ reason: :insufficient_permissions
+ )
+ end
+
# For parent child-pipelines only (not multi-project)
def cancel_children
pipeline.all_child_pipelines.each do |child_pipeline|
if execute_async?
::Ci::CancelPipelineWorker.perform_async(
child_pipeline.id,
- @auto_canceled_by_pipeline_id
+ @auto_canceled_by_pipeline&.id
)
else
# cascade_to_children is false because we iterate through children
@@ -113,7 +122,7 @@ module Ci
current_user: nil,
cascade_to_children: false,
execute_async: execute_async?,
- auto_canceled_by_pipeline_id: @auto_canceled_by_pipeline_id
+ auto_canceled_by_pipeline: @auto_canceled_by_pipeline
).force_execute
end
end
diff --git a/app/services/ci/catalog/resources/create_service.rb b/app/services/ci/catalog/resources/create_service.rb
new file mode 100644
index 00000000000..89367c70e82
--- /dev/null
+++ b/app/services/ci/catalog/resources/create_service.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Ci
+ module Catalog
+ module Resources
+ class CreateService
+ include Gitlab::Allowable
+
+ attr_reader :project, :current_user
+
+ def initialize(project, user)
+ @current_user = user
+ @project = project
+ end
+
+ def execute
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :add_catalog_resource, project)
+
+ catalog_resource = Ci::Catalog::Resource.new(project: project)
+
+ if catalog_resource.valid?
+ catalog_resource.save!
+ ServiceResponse.success(payload: catalog_resource)
+ else
+ ServiceResponse.error(message: catalog_resource.errors.full_messages.join(', '))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/ci/catalog/resources/release_service.rb b/app/services/ci/catalog/resources/release_service.rb
new file mode 100644
index 00000000000..ad77bff3ef9
--- /dev/null
+++ b/app/services/ci/catalog/resources/release_service.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Ci
+ module Catalog
+ module Resources
+ class ReleaseService
+ def initialize(release)
+ @release = release
+ @project = release.project
+ @errors = []
+ end
+
+ def execute
+ validate_catalog_resource
+ create_version
+
+ if errors.empty?
+ ServiceResponse.success
+ else
+ ServiceResponse.error(message: errors.join(', '))
+ end
+ end
+
+ private
+
+ attr_reader :project, :errors, :release
+
+ def validate_catalog_resource
+ response = Ci::Catalog::Resources::ValidateService.new(project, release.sha).execute
+ return if response.success?
+
+ errors << response.message
+ end
+
+ def create_version
+ return if errors.present?
+
+ response = Ci::Catalog::Resources::Versions::CreateService.new(release).execute
+ return if response.success?
+
+ errors << response.message
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/ci/catalog/resources/validate_service.rb b/app/services/ci/catalog/resources/validate_service.rb
index 9e8986ba6fc..0e842fb7405 100644
--- a/app/services/ci/catalog/resources/validate_service.rb
+++ b/app/services/ci/catalog/resources/validate_service.rb
@@ -4,7 +4,7 @@ module Ci
module Catalog
module Resources
class ValidateService
- attr_reader :project
+ MINIMUM_AMOUNT_OF_COMPONENTS = 1
def initialize(project, ref)
@project = project
@@ -13,30 +13,38 @@ module Ci
end
def execute
- check_project_readme
- check_project_description
+ verify_presence_project_readme
+ verify_presence_project_description
+ scan_directory_for_components
if errors.empty?
ServiceResponse.success
else
- ServiceResponse.error(message: errors.join(' , '))
+ ServiceResponse.error(message: errors.join(', '))
end
end
private
- attr_reader :ref, :errors
+ attr_reader :project, :ref, :errors
- def check_project_description
+ def verify_presence_project_readme
+ return if project_has_readme?
+
+ errors << 'Project must have a README'
+ end
+
+ def verify_presence_project_description
return if project.description.present?
errors << 'Project must have a description'
end
- def check_project_readme
- return if project_has_readme?
+ def scan_directory_for_components
+ return if Ci::Catalog::ComponentsProject.new(project).fetch_component_paths(ref,
+ limit: MINIMUM_AMOUNT_OF_COMPONENTS).any?
- errors << 'Project must have a README'
+ errors << 'Project must contain components. Ensure you are using the correct directory structure'
end
def project_has_readme?
diff --git a/app/services/ci/catalog/resources/versions/create_service.rb b/app/services/ci/catalog/resources/versions/create_service.rb
new file mode 100644
index 00000000000..863bad43271
--- /dev/null
+++ b/app/services/ci/catalog/resources/versions/create_service.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+module Ci
+ module Catalog
+ module Resources
+ module Versions
+ class CreateService
+ def initialize(release)
+ @project = release.project
+ @release = release
+ @errors = []
+ @version = nil
+ @components_project = Ci::Catalog::ComponentsProject.new(project)
+ end
+
+ def execute
+ build_catalog_resource_version
+ fetch_and_build_components if Feature.enabled?(:ci_catalog_create_metadata, project)
+ publish_catalog_resource!
+
+ if errors.empty?
+ ServiceResponse.success
+ else
+ ServiceResponse.error(message: errors.flatten.first(10).join(', '))
+ end
+ end
+
+ private
+
+ attr_reader :project, :errors, :release, :components_project
+
+ def build_catalog_resource_version
+ return error('Project is not a catalog resource') unless project.catalog_resource
+
+ @version = Ci::Catalog::Resources::Version.new(
+ release: release,
+ catalog_resource: project.catalog_resource,
+ project: project
+ )
+ end
+
+ def fetch_and_build_components
+ return if errors.present?
+
+ max_components = Ci::Catalog::ComponentsProject::COMPONENTS_LIMIT
+ component_paths = components_project.fetch_component_paths(release.sha, limit: max_components + 1)
+
+ if component_paths.size > max_components
+ return error("Release cannot contain more than #{max_components} components")
+ end
+
+ build_components(component_paths)
+ end
+
+ def build_components(component_paths)
+ paths_with_oids = component_paths.map { |path| [release.sha, path] }
+ blobs = project.repository.blobs_at(paths_with_oids)
+
+ blobs.each do |blob|
+ metadata = extract_metadata(blob)
+ build_catalog_resource_component(metadata)
+ end
+ rescue ::Gitlab::Config::Loader::FormatError => e
+ error(e)
+ end
+
+ def extract_metadata(blob)
+ {
+ name: components_project.extract_component_name(blob.path),
+ inputs: components_project.extract_inputs(blob.data),
+ path: blob.path
+ }
+ end
+
+ def build_catalog_resource_component(metadata)
+ return if errors.present?
+
+ component = @version.components.build(
+ name: metadata[:name],
+ project: @version.project,
+ inputs: metadata[:inputs],
+ catalog_resource: @version.catalog_resource,
+ path: metadata[:path],
+ created_at: Time.current
+ )
+
+ return if component.valid?
+
+ error("Build component error: #{component.errors.full_messages.join(', ')}")
+ end
+
+ def publish_catalog_resource!
+ return if errors.present?
+
+ ::Ci::Catalog::Resources::Version.transaction do
+ BulkInsertableAssociations.with_bulk_insert do
+ @version.save!
+ end
+
+ project.catalog_resource.publish!
+ end
+ end
+
+ def error(message)
+ errors << message
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/ci/destroy_pipeline_service.rb b/app/services/ci/destroy_pipeline_service.rb
index a9d2e17657e..7adf573687a 100644
--- a/app/services/ci/destroy_pipeline_service.rb
+++ b/app/services/ci/destroy_pipeline_service.rb
@@ -28,3 +28,5 @@ module Ci
end
end
end
+
+Ci::DestroyPipelineService.prepend_mod
diff --git a/app/services/ci/enqueue_job_service.rb b/app/services/ci/enqueue_job_service.rb
index 9e3bea3fd28..db616473336 100644
--- a/app/services/ci/enqueue_job_service.rb
+++ b/app/services/ci/enqueue_job_service.rb
@@ -11,11 +11,14 @@ module Ci
end
def execute(&transition)
- job.user = current_user
- job.job_variables_attributes = variables if variables
-
transition ||= ->(job) { job.enqueue! }
- Gitlab::OptimisticLocking.retry_lock(job, name: 'ci_enqueue_job', &transition)
+
+ Gitlab::OptimisticLocking.retry_lock(job, name: 'ci_enqueue_job') do |job|
+ job.user = current_user
+ job.job_variables_attributes = variables if variables
+
+ transition.call(job)
+ end
ResetSkippedJobsService.new(job.project, current_user).execute(job)
diff --git a/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb b/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb
index c18984953a1..224b2d96205 100644
--- a/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb
+++ b/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb
@@ -88,7 +88,7 @@ module Ci
::Ci::CancelPipelineService.new(
pipeline: cancelable_pipeline,
current_user: nil,
- auto_canceled_by_pipeline_id: pipeline.id,
+ auto_canceled_by_pipeline: pipeline,
cascade_to_children: false
).force_execute
end
diff --git a/app/services/ci/pipelines/update_metadata_service.rb b/app/services/ci/pipelines/update_metadata_service.rb
new file mode 100644
index 00000000000..2f2d648c13d
--- /dev/null
+++ b/app/services/ci/pipelines/update_metadata_service.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Ci
+ module Pipelines
+ class UpdateMetadataService
+ def initialize(pipeline, params)
+ @pipeline = pipeline
+ @params = params
+ end
+
+ def execute
+ metadata = pipeline.pipeline_metadata
+
+ metadata = pipeline.build_pipeline_metadata(project: pipeline.project) if metadata.nil?
+
+ params[:name] = params[:name].strip if params.key?(:name)
+
+ if metadata.update(params)
+ ServiceResponse.success(message: 'Pipeline metadata was updated', payload: pipeline)
+ else
+ ServiceResponse.error(message: 'Failed to update pipeline', payload: metadata.errors.full_messages,
+ reason: :bad_request)
+ end
+ end
+
+ private
+
+ attr_reader :pipeline, :params
+ end
+ end
+end
diff --git a/app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb b/app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb
index 319186ce030..4e9e9a2effe 100644
--- a/app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb
+++ b/app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb
@@ -7,13 +7,12 @@ module Ci
BATCH_SIZE = 50
ENQUEUE_INTERVAL_SECONDS = 0.1
+ EXCLUDED_IDS_LIMIT = 1000
def execute(ci_ref, before_pipeline: nil)
- pipelines_scope = ci_ref.pipelines.artifacts_locked
- pipelines_scope = pipelines_scope.before_pipeline(before_pipeline) if before_pipeline
total_new_entries = 0
- pipelines_scope.each_batch(of: BATCH_SIZE) do |batch|
+ pipelines_scope(ci_ref, before_pipeline).each_batch(of: BATCH_SIZE) do |batch|
pipeline_ids = batch.pluck(:id) # rubocop: disable CodeReuse/ActiveRecord
total_added = Ci::UnlockPipelineRequest.enqueue(pipeline_ids)
total_new_entries += total_added
@@ -27,6 +26,34 @@ module Ci
total_new_entries: total_new_entries
)
end
+
+ private
+
+ def pipelines_scope(ci_ref, before_pipeline)
+ scope = ci_ref.pipelines.artifacts_locked
+
+ if before_pipeline
+ # We use `same_family_pipeline_ids.map(&:id)` to force run the query and
+ # specifically pass the array of IDs to the NOT IN condition. If not, we would
+ # end up running the subquery for same_family_pipeline_ids on each batch instead.
+ excluded_ids = before_pipeline.same_family_pipeline_ids.map(&:id)
+ scope = scope.created_before_id(before_pipeline.id)
+
+ # When unlocking previous pipelines, we still want to keep the
+ # last successful CI source pipeline locked.
+ # If before_pipeline is not provided, like in the case of deleting a ref,
+ # we want to unlock all pipelines instead.
+ ci_ref.last_successful_ci_source_pipeline.try do |pipeline|
+ excluded_ids.concat(pipeline.same_family_pipeline_ids.map(&:id))
+ end
+
+ # We add a limit to the excluded IDs just to be safe and avoid any
+ # arity issues with the NOT IN query.
+ scope = scope.where.not(id: excluded_ids.take(EXCLUDED_IDS_LIMIT)) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ scope
+ end
end
end
end
diff --git a/app/services/ci/retry_job_service.rb b/app/services/ci/retry_job_service.rb
index d7c3e9e7f64..a8ea5ac6df0 100644
--- a/app/services/ci/retry_job_service.rb
+++ b/app/services/ci/retry_job_service.rb
@@ -39,10 +39,6 @@ module Ci
::Ci::CopyCrossDatabaseAssociationsService.new.execute(job, new_job)
- if Feature.disabled?(:create_deployment_only_for_processable_jobs, project)
- ::Deployments::CreateForJobService.new.execute(new_job)
- end
-
::MergeRequests::AddTodoWhenBuildFailsService
.new(project: project)
.close(new_job)
diff --git a/app/services/concerns/update_repository_storage_methods.rb b/app/services/concerns/update_repository_storage_methods.rb
index 50963cc58b2..aff36d6943e 100644
--- a/app/services/concerns/update_repository_storage_methods.rb
+++ b/app/services/concerns/update_repository_storage_methods.rb
@@ -32,9 +32,9 @@ module UpdateRepositoryStorageMethods
end
end
- repository_storage_move.transaction do
- repository_storage_move.finish_replication!
+ repository_storage_move.finish_replication!
+ repository_storage_move.transaction do
track_repository(destination_storage_name)
end
diff --git a/app/services/container_registry/protection/create_rule_service.rb b/app/services/container_registry/protection/create_rule_service.rb
new file mode 100644
index 00000000000..34ec6f42b19
--- /dev/null
+++ b/app/services/container_registry/protection/create_rule_service.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module ContainerRegistry
+ module Protection
+ class CreateRuleService < BaseService
+ ALLOWED_ATTRIBUTES = %i[
+ container_path_pattern
+ push_protected_up_to_access_level
+ delete_protected_up_to_access_level
+ ].freeze
+
+ def execute
+ unless can?(current_user, :admin_container_image, project)
+ error_message = _('Unauthorized to create a container registry protection rule')
+ return service_response_error(message: error_message)
+ end
+
+ container_registry_protection_rule =
+ project.container_registry_protection_rules.create(params.slice(*ALLOWED_ATTRIBUTES))
+
+ unless container_registry_protection_rule.persisted?
+ return service_response_error(message: container_registry_protection_rule.errors.full_messages.to_sentence)
+ end
+
+ ServiceResponse.success(payload: { container_registry_protection_rule: container_registry_protection_rule })
+ rescue StandardError => e
+ service_response_error(message: e.message)
+ end
+
+ private
+
+ def service_response_error(message:)
+ ServiceResponse.error(
+ message: message,
+ payload: { container_registry_protection_rule: nil }
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/draft_notes/publish_service.rb b/app/services/draft_notes/publish_service.rb
index a7a2ad63c1c..5ba7f829c8e 100644
--- a/app/services/draft_notes/publish_service.rb
+++ b/app/services/draft_notes/publish_service.rb
@@ -81,7 +81,9 @@ module DraftNotes
end
def set_reviewed
- ::MergeRequests::MarkReviewerReviewedService.new(project: project, current_user: current_user).execute(merge_request)
+ return if Feature.enabled?(:mr_request_changes, current_user)
+
+ ::MergeRequests::UpdateReviewerStateService.new(project: project, current_user: current_user).execute(merge_request, "reviewed")
end
def capture_diff_note_positions(notes)
diff --git a/app/services/environments/auto_recover_service.rb b/app/services/environments/auto_recover_service.rb
new file mode 100644
index 00000000000..d52f90bbe50
--- /dev/null
+++ b/app/services/environments/auto_recover_service.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Environments
+ class AutoRecoverService
+ include ::Gitlab::ExclusiveLeaseHelpers
+ include ::Gitlab::LoopHelpers
+
+ BATCH_SIZE = 100
+ LOOP_TIMEOUT = 45.minutes
+ LOOP_LIMIT = 1000
+ EXCLUSIVE_LOCK_KEY = 'environments:auto_recover:lock'
+ LOCK_TIMEOUT = 50.minutes
+
+ ##
+ # Recover environments that are stuck stopping on a GitLab instance
+ #
+ # This auto stop process cannot run for more than 45 minutes. This is for
+ # preventing multiple `AutoStopCronWorker` CRON jobs run concurrently,
+ # which is scheduled at every hour.
+ def execute
+ in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
+ loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
+ recover_in_batch
+ end
+ end
+ end
+
+ private
+
+ def recover_in_batch
+ environments = Environment.preload_project.select(:id, :project_id).long_stopping.limit(BATCH_SIZE)
+
+ return false if environments.empty?
+
+ Environments::AutoRecoverWorker.bulk_perform_async_with_contexts(
+ environments,
+ arguments_proc: ->(environment) { environment.id },
+ context_proc: ->(environment) { { project: environment.project } }
+ )
+
+ true
+ end
+ end
+end
diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb
index a2eb4f1f396..c6214311692 100644
--- a/app/services/git/branch_hooks_service.rb
+++ b/app/services/git/branch_hooks_service.rb
@@ -110,7 +110,6 @@ module Git
end
def track_ci_config_change_event
- return unless ::ServicePing::ServicePingSettings.enabled?
return unless default_branch?
commits_changing_ci_config.each do |commit|
diff --git a/app/services/google_cloud/generate_pipeline_service.rb b/app/services/google_cloud/generate_pipeline_service.rb
index 30c358687aa..97d008db76b 100644
--- a/app/services/google_cloud/generate_pipeline_service.rb
+++ b/app/services/google_cloud/generate_pipeline_service.rb
@@ -67,7 +67,7 @@ module GoogleCloud
end
def default_branch_gitlab_ci_yml
- @default_branch_gitlab_ci_yml ||= project.repository.gitlab_ci_yml_for(project.default_branch)
+ @default_branch_gitlab_ci_yml ||= project.ci_config_for(project.default_branch)
end
def pipeline_content(include_path)
diff --git a/app/services/groups/ssh_certificates/create_service.rb b/app/services/groups/ssh_certificates/create_service.rb
index 6890901c306..e4570078395 100644
--- a/app/services/groups/ssh_certificates/create_service.rb
+++ b/app/services/groups/ssh_certificates/create_service.rb
@@ -3,9 +3,10 @@
module Groups
module SshCertificates
class CreateService
- def initialize(group, params)
+ def initialize(group, params, current_user)
@group = group
@params = params
+ @current_user = current_user
end
def execute
@@ -41,7 +42,7 @@ module Groups
private
- attr_reader :group, :params
+ attr_reader :group, :params, :current_user
def generate_fingerprint(key)
Gitlab::SSHPublicKey.new(key).fingerprint_sha256&.delete_prefix('SHA256:')
@@ -49,3 +50,5 @@ module Groups
end
end
end
+
+Groups::SshCertificates::CreateService.prepend_mod_with('Groups::SshCertificates::CreateService')
diff --git a/app/services/groups/ssh_certificates/destroy_service.rb b/app/services/groups/ssh_certificates/destroy_service.rb
index 7a450d5bee6..5f7bba12878 100644
--- a/app/services/groups/ssh_certificates/destroy_service.rb
+++ b/app/services/groups/ssh_certificates/destroy_service.rb
@@ -3,16 +3,17 @@
module Groups
module SshCertificates
class DestroyService
- def initialize(group, params)
+ def initialize(group, params, current_user)
@group = group
@params = params
+ @current_user = current_user
end
def execute
ssh_certificate = group.ssh_certificates.find(params[:ssh_certificates_id])
ssh_certificate.destroy!
- ServiceResponse.success
+ ServiceResponse.success(payload: { ssh_certificate: ssh_certificate })
rescue ActiveRecord::RecordNotFound
ServiceResponse.error(
@@ -29,7 +30,9 @@ module Groups
private
- attr_reader :group, :params
+ attr_reader :group, :params, :current_user
end
end
end
+
+Groups::SshCertificates::DestroyService.prepend_mod_with('Groups::SshCertificates::DestroyService')
diff --git a/app/services/import/validate_remote_git_endpoint_service.rb b/app/services/import/validate_remote_git_endpoint_service.rb
index a994072c4aa..8297757997f 100644
--- a/app/services/import/validate_remote_git_endpoint_service.rb
+++ b/app/services/import/validate_remote_git_endpoint_service.rb
@@ -13,6 +13,8 @@ module Import
GIT_PROTOCOL_PKT_LEN = 4
GIT_MINIMUM_RESPONSE_LENGTH = GIT_PROTOCOL_PKT_LEN + GIT_EXPECTED_FIRST_PACKET_LINE.length
EXPECTED_CONTENT_TYPE = "application/x-#{GIT_SERVICE_NAME}-advertisement"
+ INVALID_BODY_MESSAGE = 'Not a git repository: Invalid response body'
+ INVALID_CONTENT_TYPE_MESSAGE = 'Not a git repository: Invalid content-type'
def initialize(params)
@params = params
@@ -30,32 +32,35 @@ module Import
uri.fragment = nil
url = Gitlab::Utils.append_path(uri.to_s, "/info/refs?service=#{GIT_SERVICE_NAME}")
- response_body = ''
- result = nil
- Gitlab::HTTP.try_get(url, stream_body: true, follow_redirects: false, basic_auth: auth) do |fragment|
- response_body += fragment
- next if response_body.length < GIT_MINIMUM_RESPONSE_LENGTH
-
- result = if status_code_is_valid(fragment) && content_type_is_valid(fragment) && response_body_is_valid(response_body)
- :success
- else
- :error
- end
-
- # We are interested only in the first chunks of the response
- # So we're using stream_body: true and breaking when receive enough body
- break
- end
+ response, response_body = http_get_and_extract_first_chunks(url)
- if result == :success
- ServiceResponse.success
- else
- ServiceResponse.error(message: "#{uri} is not a valid HTTP Git repository")
- end
+ validate(uri, response, response_body)
+ rescue *Gitlab::HTTP::HTTP_ERRORS => err
+ error_result("HTTP #{err.class.name.underscore} error: #{err.message}")
+ rescue StandardError => err
+ ServiceResponse.error(
+ message: "Internal #{err.class.name.underscore} error: #{err.message}",
+ reason: 500
+ )
end
private
+ def http_get_and_extract_first_chunks(url)
+ # We are interested only in the first chunks of the response
+ # So we're using stream_body: true and breaking when receive enough body
+ response = nil
+ response_body = ''
+
+ Gitlab::HTTP.get(url, stream_body: true, follow_redirects: false, basic_auth: auth) do |response_chunk|
+ response = response_chunk
+ response_body += response_chunk
+ break if GIT_MINIMUM_RESPONSE_LENGTH <= response_body.length
+ end
+
+ [response, response_body]
+ end
+
def auth
unless @params[:user].to_s.blank?
{
@@ -65,15 +70,37 @@ module Import
end
end
- def status_code_is_valid(fragment)
- fragment.http_response.code == '200'
+ def validate(uri, response, response_body)
+ return status_code_error(uri, response) unless status_code_is_valid?(response)
+ return error_result(INVALID_CONTENT_TYPE_MESSAGE) unless content_type_is_valid?(response)
+ return error_result(INVALID_BODY_MESSAGE) unless response_body_is_valid?(response_body)
+
+ ServiceResponse.success
+ end
+
+ def status_code_error(uri, response)
+ http_code = response.http_response.code.to_i
+ message = response.http_response.message || Rack::Utils::HTTP_STATUS_CODES[http_code]
+
+ error_result(
+ "#{uri} endpoint error: #{http_code}#{message.presence&.prepend(' ')}",
+ http_code
+ )
+ end
+
+ def error_result(message, reason = nil)
+ ServiceResponse.error(message: message, reason: reason)
+ end
+
+ def status_code_is_valid?(response)
+ response.http_response.code == '200'
end
- def content_type_is_valid(fragment)
- fragment.http_response['content-type'] == EXPECTED_CONTENT_TYPE
+ def content_type_is_valid?(response)
+ response.http_response['content-type'] == EXPECTED_CONTENT_TYPE
end
- def response_body_is_valid(response_body)
+ def response_body_is_valid?(response_body)
response_body.match?(GIT_BODY_MESSAGE_REGEXP)
end
end
diff --git a/app/services/jira_connect_subscriptions/create_service.rb b/app/services/jira_connect_subscriptions/create_service.rb
index d5ab3800dcf..f537da5c091 100644
--- a/app/services/jira_connect_subscriptions/create_service.rb
+++ b/app/services/jira_connect_subscriptions/create_service.rb
@@ -11,7 +11,7 @@ module JiraConnectSubscriptions
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. ' \
+ return error(s_('JiraConnect|The Jira user is not a site or organization administrator. ' \
'Check the permissions in Jira and try again.'), 403)
end
@@ -25,7 +25,7 @@ module JiraConnectSubscriptions
private
def can_administer_jira?
- params[:jira_user]&.site_admin?
+ params[:jira_user]&.jira_admin?
end
def create_subscription
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index 9cedc7ee3a5..b453098e27a 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -21,15 +21,16 @@ 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
+ if adding_at_least_one_owner && cannot_assign_owner_responsibilities_to_member_in_project?
+ raise Gitlab::Access::AccessDeniedError
+ end
validate_invite_source!
validate_invitable!
add_members
+ after_add_hooks
+
enqueue_onboarding_progress_action
publish_event!
@@ -73,8 +74,8 @@ module Members
return unless user_limit && invites.size > user_limit
- raise TooManyInvitesError,
- format(s_("AddMember|Too many users specified (limit is %{user_limit})"), user_limit: user_limit)
+ message = format(s_("AddMember|Too many users specified (limit is %{user_limit})"), user_limit: user_limit)
+ raise TooManyInvitesError, message
end
def blank_invites_message
@@ -82,16 +83,24 @@ module Members
end
def add_members
- @members = source.add_members(
- invites,
- params[:access_level],
- expires_at: params[:expires_at],
- current_user: current_user
+ @members = creator_service.add_members(
+ source, invites, params[:access_level], **create_params
)
members.each { |member| process_result(member) }
end
+ def creator_service
+ "Members::#{source.class.to_s.pluralize}::CreatorService".constantize
+ end
+
+ def create_params
+ {
+ expires_at: params[:expires_at],
+ current_user: current_user
+ }
+ end
+
def process_result(member)
existing_errors = member.errors.full_messages
@@ -116,6 +125,10 @@ module Members
existing_errors.concat(member.errors.full_messages).uniq
end
+ def after_add_hooks
+ # overridden in subclasses/ee
+ end
+
def after_execute(member:)
super
@@ -123,11 +136,13 @@ module Members
end
def track_invite_source(member)
- Gitlab::Tracking.event(self.class.name,
- 'create_member',
- label: invite_source,
- property: tracking_property(member),
- user: current_user)
+ Gitlab::Tracking.event(
+ self.class.name,
+ 'create_member',
+ label: invite_source,
+ property: tracking_property(member),
+ user: current_user
+ )
end
def invite_source
@@ -148,11 +163,15 @@ module Members
end
def enqueue_onboarding_progress_action
- return unless member_created_namespace_id
+ return unless at_least_one_member_created?
Onboarding::UserAddedWorker.perform_async(member_created_namespace_id)
end
+ def at_least_one_member_created?
+ member_created_namespace_id.present?
+ end
+
def result
if errors.any?
error(formatted_errors)
@@ -166,7 +185,7 @@ module Members
end
def publish_event!
- return unless member_created_namespace_id
+ return unless at_least_one_member_created?
Gitlab::EventStore.publish(
Members::MembersAddedEvent.new(data: {
diff --git a/app/services/merge_requests/mark_reviewer_reviewed_service.rb b/app/services/merge_requests/mark_reviewer_reviewed_service.rb
deleted file mode 100644
index 96747eabcf6..00000000000
--- a/app/services/merge_requests/mark_reviewer_reviewed_service.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-module MergeRequests
- class MarkReviewerReviewedService < MergeRequests::BaseService
- def execute(merge_request)
- return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request)
-
- reviewer = merge_request.find_reviewer(current_user)
-
- if reviewer
- return error("Failed to update reviewer") unless reviewer.update(state: :reviewed)
-
- trigger_merge_request_reviewers_updated(merge_request)
-
- success
- else
- error("Reviewer not found")
- end
- end
- end
-end
diff --git a/app/services/merge_requests/mergeability/check_base_service.rb b/app/services/merge_requests/mergeability/check_base_service.rb
index e1c4d751296..b8a275b6c32 100644
--- a/app/services/merge_requests/mergeability/check_base_service.rb
+++ b/app/services/merge_requests/mergeability/check_base_service.rb
@@ -42,6 +42,11 @@ module MergeRequests
.failed(payload: default_payload(args))
end
+ def inactive(**args)
+ Gitlab::MergeRequests::Mergeability::CheckResult
+ .inactive(payload: default_payload(args))
+ end
+
def default_payload(args)
args.merge(identifier: self.class.identifier)
end
diff --git a/app/services/merge_requests/mergeability/check_ci_status_service.rb b/app/services/merge_requests/mergeability/check_ci_status_service.rb
index f7fa3259d97..b4e60e964b7 100644
--- a/app/services/merge_requests/mergeability/check_ci_status_service.rb
+++ b/app/services/merge_requests/mergeability/check_ci_status_service.rb
@@ -7,6 +7,8 @@ module MergeRequests
end
def execute
+ return inactive unless merge_request.only_allow_merge_if_pipeline_succeeds?
+
if merge_request.mergeable_ci_state?
success
else
diff --git a/app/services/merge_requests/mergeability/check_discussions_status_service.rb b/app/services/merge_requests/mergeability/check_discussions_status_service.rb
index 34db5f8a944..f9cff5d1e5f 100644
--- a/app/services/merge_requests/mergeability/check_discussions_status_service.rb
+++ b/app/services/merge_requests/mergeability/check_discussions_status_service.rb
@@ -7,6 +7,8 @@ module MergeRequests
end
def execute
+ return inactive unless merge_request.only_allow_merge_if_all_discussions_are_resolved?
+
if merge_request.mergeable_discussions_state?
success
else
diff --git a/app/services/merge_requests/mergeability/check_rebase_status_service.rb b/app/services/merge_requests/mergeability/check_rebase_status_service.rb
index 2163fec8bd6..02cd0587be0 100644
--- a/app/services/merge_requests/mergeability/check_rebase_status_service.rb
+++ b/app/services/merge_requests/mergeability/check_rebase_status_service.rb
@@ -8,6 +8,8 @@ module MergeRequests
end
def execute
+ return inactive unless merge_request.project.ff_merge_must_be_possible?
+
if merge_request.should_be_rebased?
failure(reason: failure_reason)
else
diff --git a/app/services/merge_requests/mergeability/detailed_merge_status_service.rb b/app/services/merge_requests/mergeability/detailed_merge_status_service.rb
index 86c8122604c..92f0fb0429c 100644
--- a/app/services/merge_requests/mergeability/detailed_merge_status_service.rb
+++ b/app/services/merge_requests/mergeability/detailed_merge_status_service.rb
@@ -18,10 +18,10 @@ module MergeRequests
# If everything else is mergeable, but CI is not, the frontend expects two potential states to be returned
# See discussion: gitlab.com/gitlab-org/gitlab/-/merge_requests/96778#note_1093063523
- if check_ci_results.success?
- :mergeable
- else
+ if check_ci_results.failed?
ci_check_failure_reason
+ else
+ :mergeable
end
else
check_results.payload[:failure_reason]
diff --git a/app/services/merge_requests/mergeability/run_checks_service.rb b/app/services/merge_requests/mergeability/run_checks_service.rb
index 5150c03d0a3..92f3e5e951a 100644
--- a/app/services/merge_requests/mergeability/run_checks_service.rb
+++ b/app/services/merge_requests/mergeability/run_checks_service.rb
@@ -65,7 +65,7 @@ module MergeRequests
end
def all_results_success?
- results.all?(&:success?)
+ results.none?(&:failed?)
end
def failure_reason
diff --git a/app/services/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb
index 1890addf692..3f972e747b9 100644
--- a/app/services/merge_requests/push_options_handler_service.rb
+++ b/app/services/merge_requests/push_options_handler_service.rb
@@ -9,7 +9,12 @@ module MergeRequests
def initialize(project:, current_user:, changes:, push_options:, params: {})
super(project: project, current_user: current_user, params: params)
- @target_project = @project.default_merge_request_target
+ @target_project = if push_options[:target_project]
+ Project.find_by_full_path(push_options[:target_project])
+ else
+ @project.default_merge_request_target
+ end
+
@changes = Gitlab::ChangesList.new(changes)
@push_options = push_options
@errors = []
@@ -63,6 +68,10 @@ module MergeRequests
return
end
+ unless project == target_project || project.in_fork_network_of?(target_project)
+ errors << "Projects #{project.full_path} and #{target_project.full_path} are not in the same network"
+ end
+
unless target_project.merge_requests_enabled?
errors << "Merge requests are not enabled for project #{target_project.full_path}"
end
diff --git a/app/services/merge_requests/update_reviewer_state_service.rb b/app/services/merge_requests/update_reviewer_state_service.rb
new file mode 100644
index 00000000000..e2252f55fd3
--- /dev/null
+++ b/app/services/merge_requests/update_reviewer_state_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class UpdateReviewerStateService < MergeRequests::BaseService
+ def execute(merge_request, state)
+ return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request)
+
+ reviewer = merge_request.find_reviewer(current_user)
+
+ if reviewer
+ return error("Failed to update reviewer") unless reviewer.update(state: state)
+
+ trigger_merge_request_reviewers_updated(merge_request)
+
+ return success if state != 'requested_changes'
+
+ if merge_request.approved_by?(current_user) && !remove_approval(merge_request)
+ return error("Failed to remove approval")
+ end
+
+ success
+ else
+ error("Reviewer not found")
+ end
+ end
+
+ private
+
+ def remove_approval(merge_request)
+ MergeRequests::RemoveApprovalService.new(project: project, current_user: current_user)
+ .execute(merge_request)
+ end
+ end
+end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 37a829e3014..fb6544a910a 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -168,6 +168,7 @@ module MergeRequests
merge_request.target_branch
)
+ delete_approvals_on_target_branch_change(merge_request)
refresh_pipelines_on_merge_requests(merge_request, allow_duplicate: true)
abort_auto_merge(merge_request, 'target branch was changed')
@@ -321,6 +322,10 @@ module MergeRequests
def trigger_merge_request_status_updated(merge_request)
GraphqlTriggers.merge_request_merge_status_updated(merge_request)
end
+
+ def delete_approvals_on_target_branch_change(_merge_request)
+ # Overridden in EE. No-op since we only want to delete approvals in EE.
+ end
end
end
diff --git a/app/services/ml/create_candidate_service.rb b/app/services/ml/create_candidate_service.rb
new file mode 100644
index 00000000000..53913c3fb19
--- /dev/null
+++ b/app/services/ml/create_candidate_service.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Ml
+ class CreateCandidateService
+ def initialize(experiment, params = {})
+ @experiment = experiment
+ @name = params[:name]
+ @user = params[:user]
+ @start_time = params[:start_time]
+ @model_version = params[:model_version]
+ end
+
+ def execute
+ Ml::Candidate.create!(
+ experiment: experiment,
+ project: experiment.project,
+ name: candidate_name,
+ start_time: start_time || 0,
+ user: user,
+ model_version: model_version
+ )
+ end
+
+ private
+
+ def candidate_name
+ name.presence || random_candidate_name
+ end
+
+ def random_candidate_name
+ parts = Array.new(3).map { FFaker::Animal.common_name.downcase.delete(' ') } << rand(10000)
+ parts.join('-').truncate(255)
+ end
+
+ attr_reader :name, :user, :experiment, :start_time, :model_version
+ end
+end
diff --git a/app/services/ml/create_model_service.rb b/app/services/ml/create_model_service.rb
new file mode 100644
index 00000000000..5c179d8edf7
--- /dev/null
+++ b/app/services/ml/create_model_service.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Ml
+ class CreateModelService
+ def initialize(project, name, user = nil, description = nil, metadata = [])
+ @project = project
+ @name = name
+ @description = description
+ @metadata = metadata
+ @user = user
+ end
+
+ def execute
+ ApplicationRecord.transaction do
+ model = Ml::Model.create!(
+ project: @project,
+ name: @name,
+ user: (@user.is_a?(User) ? @user : nil),
+ description: @description,
+ default_experiment: default_experiment
+ )
+
+ add_metadata(model, @metadata)
+
+ model
+ end
+ end
+
+ private
+
+ def default_experiment
+ @default_experiment ||= Ml::FindOrCreateExperimentService.new(@project, @name).execute
+ end
+
+ def add_metadata(model, metadata_key_value)
+ return unless model.present? && metadata_key_value.present?
+
+ entities = metadata_key_value.map do |d|
+ {
+ model_id: model.id,
+ name: d[:key],
+ value: d[:value]
+ }
+ end
+
+ entities.each do |entry|
+ ::Ml::ModelMetadata.create!(entry)
+ end
+ end
+ end
+end
diff --git a/app/services/ml/experiment_tracking/candidate_repository.rb b/app/services/ml/experiment_tracking/candidate_repository.rb
index 436f06e3ca5..8739379912a 100644
--- a/app/services/ml/experiment_tracking/candidate_repository.rb
+++ b/app/services/ml/experiment_tracking/candidate_repository.rb
@@ -15,12 +15,13 @@ module Ml
end
def create!(experiment, start_time, tags = nil, name = nil)
- candidate = experiment.candidates.create!(
+ create_params = {
+ start_time: start_time,
user: user,
- name: candidate_name(name, tags),
- project: project,
- start_time: start_time || 0
- )
+ name: candidate_name(name, tags)
+ }
+
+ candidate = Ml::CreateCandidateService.new(experiment, create_params).execute
add_tags(candidate, tags)
@@ -103,17 +104,12 @@ module Ml
end
def candidate_name(name, tags)
- name.presence || candidate_name_from_tags(tags) || random_candidate_name
+ name.presence || candidate_name_from_tags(tags)
end
def candidate_name_from_tags(tags)
tags&.detect { |t| t[:key] == 'mlflow.runName' }&.dig(:value)
end
-
- def random_candidate_name
- parts = Array.new(3).map { FFaker::Animal.common_name.downcase.delete(' ') } << rand(10000)
- parts.join('-').truncate(255)
- end
end
end
end
diff --git a/app/services/ml/find_model_service.rb b/app/services/ml/find_model_service.rb
new file mode 100644
index 00000000000..23ca0266629
--- /dev/null
+++ b/app/services/ml/find_model_service.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Ml
+ class FindModelService
+ def initialize(project, name)
+ @project = project
+ @name = name
+ end
+
+ def execute
+ Ml::Model.by_project_id_and_name(@project.id, @name)
+ end
+ end
+end
diff --git a/app/services/ml/find_or_create_model_service.rb b/app/services/ml/find_or_create_model_service.rb
index 66dec7a6234..9199730e84b 100644
--- a/app/services/ml/find_or_create_model_service.rb
+++ b/app/services/ml/find_or_create_model_service.rb
@@ -2,21 +2,17 @@
module Ml
class FindOrCreateModelService
- def initialize(project, model_name)
+ def initialize(project, name, user = nil, description = nil, metadata = [])
@project = project
- @name = model_name
+ @name = name
+ @description = description
+ @metadata = metadata
+ @user = user
end
def execute
- Ml::Model.find_or_create(
- project,
- name,
- Ml::FindOrCreateExperimentService.new(project, name).execute
- )
+ FindModelService.new(@project, @name).execute ||
+ CreateModelService.new(@project, @name, @user, @description, @metadata).execute
end
-
- private
-
- attr_reader :name, :project
end
end
diff --git a/app/services/ml/find_or_create_model_version_service.rb b/app/services/ml/find_or_create_model_version_service.rb
index f4d3f3e72d3..a5e9bf997cc 100644
--- a/app/services/ml/find_or_create_model_version_service.rb
+++ b/app/services/ml/find_or_create_model_version_service.rb
@@ -7,15 +7,20 @@ module Ml
@name = params[:model_name]
@version = params[:version]
@package = params[:package]
+ @description = params[:description]
end
def execute
- model = Ml::FindOrCreateModelService.new(project, name).execute
- Ml::ModelVersion.find_or_create!(model, version, package)
- end
+ model = Ml::FindOrCreateModelService.new(@project, @name).execute
+
+ model_version = Ml::ModelVersion.find_or_create!(model, @version, @package, @description)
- private
+ model_version.candidate = ::Ml::CreateCandidateService.new(
+ model.default_experiment,
+ { model_version: model_version }
+ ).execute
- attr_reader :version, :name, :project, :package
+ model_version
+ end
end
end
diff --git a/app/services/ml/model_versions/get_model_version_service.rb b/app/services/ml/model_versions/get_model_version_service.rb
new file mode 100644
index 00000000000..e8794689d73
--- /dev/null
+++ b/app/services/ml/model_versions/get_model_version_service.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Ml
+ module ModelVersions
+ class GetModelVersionService
+ def initialize(project, name, version)
+ @project = project
+ @name = name
+ @version = version
+ end
+
+ def execute
+ Ml::ModelVersion.by_project_id_name_and_version(
+ @project.id,
+ @name,
+ @version
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/ml/update_model_service.rb b/app/services/ml/update_model_service.rb
new file mode 100644
index 00000000000..dade6c72588
--- /dev/null
+++ b/app/services/ml/update_model_service.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Ml
+ class UpdateModelService
+ def initialize(model, description)
+ @model = model
+ @description = description
+ end
+
+ def execute
+ @model.update!(description: @description)
+
+ @model
+ end
+ end
+end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 1af26377b71..a63b1cf375f 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -226,8 +226,10 @@ module Notes
end
def set_reviewed(note)
- ::MergeRequests::MarkReviewerReviewedService.new(project: project, current_user: current_user)
- .execute(note.noteable)
+ return if Feature.enabled?(:mr_request_changes, current_user)
+
+ ::MergeRequests::UpdateReviewerStateService.new(project: project, current_user: current_user)
+ .execute(note.noteable, "reviewed")
end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index f1781b3d3c5..5099272a212 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -358,7 +358,7 @@ class NotificationService
def review_requested_of_merge_request(merge_request, current_user, reviewer)
recipients = NotificationRecipients::BuildService.build_requested_review_recipients(merge_request, current_user, reviewer)
- deliver_option = review_request_deliver_options(merge_request.project, reviewer)
+ deliver_option = review_request_deliver_options(merge_request.project)
recipients.each do |recipient|
mailer
@@ -975,7 +975,7 @@ class NotificationService
{}
end
- def review_request_deliver_options(project, user)
+ def review_request_deliver_options(project)
# Overridden in EE
{}
end
diff --git a/app/services/organizations/base_service.rb b/app/services/organizations/base_service.rb
new file mode 100644
index 00000000000..19bbc64ebdd
--- /dev/null
+++ b/app/services/organizations/base_service.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Organizations
+ class BaseService
+ include BaseServiceUtility
+
+ attr_reader :current_user, :params
+
+ def initialize(current_user: nil, params: {})
+ @current_user = current_user
+ @params = params.dup
+ end
+ end
+end
diff --git a/app/services/organizations/create_service.rb b/app/services/organizations/create_service.rb
new file mode 100644
index 00000000000..89c579032d2
--- /dev/null
+++ b/app/services/organizations/create_service.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Organizations
+ class CreateService < ::Organizations::BaseService
+ def execute
+ return error_no_permissions unless current_user&.can?(:create_organization)
+
+ organization = Organization.create(params)
+
+ return error_creating(organization) unless organization.persisted?
+
+ ServiceResponse.success(payload: organization)
+ end
+
+ private
+
+ def error_no_permissions
+ ServiceResponse.error(message: [_('You have insufficient permissions to create organizations')])
+ end
+
+ def error_creating(organization)
+ message = organization.errors.full_messages || _('Failed to create organization')
+
+ ServiceResponse.error(message: Array(message))
+ end
+ end
+end
diff --git a/app/services/packages/ml_model/create_package_file_service.rb b/app/services/packages/ml_model/create_package_file_service.rb
index b1e8e814015..ff569a8eecf 100644
--- a/app/services/packages/ml_model/create_package_file_service.rb
+++ b/app/services/packages/ml_model/create_package_file_service.rb
@@ -37,7 +37,8 @@ module Packages
model_version_params = {
model_name: package.name,
version: package.version,
- package: package
+ package: package,
+ user: current_user
}
Ml::FindOrCreateModelVersionService.new(project, model_version_params).execute
diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb
index d599cecc8da..0f0dc297e9a 100644
--- a/app/services/packages/npm/create_package_service.rb
+++ b/app/services/packages/npm/create_package_service.rb
@@ -12,6 +12,7 @@ module Packages
return error('Version is empty.', 400) if version.blank?
return error('Attachment data is empty.', 400) if attachment['data'].blank?
return error('Package already exists.', 403) if current_package_exists?
+ return error('Package protected.', 403) if current_package_protected?
return error('File is too large.', 400) if file_size_exceeded?
package = try_obtain_lease do
@@ -56,6 +57,13 @@ module Packages
.exists?
end
+ def current_package_protected?
+ return false if Feature.disabled?(:packages_protected_packages, project)
+
+ user_project_authorization_access_level = current_user.max_member_access_for_project(project.id)
+ project.package_protection_rules.push_protected_from?(access_level: user_project_authorization_access_level, package_name: name, package_type: :npm)
+ end
+
def name
params[:name]
end
diff --git a/app/services/packages/nuget/check_duplicates_service.rb b/app/services/packages/nuget/check_duplicates_service.rb
index 7ad9038d7c1..33a66c2bce1 100644
--- a/app/services/packages/nuget/check_duplicates_service.rb
+++ b/app/services/packages/nuget/check_duplicates_service.rb
@@ -49,40 +49,30 @@ module Packages
strong_memoize_attr :existing_package
def metadata
- if remote_package_file?
- ExtractMetadataContentService
+ if params[:remote_url].present?
+ ::Packages::Nuget::ExtractMetadataContentService
.new(nuspec_file_content)
.execute
.payload
else # to cover the case when package file is on disk not in object storage
- MetadataExtractionService
- .new(mock_package_file)
- .execute
- .payload
+ Zip::InputStream.open(params[:file]) do |zip|
+ ::Packages::Nuget::MetadataExtractionService
+ .new(zip)
+ .execute
+ .payload
+ end
end
end
strong_memoize_attr :metadata
- def remote_package_file?
- params[:remote_url].present?
- end
-
def nuspec_file_content
- ExtractRemoteMetadataFileService
+ ::Packages::Nuget::ExtractRemoteMetadataFileService
.new(params[:remote_url])
.execute
.payload
- rescue ExtractRemoteMetadataFileService::ExtractionError => e
+ rescue ::Packages::Nuget::ExtractRemoteMetadataFileService::ExtractionError => e
raise ExtractionError, e.message
end
-
- def mock_package_file
- ::Packages::PackageFile.new(
- params
- .slice(:file, :file_name)
- .merge(package: ::Packages::Package.nuget.build)
- )
- end
end
end
end
diff --git a/app/services/packages/nuget/extract_metadata_file_service.rb b/app/services/packages/nuget/extract_metadata_file_service.rb
index fd4f9b5d1c1..1daf0aba8d6 100644
--- a/app/services/packages/nuget/extract_metadata_file_service.rb
+++ b/app/services/packages/nuget/extract_metadata_file_service.rb
@@ -20,7 +20,7 @@ module Packages
attr_reader :package_zip_file
def nuspec_file_content
- entry = package_zip_file.glob('*.nuspec').first
+ entry = extract_nuspec_file
raise ExtractionError, 'nuspec file not found' unless entry
raise ExtractionError, 'nuspec file too big' if MAX_FILE_SIZE < entry.size
@@ -32,6 +32,16 @@ module Packages
rescue Zip::EntrySizeError => e
raise ExtractionError, "nuspec file has the wrong entry size: #{e.message}"
end
+
+ def extract_nuspec_file
+ if package_zip_file.is_a?(Zip::InputStream)
+ while (entry = package_zip_file.get_next_entry) # rubocop:disable Lint/AssignmentInCondition -- Following https://github.com/rubyzip/rubyzip#notes-on-zipinputstream and that's why the disable rubocop rule
+ break entry if entry.name.end_with?('.nuspec')
+ end
+ else
+ package_zip_file.glob('*.nuspec').first
+ end
+ end
end
end
end
diff --git a/app/services/packages/nuget/metadata_extraction_service.rb b/app/services/packages/nuget/metadata_extraction_service.rb
index 53189063c85..813cb8e0979 100644
--- a/app/services/packages/nuget/metadata_extraction_service.rb
+++ b/app/services/packages/nuget/metadata_extraction_service.rb
@@ -3,8 +3,8 @@
module Packages
module Nuget
class MetadataExtractionService
- def initialize(package_file)
- @package_file = package_file
+ def initialize(package_zip_file)
+ @package_zip_file = package_zip_file
end
def execute
@@ -13,19 +13,20 @@ module Packages
private
- attr_reader :package_file
+ attr_reader :package_zip_file
def metadata
- ExtractMetadataContentService
+ ::Packages::Nuget::ExtractMetadataContentService
.new(nuspec_file_content)
.execute
.payload
end
def nuspec_file_content
- ProcessPackageFileService
- .new(package_file)
- .execute[:nuspec_file_content]
+ ::Packages::Nuget::ExtractMetadataFileService
+ .new(package_zip_file)
+ .execute
+ .payload
end
end
end
diff --git a/app/services/packages/nuget/process_package_file_service.rb b/app/services/packages/nuget/process_package_file_service.rb
index fa7a84ee3d6..99b59bd3322 100644
--- a/app/services/packages/nuget/process_package_file_service.rb
+++ b/app/services/packages/nuget/process_package_file_service.rb
@@ -4,7 +4,6 @@ module Packages
module Nuget
class ProcessPackageFileService
ExtractionError = Class.new(StandardError)
- NUGET_SYMBOL_FILE_EXTENSION = '.snupkg'
def initialize(package_file)
@package_file = package_file
@@ -13,14 +12,9 @@ module Packages
def execute
raise ExtractionError, 'invalid package file' unless valid_package_file?
- nuspec_content = nil
-
with_zip_file do |zip_file|
- nuspec_content = nuspec_file_content(zip_file)
- create_symbol_files(zip_file) if symbol_package_file?
+ ::Packages::Nuget::UpdatePackageFromMetadataService.new(package_file, zip_file).execute
end
-
- ServiceResponse.success(payload: { nuspec_file_content: nuspec_content })
end
private
@@ -38,23 +32,6 @@ module Packages
Zip::File.open(open_file.file_path, &block) # rubocop: disable Performance/Rubyzip
end
end
-
- def nuspec_file_content(zip_file)
- ::Packages::Nuget::ExtractMetadataFileService
- .new(zip_file)
- .execute
- .payload
- end
-
- def create_symbol_files(zip_file)
- ::Packages::Nuget::Symbols::CreateSymbolFilesService
- .new(package_file.package, zip_file)
- .execute
- end
-
- def symbol_package_file?
- package_file.file_name.end_with?(NUGET_SYMBOL_FILE_EXTENSION)
- end
end
end
end
diff --git a/app/services/packages/nuget/symbols/create_symbol_files_service.rb b/app/services/packages/nuget/symbols/create_symbol_files_service.rb
index 03e14ba00e1..5f0b8762054 100644
--- a/app/services/packages/nuget/symbols/create_symbol_files_service.rb
+++ b/app/services/packages/nuget/symbols/create_symbol_files_service.rb
@@ -18,7 +18,7 @@ module Packages
process_symbol_entries
rescue ExtractionError => e
- Gitlab::ErrorTracking.log_exception(e, class: self.class.name, package_id: package.id)
+ Gitlab::ErrorTracking.track_exception(e, class: self.class.name, package_id: package.id)
end
private
@@ -31,7 +31,7 @@ module Packages
raise ExtractionError, 'too many symbol entries' if index >= SYMBOL_ENTRIES_LIMIT
entry.extract(tmp_file.path) { true }
- File.open(tmp_file.path) do |file|
+ File.open(tmp_file.path, 'rb') do |file|
create_symbol(entry.name, file)
end
end
@@ -43,25 +43,27 @@ module Packages
end
def create_symbol(path, file)
- signature = extract_signature(file.read(1.kilobyte))
- return if signature.blank?
+ signature, checksum = extract_signature_and_checksum(file)
+ return if signature.blank? || checksum.blank?
::Packages::Nuget::Symbol.create!(
package: package,
file: { tempfile: file, filename: path.downcase, content_type: CONTENT_TYPE },
file_path: path,
signature: signature,
- size: file.size
+ size: file.size,
+ file_sha256: checksum
)
rescue StandardError => e
- Gitlab::ErrorTracking.log_exception(e, class: self.class.name, package_id: package.id)
+ Gitlab::ErrorTracking.track_exception(e, class: self.class.name, package_id: package.id)
end
- def extract_signature(content_fragment)
- ExtractSymbolSignatureService
- .new(content_fragment)
+ def extract_signature_and_checksum(file)
+ ::Packages::Nuget::Symbols::ExtractSignatureAndChecksumService
+ .new(file)
.execute
.payload
+ .values_at(:signature, :checksum)
end
end
end
diff --git a/app/services/packages/nuget/symbols/extract_symbol_signature_service.rb b/app/services/packages/nuget/symbols/extract_signature_and_checksum_service.rb
index c2ccdb517b5..fd37d139145 100644
--- a/app/services/packages/nuget/symbols/extract_symbol_signature_service.rb
+++ b/app/services/packages/nuget/symbols/extract_signature_and_checksum_service.rb
@@ -3,45 +3,43 @@
module Packages
module Nuget
module Symbols
- class ExtractSymbolSignatureService
+ class ExtractSignatureAndChecksumService
include Gitlab::Utils::StrongMemoize
# More information about the GUID format can be found here:
# https://github.com/dotnet/symstore/blob/main/docs/specs/SSQP_Key_Conventions.md#key-formatting-basic-rules
GUID_START_INDEX = 7
- GUID_END_INDEX = 22
+ GUID_END_INDEX = 26
+ SIGNATURE_LENGTH = 16
+ TWENTY_ZEROED_BYTES = "\u0000" * 20
GUID_PARTS_LENGTHS = [4, 2, 2, 8].freeze
GUID_AGE_PART = 'FFFFFFFF'
TWO_CHARACTER_HEX_REGEX = /\h{2}/
+ GUID_CHUNK_SIZE = 256.bytes
+ SHA_CHUNK_SIZE = 16.kilobytes
# The extraction of the signature in this service is based on the following documentation:
# https://github.com/dotnet/symstore/blob/main/docs/specs/SSQP_Key_Conventions.md#portable-pdb-signature
- def initialize(symbol_content)
- @symbol_content = symbol_content
+ def initialize(file)
+ @file = file
end
def execute
return error_response unless signature
- ServiceResponse.success(payload: signature)
+ ServiceResponse.success(payload: { signature: signature, checksum: checksum })
end
private
- attr_reader :symbol_content
+ attr_reader :file
def signature
- # Find the index of the first occurrence of 'Blob'
- guid_index = symbol_content.index('Blob')
- return if guid_index.nil?
-
- # Extract the binary GUID from the symbol content
- guid = symbol_content[(guid_index + GUID_START_INDEX)..(guid_index + GUID_END_INDEX)]
- return if guid.nil?
+ return unless pdb_id
# Convert the GUID into an array of two-character hex strings
- guid = guid.unpack('H*').flat_map { |el| el.scan(TWO_CHARACTER_HEX_REGEX) }
+ guid = pdb_id.first(SIGNATURE_LENGTH).unpack('H*').flat_map { |el| el.scan(TWO_CHARACTER_HEX_REGEX) }
# Reorder the GUID parts based on arbitrary lengths
guid = GUID_PARTS_LENGTHS.map { |length| guid.shift(length) }
@@ -54,6 +52,36 @@ module Packages
end
strong_memoize_attr :signature
+ # https://github.com/dotnet/corefx/blob/master/src/System.Reflection.Metadata/specs/PE-COFF.md#portable-pdb-checksum
+ def checksum
+ sha = OpenSSL::Digest.new('SHA256')
+ count = 0
+ chunk = (+'').force_encoding(Encoding::BINARY)
+ file.rewind
+
+ while file.read(SHA_CHUNK_SIZE, chunk)
+ count += 1
+ chunk[pdb_id] = TWENTY_ZEROED_BYTES if count == 1
+ sha.update(chunk)
+ end
+
+ sha.hexdigest
+ end
+
+ def pdb_id
+ # The ID is located in the first 256 bytes of the symbol `.pdb` file
+ chunk = file.read(GUID_CHUNK_SIZE)
+ return unless chunk
+
+ # Find the index of the first occurrence of 'Blob'
+ guid_index = chunk.index('Blob')
+ return unless guid_index
+
+ # Extract the binary GUID from the symbol content
+ chunk[(guid_index + GUID_START_INDEX)..(guid_index + GUID_END_INDEX)]
+ end
+ strong_memoize_attr :pdb_id
+
def error_response
ServiceResponse.error(message: 'Could not find the signature in the symbol file')
end
diff --git a/app/services/packages/nuget/update_package_from_metadata_service.rb b/app/services/packages/nuget/update_package_from_metadata_service.rb
index 4cec4ed2fae..b7411d5f8a8 100644
--- a/app/services/packages/nuget/update_package_from_metadata_service.rb
+++ b/app/services/packages/nuget/update_package_from_metadata_service.rb
@@ -13,11 +13,11 @@ module Packages
INVALID_METADATA_ERROR_SYMBOL_MESSAGE = 'package name, version and/or description not found in metadata'
MISSING_MATCHING_PACKAGE_ERROR_MESSAGE = 'symbol package is invalid, matching package does not exist'
- InvalidMetadataError = Class.new(StandardError)
- ZipError = Class.new(StandardError)
+ InvalidMetadataError = ZipError = Class.new(StandardError)
- def initialize(package_file)
+ def initialize(package_file, package_zip_file)
@package_file = package_file
+ @package_zip_file = package_zip_file
end
def execute
@@ -57,7 +57,7 @@ module Packages
build_infos = package_to_destroy&.build_infos || []
update_package(target_package, build_infos)
- update_symbol_files(target_package, package_to_destroy) if symbol_package?
+ create_symbol_files
::Packages::UpdatePackageFileService.new(@package_file, package_id: target_package.id, file_name: package_filename)
.execute
package_to_destroy&.destroy!
@@ -79,8 +79,12 @@ module Packages
raise InvalidMetadataError, e.message
end
- def update_symbol_files(package, package_to_destroy)
- package_to_destroy.nuget_symbols.update_all(package_id: package.id)
+ def create_symbol_files
+ return unless symbol_package?
+
+ ::Packages::Nuget::Symbols::CreateSymbolFilesService
+ .new(existing_package, @package_zip_file)
+ .execute
end
def valid_metadata?
@@ -145,9 +149,10 @@ module Packages
def symbol_package?
package_types.include?(SYMBOL_PACKAGE_IDENTIFIER)
end
+ strong_memoize_attr :symbol_package?
def metadata
- ::Packages::Nuget::MetadataExtractionService.new(@package_file).execute.payload
+ ::Packages::Nuget::MetadataExtractionService.new(@package_zip_file).execute.payload
end
strong_memoize_attr :metadata
diff --git a/app/services/packages/protection/delete_rule_service.rb b/app/services/packages/protection/delete_rule_service.rb
new file mode 100644
index 00000000000..a1fa111b57b
--- /dev/null
+++ b/app/services/packages/protection/delete_rule_service.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Packages
+ module Protection
+ class DeleteRuleService
+ include Gitlab::Allowable
+
+ def initialize(package_protection_rule, current_user:)
+ if package_protection_rule.blank? || current_user.blank?
+ raise ArgumentError,
+ 'package_protection_rule and current_user must be set'
+ end
+
+ @package_protection_rule = package_protection_rule
+ @current_user = current_user
+ end
+
+ def execute
+ unless can?(current_user, :admin_package, package_protection_rule.project)
+ error_message = _('Unauthorized to delete a package protection rule')
+ return service_response_error(message: error_message)
+ end
+
+ deleted_package_protection_rule = package_protection_rule.destroy!
+
+ ServiceResponse.success(payload: { package_protection_rule: deleted_package_protection_rule })
+ rescue StandardError => e
+ service_response_error(message: e.message)
+ end
+
+ private
+
+ attr_reader :package_protection_rule, :current_user
+
+ def service_response_error(message:)
+ ServiceResponse.error(
+ message: message,
+ payload: { package_protection_rule: nil }
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/packages/pypi/create_package_service.rb b/app/services/packages/pypi/create_package_service.rb
index 087a8e42a66..fca7b1bca37 100644
--- a/app/services/packages/pypi/create_package_service.rb
+++ b/app/services/packages/pypi/create_package_service.rb
@@ -9,7 +9,13 @@ module Packages
::Packages::Package.transaction do
meta = Packages::Pypi::Metadatum.new(
package: created_package,
- required_python: params[:requires_python] || ''
+ required_python: params[:requires_python] || '',
+ metadata_version: params[:metadata_version],
+ author_email: params[:author_email],
+ description: params[:description],
+ description_content_type: params[:description_content_type],
+ summary: params[:summary],
+ keywords: params[:keywords]
)
unless meta.valid?
diff --git a/app/services/packages/update_tags_service.rb b/app/services/packages/update_tags_service.rb
index cf1acc6ee19..014d5501b76 100644
--- a/app/services/packages/update_tags_service.rb
+++ b/app/services/packages/update_tags_service.rb
@@ -32,7 +32,8 @@ module Packages
package_id: @package.id,
name: tag,
created_at: now,
- updated_at: now
+ updated_at: now,
+ project_id: @package.project_id
}
end
end
diff --git a/app/services/pages/delete_service.rb b/app/services/pages/delete_service.rb
index dcee4c5b665..96b451aeba4 100644
--- a/app/services/pages/delete_service.rb
+++ b/app/services/pages/delete_service.rb
@@ -3,7 +3,7 @@
module Pages
class DeleteService < BaseService
def execute
- project.mark_pages_as_not_deployed
+ PagesDeployment.deactivate_all(project)
# project.pages_domains.delete_all will just nullify project_id:
# > If no :dependent option is given, then it will follow the default
diff --git a/app/services/personal_access_tokens/rotate_service.rb b/app/services/personal_access_tokens/rotate_service.rb
index b765aacef68..32710629caf 100644
--- a/app/services/personal_access_tokens/rotate_service.rb
+++ b/app/services/personal_access_tokens/rotate_service.rb
@@ -9,7 +9,7 @@ module PersonalAccessTokens
@token = token
end
- def execute
+ def execute(params = {})
return ServiceResponse.error(message: _('token already revoked')) if token.revoked?
response = ServiceResponse.success
@@ -21,7 +21,7 @@ module PersonalAccessTokens
end
target_user = token.user
- new_token = target_user.personal_access_tokens.create(create_token_params(token))
+ new_token = target_user.personal_access_tokens.create(create_token_params(token, params))
if new_token.persisted?
response = ServiceResponse.success(payload: { personal_access_token: new_token })
@@ -39,12 +39,13 @@ module PersonalAccessTokens
attr_reader :current_user, :token
- def create_token_params(token)
+ def create_token_params(token, params)
+ expires_at = params[:expires_at] || (Date.today + EXPIRATION_PERIOD)
{ name: token.name,
previous_personal_access_token_id: token.id,
impersonation: token.impersonation,
scopes: token.scopes,
- expires_at: Date.today + EXPIRATION_PERIOD }
+ expires_at: expires_at }
end
end
end
diff --git a/app/services/projects/container_repository/gitlab/delete_tags_service.rb b/app/services/projects/container_repository/gitlab/delete_tags_service.rb
index a5b7f4bbb6f..6dc50dac7a4 100644
--- a/app/services/projects/container_repository/gitlab/delete_tags_service.rb
+++ b/app/services/projects/container_repository/gitlab/delete_tags_service.rb
@@ -34,7 +34,7 @@ module Projects
@tag_names.each do |name|
raise TimeoutError if timeout?(start_time)
- if @container_repository.delete_tag_by_name(name)
+ if @container_repository.delete_tag(name)
@deleted_tags.append(name)
end
end
diff --git a/app/services/projects/container_repository/third_party/delete_tags_service.rb b/app/services/projects/container_repository/third_party/delete_tags_service.rb
index 942df177bea..ae3f1cc23d6 100644
--- a/app/services/projects/container_repository/third_party/delete_tags_service.rb
+++ b/app/services/projects/container_repository/third_party/delete_tags_service.rb
@@ -30,7 +30,7 @@ module Projects
# Deletes the dummy image
# All created tag digests are the same since they all have the same dummy image.
# a single delete is sufficient to remove all tags with it
- if deleted_tags.any? && @container_repository.delete_tag_by_digest(deleted_tags.each_value.first)
+ if deleted_tags.any? && @container_repository.delete_tag(deleted_tags.each_value.first)
success(deleted: deleted_tags.keys)
else
error("could not delete tags: #{@tag_names.join(', ')}".truncate(1000))
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index a2a2f9d2800..8c86646ba5c 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -18,6 +18,15 @@ module Projects
return false unless can?(current_user, :remove_project, project)
project.update_attribute(:pending_delete, true)
+
+ # There is a possibility of active repository move processes for
+ # project and snippets. An attempt to delete the project at the same time
+ # can lead to race condition and an inconsistent state.
+ #
+ # This validation stops the project delete process if it detects active
+ # repository move schedules for it.
+ validate_active_repositories_move!
+
# Flush the cache for both repositories. This has to be done _before_
# removing the physical repositories as some expiration code depends on
# Git data (e.g. a list of branch names).
@@ -50,6 +59,16 @@ module Projects
private
+ def validate_active_repositories_move!
+ if project.repository_storage_moves.scheduled_or_started.exists?
+ raise_error(s_("DeleteProject|Couldn't remove the project. A project repository storage move is in progress. Try again when it's complete."))
+ end
+
+ if ::ProjectSnippet.by_project(project).with_repository_storage_moves.merge(::Snippets::RepositoryStorageMove.scheduled_or_started).exists?
+ raise_error(s_("DeleteProject|Couldn't remove the project. A related snippet repository storage move is in progress. Try again when it's complete."))
+ end
+ end
+
def trash_project_repositories!
unless remove_repository(project.repository)
raise_error(s_('DeleteProject|Failed to remove project repository. Please try again or contact administrator.'))
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index aace8846afc..168420b17bf 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -17,6 +17,10 @@ module Projects
@valid_fork_targets ||= ForkTargetsFinder.new(@project, current_user).execute(options)
end
+ def valid_fork_branch?(branch)
+ @project.repository.branch_exists?(branch)
+ end
+
def valid_fork_target?(namespace = target_namespace)
return true if current_user.admin?
@@ -68,7 +72,8 @@ module Projects
external_authorization_classification_label: @project.external_authorization_classification_label,
suggestion_commit_message: @project.suggestion_commit_message,
merge_commit_template: @project.merge_commit_template,
- squash_commit_template: @project.squash_commit_template
+ squash_commit_template: @project.squash_commit_template,
+ import_data: { data: { fork_branch: branch } }
}
if @project.avatar.present? && @project.avatar.image?
@@ -145,6 +150,12 @@ module Projects
def stream_audit_event(forked_project)
# Defined in EE
end
+
+ def branch
+ # We extract branch name from @params[:branches] because the front end
+ # insists on sending it as 'branches'.
+ @params[:branches]
+ end
end
end
diff --git a/app/services/projects/group_links/destroy_service.rb b/app/services/projects/group_links/destroy_service.rb
index a2307bfebf0..e0218ae087e 100644
--- a/app/services/projects/group_links/destroy_service.rb
+++ b/app/services/projects/group_links/destroy_service.rb
@@ -3,8 +3,10 @@
module Projects
module GroupLinks
class DestroyService < BaseService
- def execute(group_link)
- return false unless group_link
+ def execute(group_link, skip_authorization: false)
+ unless valid_to_destroy?(group_link, skip_authorization)
+ return ServiceResponse.error(message: 'Not found', reason: :not_found)
+ end
if group_link.project.private?
TodosDestroyer::ProjectPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id)
@@ -12,20 +14,29 @@ module Projects
TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, nil, project.id)
end
- group_link.destroy.tap do |link|
- refresh_project_authorizations_asynchronously(link.project)
+ link = group_link.destroy
- # Until we compare the inconsistency rates of the new specialized worker and
- # the old approach, we still run AuthorizedProjectsWorker
- # but with some delay and lower urgency as a safety net.
- link.group.refresh_members_authorized_projects(
- priority: UserProjectAccessChangedService::LOW_PRIORITY
- )
- end
+ refresh_project_authorizations_asynchronously(link.project)
+
+ # Until we compare the inconsistency rates of the new specialized worker and
+ # the old approach, we still run AuthorizedProjectsWorker
+ # but with some delay and lower urgency as a safety net.
+ link.group.refresh_members_authorized_projects(
+ priority: UserProjectAccessChangedService::LOW_PRIORITY
+ )
+
+ ServiceResponse.success(payload: { link: link })
end
private
+ def valid_to_destroy?(group_link, skip_authorization)
+ return false unless group_link
+ return true if skip_authorization
+
+ current_user.can?(:admin_project_group_link, group_link)
+ end
+
def refresh_project_authorizations_asynchronously(project)
AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id)
end
diff --git a/app/services/projects/group_links/update_service.rb b/app/services/projects/group_links/update_service.rb
index 9b2565adaca..04f1552d929 100644
--- a/app/services/projects/group_links/update_service.rb
+++ b/app/services/projects/group_links/update_service.rb
@@ -10,15 +10,23 @@ module Projects
end
def execute(group_link_params)
+ return ServiceResponse.error(message: 'Not found', reason: :not_found) unless allowed_to_update?
+
group_link.update!(group_link_params)
refresh_authorizations if requires_authorization_refresh?(group_link_params)
+
+ ServiceResponse.success
end
private
attr_reader :group_link
+ def allowed_to_update?
+ current_user.can?(:admin_project_member, project)
+ end
+
def refresh_authorizations
AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id)
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index ab38efff7c9..83b28840d39 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -33,7 +33,7 @@ module Projects
break error('The uploaded artifact size does not match the expected value') unless deployment
break error(deployment_update.errors.first.full_message) unless deployment_update.valid?
- update_project_pages_deployment(deployment)
+ deactive_old_deployments(deployment)
success
end
rescue StandardError => e
@@ -45,7 +45,6 @@ module Projects
def success
commit_status.success
- @project.mark_pages_as_deployed
publish_deployed_event
super
end
@@ -84,11 +83,11 @@ module Projects
def create_pages_deployment(artifacts_path, build)
File.open(artifacts_path) do |file|
attributes = pages_deployment_attributes(file, build)
- deployment = project.pages_deployments.create!(**attributes)
+ deployment = project.pages_deployments.build(**attributes)
- break if deployment.size != file.size || deployment.file.size != file.size
+ break if deployment.file.size != file.size
- deployment
+ deployment.tap(&:save!)
end
end
@@ -103,9 +102,7 @@ module Projects
}
end
- def update_project_pages_deployment(deployment)
- project.update_pages_deployment!(deployment)
-
+ def deactive_old_deployments(deployment)
PagesDeployment.deactivate_deployments_older_than(
deployment,
time: OLD_DEPLOYMENTS_DESTRUCTION_DELAY.from_now)
diff --git a/app/services/projects/update_repository_storage_service.rb b/app/services/projects/update_repository_storage_service.rb
index 85fb1890fcd..a9f6afb26c9 100644
--- a/app/services/projects/update_repository_storage_service.rb
+++ b/app/services/projects/update_repository_storage_service.rb
@@ -8,7 +8,9 @@ module Projects
private
- def track_repository(_destination_storage_name)
+ def track_repository(destination_storage_name)
+ project.update!(repository_storage: destination_storage_name)
+
# Connect project to pool repository from the new shard
project.swap_pool_repository!
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index e5e39247dbf..336e887c241 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -58,11 +58,11 @@ module Projects
def validate!
unless valid_visibility_level_change?(project, project.visibility_attribute_value(params))
- raise ValidationError, s_('UpdateProject|New visibility level not allowed!')
+ raise_validation_error(s_('UpdateProject|New visibility level not allowed!'))
end
if renaming_project_with_container_registry_tags?
- raise ValidationError, s_('UpdateProject|Cannot rename project because it contains container registry tags!')
+ raise_validation_error(s_('UpdateProject|Cannot rename project because it contains container registry tags!'))
end
validate_default_branch_change
@@ -78,21 +78,22 @@ module Projects
params[:previous_default_branch] = previous_default_branch
if !project.root_ref?(new_default_branch) && has_custom_head_branch?
- raise ValidationError,
+ raise_validation_error(
format(
s_("UpdateProject|Could not set the default branch. Do you have a branch named 'HEAD' in your repository? (%{linkStart}How do I fix this?%{linkEnd})"),
linkStart: ambiguous_head_documentation_link, linkEnd: '</a>'
).html_safe
+ )
end
after_default_branch_change(previous_default_branch)
else
- raise ValidationError, s_("UpdateProject|Could not set the default branch")
+ raise_validation_error(s_("UpdateProject|Could not set the default branch"))
end
end
def ambiguous_head_documentation_link
- url = Rails.application.routes.url_helpers.help_page_path('user/project/repository/branches/index.md', anchor: 'error-ambiguous-head-branch-exists')
+ url = Rails.application.routes.url_helpers.help_page_path('user/project/repository/branches/index', anchor: 'error-ambiguous-head-branch-exists')
format('<a href="%{url}" target="_blank" rel="noopener noreferrer">', url: url)
end
@@ -144,6 +145,10 @@ module Projects
AfterRenameService.new(project, path_before: project.path_before_last_save, full_path_before: project.full_path_before_last_save)
end
+ def raise_validation_error(message)
+ raise ValidationError, message
+ end
+
def update_failed!
model_errors = project.errors.full_messages.to_sentence
error_message = model_errors.presence || s_('UpdateProject|Project could not be updated!')
diff --git a/app/services/releases/base_service.rb b/app/services/releases/base_service.rb
index 5d6cb372653..088776b896c 100644
--- a/app/services/releases/base_service.rb
+++ b/app/services/releases/base_service.rb
@@ -111,6 +111,10 @@ module Releases
# overridden in EE
def project_group_id; end
+
+ def audit(release, action:)
+ # overridden in EE
+ end
end
end
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
index 95e0861a37a..38c9e6d60a7 100644
--- a/app/services/releases/create_service.rb
+++ b/app/services/releases/create_service.rb
@@ -18,12 +18,6 @@ module Releases
return tag unless tag.is_a?(Gitlab::Git::Tag)
- if project.catalog_resource
- response = Ci::Catalog::Resources::ValidateService.new(project, ref).execute
-
- return error(response.message) if response.error?
- end
-
create_release(tag, evidence_pipeline)
end
@@ -56,6 +50,12 @@ module Releases
def create_release(tag, evidence_pipeline)
release = build_release(tag)
+ if project.catalog_resource && release.valid?
+ response = Ci::Catalog::Resources::ReleaseService.new(release).execute
+
+ return error(response.message, 422) if response.error?
+ end
+
release.save!
notify_create_release(release)
@@ -64,6 +64,8 @@ module Releases
create_evidence!(release, evidence_pipeline)
+ audit(release, action: :created)
+
success(tag: tag, release: release)
rescue StandardError => e
error(e.message, 400)
diff --git a/app/services/releases/destroy_service.rb b/app/services/releases/destroy_service.rb
index 78613c05ff1..1e8338651a8 100644
--- a/app/services/releases/destroy_service.rb
+++ b/app/services/releases/destroy_service.rb
@@ -11,6 +11,8 @@ module Releases
execute_hooks(release, 'delete')
+ audit(release, action: :deleted)
+
success(tag: existing_tag, release: release)
else
error(release.errors.messages || '400 Bad request', 400)
diff --git a/app/services/releases/update_service.rb b/app/services/releases/update_service.rb
index c11d9468814..13ece1c10c8 100644
--- a/app/services/releases/update_service.rb
+++ b/app/services/releases/update_service.rb
@@ -19,6 +19,8 @@ module Releases
ApplicationRecord.transaction do
if release.update(params)
execute_hooks(release, 'update')
+ audit(release, action: :updated)
+ audit(release, action: :milestones_updated) if milestones_updated?(previous_milestones)
success(tag: existing_tag, release: release, milestones_updated: milestones_updated?(previous_milestones))
else
error(release.errors.messages || '400 Bad request', 400)
diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb
index 1c496aa5e77..824b1a8c377 100644
--- a/app/services/resource_access_tokens/create_service.rb
+++ b/app/services/resource_access_tokens/create_service.rb
@@ -17,6 +17,8 @@ module ResourceAccessTokens
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)
+ return error("Access level of the token can't be greater the access level of the user who created the token") unless validate_access_level(access_level)
+
return error(s_('AccessTokens|Access token limit reached')) if reached_access_token_limit?
user = create_user
@@ -125,6 +127,14 @@ module ResourceAccessTokens
ServiceResponse.success(payload: { access_token: access_token })
end
+ def validate_access_level(access_level)
+ return true unless resource.is_a?(Project)
+ return true if current_user.bot?
+ return true if current_user.can?(:manage_owners, resource)
+
+ current_user.authorized_project?(resource, access_level.to_i)
+ end
+
def do_not_allow_owner_access_level_for_project_bot?(access_level)
resource.is_a?(Project) &&
access_level.to_i == Gitlab::Access::OWNER &&
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 e675bb61072..9943fd4910b 100644
--- a/app/services/resource_events/base_synthetic_notes_builder_service.rb
+++ b/app/services/resource_events/base_synthetic_notes_builder_service.rb
@@ -44,10 +44,9 @@ module ResourceEvents
end
def resource_parent
- strong_memoize(:resource_parent) do
- resource.project || resource.group
- end
+ resource.try(:resource_parent) || resource.project || resource.group
end
+ strong_memoize_attr :resource_parent
def table_name
raise NotImplementedError
diff --git a/app/services/resource_events/merge_into_notes_service.rb b/app/services/resource_events/merge_into_notes_service.rb
index ea465c1e75e..eb0023937b2 100644
--- a/app/services/resource_events/merge_into_notes_service.rb
+++ b/app/services/resource_events/merge_into_notes_service.rb
@@ -37,4 +37,4 @@ module ResourceEvents
end
end
-ResourceEvents::MergeIntoNotesService.prepend_mod_with('ResourceEvents::MergeIntoNotesService')
+ResourceEvents::MergeIntoNotesService.prepend_mod
diff --git a/app/services/security/ci_configuration/sast_parser_service.rb b/app/services/security/ci_configuration/sast_parser_service.rb
index 16a9efcefdf..f466dd0b649 100644
--- a/app/services/security/ci_configuration/sast_parser_service.rb
+++ b/app/services/security/ci_configuration/sast_parser_service.rb
@@ -89,17 +89,15 @@ module Security
def gitlab_ci_yml_attributes
@gitlab_ci_yml_attributes ||= begin
- config_content = @project.repository.blob_data_at(@project.repository.root_ref_sha, ci_config_file)
+ config_content = @project.repository.blob_data_at(
+ @project.repository.root_ref_sha, @project.ci_config_path_or_default
+ )
return {} unless config_content
build_sast_attributes(config_content)
end
end
- def ci_config_file
- '.gitlab-ci.yml'
- end
-
def build_sast_attributes(content)
options = { project: @project, user: current_user, sha: @project.repository.commit.sha }
yaml_result = Gitlab::Ci::YamlProcessor.new(content, options).execute
diff --git a/app/services/service_desk/custom_email_verifications/update_service.rb b/app/services/service_desk/custom_email_verifications/update_service.rb
index 5ef36ce0576..fbd217e3a3e 100644
--- a/app/services/service_desk/custom_email_verifications/update_service.rb
+++ b/app/services/service_desk/custom_email_verifications/update_service.rb
@@ -8,7 +8,7 @@ module ServiceDesk
def execute
return error_feature_flag_disabled unless Feature.enabled?(:service_desk_custom_email, project)
return error_parameter_missing if settings.blank? || verification.blank?
- return error_already_finished if already_finished_and_no_mail?
+ return error_already_finished if verification.finished?
return error_already_failed if already_failed_and_no_mail?
verification_error = verify
@@ -39,10 +39,6 @@ module ServiceDesk
@verification ||= settings.custom_email_verification
end
- def already_finished_and_no_mail?
- verification.finished? && mail.blank?
- end
-
def already_failed_and_no_mail?
verification.failed? && mail.blank?
end
diff --git a/app/services/service_desk/custom_emails/create_service.rb b/app/services/service_desk/custom_emails/create_service.rb
index 305f5b3fa11..c06c836f0fa 100644
--- a/app/services/service_desk/custom_emails/create_service.rb
+++ b/app/services/service_desk/custom_emails/create_service.rb
@@ -42,6 +42,8 @@ module ServiceDesk
def create_credential
credential = ::ServiceDesk::CustomEmailCredential.new(create_credential_params.merge(project: project))
credential.save
+ rescue ArgumentError
+ false
end
def create_verification
@@ -53,7 +55,7 @@ module ServiceDesk
end
def create_credential_params
- ensure_params.permit(:smtp_address, :smtp_port, :smtp_username, :smtp_password)
+ ensure_params.permit(:smtp_address, :smtp_port, :smtp_username, :smtp_password, :smtp_authentication)
end
def ensure_params
diff --git a/app/services/service_desk_settings/update_service.rb b/app/services/service_desk_settings/update_service.rb
index 182022beb1d..f8b825923f3 100644
--- a/app/services/service_desk_settings/update_service.rb
+++ b/app/services/service_desk_settings/update_service.rb
@@ -9,6 +9,8 @@ module ServiceDeskSettings
params[:project_key] = nil if params[:project_key].blank?
+ apply_feature_flag_restrictions!
+
# We want to know when custom email got enabled
write_log_message = params[:custom_email_enabled].present? && !settings.custom_email_enabled?
@@ -20,5 +22,14 @@ module ServiceDeskSettings
ServiceResponse.error(message: settings.errors.full_messages.to_sentence)
end
end
+
+ private
+
+ def apply_feature_flag_restrictions!
+ return if Feature.enabled?(:issue_email_participants, project)
+ return unless params.include?(:add_external_participants_from_cc)
+
+ params.delete(:add_external_participants_from_cc)
+ end
end
end
diff --git a/app/services/spam/spam_action_service.rb b/app/services/spam/spam_action_service.rb
index 6ec8d09c37c..cca0bb709aa 100644
--- a/app/services/spam/spam_action_service.rb
+++ b/app/services/spam/spam_action_service.rb
@@ -78,14 +78,17 @@ module Spam
when BLOCK_USER
target.spam!
create_spam_log
+ create_spam_abuse_event(result)
ban_user!
when DISALLOW
target.spam!
create_spam_log
+ create_spam_abuse_event(result)
when CONDITIONAL_ALLOW
# This means "require a CAPTCHA to be solved"
target.needs_recaptcha!
create_spam_log
+ create_spam_abuse_event(result)
when OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM
create_spam_log
when ALLOW
@@ -118,6 +121,22 @@ module Spam
target.spam_log = spam_log
end
+ def create_spam_abuse_event(result)
+ params = {
+ user_id: user.id,
+ title: target.spam_title,
+ description: target.spam_description,
+ source_ip: spam_params&.ip_address,
+ user_agent: spam_params&.user_agent,
+ noteable_type: noteable_type,
+ verdict: result
+ }
+
+ target.run_after_commit_or_now do
+ Abuse::SpamAbuseEventsWorker.perform_async(params)
+ end
+ end
+
def ban_user!
UserCustomAttribute.set_banned_by_spam_log(target.spam_log)
diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb
index 8442ff81d41..c584d5ccca3 100644
--- a/app/services/system_notes/issuables_service.rb
+++ b/app/services/system_notes/issuables_service.rb
@@ -437,7 +437,7 @@ module SystemNotes
def discussion_lock
action = noteable.discussion_locked? ? 'locked' : 'unlocked'
- body = "#{action} this #{noteable.class.to_s.titleize.downcase}"
+ body = "#{action} the discussion in this #{noteable.class.to_s.titleize.downcase}"
if action == 'locked'
track_issue_event(:track_issue_locked_action)
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index 32acc3f170d..6ec87df9f76 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -72,6 +72,8 @@ module Users
changes.remove_projects_for_user(user, remove)
end.apply!
+ user.update!(project_authorizations_recalculated_at: Time.zone.now) if remove.any? || add.any?
+
# Since we batch insert authorization rows, Rails' associations may get
# out of sync. As such we force a reload of the User object.
user.reset
diff --git a/app/services/users/upsert_credit_card_validation_service.rb b/app/services/users/upsert_credit_card_validation_service.rb
index 62df676db25..e0f81971944 100644
--- a/app/services/users/upsert_credit_card_validation_service.rb
+++ b/app/services/users/upsert_credit_card_validation_service.rb
@@ -2,41 +2,68 @@
module Users
class UpsertCreditCardValidationService < BaseService
+ attr_reader :params
+
def initialize(params)
@params = params.to_h.with_indifferent_access
end
def execute
- user_id = params.fetch(:user_id)
-
- @params = {
- user_id: user_id,
- credit_card_validated_at: params.fetch(:credit_card_validated_at),
- expiration_date: get_expiration_date(params),
- last_digits: Integer(params.fetch(:credit_card_mask_number), 10),
- network: params.fetch(:credit_card_type),
- holder_name: params.fetch(:credit_card_holder_name)
- }
-
credit_card = Users::CreditCardValidation.find_or_initialize_by_user(user_id)
- credit_card.update(@params.except(:user_id))
+ credit_card_params = {
+ credit_card_validated_at: credit_card_validated_at,
+ last_digits: last_digits,
+ holder_name: holder_name,
+ network: network,
+ expiration_date: expiration_date
+ }
+
+ credit_card.update(credit_card_params)
- ServiceResponse.success(message: 'CreditCardValidation was set')
- rescue ActiveRecord::InvalidForeignKey, ActiveRecord::NotNullViolation => e
- ServiceResponse.error(message: "Could not set CreditCardValidation: #{e.message}")
+ success
+ rescue ActiveRecord::InvalidForeignKey, ActiveRecord::NotNullViolation
+ error
rescue StandardError => e
- Gitlab::ErrorTracking.track_exception(e, params: @params, class: self.class.to_s)
- ServiceResponse.error(message: "Could not set CreditCardValidation: #{e.message}")
+ Gitlab::ErrorTracking.track_exception(e)
+ error
end
private
- def get_expiration_date(params)
+ def user_id
+ params.fetch(:user_id)
+ end
+
+ def credit_card_validated_at
+ params.fetch(:credit_card_validated_at)
+ end
+
+ def last_digits
+ Integer(params.fetch(:credit_card_mask_number), 10)
+ end
+
+ def holder_name
+ params.fetch(:credit_card_holder_name)
+ end
+
+ def network
+ params.fetch(:credit_card_type)
+ end
+
+ def expiration_date
year = params.fetch(:credit_card_expiration_year)
month = params.fetch(:credit_card_expiration_month)
Date.new(year, month, -1) # last day of the month
end
+
+ def success
+ ServiceResponse.success(message: _('Credit card validation record saved'))
+ end
+
+ def error
+ ServiceResponse.error(message: _('Error saving credit card validation record'))
+ end
end
end
diff --git a/app/services/verify_pages_domain_service.rb b/app/services/verify_pages_domain_service.rb
index 59c73aa929c..f5dfe13539b 100644
--- a/app/services/verify_pages_domain_service.rb
+++ b/app/services/verify_pages_domain_service.rb
@@ -79,7 +79,7 @@ class VerifyPagesDomainService < BaseService
# A domain is only expired until `disable!` has been called
def expired?
- domain.enabled_until && domain.enabled_until < Time.current
+ domain.enabled_until&.past?
end
def dns_record_present?
diff --git a/app/services/vs_code/settings/delete_service.rb b/app/services/vs_code/settings/delete_service.rb
new file mode 100644
index 00000000000..a2edd734eb2
--- /dev/null
+++ b/app/services/vs_code/settings/delete_service.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module VsCode
+ module Settings
+ class DeleteService
+ def initialize(current_user:)
+ @current_user = current_user
+ end
+
+ def execute
+ VsCodeSetting.by_user(current_user).delete_all
+
+ ServiceResponse.success
+ end
+
+ private
+
+ attr_reader :current_user
+ end
+ end
+end
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 27b29feed50..035f1754cbb 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -83,13 +83,13 @@ class WebHookService
log_execution(
response: response,
- execution_duration: Gitlab::Metrics::System.monotonic_time - start_time
+ execution_duration: ::Gitlab::Metrics::System.monotonic_time - start_time
)
ServiceResponse.success(message: response.body, payload: { http_status: response.code })
rescue *Gitlab::HTTP::HTTP_ERRORS,
Gitlab::Json::LimitedEncoder::LimitExceeded, URI::InvalidURIError => e
- execution_duration = Gitlab::Metrics::System.monotonic_time - start_time
+ execution_duration = ::Gitlab::Metrics::System.monotonic_time - start_time
error_message = e.to_s
log_execution(
@@ -110,10 +110,10 @@ class WebHookService
break log_recursion_blocked if recursion_blocked?
params = {
- recursion_detection_request_uuid: Gitlab::WebHooks::RecursionDetection::UUID.instance.request_uuid
+ "recursion_detection_request_uuid" => Gitlab::WebHooks::RecursionDetection::UUID.instance.request_uuid
}.compact
- WebHookWorker.perform_async(hook.id, data, hook_name, params)
+ WebHookWorker.perform_async(hook.id, data.deep_stringify_keys, hook_name.to_s, params)
end
end
@@ -170,7 +170,9 @@ class WebHookService
def queue_log_execution_with_retry(log_data, category)
retried = false
begin
- ::WebHooks::LogExecutionWorker.perform_async(hook.id, log_data, category, uniqueness_token)
+ ::WebHooks::LogExecutionWorker.perform_async(
+ hook.id, log_data.deep_stringify_keys, category.to_s, uniqueness_token.to_s
+ )
rescue Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError
raise if retried
diff --git a/app/validators/ip_cidr_array_validator.rb b/app/validators/ip_cidr_array_validator.rb
new file mode 100644
index 00000000000..fff1368508f
--- /dev/null
+++ b/app/validators/ip_cidr_array_validator.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# IpCidrArrayValidator
+#
+# Validates that an array of IP are a valid IPv4 or IPv6 CIDR address.
+#
+# Example:
+#
+# class Group < ActiveRecord::Base
+# validates :ip_array, presence: true, ip_cidr_array: true
+# end
+
+class IpCidrArrayValidator < ActiveModel::EachValidator # rubocop:disable Gitlab/NamespacedClass -- This is a globally shareable validator, but it's unclear what namespace it should belong in
+ def validate_each(record, attribute, value)
+ unless value.is_a?(Array)
+ record.errors.add(attribute, _("must be an array of CIDR values"))
+ return
+ end
+
+ value.each do |cidr|
+ single_validator = IpCidrValidator.new(attributes: attribute)
+ single_validator.validate_each(record, attribute, cidr)
+ end
+ end
+end
diff --git a/app/validators/ip_cidr_validator.rb b/app/validators/ip_cidr_validator.rb
new file mode 100644
index 00000000000..b1760a99d6d
--- /dev/null
+++ b/app/validators/ip_cidr_validator.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+# IpCidrValidator
+#
+# Validates that an IP is a valid IPv4 or IPv6 CIDR address.
+#
+# Example:
+#
+# class Group < ActiveRecord::Base
+# validates :ip, presence: true, ip_cidr: true
+# end
+
+class IpCidrValidator < ActiveModel::EachValidator # rubocop:disable Gitlab/NamespacedClass -- This is a globally shareable validator, but it's unclear what namespace it should belong in
+ def validate_each(record, attribute, value)
+ # NOTE: We want this to be usable for nullable fields, so we don't validate presence.
+ # Use a separate `presence` validation for the field if needed.
+ return true if value.blank?
+
+ # rubocop:disable Layout/LineLength -- The error message is bigger than the line limit
+ unless valid_cidr_format?(value)
+ record.errors.add(
+ attribute,
+ format(_(
+ "IP '%{value}' is not a valid CIDR: IP should be followed by a slash followed by an integer subnet mask (for example: '192.168.1.0/24')"),
+ value: value
+ )
+ )
+ return
+ end
+ # rubocop:enable Layout/LineLength
+
+ IPAddress.parse(value)
+ rescue ArgumentError => e
+ record.errors.add(
+ attribute,
+ format(_("IP '%{value}' is not a valid CIDR: %{message}"), value: value, message: e.message)
+ )
+ end
+
+ private
+
+ def valid_cidr_format?(cidr)
+ cidr.count('/') == 1 && cidr.split('/').last =~ /^\d+$/
+ end
+end
diff --git a/app/validators/json_schemas/activity_pub_follow_payload.json b/app/validators/json_schemas/activity_pub_follow_payload.json
new file mode 100644
index 00000000000..1f453ce840f
--- /dev/null
+++ b/app/validators/json_schemas/activity_pub_follow_payload.json
@@ -0,0 +1,53 @@
+{
+ "description": "ActivityPub Follow activity payload",
+ "type": "object",
+ "required": [
+ "@context",
+ "id",
+ "type",
+ "actor",
+ "object"
+ ],
+ "properties": {
+ "@context": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array"
+ }
+ ]
+ },
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "actor": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "id": {
+ "type": "string"
+ },
+ "inbox": {
+ "type": "string"
+ },
+ "additionalProperties": true
+ }
+ ]
+ },
+ "object": {
+ "type": "string"
+ },
+ "additionalProperties": true
+ }
+}
diff --git a/app/validators/json_schemas/vulnerability_cvss_vectors.json b/app/validators/json_schemas/vulnerability_cvss_vectors.json
index 7ec1339e974..0da6de0a69d 100644
--- a/app/validators/json_schemas/vulnerability_cvss_vectors.json
+++ b/app/validators/json_schemas/vulnerability_cvss_vectors.json
@@ -9,14 +9,14 @@
"type": "string",
"default": "unknown"
},
- "vector_string": {
+ "vector": {
"type": "string",
"example": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
}
},
"required": [
"vendor",
- "vector_string"
+ "vector"
]
}
}
diff --git a/app/views/admin/abuse_reports/show.html.haml b/app/views/admin/abuse_reports/show.html.haml
index bd7a1054b5d..ff9ac6a052c 100644
--- a/app/views/admin/abuse_reports/show.html.haml
+++ b/app/views/admin/abuse_reports/show.html.haml
@@ -1,5 +1,6 @@
- add_to_breadcrumbs _('Abuse Reports'), admin_abuse_reports_path
- breadcrumb_title @abuse_report.user&.name
+- @content_class = "limit-container-width" unless fluid_layout
- page_title @abuse_report.user&.name, _('Abuse Reports')
#js-abuse-reports-detail-view{ data: abuse_report_data(@abuse_report) }
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 4e55c99e445..1d58b0106c4 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -24,12 +24,13 @@
%span.form-text.text-muted#session_expire_delay_help_block= _('Restart GitLab to apply changes.')
.form-group
= f.label :remember_me_enabled, _('Remember me'), class: 'label-light'
- - remember_me_help_link = help_page_path('user/profile/index.md', anchor: 'stay-signed-in-for-two-weeks')
+ - remember_me_help_link = help_page_path('user/profile/index', anchor: 'stay-signed-in-for-two-weeks')
- remember_me_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: remember_me_help_link }
= f.gitlab_ui_checkbox_component :remember_me_enabled, _('Allow users to extend their session'), help_text: _("Users can select 'Remember me' on sign-in to keep their session active beyond the session duration. %{link_start}Learn more.%{link_end}").html_safe % { link_start: remember_me_help_link_start, link_end: '</a>'.html_safe }
= render_if_exists 'admin/application_settings/git_two_factor_session_expiry', form: f
= render_if_exists 'admin/application_settings/personal_access_token_expiration_policy', form: f
+ = render_if_exists 'admin/application_settings/service_access_tokens_expiration_enforced', form: f
= render_if_exists 'admin/application_settings/ssh_key_expiration_policy', form: f
.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 c08270a8522..8092299fb61 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -4,7 +4,7 @@
%fieldset
.form-group
- - devops_help_link_url = help_page_path('topics/autodevops/index.md')
+ - devops_help_link_url = help_page_path('topics/autodevops/index')
- devops_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: devops_help_link_url }
= f.gitlab_ui_checkbox_component :auto_devops_enabled, s_('CICD|Default to Auto DevOps pipeline for all projects'), help_text: s_('CICD|The Auto DevOps pipeline runs by default in all projects with no CI/CD configuration file. %{link_start}What is Auto DevOps?%{link_end}').html_safe % { link_start: devops_help_link_start, link_end: '</a>'.html_safe }
.form-group
@@ -12,7 +12,7 @@
= f.text_field :auto_devops_domain, class: 'form-control gl-form-input', placeholder: 'example.com'
.form-text.text-muted
= s_("AdminSettings|The default domain to use for Auto Review Apps and Auto Deploy stages in all projects.")
- = link_to _('Learn more.'), help_page_path('topics/autodevops/stages.md', anchor: 'auto-review-apps'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('topics/autodevops/stages', anchor: 'auto-review-apps'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.gitlab_ui_checkbox_component :shared_runners_enabled, s_("AdminSettings|Enable shared runners for new projects"), help_text: s_("AdminSettings|All new projects can use the instance's shared runners by default.")
@@ -59,6 +59,8 @@
.form-group
= f.gitlab_ui_checkbox_component :suggest_pipeline_enabled, s_('AdminSettings|Enable pipeline suggestion banner'), help_text: s_('AdminSettings|Display a banner on merge requests in projects with no pipelines to initiate steps to add a .gitlab-ci.yml file.')
#js-runner-token-expiration-intervals{ data: runner_token_expiration_interval_attributes }
+ .form-group
+ = f.gitlab_ui_checkbox_component :enable_artifact_external_redirect_warning_page, s_('AdminSettings|Enable the external redirect warning page for job artifacts'), help_text: s_('AdminSettings|Show a redirect page that warns you about user-generated content in GitLab Pages.')
= f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_diagramsnet.html.haml b/app/views/admin/application_settings/_diagramsnet.html.haml
index 0cf44938881..0d44b38b0e0 100644
--- a/app/views/admin/application_settings/_diagramsnet.html.haml
+++ b/app/views/admin/application_settings/_diagramsnet.html.haml
@@ -7,7 +7,7 @@
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Render diagrams in your documents using diagrams.net.')
- = link_to _('Learn more.'), help_page_path('administration/integration/diagrams_net.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/integration/diagrams_net'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-diagramsnet-settings'), html: { class: 'fieldset-form', id: 'diagramsnet-settings' } do |f|
= form_errors(@application_setting) if expanded
diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml
index 2d45391a839..a9bc8ab9d32 100644
--- a/app/views/admin/application_settings/_email.html.haml
+++ b/app/views/admin/application_settings/_email.html.haml
@@ -10,7 +10,7 @@
= f.label :commit_email_hostname, _('Custom hostname (for private commit emails)'), class: 'label-bold'
= f.text_field :commit_email_hostname, class: 'form-control gl-form-input'
.form-text.text-muted
- - commit_email_hostname_docs_link = link_to _('Learn more'), help_page_path('administration/settings/email.md', anchor: 'custom-hostname-for-private-commit-emails'), target: '_blank', rel: 'noopener noreferrer'
+ - commit_email_hostname_docs_link = link_to _('Learn more'), help_page_path('administration/settings/email', anchor: 'custom-hostname-for-private-commit-emails'), target: '_blank', rel: 'noopener noreferrer'
= _("Hostname used in private commit emails. %{learn_more}").html_safe % { learn_more: commit_email_hostname_docs_link }
= render_if_exists 'admin/application_settings/email_additional_text_setting', form: f
diff --git a/app/views/admin/application_settings/_error_tracking.html.haml b/app/views/admin/application_settings/_error_tracking.html.haml
index 6754dd99bbc..ab4ed9917a0 100644
--- a/app/views/admin/application_settings/_error_tracking.html.haml
+++ b/app/views/admin/application_settings/_error_tracking.html.haml
@@ -7,8 +7,8 @@
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
- = _('Allows projects to track errors using an Opstrace integration.').html_safe % { link: help_page_path('operations/error_tracking.md') }
- = link_to _('Learn more.'), help_page_path('operations/error_tracking.md'), target: '_blank', rel: 'noopener noreferrer'
+ = _('Allows projects to track errors using an Opstrace integration.').html_safe % { link: help_page_path('operations/error_tracking') }
+ = link_to _('Learn more.'), help_page_path('operations/error_tracking'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
diff --git a/app/views/admin/application_settings/_floc.html.haml b/app/views/admin/application_settings/_floc.html.haml
index e1576e84e66..27df417d225 100644
--- a/app/views/admin/application_settings/_floc.html.haml
+++ b/app/views/admin/application_settings/_floc.html.haml
@@ -7,7 +7,7 @@
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
- - floc_link_url = help_page_path('administration/settings/floc.md')
+ - floc_link_url = help_page_path('administration/settings/floc')
- floc_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: floc_link_url }
= html_escape(s_('FloC|Configure whether you want to participate in FLoC. %{floc_link_start}What is FLoC?%{floc_link_end}')) % { floc_link_start: floc_link_start, floc_link_end: '</a>'.html_safe }
diff --git a/app/views/admin/application_settings/_gitlab_shell_operation_limits.html.haml b/app/views/admin/application_settings/_gitlab_shell_operation_limits.html.haml
index 64549b97bd1..22372146ea1 100644
--- a/app/views/admin/application_settings/_gitlab_shell_operation_limits.html.haml
+++ b/app/views/admin/application_settings/_gitlab_shell_operation_limits.html.haml
@@ -6,7 +6,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= s_('ShellOperations|Limit the number of Git operations a user can perform per minute, per repository.')
- = link_to _('Learn more.'), help_page_path('administration/settings/rate_limits_on_git_ssh_operations.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/rate_limits_on_git_ssh_operations'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-gitlab-shell-operation-limits-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
@@ -15,5 +15,5 @@
.form-group
= f.label :gitlab_shell_operation_limit, s_('ShellOperations|Maximum number of Git operations per minute'), class: 'gl-font-bold'
= f.number_field :gitlab_shell_operation_limit, class: 'form-control gl-form-input'
-
+ %span.form-text.text-muted= _('Set to 0 to disable the limit.')
= f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_gitpod.html.haml b/app/views/admin/application_settings/_gitpod.html.haml
index 1f56487cea4..ce8c390baa5 100644
--- a/app/views/admin/application_settings/_gitpod.html.haml
+++ b/app/views/admin/application_settings/_gitpod.html.haml
@@ -8,7 +8,7 @@
= expanded ? _('Collapse') : _('Expand')
.gl-text-secondary.gl-mb-5
#js-gitpod-settings-help-text{ data: {"message" => gitpod_enable_description, "message-url" => "https://gitpod.io/" } }
- = link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information')
+ = link_to sprite_icon('question-o'), help_page_path('integration/gitpod'), target: '_blank', class: 'has-tooltip', title: _('More information')
.settings-content
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-gitpod-settings'), html: { class: 'fieldset-form', id: 'gitpod-settings' } do |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 8cb7915f847..269a1497324 100644
--- a/app/views/admin/application_settings/_import_export_limits.html.haml
+++ b/app/views/admin/application_settings/_import_export_limits.html.haml
@@ -2,8 +2,7 @@
= form_errors(@application_setting)
%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 }
-
+ = html_escape(_("Set to 0 to disable the limits."))
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_kroki.html.haml b/app/views/admin/application_settings/_kroki.html.haml
index 4dbca235a73..0f1316996fa 100644
--- a/app/views/admin/application_settings/_kroki.html.haml
+++ b/app/views/admin/application_settings/_kroki.html.haml
@@ -7,7 +7,7 @@
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('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'
+ = link_to _('Learn more.'), help_page_path('administration/integration/kroki'), 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
diff --git a/app/views/admin/application_settings/_localization.html.haml b/app/views/admin/application_settings/_localization.html.haml
index 25038e6f221..62849a81633 100644
--- a/app/views/admin/application_settings/_localization.html.haml
+++ b/app/views/admin/application_settings/_localization.html.haml
@@ -7,11 +7,11 @@
= f.select :first_day_of_week, first_day_of_week_choices, {}, class: 'form-control'
.form-text.text-muted
= _('Default first day of the week in calendars and date pickers.')
- = link_to _('Learn more.'), help_page_path('administration/settings/localization.md', anchor: 'change-the-default-first-day-of-the-week'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/localization', anchor: 'change-the-default-first-day-of-the-week'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.label :time_tracking, _('Time tracking'), class: 'label-bold'
- - time_tracking_help_link = help_page_path('user/project/time_tracking.md')
+ - time_tracking_help_link = help_page_path('user/project/time_tracking')
- time_tracking_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: time_tracking_help_link }
= f.gitlab_ui_checkbox_component :time_tracking_limit_to_hours, _('Limit display of time tracking units to hours.'), help_text: _('Display time tracking in issues in total hours only. %{link_start}What is time tracking?%{link_end}').html_safe % { link_start: time_tracking_help_link_start, link_end: '</a>'.html_safe }
diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml
index f36fbd8d68c..a4ec3a31584 100644
--- a/app/views/admin/application_settings/_outbound.html.haml
+++ b/app/views/admin/application_settings/_outbound.html.haml
@@ -26,7 +26,7 @@
= f.text_area :outbound_local_requests_allowlist_raw, placeholder: "example.com, 192.168.1.1, xn--itlab-j1a.com", class: 'form-control gl-form-input', rows: 8
%span.form-text.text-muted
= s_('OutboundRequests|Requests can be made to these IP addresses and domains even when local requests are not allowed. IP ranges such as %{code_start}1:0:0:0:0:0:0:0/124%{code_end} and %{code_start}127.0.0.0/28%{code_end} are supported. Domain wildcards are not supported. To separate entries, use commas, semicolons, or newlines. The allowlist can have a maximum of 1000 entries. Domains must be IDNA-encoded.').html_safe % { code_start: '<code>'.html_safe, code_end: '</code>'.html_safe }
- = link_to _('Learn more.'), help_page_path('security/webhooks.md', anchor: 'allow-outbound-requests-to-certain-ip-addresses-and-domains'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('security/webhooks', anchor: 'allow-outbound-requests-to-certain-ip-addresses-and-domains'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.gitlab_ui_checkbox_component :dns_rebinding_protection_enabled,
diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml
index bfa548b70e5..14c785509bd 100644
--- a/app/views/admin/application_settings/_performance.html.haml
+++ b/app/views/admin/application_settings/_performance.html.haml
@@ -11,16 +11,16 @@
= f.label :raw_blob_request_limit, _('Raw blob request rate limit per minute'), class: 'label-bold'
= f.number_field :raw_blob_request_limit, class: 'form-control gl-form-input'
.form-text.text-muted
- = _('Maximum number of requests per minute for each raw path (default is `300`). Set to `0` to disable throttling.')
+ = _('Maximum number of requests per minute for each raw path (default is 300). Set to 0 to disable throttling.')
.form-group
= f.label :push_event_hooks_limit, class: 'label-bold'
= f.number_field :push_event_hooks_limit, class: 'form-control gl-form-input'
.form-text.text-muted
- = _('Maximum number of changes (branches or tags) in a single push above which webhooks and integrations are not triggered (default is `3`). Setting to `0` does not disable throttling.')
+ = _('Maximum number of changes (branches or tags) in a single push above which webhooks and integrations are not triggered (default is 3). Setting to 0 does not disable throttling.')
.form-group
= f.label :push_event_activities_limit, class: 'label-bold'
= f.number_field :push_event_activities_limit, class: 'form-control gl-form-input'
.form-text.text-muted
- = _('Maximum number of changes (branches or tags) in a single push above which a bulk push event is created (default is `3`). Setting to `0` does not disable throttling.')
+ = _('Maximum number of changes (branches or tags) in a single push above which a bulk push event is created (default is 3). Setting to 0 does not disable throttling.')
= f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml
index a8b758f7324..c673bf72397 100644
--- a/app/views/admin/application_settings/_plantuml.html.haml
+++ b/app/views/admin/application_settings/_plantuml.html.haml
@@ -7,7 +7,7 @@
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Render diagrams in your documents using PlantUML.')
- = link_to _('Learn more.'), help_page_path('administration/integration/plantuml.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/integration/plantuml'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-plantuml-settings'), html: { class: 'fieldset-form', id: 'plantuml-settings' } do |f|
= form_errors(@application_setting) if expanded
diff --git a/app/views/admin/application_settings/_projects_api_limits.html.haml b/app/views/admin/application_settings/_projects_api_limits.html.haml
index dde8ab07958..c9eff76916a 100644
--- a/app/views/admin/application_settings/_projects_api_limits.html.haml
+++ b/app/views/admin/application_settings/_projects_api_limits.html.haml
@@ -6,7 +6,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set the per-IP address rate limit applicable to unauthenticated requests for getting a list of projects via the API.')
- = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_projects_api.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_projects_api'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-projects-api-limits-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
@@ -16,6 +16,6 @@
= f.label :projects_api_rate_limit_unauthenticated, _('Maximum requests per 10 minutes per IP address'), class: 'label-bold'
= f.number_field :projects_api_rate_limit_unauthenticated, class: 'form-control gl-form-input'
.form-text.gl-text-gray-600
- = _("Set this number to 0 to disable the limit.")
+ = _("Set to 0 to disable the limit.")
= f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml
index 5751ae9059a..cb1a0a40566 100644
--- a/app/views/admin/application_settings/_repository_check.html.haml
+++ b/app/views/admin/application_settings/_repository_check.html.haml
@@ -21,7 +21,7 @@
%h4= _("Housekeeping")
.form-group
- help_text = _("Run housekeeping tasks to automatically optimize Git repositories. Disabling this option will cause performance to degenerate over time.")
- - help_link = link_to _('Learn more.'), help_page_path('administration/housekeeping.md', anchor: 'heuristical-housekeeping'), target: '_blank', rel: 'noopener noreferrer'
+ - help_link = link_to _('Learn more.'), help_page_path('administration/housekeeping', anchor: 'heuristical-housekeeping'), target: '_blank', rel: 'noopener noreferrer'
= f.gitlab_ui_checkbox_component :housekeeping_enabled,
_("Enable automatic repository housekeeping"),
help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
index 066d77c792b..412098cfae4 100644
--- a/app/views/admin/application_settings/_repository_storage.html.haml
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -5,7 +5,7 @@
.sub-section
%h4= _('Hashed repository storage paths')
.form-group
- - repository_storage_help_link_url = help_page_path('administration/repository_storage_types.md')
+ - repository_storage_help_link_url = help_page_path('administration/repository_storage_types')
- repository_storage_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: repository_storage_help_link_url }
= f.gitlab_ui_checkbox_component :hashed_storage_enabled,
_('Use hashed storage'),
@@ -17,10 +17,10 @@
.form-group
.form-text
%p.text-secondary
- - weights_link_url = help_page_path('administration/repository_storage_paths.md', anchor: 'configure-where-new-repositories-are-stored')
+ - weights_link_url = help_page_path('administration/repository_storage_paths', anchor: 'configure-where-new-repositories-are-stored')
- weights_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: weights_link_url }
= html_escape(s_('Enter %{weights_link_start}weights%{weights_link_end} for storages for new repositories. Configured storages appear below.')) % { weights_link_start: weights_link_start, weights_link_end: '</a>'.html_safe }
- = link_to _('Learn more.'), help_page_path('administration/repository_storage_paths.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/repository_storage_paths'), target: '_blank', rel: 'noopener noreferrer'
.form-check
= f.fields_for :repository_storages_weighted, storage_weights do |storage_form|
- Gitlab.config.repositories.storages.each_key do |storage|
diff --git a/app/views/admin/application_settings/_runner_registrars_form.html.haml b/app/views/admin/application_settings/_runner_registrars_form.html.haml
index b112c273aad..36bab2f6650 100644
--- a/app/views/admin/application_settings/_runner_registrars_form.html.haml
+++ b/app/views/admin/application_settings/_runner_registrars_form.html.haml
@@ -7,7 +7,7 @@
= s_('Runners|Runner version management')
%span.form-text.gl-mb-3.gl-mt-0
- help_text = s_('Runners|Official runner version data is periodically fetched from GitLab.com to determine whether the runners need upgrades.')
- - learn_more_link = link_to _('Learn more.'), help_page_path('ci/runners/runners_scope.md', anchor: 'determine-which-runners-need-to-be-upgraded'), target: '_blank', rel: 'noopener noreferrer'
+ - learn_more_link = link_to _('Learn more.'), help_page_path('ci/runners/runners_scope', anchor: 'determine-which-runners-need-to-be-upgraded'), target: '_blank', rel: 'noopener noreferrer'
= f.gitlab_ui_checkbox_component :update_runner_versions_enabled,
s_('Runners|Fetch GitLab Runner release version data from GitLab.com'),
help_text: '%{help_text} %{learn_more_link}'.html_safe % { help_text: help_text, learn_more_link: learn_more_link }
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index 5518122b5cf..0f20864fc68 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -19,7 +19,7 @@
.form-group
= f.label :two_factor_authentication, _('Two-factor authentication'), class: 'label-bold'
- help_text = _('Enforce two-factor authentication for all user sign-ins.')
- - help_link = link_to _('Learn more.'), help_page_path('security/two_factor_authentication.md'), target: '_blank', rel: 'noopener noreferrer'
+ - help_link = link_to _('Learn more.'), help_page_path('security/two_factor_authentication'), target: '_blank', rel: 'noopener noreferrer'
= f.gitlab_ui_checkbox_component :require_two_factor_authentication,
_('Enforce two-factor authentication'),
help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
@@ -39,7 +39,7 @@
.form-group
= f.label :unknown_sign_in, _('Email notification for unknown sign-ins'), class: 'label-bold'
- help_text = _('Notify users by email when sign-in location is not recognized.')
- - help_link = link_to _('Learn more.'), help_page_path('user/profile/notifications.md', anchor: 'notifications-for-unknown-sign-ins'), target: '_blank', rel: 'noopener noreferrer'
+ - help_link = link_to _('Learn more.'), help_page_path('user/profile/notifications', anchor: 'notifications-for-unknown-sign-ins'), target: '_blank', rel: 'noopener noreferrer'
= f.gitlab_ui_checkbox_component :notify_on_unknown_sign_in,
_('Enable email notification'),
help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
diff --git a/app/views/admin/application_settings/_sourcegraph.html.haml b/app/views/admin/application_settings/_sourcegraph.html.haml
index 61ec841bb83..e61947e3cff 100644
--- a/app/views/admin/application_settings/_sourcegraph.html.haml
+++ b/app/views/admin/application_settings/_sourcegraph.html.haml
@@ -12,7 +12,7 @@
- link_end = "#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}</a>".html_safe
= s_('SourcegraphAdmin|Enable code intelligence powered by %{link_start}Sourcegraph%{link_end} on your GitLab instance\'s code views and merge requests.').html_safe % { link_start: link_start, link_end: link_end }
%span
- = link_to s_('SourcegraphAdmin|Learn more.'), help_page_path('integration/sourcegraph.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to s_('SourcegraphAdmin|Learn more.'), help_page_path('integration/sourcegraph'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml
index abc7abe92ad..4e21717a4e6 100644
--- a/app/views/admin/application_settings/_spam.html.haml
+++ b/app/views/admin/application_settings/_spam.html.haml
@@ -8,7 +8,7 @@
= _('reCAPTCHA helps prevent credential stuffing.')
= link_to _('Only reCAPTCHA v2 is supported:'), 'https://developers.google.com/recaptcha/docs/versions', target: '_blank', rel: 'noopener noreferrer'
.form-group
- - spam_help_link_url = help_page_path('integration/recaptcha.md')
+ - spam_help_link_url = help_page_path('integration/recaptcha')
- spam_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: spam_help_link_url }
= f.gitlab_ui_checkbox_component :recaptcha_enabled, _("Enable reCAPTCHA"),
help_text: _('Helps prevent bots from creating accounts. %{link_start}How do I configure it?%{link_end}').html_safe % { link_start: spam_help_link_start, link_end: '</a>'.html_safe }
@@ -40,7 +40,7 @@
= _('Akismet')
%p
= _('Akismet helps prevent the creation of spam issues in public projects.')
- = link_to _('How do I configure Akismet?'), help_page_path('integration/akismet.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('How do I configure Akismet?'), help_page_path('integration/akismet'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.gitlab_ui_checkbox_component :akismet_enabled, _('Enable Akismet'),
diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml
index 8da441d5245..2afcb26b43b 100644
--- a/app/views/admin/application_settings/_terms.html.haml
+++ b/app/views/admin/application_settings/_terms.html.haml
@@ -10,5 +10,5 @@
= f.text_area :terms, class: 'form-control gl-form-input', rows: 8
.form-text.text-muted
= _("Markdown supported.")
- = link_to _('What is Markdown?'), help_page_path('user/markdown.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('What is Markdown?'), help_page_path('user/markdown'), target: '_blank', rel: 'noopener noreferrer'
= f.submit _("Save changes"), pajamas_button: true
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
index 2d51dc2a6f2..dd9820d064a 100644
--- a/app/views/admin/application_settings/_usage.html.haml
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -13,18 +13,30 @@
.form-group
- can_be_configured = @application_setting.usage_ping_can_be_configured?
- service_ping_link_start = link_start % { url: help_page_path('development/internal_analytics/service_ping/index') }
- - deactivating_service_ping_link_start = link_start % { url: help_page_path('administration/settings/usage_statistics', anchor: 'disable-usage-statistics-with-the-configuration-file') }
+ - deactivating_service_ping_link_start = link_start % { url: help_page_path('administration/settings/usage_statistics', anchor: 'through-the-configuration-file') }
- usage_ping_help_text = s_('AdminSettings|To help improve GitLab and its user experience, GitLab periodically collects usage information. %{link_start}What information is shared with GitLab Inc.?%{link_end}').html_safe % { link_start: service_ping_link_start, link_end: link_end }
- disabled_help_text = s_('AdminSettings|Service ping is disabled in your configuration file, and cannot be enabled through this form. For more information, see the documentation on %{link_start}deactivating service ping%{link_end}.').html_safe % { link_start: deactivating_service_ping_link_start, link_end: link_end }
= f.gitlab_ui_checkbox_component :usage_ping_enabled, s_('AdminSettings|Enable Service Ping'),
help_text: can_be_configured ? usage_ping_help_text : disabled_help_text,
checkbox_options: { disabled: !can_be_configured, data: { testid: 'enable-usage-data-checkbox' } }
.form-text.gl-pl-6
- - if can_be_configured
- = 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) } }
+ - if @service_ping_data.present?
+ = 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= s_('AdminSettings|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= s_('AdminSettings|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,
+ dismissible: false,
+ title: s_('AdminSettings|Service Ping payload not found in the application cache')) do |c|
+
+ - c.with_body do
+ - generate_manually_link = link_to('', help_page_path('development/internal_analytics/service_ping/troubleshooting', anchor: 'generate-service-ping'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(s_('AdminSettings|%{generate_manually_link_start}Generate%{generate_manually_link_end} Service Ping to preview and download service usage data payload.'), tag_pair(generate_manually_link, :generate_manually_link_start, :generate_manually_link_end))
+
.form-group
- usage_ping_enabled = @application_setting.usage_ping_enabled?
- label = s_('AdminSettings|Enable Registration Features')
diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml
index dad0bf08bb0..d84fbe94f65 100644
--- a/app/views/admin/application_settings/general.html.haml
+++ b/app/views/admin/application_settings/general.html.haml
@@ -66,7 +66,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set sign-in restrictions for all users.')
- = link_to _('Learn more.'), help_page_path('administration/settings/sign_in_restrictions.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/sign_in_restrictions'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'signin'
@@ -78,7 +78,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Add a Terms of Service agreement and Privacy Policy for users of this GitLab instance.')
- = link_to _('Learn more.'), help_page_path('administration/settings/terms.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/terms'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'terms'
@@ -95,7 +95,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set the maximum session time for a web terminal.')
- = link_to _('How do I use a web terminal?'), help_page_path('ci/environments/index.md', anchor: 'web-terminals-deprecated'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('How do I use a web terminal?'), help_page_path('ci/environments/index', anchor: 'web-terminals-deprecated'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'terminal'
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 188359158ef..23f536bd6d4 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -24,7 +24,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Link to your Grafana instance.')
- = link_to _('Learn more.'), help_page_path('administration/monitoring/performance/grafana_configuration.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/monitoring/performance/grafana_configuration'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'grafana'
@@ -37,11 +37,11 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Enable access to the performance bar for non-administrators in a given group.')
- = link_to _('Learn more.'), help_page_path('administration/monitoring/performance/performance_bar.md', anchor: 'enable-the-performance-bar-for-non-administrators'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/monitoring/performance/performance_bar', anchor: 'enable-the-performance-bar-for-non-administrators'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'performance_bar'
-%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'usage_statistics_settings_content' } }
+%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded_by_default?), data: { testid: 'usage-statistics-settings-content' } }
.settings-header#usage-statistics
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Usage statistics')
@@ -53,7 +53,7 @@
= render 'usage'
- if Feature.enabled?(:configure_sentry_in_application_settings)
- %section.settings.as-sentry.no-animate#js-sentry-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'sentry_settings_content' } }
+ %section.settings.as-sentry.no-animate#js-sentry-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Sentry')
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
index 849c5c749e0..ae5f7a5cec3 100644
--- a/app/views/admin/application_settings/network.html.haml
+++ b/app/views/admin/application_settings/network.html.haml
@@ -22,11 +22,11 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set limits for web and API requests.')
- = link_to _('Learn more.'), help_page_path('administration/settings/user_and_ip_rate_limits.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/user_and_ip_rate_limits'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'ip_limits'
-%section.settings.as-packages-limits.no-animate#js-packages-limits-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'packages_limits_content' } }
+%section.settings.as-packages-limits.no-animate#js-packages-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Package registry rate limits')
@@ -34,7 +34,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set rate limits for package registry API requests that supersede the general user and IP rate limits.')
- = link_to _('Learn more.'), help_page_path('administration/settings/package_registry_rate_limits.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/package_registry_rate_limits'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render partial: 'network_rate_limits', locals: { anchor: 'js-packages-limits-settings', setting_fragment: 'packages_api' }
@@ -68,11 +68,11 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Configure specific limits for deprecated API requests that supersede the general user and IP rate limits.')
- = link_to _('Which API requests are affected?'), help_page_path('administration/settings/deprecated_api_rate_limits.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Which API requests are affected?'), help_page_path('administration/settings/deprecated_api_rate_limits'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render partial: 'network_rate_limits', locals: { anchor: 'js-deprecated-limits-settings', setting_fragment: 'deprecated_api' }
-%section.settings.as-git-lfs-limits.no-animate#js-git-lfs-limits-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'git_lfs_limits_content' } }
+%section.settings.as-git-lfs-limits.no-animate#js-git-lfs-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Git LFS Rate Limits')
@@ -80,7 +80,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Configure specific limits for Git LFS requests that supersede the general user and IP rate limits.')
- = link_to _('Learn more.'), help_page_path('administration/settings/git_lfs_rate_limits.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/git_lfs_rate_limits'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'git_lfs_limits'
@@ -96,7 +96,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= s_('OutboundRequests|Allow requests to the local network from hooks and integrations.')
- = link_to _('Learn more.'), help_page_path('security/webhooks.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('security/webhooks'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'outbound'
@@ -108,7 +108,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Rate limit access to specified paths.')
- = link_to _('Learn more.'), help_page_path('administration/settings/protected_paths.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/protected_paths'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'protected_paths'
@@ -121,7 +121,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Limit the number of issues and epics per minute a user can create through web and API requests.')
- = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_issues_creation.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_issues_creation'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'issue_limits'
@@ -133,7 +133,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set the per-user rate limit for notes created by web or API requests.')
- = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_notes_creation.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_notes_creation'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'note_limits'
@@ -145,7 +145,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set the per-user rate limit for getting a user by ID via the API.')
- = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_users_api.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_users_api'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'users_api_limits'
@@ -159,7 +159,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set per-user rate limits for imports and exports of projects and groups.')
- = link_to _('Learn more.'), help_page_path('administration/settings/import_export_rate_limits.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/import_export_rate_limits'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'import_export_limits'
@@ -171,7 +171,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Limit the number of pipeline creation requests per minute. This limit includes pipelines created through the UI, the API, and by background processing.')
- = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_pipelines_creation.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_pipelines_creation'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'pipeline_limits'
diff --git a/app/views/admin/application_settings/preferences.html.haml b/app/views/admin/application_settings/preferences.html.haml
index 4590b6f4586..3543e1d918a 100644
--- a/app/views/admin/application_settings/preferences.html.haml
+++ b/app/views/admin/application_settings/preferences.html.haml
@@ -33,7 +33,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Additional text for the sign-in and Help page.')
- = link_to _('Learn more.'), help_page_path('administration/settings/help_page.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/help_page'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'help_page'
@@ -56,7 +56,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Adjust how frequently the GitLab UI polls for updates.')
- = link_to _('Learn more.'), help_page_path('administration/polling.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/polling'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'realtime'
@@ -69,7 +69,7 @@
%p.gl-text-secondary
= _('Configure Gitaly timeouts.')
%span
- = link_to _('Learn more.'), help_page_path('administration/settings/gitaly_timeouts.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/gitaly_timeouts'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'gitaly'
@@ -93,7 +93,7 @@
%p.gl-text-secondary
= _('Limit the size of Sidekiq jobs stored in Redis.')
%span
- = link_to _('Learn more.'), help_page_path('administration/settings/sidekiq_job_limits.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/sidekiq_job_limits'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'sidekiq_job_limits'
@@ -106,6 +106,6 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= s_('TerraformLimits|Limits for Terraform features')
- = link_to s_('TerraformLimits|Learn more about Terraform limits.'), help_page_path('administration/settings/terraform_limits.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to s_('TerraformLimits|Learn more about Terraform limits.'), help_page_path('administration/settings/terraform_limits'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'terraform_limits'
diff --git a/app/views/admin/application_settings/reporting.html.haml b/app/views/admin/application_settings/reporting.html.haml
index 91fabb505c2..49279c4584b 100644
--- a/app/views/admin/application_settings/reporting.html.haml
+++ b/app/views/admin/application_settings/reporting.html.haml
@@ -25,7 +25,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Receive notification of abuse reports by email.')
- = link_to _('Learn more.'), help_page_path('administration/review_abuse_reports.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/review_abuse_reports'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'abuse'
diff --git a/app/views/admin/application_settings/repository.html.haml b/app/views/admin/application_settings/repository.html.haml
index c7a2fca00ef..0b31da36804 100644
--- a/app/views/admin/application_settings/repository.html.haml
+++ b/app/views/admin/application_settings/repository.html.haml
@@ -22,11 +22,11 @@
= expanded_by_default? ? 'Collapse' : 'Expand'
%p.gl-text-secondary
= _('Configure repository mirroring.')
- = link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render partial: 'repository_mirrors_form'
-%section.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'repository_storage_settings_content' } }
+%section.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Repository storage')
@@ -34,7 +34,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Configure repository storage.')
- = link_to _('Learn more.'), help_page_path('administration/repository_storage_paths.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/repository_storage_paths'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'repository_storage'
@@ -45,9 +45,9 @@
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
- - repository_checks_link_url = help_page_path('administration/repository_checks.md')
+ - repository_checks_link_url = help_page_path('administration/repository_checks')
- repository_checks_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: repository_checks_link_url }
- - housekeeping_link_url = help_page_path('administration/housekeeping.md')
+ - housekeeping_link_url = help_page_path('administration/housekeeping')
- housekeeping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: housekeeping_link_url }
= html_escape(s_('Configure %{repository_checks_link_start}repository checks%{link_end} and %{housekeeping_link_start}housekeeping%{link_end} on repositories.')) % { repository_checks_link_start: repository_checks_link_start, housekeeping_link_start: housekeeping_link_start, link_end: '</a>'.html_safe }
.settings-content
@@ -61,6 +61,6 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Serve repository static objects (for example, archives and blobs) from external storage.')
- = link_to _('Learn more.'), help_page_path('administration/static_objects_external_storage.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/static_objects_external_storage'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'repository_static_objects'
diff --git a/app/views/admin/application_settings/service_usage_data.html.haml b/app/views/admin/application_settings/service_usage_data.html.haml
deleted file mode 100644
index 9f73099465c..00000000000
--- a/app/views/admin/application_settings/service_usage_data.html.haml
+++ /dev/null
@@ -1,29 +0,0 @@
-- name = _("Service usage data")
-
-- breadcrumb_title name
-- page_title name
-- add_page_specific_style 'page_bundles/settings'
-- payload_class = 'js-service-ping-payload'
-- @force_desktop_expanded_sidebar = true
-
-%section.js-search-settings-section
- %h3= name
-
- - if @service_ping_data_present
- = 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,
- dismissible: false,
- title: _('Service Ping payload not found in the application cache')) do |c|
-
- - c.with_body do
- - enable_service_ping_link = link_to('', help_page_path('administration/settings/usage_statistics', anchor: 'enable-or-disable-usage-statistics'), target: '_blank', rel: 'noopener noreferrer')
- - generate_manually_link = link_to('', help_page_path('development/internal_analytics/service_ping/troubleshooting', anchor: 'generate-service-ping'), target: '_blank', rel: 'noopener noreferrer')
-
- = safe_format(s_('%{enable_service_ping_link_start}Enable%{enable_service_ping_link_end} or %{generate_manually_link_start}generate%{generate_manually_link_end} Service Ping to preview and download service usage data payload.'), tag_pair(enable_service_ping_link, :enable_service_ping_link_start, :enable_service_ping_link_end), tag_pair(generate_manually_link, :generate_manually_link_start, :generate_manually_link_end))
diff --git a/app/views/admin/background_migrations/index.html.haml b/app/views/admin/background_migrations/index.html.haml
index 9550ea2884e..e7212f00e5b 100644
--- a/app/views/admin/background_migrations/index.html.haml
+++ b/app/views/admin/background_migrations/index.html.haml
@@ -1,7 +1,7 @@
- page_title s_('BackgroundMigrations|Background Migrations')
- @breadcrumb_link = admin_background_migrations_path(database: params[:database])
-.gl-display-flex.gl-sm-flex-direction-column.gl-sm-align-items-flex-end.gl-pb-5.gl-border-b-1.gl-border-b-solid.gl-border-b-gray-100
+.gl-display-flex.gl-flex-direction-column.gl-md-flex-direction-row.gl-sm-align-items-flex-end.gl-pb-5.gl-border-b-1.gl-border-b-solid.gl-border-b-gray-100
.gl-flex-grow-1
%h3= s_('BackgroundMigrations|Background Migrations')
%p.light.gl-mb-0
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 4973c0f985c..bf00fbfd81d 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -89,7 +89,7 @@
= feature_entry(_('LDAP'),
enabled: Gitlab.config.ldap.enabled,
- doc_href: help_page_path('administration/auth/ldap/index.md'))
+ doc_href: help_page_path('administration/auth/ldap/index'))
= feature_entry(_('Gravatar'),
href: general_admin_application_settings_path(anchor: 'js-account-settings'),
diff --git a/app/views/admin/dev_ops_report/_score.html.haml b/app/views/admin/dev_ops_report/_score.html.haml
index a504563ad91..59cb30e8447 100644
--- a/app/views/admin/dev_ops_report/_score.html.haml
+++ b/app/views/admin/dev_ops_report/_score.html.haml
@@ -1,6 +1,6 @@
- service_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
- if !service_ping_enabled
- #js-devops-service-ping-disabled{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_service_ping_path: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/internal_analytics/service_ping/index.md') } }
+ #js-devops-service-ping-disabled{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_service_ping_path: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/internal_analytics/service_ping/index') } }
- else
#js-devops-score{ data: { devops_score_metrics: devops_score_metrics(@metric).to_json, no_data_image_path: image_path('dev_ops_report_no_data.svg'), devops_score_intro_image_path: image_path('dev_ops_report_overview.svg') } }
diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml
index 6aed8508a6a..878692438d4 100644
--- a/app/views/admin/spam_logs/_spam_log.html.haml
+++ b/app/views/admin/spam_logs/_spam_log.html.haml
@@ -29,7 +29,7 @@
variant: :danger,
method: :delete,
href: admin_spam_log_path(spam_log, remove_user: true),
- button_options: { data: { confirm: _("USER %{user_name} WILL BE REMOVED! Are you sure?") % { user_name: user.name }, confirm_btn_variant: 'danger' }, aria: { label: _('Remove user') } }) do
+ button_options: { data: { confirm: _("User %{user_name} will be removed! Are you sure?") % { user_name: user.name }, confirm_btn_variant: 'danger' }, aria: { label: _('Remove user') } }) do
= _('Remove user')
%td
-# TODO: Remove conditonal once spamcheck supports this https://gitlab.com/gitlab-com/gl-security/engineering-and-research/automation-team/spam/spamcheck/-/issues/190
@@ -48,11 +48,23 @@
= render Pajamas::ButtonComponent.new(size: :small,
method: :put,
href: block_admin_user_path(user),
- button_options: { class: 'gl-mb-3', data: {confirm: _('USER WILL BE BLOCKED! Are you sure?')} }) do
+ button_options: { class: 'gl-mb-3', data: {confirm: _('User will be blocked! Are you sure?')} }) do
= _('Block user')
- else
= render Pajamas::ButtonComponent.new(size: :small, button_options: { class: 'disabled gl-mb-3'}) do
= _("Already blocked")
+ - if user && !user.trusted?
+ = render Pajamas::ButtonComponent.new(size: :small,
+ method: :put,
+ href: trust_admin_user_path(user),
+ button_options: { class: 'gl-mb-3', data: {confirm: _('User will be allowed to create possible spam! Are you sure?')} }) do
+ = _('Trust user')
+ - else
+ = render Pajamas::ButtonComponent.new(size: :small,
+ method: :put,
+ href: untrust_admin_user_path(user),
+ button_options: { class: 'gl-mb-3', data: {confirm: _('User will not be allowed to create possible spam! Are you sure?')} }) do
+ = _('Untrust user')
= render Pajamas::ButtonComponent.new(size: :small,
method: :delete,
href: [:admin, spam_log],
diff --git a/app/views/admin/topics/_form.html.haml b/app/views/admin/topics/_form.html.haml
index 2638e45c9eb..c61be1182e0 100644
--- a/app/views/admin/topics/_form.html.haml
+++ b/app/views/admin/topics/_form.html.haml
@@ -4,7 +4,7 @@
.form-group
= f.label :name do
= _("Topic slug (name)")
- = f.text_field :name, placeholder: _('my-topic'), class: 'form-control input-lg', data: { qa_selector: 'topic_name_field' },
+ = f.text_field :name, placeholder: _('my-topic'), class: 'form-control input-lg',
required: true,
title: _('Please fill in a name for your topic.'),
autofocus: true
@@ -12,7 +12,7 @@
.form-group
= f.label :title do
= _("Topic title")
- = f.text_field :title, placeholder: _('My topic'), class: 'form-control input-lg', data: { qa_selector: 'topic_title_field' },
+ = f.text_field :title, placeholder: _('My topic'), class: 'form-control input-lg',
required: true,
title: _('Please fill in a title for your topic.')
diff --git a/app/views/admin/topics/_topic.html.haml b/app/views/admin/topics/_topic.html.haml
index 3e8a023ec9f..4e8b1394e06 100644
--- a/app/views/admin/topics/_topic.html.haml
+++ b/app/views/admin/topics/_topic.html.haml
@@ -1,7 +1,7 @@
- topic = local_assigns.fetch(:topic)
- title = topic.title || topic.name
-%li.topic-row.gl-py-3.gl-align-items-center{ class: 'gl-display-flex!', data: { qa_selector: 'topic_row_content' } }
+%li.topic-row.gl-py-3.gl-align-items-center{ class: 'gl-display-flex!' }
= render Pajamas::AvatarComponent.new(topic, size: 32, alt: '')
.gl-min-w-0.gl-flex-grow-1.gl-ml-3
diff --git a/app/views/admin/topics/index.html.haml b/app/views/admin/topics/index.html.haml
index 6d64fa1983f..46c1b9ac5c4 100644
--- a/app/views/admin/topics/index.html.haml
+++ b/app/views/admin/topics/index.html.haml
@@ -6,7 +6,7 @@
= form_tag admin_topics_path, method: :get do |f|
- search = params.fetch(:search, nil)
.search-field-holder
- = search_field_tag :search, search, class: "form-control gl-form-input search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: _('Search by name'), data: { qa_selector: 'topic_search_field' }
+ = search_field_tag :search, search, class: "form-control gl-form-input search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: _('Search by name')
= sprite_icon('search', css_class: 'search-icon')
.gl-flex-grow-1
.js-merge-topics{ data: { path: merge_admin_topics_path } }
diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml
index d4a9009a0cf..bbb068c3680 100644
--- a/app/views/admin/users/_users.html.haml
+++ b/app/views/admin/users/_users.html.haml
@@ -44,6 +44,9 @@
= gl_tab_link_to admin_users_path(filter: "wop"), { item_active: active_when(params[:filter] == 'wop'), class: 'gl-border-0!' } do
= s_('AdminUsers|Without projects')
= gl_tab_counter_badge(limited_counter_with_delimiter(User.without_projects))
+ = gl_tab_link_to admin_users_path(filter: "trusted"), { item_active: active_when(params[:filter] == 'trusted'), class: 'gl-border-0!' } do
+ = s_('AdminUsers|Trusted')
+ = gl_tab_counter_badge(limited_counter_with_delimiter(User.trusted))
.nav-controls
= render_if_exists 'admin/users/admin_email_users'
= render_if_exists 'admin/users/admin_export_user_permissions'
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index fa89c3d4b4f..bbf1e3b0b2f 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -1,3 +1,4 @@
+-# rubocop: disable CodeReuse/ActiveRecord
- add_to_breadcrumbs _("Users"), admin_users_path
- breadcrumb_title @user.name
- page_title _("Groups and projects"), @user.name, _("Users")
@@ -9,7 +10,7 @@
= _('Groups')
- c.with_body do
%ul.hover-list
- - @user.group_members.includes(:source).each do |group_member| # rubocop: disable CodeReuse/ActiveRecord
+ - @user.group_members.includes(:source).find_each do |group_member|
- group = group_member.group
%li.group_member
%strong= link_to group.name, admin_group_path(group)
@@ -50,3 +51,4 @@
- if member.respond_to? :project
= link_button_to nil, project_project_member_path(project, member), data: { confirm: remove_member_message(member), confirm_btn_variant: 'danger' }, aria: { label: _('Remove') }, remote: true, method: :delete, class: 'gl-ml-3', title: _('Remove user from project'), variant: :danger, size: :small, icon: 'remove'
+-# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml
deleted file mode 100644
index e3b409dea76..00000000000
--- a/app/views/ci/status/_badge.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-- status = local_assigns.fetch(:status)
-- link = local_assigns.fetch(:link, true)
-- title = local_assigns.fetch(:title, nil)
-- css_classes = "gl-display-inline-flex gl-align-items-center gl-gap-2 gl-line-height-0 gl-px-3 gl-py-2 gl-rounded-base ci-status ci-#{status.group} #{'has-tooltip' if title.present?}"
-
-- if link && status.has_details?
- = link_to status.details_path, class: css_classes, title: title, data: { html: title.present? } do
- = sprite_icon(status.icon)
- = status.text
-- else
- %span{ class: css_classes, title: title, data: { html: title.present? } }
- = sprite_icon(status.icon)
- = status.text
diff --git a/app/views/ci/status/_icon.html.haml b/app/views/ci/status/_icon.html.haml
index 9fa5734d6b6..bcb83874bfb 100644
--- a/app/views/ci/status/_icon.html.haml
+++ b/app/views/ci/status/_icon.html.haml
@@ -1,10 +1,7 @@
- status = local_assigns.fetch(:status)
-- tooltip_placement = local_assigns.fetch(:tooltip_placement, "left")
- path = local_assigns.fetch(:path, status.has_details? ? status.details_path : nil)
-- option_css_classes = local_assigns.fetch(:option_css_classes, '')
-- css_classes = "gl-px-2 #{option_css_classes}"
-- ci_css_classes = "ci-status-link ci-status-icon ci-status-icon-#{status.group} gl-line-height-1"
-- title = s_("PipelineStatusTooltip|Pipeline: %{ci_status}") % {ci_status: status.label}
+- tooltip_placement = local_assigns.fetch(:tooltip_placement, "left")
+- option_css_classes = local_assigns.fetch(:option_css_classes, nil)
+- show_status_text = local_assigns.fetch(:show_status_text, false)
-= gl_badge_tag(variant: badge_variant(status), size: :md, href: path, class: css_classes, title: title, data: { toggle: 'tooltip', placement: tooltip_placement, testid: "ci-status-badge" }) do
- = content_tag :span, sprite_icon(status.icon, size: 16), class: ci_css_classes
+= render_ci_icon(status, path, tooltip_placement: tooltip_placement, option_css_classes: option_css_classes, show_status_text: show_status_text)
diff --git a/app/views/clusters/clusters/_advanced_settings.html.haml b/app/views/clusters/clusters/_advanced_settings.html.haml
index a818f8a5c26..57111dd6232 100644
--- a/app/views/clusters/clusters/_advanced_settings.html.haml
+++ b/app/views/clusters/clusters/_advanced_settings.html.haml
@@ -30,7 +30,7 @@
selected: @cluster.management_project_id } }
%p.text-muted.gl-mt-n5
= html_escape(s_('ClusterIntegration|A cluster management project can be used to run deployment jobs with Kubernetes %{code_open}cluster-admin%{code_close} privileges.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- = link_to _('More information'), help_page_path('user/clusters/management_project.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('More information'), help_page_path('user/clusters/management_project'), target: '_blank', rel: 'noopener noreferrer'
= field.submit _('Save changes'), pajamas_button: true
.sub-section.form-group
diff --git a/app/views/clusters/clusters/_deprecation_alert.html.haml b/app/views/clusters/clusters/_deprecation_alert.html.haml
index 4f35ba78cc6..cfc3418b1b5 100644
--- a/app/views/clusters/clusters/_deprecation_alert.html.haml
+++ b/app/views/clusters/clusters/_deprecation_alert.html.haml
@@ -2,6 +2,6 @@
- c.with_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' }
- - docs_link_start = link_start % { url: help_page_path('user/clusters/agent/index.md') }
+ - docs_link_start = link_start % { url: help_page_path('user/clusters/agent/index') }
- link_end = '</a>'.html_safe
= s_('ClusterIntegration|This process is %{issue_link_start}deprecated%{issue_link_end}. Use the %{docs_link_start}the GitLab agent for Kubernetes%{docs_link_end} instead.').html_safe % { docs_link_start: docs_link_start, docs_link_end: link_end, issue_link_start: issue_link_start, issue_link_end: link_end }
diff --git a/app/views/clusters/clusters/_multiple_clusters_message.html.haml b/app/views/clusters/clusters/_multiple_clusters_message.html.haml
index 04c1f9b6e7a..2878bb1371c 100644
--- a/app/views/clusters/clusters/_multiple_clusters_message.html.haml
+++ b/app/views/clusters/clusters/_multiple_clusters_message.html.haml
@@ -1,4 +1,4 @@
-- autodevops_help_url = help_page_path('topics/autodevops/multiple_clusters_auto_devops.md')
+- autodevops_help_url = help_page_path('topics/autodevops/multiple_clusters_auto_devops')
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
- help_link_end = '</a>'.html_safe
diff --git a/app/views/clusters/clusters/_namespace.html.haml b/app/views/clusters/clusters/_namespace.html.haml
index 34576b6e5af..9c20a409b18 100644
--- a/app/views/clusters/clusters/_namespace.html.haml
+++ b/app/views/clusters/clusters/_namespace.html.haml
@@ -1,6 +1,6 @@
- managed_namespace_help_text = s_('ClusterIntegration|Set a prefix for your namespaces. If not set, defaults to your project path. If modified, existing environments will use their current namespaces until the cluster cache is cleared.')
- non_managed_namespace_help_text = s_('ClusterIntegration|The namespace associated with your project. This will be used for deploy boards, and Web terminals.')
-- managed_namespace_help_link = link_to _('More information'), help_page_path('user/project/clusters/gitlab_managed_clusters.md'), target: '_blank', rel: 'noopener noreferrer'
+- managed_namespace_help_link = link_to _('More information'), help_page_path('user/project/clusters/gitlab_managed_clusters'), target: '_blank', rel: 'noopener noreferrer'
.js-namespace-prefixed
.form-group
diff --git a/app/views/clusters/clusters/_provider_details_form.html.haml b/app/views/clusters/clusters/_provider_details_form.html.haml
index 4b7164f9845..f675ea5865e 100644
--- a/app/views/clusters/clusters/_provider_details_form.html.haml
+++ b/app/views/clusters/clusters/_provider_details_form.html.haml
@@ -1,35 +1,35 @@
= gitlab_ui_form_for cluster, url: update_cluster_url_path, html: { class: 'js-provider-details gl-show-field-errors', role: 'form' },
as: :cluster do |field|
.form-group
- - copy_name_btn = deprecated_clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'),
- class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold required'
.input-group.gl-field-error-anchor
= field.text_field :name, class: 'form-control js-select-on-focus cluster-name', required: true,
title: s_('ClusterIntegration|Cluster name is required.'),
- readonly: cluster.read_only_kubernetes_platform_fields?,
- append: copy_name_btn
+ readonly: cluster.read_only_kubernetes_platform_fields?
+ - if cluster.read_only_kubernetes_platform_fields?
+ .input-group-append
+ = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), variant: :default, category: :primary, size: :medium)
= field.fields_for :platform_kubernetes, platform do |platform_field|
.form-group
- - copy_api_url = deprecated_clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'),
- class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
= platform_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-bold required'
.input-group.gl-field-error-anchor
= platform_field.text_field :api_url, class: 'form-control js-select-on-focus', required: true,
title: s_('ClusterIntegration|API URL should be a valid http/https url.'),
- readonly: cluster.read_only_kubernetes_platform_fields?,
- append: copy_api_url
+ readonly: cluster.read_only_kubernetes_platform_fields?
+ - if cluster.read_only_kubernetes_platform_fields?
+ .input-group-append
+ = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'), variant: :default, category: :primary, size: :medium)
.form-group
- - copy_ca_cert_btn = deprecated_clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'),
- class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
= platform_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-bold'
- .input-group.gl-field-error-anchor
- = platform_field.text_area :ca_cert, class: 'form-control js-select-on-focus', rows: '10',
+ .input-group.gl-field-error-anchor.markdown-code-block
+ = platform_field.text_area :ca_cert, class: 'gl-rounded-top-right-base! gl-rounded-bottom-right-base! form-control js-select-on-focus', rows: '10',
readonly: cluster.read_only_kubernetes_platform_fields?,
- placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'),
- append: copy_ca_cert_btn
+ placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)')
+ - if cluster.read_only_kubernetes_platform_fields?
+ %copy-code
+ = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), variant: :default, category: :primary, size: :medium, class: 'copy-code')
.form-group
= platform_field.label :token, s_('ClusterIntegration|Enter new Service Token'), class: 'label-bold required'
@@ -51,7 +51,7 @@
= field.label :managed, s_('ClusterIntegration|GitLab-managed cluster'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Allow GitLab to manage namespaces and service accounts for this cluster.')
- = link_to _('More information'), help_page_path('user/project/clusters/gitlab_managed_clusters.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('More information'), help_page_path('user/project/clusters/gitlab_managed_clusters'), target: '_blank', rel: 'noopener noreferrer'
.form-group
.form-check
@@ -59,7 +59,7 @@
= field.label :namespace_per_environment, s_('ClusterIntegration|Namespace per environment'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Deploy each environment to its own namespace. Otherwise, environments within a project share a project-wide namespace. Note that anyone who can trigger a deployment of a namespace can read its secrets. If modified, existing environments will use their current namespaces until the cluster cache is cleared.')
- = link_to _('More information'), help_page_path('user/project/clusters/deploy_to_cluster.md', anchor: 'custom-namespace'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('More information'), help_page_path('user/project/clusters/deploy_to_cluster', anchor: 'custom-namespace'), target: '_blank', rel: 'noopener noreferrer'
- if cluster.allow_user_defined_namespace?
= render('clusters/clusters/namespace', platform_field: platform_field, field: field)
diff --git a/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml
index 49dab193da8..8ac232ac7ca 100644
--- a/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml
+++ b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml
@@ -2,9 +2,9 @@
- eks_label = s_('ClusterIntegration|Amazon EKS')
- civo_label = s_('ClusterIntegration|Civo Kubernetes')
- create_cluster_label = s_('ClusterIntegration|Where do you want to create a cluster?')
-- eks_help_path = help_page_path('user/infrastructure/clusters/connect/new_eks_cluster.md')
-- gke_help_path = help_page_path('user/infrastructure/clusters/connect/new_gke_cluster.md')
-- civo_help_path = help_page_path('user/infrastructure/clusters/connect/new_civo_cluster.md')
+- eks_help_path = help_page_path('user/infrastructure/clusters/connect/new_eks_cluster')
+- gke_help_path = help_page_path('user/infrastructure/clusters/connect/new_gke_cluster')
+- civo_help_path = help_page_path('user/infrastructure/clusters/connect/new_civo_cluster')
.gl-py-5.gl-md-pl-5.gl-md-pr-5
%h4.gl-mb-5
diff --git a/app/views/clusters/clusters/connect.html.haml b/app/views/clusters/clusters/connect.html.haml
index a6e1837badf..68e5fcb277b 100644
--- a/app/views/clusters/clusters/connect.html.haml
+++ b/app/views/clusters/clusters/connect.html.haml
@@ -5,7 +5,7 @@
= render 'deprecation_alert'
.gl-md-display-flex.gl-mt-3
- .gl-w-quarter.gl-xs-w-full.gl-flex-shrink-0.gl-md-mr-5
+ .gl-w-full.gl-sm-w-25p.gl-flex-shrink-0.gl-md-mr-5
= render 'sidebar', is_connect_page: true
.gl-w-full
#js-cluster-new
diff --git a/app/views/clusters/clusters/new_cluster_docs.html.haml b/app/views/clusters/clusters/new_cluster_docs.html.haml
index 72c70f35e22..d58c844382d 100644
--- a/app/views/clusters/clusters/new_cluster_docs.html.haml
+++ b/app/views/clusters/clusters/new_cluster_docs.html.haml
@@ -5,7 +5,7 @@
= render_gcp_signup_offer
.gl-md-display-flex.gl-mt-3
- .gl-w-quarter.gl-xs-w-full.gl-flex-shrink-0.gl-md-mr-5
+ .gl-w-full.gl-sm-w-25p.gl-flex-shrink-0.gl-md-mr-5
= render 'sidebar', is_connect_page: false
.gl-w-full
= render 'clusters/clusters/cloud_providers/cloud_provider_selector'
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index 1287f4e689f..22dee5876c2 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -12,10 +12,10 @@
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
provider_type: @cluster.provider_type,
- help_path: help_page_path('user/infrastructure/clusters/index.md'),
- environments_help_path: help_page_path('ci/environments/index.md', anchor: 'create-a-static-environment'),
- clusters_help_path: help_page_path('user/project/clusters/deploy_to_cluster.md'),
- deploy_boards_help_path: help_page_path('user/project/deploy_boards.md', anchor: 'enabling-deploy-boards'),
+ help_path: help_page_path('user/infrastructure/clusters/index'),
+ environments_help_path: help_page_path('ci/environments/index', anchor: 'create-a-static-environment'),
+ clusters_help_path: help_page_path('user/project/clusters/deploy_to_cluster'),
+ deploy_boards_help_path: help_page_path('user/project/deploy_boards', anchor: 'enabling-deploy-boards'),
cluster_id: @cluster.id } }
.js-cluster-application-notice
diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index 4ecef4b76ce..6a5acf4f507 100644
--- a/app/views/clusters/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -58,7 +58,7 @@
= field.label :managed, s_('ClusterIntegration|GitLab-managed cluster'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Allow GitLab to manage namespaces and service accounts for this cluster.')
- = link_to _('Learn more.'), help_page_path('user/project/clusters/gitlab_managed_clusters.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('user/project/clusters/gitlab_managed_clusters'), target: '_blank', rel: 'noopener noreferrer'
.form-group
.form-check
@@ -66,7 +66,7 @@
= field.label :namespace_per_environment, s_('ClusterIntegration|Namespace per environment'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Deploy each environment to its own namespace. Otherwise, environments within a project share a project-wide namespace. Note that anyone who can trigger a deployment of a namespace can read its secrets. If modified, existing environments will use their current namespaces until the cluster cache is cleared.')
- = link_to _('Learn more.'), help_page_path('user/project/clusters/deploy_to_cluster.md', anchor: 'custom-namespace'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('user/project/clusters/deploy_to_cluster', anchor: 'custom-namespace'), target: '_blank', rel: 'noopener noreferrer'
= field.fields_for :platform_kubernetes, @user_cluster.platform_kubernetes do |platform_kubernetes_field|
- if @user_cluster.allow_user_defined_namespace?
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 74dc2277f54..7527f32274a 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -1,5 +1,6 @@
-= content_for :flash_message do
- = render 'shared/project_limit'
+- if params[:personal]
+ = content_for :flash_message do
+ = render 'shared/project_limit'
.page-title-holder.gl-display-flex.gl-align-items-center
%h1.page-title.gl-font-size-h-display= _('Projects')
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 181c79e7bd0..6920ad9cd83 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -33,7 +33,7 @@
- if todo.note.present?
\:
- %span.action-name{ data: { qa_selector: "todo_action_name_content" } }<
+ %span.action-name{ data: { testid: "todo-action-name-content" } }<
- if !todo.note.present?
= todo_action_name(todo)
- unless todo.self_assigned?
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index ab97507b3c8..4f3ca9fd71b 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -39,26 +39,26 @@
.filter-item.gl-m-2
- if params[:group_id].present?
= hidden_field_tag(:group_id, params[:group_id])
- = dropdown_tag(group_dropdown_label(params[:group_id], _("Group")), options: { toggle_class: 'js-group-search js-filter-submit gl-xs-w-full!', title: s_("Todos|Filter by group"), filter: true, filterInput: 'input#group-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-group js-filter-submit', placeholder: _("Search groups"), data: { default_label: _("Group"), display: 'static', testid: 'group-dropdown' } })
+ = dropdown_tag(group_dropdown_label(params[:group_id], _("Group")), options: { toggle_class: 'js-group-search js-filter-submit gl-w-full gl-sm-w-auto', title: s_("Todos|Filter by group"), filter: true, filterInput: 'input#group-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-group js-filter-submit', placeholder: _("Search groups"), data: { default_label: _("Group"), display: 'static', testid: 'group-dropdown' } })
.filter-item.gl-m-2
- if params[:project_id].present?
= hidden_field_tag(:project_id, params[:project_id])
- = dropdown_tag(project_dropdown_label(params[:project_id], _("Project")), options: { toggle_class: 'js-project-search js-filter-submit gl-xs-w-full!', title: s_("Todos|Filter by project"), filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', placeholder: _("Search projects"), data: { default_label: _("Project"), display: 'static' } })
+ = dropdown_tag(project_dropdown_label(params[:project_id], _("Project")), options: { toggle_class: 'js-project-search js-filter-submit gl-w-full gl-sm-w-auto', title: s_("Todos|Filter by project"), filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', placeholder: _("Search projects"), data: { default_label: _("Project"), display: 'static' } })
.filter-item.gl-m-2
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
- = dropdown_tag(user_dropdown_label(params[:author_id], _("Author")), options: { toggle_class: 'js-user-search js-filter-submit js-author-search gl-xs-w-full!', title: s_("Todos|Filter by author"), filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit', placeholder: _("Search authors"), data: { any_user: _("Any Author"), first_user: (current_user.username if current_user), project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: _("Author"), todo_filter: true, todo_state_filter: params[:state] || 'pending' } })
+ = dropdown_tag(user_dropdown_label(params[:author_id], _("Author")), options: { toggle_class: 'js-user-search js-filter-submit js-author-search gl-w-full gl-sm-w-auto', title: s_("Todos|Filter by author"), filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit', placeholder: _("Search authors"), data: { any_user: _("Any Author"), first_user: (current_user.username if current_user), project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: _("Author"), todo_filter: true, todo_state_filter: params[:state] || 'pending' } })
.filter-item.gl-m-2
- if params[:type].present?
= hidden_field_tag(:type, params[:type])
- = dropdown_tag(todo_types_dropdown_label(params[:type], _("Type")), options: { toggle_class: 'js-type-search js-filter-submit gl-xs-w-full!', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', data: { data: todo_types_options, default_label: _("Type") } })
+ = dropdown_tag(todo_types_dropdown_label(params[:type], _("Type")), options: { toggle_class: 'js-type-search js-filter-submit gl-w-full gl-sm-w-auto', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', data: { data: todo_types_options, default_label: _("Type") } })
.filter-item.actions-filter.gl-m-2
- if params[:action_id].present?
= hidden_field_tag(:action_id, params[:action_id])
- = dropdown_tag(todo_actions_dropdown_label(params[:action_id], _("Action")), options: { toggle_class: 'js-action-search js-filter-submit gl-xs-w-full!', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', data: { data: todo_actions_options, default_label: _("Action") } })
+ = dropdown_tag(todo_actions_dropdown_label(params[:action_id], _("Action")), options: { toggle_class: 'js-action-search js-filter-submit gl-w-full gl-sm-w-auto', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', data: { data: todo_actions_options, default_label: _("Action") } })
.filter-item.sort-filter.gl-my-2
.dropdown
- %button.dropdown-menu-toggle.dropdown-menu-toggle-sort{ type: 'button', class: 'gl-xs-w-full!', 'data-toggle' => 'dropdown' }
+ %button.dropdown-menu-toggle.dropdown-menu-toggle-sort{ type: 'button', class: 'gl-w-full gl-sm-w-auto', 'data-toggle' => 'dropdown' }
%span.light
- if @sort.present?
= sort_options_hash[@sort]
@@ -78,12 +78,12 @@
.row.js-todos-all
- if @allowed_todos.any?
- .col.js-todos-list-container{ data: { qa_selector: "todos_list_container" } }
+ .col.js-todos-list-container{ data: { testid: "todos-list-container" } }
.js-todos-options{ data: { per_page: @allowed_todos.count, current_page: @todos.current_page, total_pages: @todos.total_pages } }
%ul.content-list.todos-list
= render @allowed_todos
= paginate @todos, theme: "gitlab"
- .js-nothing-here-container.gl-empty-state.gl-text-center.hidden
+ .col.js-nothing-here-container.gl-empty-state.gl-text-center.hidden
.svg-content.svg-150
= image_tag 'illustrations/empty-todos-all-done-md.svg'
.text-content.gl-text-center
diff --git a/app/views/devise/shared/_sign_in_link.html.haml b/app/views/devise/shared/_sign_in_link.html.haml
index a1d10898c5b..a9f24e42d0b 100644
--- a/app/views/devise/shared/_sign_in_link.html.haml
+++ b/app/views/devise/shared/_sign_in_link.html.haml
@@ -1,4 +1,4 @@
-%p.text-center
+%p{ class: local_assigns.fetch(:wrapper_class, 'gl-text-center') }
%span.light
= _('Already have an account?')
- path_params = { redirect_to_referer: 'yes' }
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index bf1b604465b..fb60b8c08eb 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -1,77 +1,10 @@
-- max_first_name_length = max_last_name_length = 127
- borderless ||= false
-- form_resource_name = "new_#{resource_name}"
.gl-mb-3.gl-p-4{ class: (borderless ? '' : 'gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base') }
= yield :omniauth_providers_top if show_omniauth_providers
- = gitlab_ui_form_for(resource, as: form_resource_name, url: url, html: { class: 'gl-show-field-errors js-arkose-labs-form', aria: { live: 'assertive' }}, data: { testid: 'signup-form' }) do |f|
- .devise-errors
- = render 'devise/shared/error_messages', resource: resource
- - if Gitlab::CurrentSettings.invisible_captcha_enabled
- = invisible_captcha nonce: true, autocomplete: SecureRandom.alphanumeric(12)
- .name.form-row
- .col.form-group
- = f.label :first_name, _('First name'), for: 'new_user_first_name'
- = f.text_field :first_name,
- class: 'form-control gl-form-input top js-block-emoji js-validate-length',
- data: { max_length: max_first_name_length,
- max_length_message: s_('SignUp|First name is too long (maximum is %{max_length} characters).') % { max_length: max_first_name_length },
- testid: 'new-user-first-name-field' },
- required: true,
- title: _('This field is required.')
- .col.form-group
- = f.label :last_name, _('Last name'), for: 'new_user_last_name'
- = f.text_field :last_name,
- class: 'form-control gl-form-input top js-block-emoji js-validate-length',
- data: { max_length: max_last_name_length,
- max_length_message: s_('SignUp|Last name is too long (maximum is %{max_length} characters).') % { max_length: max_last_name_length },
- testid: 'new-user-last-name-field' },
- required: true,
- title: _('This field is required.')
- .username.form-group
- = f.label :username, _('Username')
- = f.text_field :username,
- class: 'form-control gl-form-input middle js-block-emoji js-validate-length js-validate-username',
- data: signup_username_data_attributes,
- pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
- required: true,
- title: _('Please create a username with only alphanumeric characters.')
- %p.validation-error.gl-text-red-500.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Username is already taken.')
- %p.validation-success.gl-text-green-600.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Username is available.')
- %p.validation-pending.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Checking username availability...')
- .form-group
- = f.label :email, _('Email')
- = f.email_field :email,
- class: 'form-control gl-form-input middle js-validate-email',
- data: { testid: 'new-user-email-field' },
- required: true,
- title: _('Please provide a valid email address.')
- %p.validation-hint.gl-field-hint.text-secondary= _('We recommend a work email address.')
- %p.validation-warning.gl-field-error-ignore.text-secondary.hide= _('This email address does not look right, are you sure you typed it correctly?')
- -# This is used for providing entry to Jihu on email verification
- = render_if_exists 'devise/shared/signup_email_additional_info'
- .form-group.gl-mb-5
- = f.label :password, _('Password')
- %input.form-control.gl-form-input.js-password{ data: { id: "#{form_resource_name}_password",
- title: s_('SignUp|Minimum length is %{minimum_password_length} characters.') % { minimum_password_length: @minimum_password_length },
- minimum_password_length: @minimum_password_length,
- testid: 'new-user-password-field',
- autocomplete: 'new-password',
- name: "#{form_resource_name}[password]" } }
- %p.gl-field-hint-valid.text-secondary= s_('SignUp|Minimum length is %{minimum_password_length} characters.') % { minimum_password_length: @minimum_password_length }
- = render_if_exists 'shared/password_requirements_list'
- = render_if_exists 'devise/shared/phone_verification', form: f
+ = render 'devise/shared/signup_box_form',
+ button_text: button_text,
+ url: url,
+ show_omniauth_providers: omniauth_enabled? && button_based_providers_enabled?
- .form-group
- - if arkose_labs_enabled?
- = render_if_exists 'devise/registrations/arkose_labs'
- - elsif show_recaptcha_sign_up?
- = recaptcha_tags nonce: content_security_policy_nonce
-
- = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { testid: 'new-user-register-button' }}) do
- = button_text
-
- = render 'devise/shared/terms_of_service_notice', button_text: button_text
-
- = yield :omniauth_providers_bottom if show_omniauth_providers
diff --git a/app/views/devise/shared/_signup_box_form.html.haml b/app/views/devise/shared/_signup_box_form.html.haml
new file mode 100644
index 00000000000..246036b72e1
--- /dev/null
+++ b/app/views/devise/shared/_signup_box_form.html.haml
@@ -0,0 +1,73 @@
+- max_first_name_length = max_last_name_length = 127
+- form_resource_name = "new_#{resource_name}"
+
+= gitlab_ui_form_for(resource, as: form_resource_name, url: url, html: { class: 'gl-show-field-errors js-arkose-labs-form', aria: { live: 'assertive' }}, data: { testid: 'signup-form' }) do |f|
+ .devise-errors
+ = render 'devise/shared/error_messages', resource: resource
+ - if Gitlab::CurrentSettings.invisible_captcha_enabled
+ = invisible_captcha nonce: true, autocomplete: SecureRandom.alphanumeric(12)
+ .name.form-row
+ .col.form-group
+ = f.label :first_name, _('First name'), for: 'new_user_first_name'
+ = f.text_field :first_name,
+ class: 'form-control gl-form-input top js-block-emoji js-validate-length',
+ data: { max_length: max_first_name_length,
+ max_length_message: s_('SignUp|First name is too long (maximum is %{max_length} characters).') % { max_length: max_first_name_length },
+ testid: 'new-user-first-name-field' },
+ required: true,
+ title: _('This field is required.')
+ .col.form-group
+ = f.label :last_name, _('Last name'), for: 'new_user_last_name'
+ = f.text_field :last_name,
+ class: 'form-control gl-form-input top js-block-emoji js-validate-length',
+ data: { max_length: max_last_name_length,
+ max_length_message: s_('SignUp|Last name is too long (maximum is %{max_length} characters).') % { max_length: max_last_name_length },
+ testid: 'new-user-last-name-field' },
+ required: true,
+ title: _('This field is required.')
+ .username.form-group
+ = f.label :username, _('Username')
+ = f.text_field :username,
+ class: 'form-control gl-form-input middle js-block-emoji js-validate-length js-validate-username',
+ data: signup_username_data_attributes,
+ pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
+ required: true,
+ title: _('Please create a username with only alphanumeric characters.')
+ %p.validation-error.gl-text-red-500.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Username is already taken.')
+ %p.validation-success.gl-text-green-600.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Username is available.')
+ %p.validation-pending.gl-field-error-ignore.gl-mt-2.field-validation.hide= _('Checking username availability...')
+ .form-group
+ = f.label :email, _('Email')
+ = f.email_field :email,
+ class: 'form-control gl-form-input middle js-validate-email',
+ data: { testid: 'new-user-email-field' },
+ required: true,
+ title: _('Please provide a valid email address.')
+ %p.validation-hint.gl-field-hint.text-secondary= _('We recommend a work email address.')
+ %p.validation-warning.gl-field-error-ignore.text-secondary.hide= _('This email address does not look right, are you sure you typed it correctly?')
+ -# This is used for providing entry to Jihu on email verification
+ = render_if_exists 'devise/shared/signup_email_additional_info'
+ .form-group.gl-mb-5
+ = f.label :password, _('Password')
+ %input.form-control.gl-form-input.js-password{ data: { id: "#{form_resource_name}_password",
+ title: s_('SignUp|Minimum length is %{minimum_password_length} characters.') % { minimum_password_length: @minimum_password_length },
+ minimum_password_length: @minimum_password_length,
+ testid: 'new-user-password-field',
+ autocomplete: 'new-password',
+ name: "#{form_resource_name}[password]" } }
+ %p.gl-field-hint-valid.text-secondary= s_('SignUp|Minimum length is %{minimum_password_length} characters.') % { minimum_password_length: @minimum_password_length }
+ = render_if_exists 'shared/password_requirements_list'
+ = render_if_exists 'devise/shared/phone_verification', form: f
+
+ .form-group
+ - if arkose_labs_enabled?
+ = render_if_exists 'devise/registrations/arkose_labs'
+ - elsif show_recaptcha_sign_up?
+ = recaptcha_tags nonce: content_security_policy_nonce
+
+ = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { testid: 'new-user-register-button' }}) do
+ = button_text
+
+ = render 'devise/shared/terms_of_service_notice', button_text: button_text
+
+= yield :omniauth_providers_bottom if show_omniauth_providers
diff --git a/app/views/devise/shared/_signup_omniauth_provider_list.haml b/app/views/devise/shared/_signup_omniauth_provider_list.haml
index e8c82e456ae..b9efcaa11b4 100644
--- a/app/views/devise/shared/_signup_omniauth_provider_list.haml
+++ b/app/views/devise/shared/_signup_omniauth_provider_list.haml
@@ -14,7 +14,10 @@
= _("Create an account using:")
.gl-display-flex.gl-justify-content-between.gl-flex-wrap
- providers.each do |provider|
- = button_to omniauth_authorize_path(:user, provider, register_omniauth_params(local_assigns)), class: "btn gl-button btn-default gl-w-full gl-mb-4 js-oauth-login #{qa_selector_for_provider(provider)}", data: { provider: provider, track_action: "#{provider}_sso", track_label: tracking_label }, id: "oauth-login-#{provider}" do
+ = button_to omniauth_authorize_path(:user, provider, register_omniauth_params(local_assigns)),
+ class: "btn gl-button btn-default gl-w-full gl-mb-4 js-oauth-login #{qa_selector_for_provider(provider)}",
+ data: { provider: provider, track_action: "#{provider}_sso", track_label: tracking_label },
+ id: "oauth-login-#{provider}" do
- if provider_has_icon?(provider)
= provider_image_tag(provider)
%span.gl-button-text
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index e34a5cebe78..5e6ebe87808 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -20,7 +20,7 @@
.discussion-with-resolve-btn
= link_to_reply_discussion(discussion)
- elsif !current_user
- .disabled-comment.text-center
+ .disabled-comment.gl-text-center.gl-text-secondary
Please
= link_to "register", new_session_path(:user, redirect_to_referer: 'yes')
or
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 83f7d743755..c28fe7c8330 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,8 +1,8 @@
- event = event.present
- if event.visible_to_user?(current_user)
- .event-item
- .event-item-timestamp
+ .event-item{ class: current_path?('users#activity') ? 'user-profile-activity gl-border-bottom-0 gl-pl-7! gl-pb-3' : '' }
+ .event-item-timestamp.gl-font-sm
#{time_ago_with_tooltip(event.created_at)}
- if event.wiki_page?
diff --git a/app/views/events/_event_scope.html.haml b/app/views/events/_event_scope.html.haml
index 67e4c538b4a..f3e3a304cfd 100644
--- a/app/views/events/_event_scope.html.haml
+++ b/app/views/events/_event_scope.html.haml
@@ -1,4 +1,4 @@
-%span.event-scope
+%span.event-scope.gl-text-truncate
= event_preposition(event)
- if event.project
= link_to_project(event.project)
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index 7ef3461a7fb..78ce24c429a 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -5,9 +5,9 @@
.event-title.d-flex.flex-wrap
= inline_event_icon(event)
- if event.target
- %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name }
+ %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name + user_profile_activity_classes }
= localized_action_name(event)
- %span.event-target-type.gl-mr-2= event.target_type_name
+ %span.event-target-type.gl-mr-2{ class: user_profile_activity_classes }= event.target_type_name
= link_to event_target_path(event), class: 'has-tooltip event-target-link gl-mr-2', title: event.target_title do
= event.target.reference_link_text
- unless event.milestone?
diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml
index f0bb07d062c..390c9ec6c89 100644
--- a/app/views/events/event/_created_project.html.haml
+++ b/app/views/events/event/_created_project.html.haml
@@ -4,7 +4,7 @@
.event-title.d-flex.flex-wrap
= inline_event_icon(event)
- %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name }
+ %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name + user_profile_activity_classes }
= event_action_name(event)
- if event.project
diff --git a/app/views/events/event/_design.html.haml b/app/views/events/event/_design.html.haml
index c1fa1aaca50..945c7465ea8 100644
--- a/app/views/events/event/_design.html.haml
+++ b/app/views/events/event/_design.html.haml
@@ -4,7 +4,7 @@
.event-title.d-flex.flex-wrap
= inline_event_icon(event)
- %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name }
+ %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name + user_profile_activity_classes }
= event.action_name
= event_design_title_html(event)
= render "events/event_scope", event: event
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index 53c59474d83..5bbece84e40 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -6,7 +6,7 @@
.event-title.d-flex.flex-wrap
= inline_event_icon(event)
- %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name }
+ %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name + user_profile_activity_classes }
= event.action_name
= event_note_title_html(event)
- title = note_target_title(event.target)
diff --git a/app/views/events/event/_private.html.haml b/app/views/events/event/_private.html.haml
index d91f30c07cb..5e9d6da3996 100644
--- a/app/views/events/event/_private.html.haml
+++ b/app/views/events/event/_private.html.haml
@@ -1,8 +1,8 @@
-.event-item
- .event-item-timestamp
+.event-item{ class: current_path?('users#activity') ? 'user-profile-activity gl-border-bottom-0 gl-pl-7! gl-pb-3' : '' }
+ .event-item-timestamp.gl-font-sm
= time_ago_with_tooltip(event.created_at)
- .system-note-image= sprite_icon('eye-slash', size: 24, css_class: 'icon')
+ .system-note-image.gl-rounded-full.gl-bg-gray-50.gl-line-height-0= sprite_icon('eye-slash', size: 14, css_class: 'icon')
= event_user_info(event)
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 0ad969116e0..ff7983a9ba4 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -6,7 +6,7 @@
.event-title.d-flex.flex-wrap
= inline_event_icon(event)
- %span.event-type.d-inline-block.gl-mr-2.pushed= event.push_activity_description
+ %span.event-type.d-inline-block.gl-mr-2.pushed{ class: user_profile_activity_classes }= event.push_activity_description
- unless event.batch_push?
%span.gl-mr-2.text-truncate
- commits_link = project_commits_path(project, event.ref_name)
diff --git a/app/views/events/event/_wiki.html.haml b/app/views/events/event/_wiki.html.haml
index cbd5ebcae12..a48c34f80d8 100644
--- a/app/views/events/event/_wiki.html.haml
+++ b/app/views/events/event/_wiki.html.haml
@@ -4,7 +4,7 @@
.event-title.d-flex.flex-wrap
= inline_event_icon(event)
- %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name }
+ %span.event-type.d-inline-block.gl-mr-2{ class: event.action_name + user_profile_activity_classes }
= event.action_name
= event_wiki_title_html(event)
= render "events/event_scope", event: event
diff --git a/app/views/explore/catalog/show.html.haml b/app/views/explore/catalog/show.html.haml
new file mode 100644
index 00000000000..7c8d788f8e3
--- /dev/null
+++ b/app/views/explore/catalog/show.html.haml
@@ -0,0 +1,3 @@
+- page_title _('CI/CD Catalog')
+
+#js-ci-cd-catalog{ data: { ci_catalog_path: explore_catalog_index_path } }
diff --git a/app/views/external_redirect/external_redirect/index.html.haml b/app/views/external_redirect/external_redirect/index.html.haml
new file mode 100644
index 00000000000..36bf98cba02
--- /dev/null
+++ b/app/views/external_redirect/external_redirect/index.html.haml
@@ -0,0 +1,12 @@
+- add_page_specific_style 'page_bundles/external_redirect'
+- page_title _("You're about to leave GitLab")
+
+- url = local_assigns.fetch(:url)
+
+.gl-max-w-62.gl-h-full.gl-display-flex.gl-justify-content-center.gl-align-items-center.gl-flex-direction-column.gl-mr-auto.gl-ml-auto.gl-px-3
+ = sprite_icon('warning', size: 48, css_class: 'gl-text-orange-300')
+ %h3.gl-mt-6= _("You're about to leave GitLab")
+ %p= safe_format(_('This link will redirect you to %{url}. If this URL looks wrong, please go back or close this window. Do you want to continue?'), url: content_tag(:code, url))
+ .gl-display-flex.gl-justify-content-center.gl-w-full
+ = render Pajamas::ButtonComponent.new(variant: :default, href: url) do
+ = _("Proceed")
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index 544acd5ae56..269a7309ec2 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -3,7 +3,7 @@
- emails_disabled = @group.emails_disabled?
.group-home-panel
- .gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-sm-flex-direction-column.gl-gap-3.gl-my-5
+ .gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-flex-direction-column.gl-md-flex-direction-row.gl-gap-3.gl-my-5
.home-panel-title-row.gl-display-flex.gl-align-items-center
.avatar-container.rect-avatar.s64.home-panel-avatar.gl-flex-shrink-0.float-none{ class: 'gl-mr-3!' }
= group_icon(@group, class: 'avatar avatar-tile s64', width: 64, height: 64, itemprop: 'logo')
diff --git a/app/views/groups/_import_group_from_another_instance_panel.html.haml b/app/views/groups/_import_group_from_another_instance_panel.html.haml
index c35bbce6ba7..6c5a27e68c4 100644
--- a/app/views/groups/_import_group_from_another_instance_panel.html.haml
+++ b/app/views/groups/_import_group_from_another_instance_panel.html.haml
@@ -24,7 +24,7 @@
= render Pajamas::AlertComponent.new(dismissible: false,
variant: :warning) do |c|
- c.with_body do
- - docs_link = link_to('', help_page_path('user/group/import/index.md', anchor: 'migrated-group-items'), target: '_blank', rel: 'noopener noreferrer')
+ - docs_link = link_to('', help_page_path('user/group/import/index', anchor: 'migrated-group-items'), target: '_blank', rel: 'noopener noreferrer')
= safe_format(s_('GroupsNew|Not all group items are migrated. %{docs_link_start}What items are migrated%{docs_link_end}?'), tag_pair(docs_link, :docs_link_start, :docs_link_end))
%p.gl-mt-3
@@ -37,12 +37,12 @@
id: 'import_gitlab_url',
data: { testid: 'import-gitlab-url' }
.form-group.gl-display-flex.gl-flex-direction-column
- = f.label :bulk_import_gitlab_access_token, s_('GroupsNew|Personal access token'), for: 'import_gitlab_token'
+ = f.label :bulk_import_gitlab_access_token, s_('GroupsNew|Personal access token'), for: 'import_gitlab_token', class: 'col-form-label'
.gl-font-weight-normal
- pat_link = link_to('', help_page_path('user/profile/personal_access_tokens'), target: '_blank')
- short_living_link = link_to('', help_page_path('security/token_overview', anchor: 'security-considerations'), target: '_blank')
= safe_format(s_('GroupsNew|Create a token with %{code_start}api%{code_end} and %{code_start}read_repository%{code_end} scopes in the %{pat_link_start}user settings%{pat_link_end} of the source GitLab instance. For %{short_living_link_start}security reasons%{short_living_link_end}, set a short expiration date for the token. Keep in mind that large migrations take more time.'), tag_pair('<code></code>'.html_safe, :code_start , :code_end), tag_pair(pat_link, :pat_link_start, :pat_link_end), tag_pair(short_living_link, :short_living_link_start, :short_living_link_end))
- = f.text_field :bulk_import_gitlab_access_token, placeholder: s_('GroupsNew|e.g. h8d3f016698e...'), class: 'gl-form-input gl-mt-3 col-xs-12 col-sm-8',
+ = f.password_field :bulk_import_gitlab_access_token, placeholder: s_('GroupsNew|e.g. h8d3f016698e...'), class: 'gl-form-input gl-mt-3 col-xs-12 col-sm-8',
required: true,
disabled: bulk_imports_disabled,
autocomplete: 'off',
diff --git a/app/views/groups/_import_group_from_file_panel.html.haml b/app/views/groups/_import_group_from_file_panel.html.haml
index e3d54e52aab..c39f5cf87c7 100644
--- a/app/views/groups/_import_group_from_file_panel.html.haml
+++ b/app/views/groups/_import_group_from_file_panel.html.haml
@@ -10,7 +10,7 @@
alert_options: { class: 'gl-mb-5' },
dismissible: false) do |c|
- c.with_body do
- - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index.md', anchor: 'migrate-groups-by-direct-transfer-recommended') }
+ - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index', anchor: 'migrate-groups-by-direct-transfer-recommended') }
- link_end = '</a>'.html_safe
= s_('GroupsNew|This feature is deprecated and replaced by group migration by direct transfer. %{docs_link_start}Learn more%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: link_end }
= render 'shared/groups/group_name_and_path_fields', f: f
diff --git a/app/views/groups/_invite_members_modal.html.haml b/app/views/groups/_invite_members_modal.html.haml
index cd3327ba9ec..d53190948fd 100644
--- a/app/views/groups/_invite_members_modal.html.haml
+++ b/app/views/groups/_invite_members_modal.html.haml
@@ -3,4 +3,8 @@
.js-invite-members-modal{ data: { is_project: 'false',
access_levels: group.access_level_roles.to_json,
reload_page_on_submit: current_path?('group_members#index').to_s,
- help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(group)).merge(users_filter_data(group)) }
+ help_link: help_page_url('user/permissions'),
+ is_signup_enabled: signup_enabled?.to_s,
+ new_users_url: new_admin_user_url,
+ is_current_user_admin: current_user&.admin?.to_s,
+ }.merge(common_invite_modal_dataset(group)).merge(users_filter_data(group)) }
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index 76758769d01..2f2edec2d80 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -17,15 +17,15 @@
= _("New project")
- c.with_body do
%ul.content-list{ class: 'gl-px-3!' }
- - @projects.each_with_index do |project, idx|
- %li.project-row.gl-align-items-center{ class: 'gl-display-flex!', data: { qa_selector: 'project_row_container', qa_index: idx } }
+ - @projects.each do |project|
+ %li.project-row.gl-align-items-center{ class: 'gl-display-flex!' }
.avatar-container.rect-avatar.s40.gl-flex-shrink-0
= project_icon(project, alt: '', class: 'avatar project-avatar s40', width: 40, height: 40)
.gl-min-w-0.gl-flex-grow-1
.title
= link_to project_path(project), class: 'js-prefetch-document' do
- %span.project-full-name{ data: { qa_selector: 'project_fullname_content' } }
- %span.namespace-name{ data: { qa_selector: 'project_namespace_content' } }
+ %span.project-full-name
+ %span.namespace-name
- if project.namespace
= project.namespace.human_name
\/
@@ -43,13 +43,12 @@
.controls.gl-flex-shrink-0.gl-ml-5
= render Pajamas::ButtonComponent.new(href: project_project_members_path(project),
variant: :link,
- button_options: { class: 'gl-mr-2', data: { qa_selector: 'project_members_button' } }) do
+ button_options: { class: 'gl-mr-2' }) do
= _('View members')
= render Pajamas::ButtonComponent.new(href: edit_project_path(project),
- size: :small,
- button_options: { data: { qa_selector: 'project_edit_button' } }) do
+ size: :small) do
= _('Edit')
- = render 'delete_project_button', project: project, data: { qa_selector: 'project_delete_button' }
+ = render 'delete_project_button', project: project
- if @projects.blank?
.nothing-here-block= _("This group has no projects yet")
diff --git a/app/views/groups/settings/_export.html.haml b/app/views/groups/settings/_export.html.haml
index 8eb9f8fc5f1..059426fd596 100644
--- a/app/views/groups/settings/_export.html.haml
+++ b/app/views/groups/settings/_export.html.haml
@@ -31,8 +31,8 @@
- if group.export_file_exists?
= render Pajamas::ButtonComponent.new(href: download_export_group_path(group), button_options: { rel: 'nofollow', data: { method: :get, qa_selector: 'download_export_link' } }) do
= _('Download export')
- = render Pajamas::ButtonComponent.new(href: export_group_path(group), button_options: { data: { method: :post, qa_selector: 'regenerate_export_group_link' } }) do
+ = render Pajamas::ButtonComponent.new(href: export_group_path(group), button_options: { data: { method: :post } }) do
= _('Regenerate export')
- else
- = render Pajamas::ButtonComponent.new(href: export_group_path(group), button_options: { data: { method: :post, qa_selector: 'export_group_link' } }) do
+ = render Pajamas::ButtonComponent.new(href: export_group_path(group), button_options: { data: { method: :post } }) do
= _('Export group')
diff --git a/app/views/groups/settings/_git_access_protocols.html.haml b/app/views/groups/settings/_git_access_protocols.html.haml
index d23f72a3055..c9cbe56e6ec 100644
--- a/app/views/groups/settings/_git_access_protocols.html.haml
+++ b/app/views/groups/settings/_git_access_protocols.html.haml
@@ -1,7 +1,7 @@
- if group.root?
.form-group
= f.label _('Enabled Git access protocols'), class: 'label-bold'
- = f.select :enabled_git_access_protocol, options_for_select(enabled_git_access_protocol_options_for_group(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?
+ = f.select :enabled_git_access_protocol, options_for_select(enabled_git_access_protocol_options_for_group(group), group.enabled_git_access_protocol), {}, class: 'form-control', 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 45fd98adbb9..8ea80700340 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -38,11 +38,12 @@
= render 'groups/settings/lfs', f: f
= render_if_exists 'groups/settings/code_suggestions', f: f, group: @group
= render_if_exists 'groups/settings/experimental_settings', f: f, group: @group
- = render_if_exists 'groups/settings/ai_third_party_settings', f: f, group: @group
+ = render_if_exists 'groups/settings/product_analytics_settings', f: f, group: @group
= 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
+ = render_if_exists 'groups/settings/service_access_tokens_expiration_enforced', f: f, group: @group
= render 'groups/settings/two_factor_auth', f: f, group: @group
= render_if_exists 'groups/personal_access_token_expiration_policy', f: f, group: @group
= render 'groups/settings/membership', f: f, group: @group
diff --git a/app/views/groups/settings/_resource_access_token_creation.html.haml b/app/views/groups/settings/_resource_access_token_creation.html.haml
index d304dba3250..7d64ab84ad2 100644
--- a/app/views/groups/settings/_resource_access_token_creation.html.haml
+++ b/app/views/groups/settings/_resource_access_token_creation.html.haml
@@ -7,4 +7,4 @@
- link_start_group = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_access_tokens_link }
= f.gitlab_ui_checkbox_component :resource_access_token_creation_allowed,
s_('GroupSettings|Users can create %{link_start_project}project access tokens%{link_end} and %{link_start_group}group access tokens%{link_end} in this group').html_safe % { link_start_project: link_start_project, link_start_group: link_start_group, link_end: '</a>'.html_safe },
- checkbox_options: { checked: group.namespace_settings.resource_access_token_creation_allowed?, data: { qa_selector: 'resource_access_token_creation_allowed_checkbox' } }
+ checkbox_options: { checked: group.namespace_settings.resource_access_token_creation_allowed? }
diff --git a/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml b/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml
index b0a5d0bd4fa..705a9704fc2 100644
--- a/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml
+++ b/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml
@@ -4,7 +4,7 @@
.form-group
= render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-3' }) do |c|
- c.with_body do
- - learn_more_link = link_to _('Learn more.'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer'
+ - learn_more_link = link_to _('Learn more.'), help_page_path('topics/autodevops/index'), target: '_blank', rel: 'noopener noreferrer'
- help_text = s_('GroupSettings|The Auto DevOps pipeline runs if no alternative CI configuration file is found.')
- badge = gl_badge_tag badge_for_auto_devops_scope(group), variant: :info
- label = s_('GroupSettings|Default to Auto DevOps pipeline for all projects within this group')
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index b4b73e9e790..dc80aeb8a30 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -1,9 +1,9 @@
- page_title _('Bitbucket import')
- header_title _('Projects'), root_path
-%h1.page-title.gl-font-size-h-display.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
.gl-display-flex.gl-align-items-center.gl-justify-content-center
- = sprite_icon('bitbucket', css_class: 'gl-mr-2')
+ = sprite_icon('bitbucket', css_class: 'gl-mr-3', size: 48)
= _('Import projects from Bitbucket')
= render 'import/githubish_status', provider: 'bitbucket', default_namespace: @namespace
diff --git a/app/views/import/bitbucket_server/new.html.haml b/app/views/import/bitbucket_server/new.html.haml
index de94f142a40..583d312154c 100644
--- a/app/views/import/bitbucket_server/new.html.haml
+++ b/app/views/import/bitbucket_server/new.html.haml
@@ -2,10 +2,11 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h1.page-title.gl-font-size-h-display.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
.gl-display-flex.gl-align-items-center.gl-justify-content-center
- = sprite_icon('bitbucket', css_class: 'gl-mr-2')
+ = sprite_icon('bitbucket', css_class: 'gl-mr-3', size: 48)
= _('Import repositories from Bitbucket Server')
+%hr
%p
= _('Enter in your Bitbucket Server URL and personal access token below')
diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml
index 7e0c7b3dd74..6994404c8c9 100644
--- a/app/views/import/bitbucket_server/status.html.haml
+++ b/app/views/import/bitbucket_server/status.html.haml
@@ -1,8 +1,8 @@
- page_title _('Bitbucket Server import')
-%h1.page-title.gl-font-size-h-display.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
.gl-display-flex.gl-align-items-center.gl-justify-content-center
- = sprite_icon('bitbucket', css_class: 'gl-mr-2')
+ = sprite_icon('bitbucket', css_class: 'gl-mr-3', size: 48)
= _('Import projects from Bitbucket Server')
= render 'import/githubish_status', provider: 'bitbucket_server', paginatable: true, default_namespace: @namespace, extra_data: { reconfigure_path: configure_import_bitbucket_server_path }
diff --git a/app/views/import/bulk_imports/details.html.haml b/app/views/import/bulk_imports/details.html.haml
new file mode 100644
index 00000000000..511bf2c38a1
--- /dev/null
+++ b/app/views/import/bulk_imports/details.html.haml
@@ -0,0 +1,5 @@
+- add_to_breadcrumbs _('New group'), new_group_path
+- add_to_breadcrumbs _('Import group'), new_group_path(anchor: 'import-group-pane')
+- page_title s_('Import|GitLab Migration details')
+
+.js-bulk-import-details
diff --git a/app/views/import/bulk_imports/history.html.haml b/app/views/import/bulk_imports/history.html.haml
index 38196f97030..57e3e60a702 100644
--- a/app/views/import/bulk_imports/history.html.haml
+++ b/app/views/import/bulk_imports/history.html.haml
@@ -3,4 +3,4 @@
- add_page_specific_style 'page_bundles/import'
- page_title _('Import history')
-#import-history-mount-element{ data: { realtime_changes_path: realtime_changes_import_bulk_imports_path(format: :json) } }
+#import-history-mount-element{ data: { details_path: details_import_bulk_imports_path, realtime_changes_path: realtime_changes_import_bulk_imports_path(format: :json) } }
diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml
index 2edd9cd5592..001f6588405 100644
--- a/app/views/import/fogbugz/new.html.haml
+++ b/app/views/import/fogbugz/new.html.haml
@@ -2,9 +2,9 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h1.page-title.gl-font-size-h-display.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
.gl-display-flex.gl-align-items-center.gl-justify-content-center
- = sprite_icon('bug', css_class: 'gl-mr-2')
+ = sprite_icon('bug', css_class: 'gl-mr-3', size: 48)
= _('Import projects from FogBugz')
%hr
diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml
index fb05e8e9724..7512e3d3935 100644
--- a/app/views/import/fogbugz/status.html.haml
+++ b/app/views/import/fogbugz/status.html.haml
@@ -1,7 +1,7 @@
- page_title _("FogBugz import")
-%h1.page-title.gl-font-size-h-display.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
.gl-display-flex.gl-align-items-center.gl-justify-content-center
- = sprite_icon('bug', css_class: 'gl-mr-2')
+ = sprite_icon('bug', css_class: 'gl-mr-3', size: 48)
= _('Import projects from FogBugz')
%p.light
diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml
index f76e9f3f6ed..dcee0c473a1 100644
--- a/app/views/import/gitea/new.html.haml
+++ b/app/views/import/gitea/new.html.haml
@@ -2,9 +2,11 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h1.page-title.gl-font-size-h-display
- = custom_icon('gitea_logo')
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
+ .gl-display-flex.gl-align-items-center.gl-justify-content-center
+ = sprite_icon('gitea', css_class: 'gl-mr-3', size: 48)
= _('Import projects from Gitea')
+%hr
%p
- link_to_personal_token = link_to(_('personal access token'), 'https://docs.gitea.io/en-us/api-usage/#authentication-via-the-api')
@@ -17,9 +19,9 @@
.col-sm-4
= text_field_tag :gitea_host_url, nil, placeholder: 'https://gitea.com', class: 'form-control gl-form-input'
.form-group.row
- = label_tag :personal_access_token, _('Personal access token'), class: 'col-form-label col-sm-2'
+ = label_tag :personal_access_token, _('Personal access token'), for: :personal_access_token, class: 'col-form-label col-sm-2'
.col-sm-4
- = text_field_tag :personal_access_token, nil, class: 'form-control gl-form-input'
+ = password_field_tag :personal_access_token, nil, class: 'form-control gl-form-input'
.form-actions
= render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm) do
= _('List your Gitea repositories')
diff --git a/app/views/import/gitea/status.html.haml b/app/views/import/gitea/status.html.haml
index 2dde642d8f0..86ab3ca85c3 100644
--- a/app/views/import/gitea/status.html.haml
+++ b/app/views/import/gitea/status.html.haml
@@ -1,6 +1,7 @@
- page_title _("Gitea import")
-%h1.page-title.gl-font-size-h-display
- = custom_icon('gitea_logo')
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
+ .gl-display-flex.gl-align-items-center.gl-justify-content-center
+ = sprite_icon('gitea', css_class: 'gl-mr-3', size: 48)
= _('Import projects from 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 5293013b813..24369ff3d39 100644
--- a/app/views/import/github/new.html.haml
+++ b/app/views/import/github/new.html.haml
@@ -3,8 +3,11 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h1.page-title.gl-font-size-h-display
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
+ .gl-display-flex.gl-align-items-center.gl-justify-content-center
+ = sprite_icon('github', css_class: 'gl-mr-3', size: 48)
= title
+%hr
%p
= import_github_authorize_message
@@ -23,9 +26,9 @@
= form_tag personal_access_token_import_github_path, method: :post do
.form-group
- %label.label-bold= _('Personal Access Token')
+ %label.col-form-label{ for: 'personal_access_token' }= _('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' }
+ = password_field_tag :personal_access_token, '', class: 'form-control gl-form-input', placeholder: _('e.g. %{token}') % { token: '8d3f016698e...' }, data: { testid: 'personal-access-token-field' }
%span.form-text.gl-text-gray-600
= import_github_personal_access_token_message
@@ -34,7 +37,5 @@
.form-actions.gl-display-flex.gl-justify-content-end
= render Pajamas::ButtonComponent.new(href: new_project_path) do
= _('Cancel')
- = render Pajamas::ButtonComponent.new(variant: :confirm,
- type: :submit,
- button_options: { class: 'gl-ml-3', data: { qa_selector: 'authenticate_button' } }) do
+ = render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit, button_options: { class: 'gl-ml-3', data: { testid: 'authenticate-button' } }) do
= _('Authenticate')
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 6f25bc75ca1..f1a61d72771 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -1,8 +1,8 @@
- title = has_ci_cd_only_params? ? _('Connect repositories from GitHub') : _('GitHub import')
- page_title title
-%h1.page-title.gl-font-size-h-display.mb-0.gl-display-flex
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
.gl-display-flex.gl-align-items-center.gl-justify-content-center
- = sprite_icon('github', css_class: 'gl-mr-2')
+ = sprite_icon('github', css_class: 'gl-mr-3', size: 48)
= _('Import repositories from GitHub')
= render 'import/githubish_status',
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index 079123e989e..b90d400a843 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -2,9 +2,9 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h1.page-title.gl-font-size-h-display.d-flex
+%h1.page-title.gl-font-size-h-display.d-flex.gl-align-items-center
.gl-display-flex.gl-align-items-center.gl-justify-content-center
- = sprite_icon('tanuki', css_class: 'gl-mr-2')
+ = sprite_icon('tanuki', css_class: 'gl-mr-3', size: 48)
= _('Import an exported GitLab project')
%hr
@@ -21,7 +21,7 @@
= file_field_tag :file, class: ''
.row
.form-actions.col-sm-12
- = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { class: 'gl-mr-2', data: { qa_selector: 'import_project_button' }}) do
+ = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { class: 'gl-mr-2', data: { testid: 'import-project-button' }}) do
= _('Import project')
= render Pajamas::ButtonComponent.new(href: new_project_path) do
= _('Cancel')
diff --git a/app/views/import/shared/_new_project_form.html.haml b/app/views/import/shared/_new_project_form.html.haml
index 6000612a285..042d94ad1b6 100644
--- a/app/views/import/shared/_new_project_form.html.haml
+++ b/app/views/import/shared/_new_project_form.html.haml
@@ -1,7 +1,7 @@
.row
.form-group.project-name.col-sm-12
= label_tag :name, _('Project name'), class: 'label-bold'
- = text_field_tag :name, @name, placeholder: "My awesome project", class: "js-project-name form-control gl-form-input input-lg", autofocus: true, required: true, aria: { required: true }, data: { qa_selector: 'project_name_field' }
+ = text_field_tag :name, @name, placeholder: "My awesome project", class: "js-project-name form-control gl-form-input input-lg", autofocus: true, required: true, aria: { required: true }, data: { testid: 'project-name-field' }
.form-group.col-12.col-sm-6.gl-pr-0
= label_tag :namespace_id, _('Project URL'), class: 'label-bold'
.input-group.gl-flex-nowrap
@@ -21,4 +21,4 @@
.gl-align-self-center.gl-pl-5 /
.form-group.col-12.col-sm-6.project-path
= label_tag :path, _('Project slug'), class: 'label-bold'
- = text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control gl-form-input", required: true, aria: { required: true }, data: { qa_selector: 'project_slug_field' }
+ = text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control gl-form-input", required: true, aria: { required: true }
diff --git a/app/views/invites/decline.html.haml b/app/views/invites/decline.html.haml
index 4a57d70cb6e..40e4455e565 100644
--- a/app/views/invites/decline.html.haml
+++ b/app/views/invites/decline.html.haml
@@ -1,5 +1,5 @@
- page_title _('Invitation declined')
-.decline-page.gl-display-flex.gl-flex-direction-column.gl-mx-auto{ class: 'gl-xs-w-full!' }
+.decline-page.gl-display-flex.gl-flex-direction-column.gl-mx-auto.gl-w-full.gl-sm-w-auto
.gl-align-self-center.gl-mb-4.gl-mt-7.gl-sm-mt-0= sprite_icon('check-circle', size: 48, css_class: 'gl-text-green-400')
%h2.gl-font-size-h2= _('You successfully declined the invitation')
%p
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 37d03bde72e..41f663c7c06 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -2,10 +2,6 @@
- site_name = _('GitLab')
- omit_og = sign_in_with_redirect?
--# This is a temporary place for the page specific style migrations to be included on all pages like page_specific_files
-- if Feature.disabled?(:page_specific_styles, current_user)
- - add_page_specific_style('page_bundles/projects')
-
%head{ omit_og ? { } : { prefix: "og: http://ogp.me/ns#" } }
%meta{ charset: "utf-8" }
%meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' }
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index f52ea801eef..fe2c2e968e8 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -7,7 +7,7 @@
- sidebar_panel = super_sidebar_nav_panel(nav: nav, user: current_user, group: group, project: @project, current_ref: current_ref, ref_type: @ref_type, viewed_user: @user, organization: @organization)
- sidebar_data = super_sidebar_context(current_user, group: group, project: @project, panel: sidebar_panel, panel_type: nav).to_json
- %aside.js-super-sidebar.super-sidebar.super-sidebar-loading{ data: { root_path: root_path, sidebar: sidebar_data, toggle_new_nav_endpoint: profile_preferences_path, force_desktop_expanded_sidebar: @force_desktop_expanded_sidebar.to_s, command_palette: command_palette_data(project: @project).to_json } }
+ %aside.js-super-sidebar.super-sidebar.super-sidebar-loading{ data: { root_path: root_path, sidebar: sidebar_data, force_desktop_expanded_sidebar: @force_desktop_expanded_sidebar.to_s, command_palette: command_palette_data(project: @project).to_json } }
- if display_whats_new?
#whats-new-app{ data: { version_digest: whats_new_version_digest } }
@@ -20,7 +20,7 @@
.mobile-overlay
= dispensable_render_if_exists 'layouts/header/verification_reminder'
.alert-wrapper.gl-force-block-formatting-context
- = dispensable_render 'shared/new_nav_announcement'
+ = dispensable_render 'shared/new_nav_for_everyone_announcement'
= dispensable_render 'shared/outdated_browser'
= dispensable_render_if_exists "layouts/header/licensed_user_count_threshold"
= dispensable_render_if_exists "layouts/header/token_expiry_notification"
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 451c66b074b..5a66cc0ddb5 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,6 +1,6 @@
- page_classes = page_class << @html_class
-- page_classes = page_classes.flatten.compact
-- body_classes = [user_application_theme, user_tab_width, @body_class, client_class_list, *custom_diff_color_classes]
+- page_classes = [user_application_theme, page_classes.flatten.compact]
+- body_classes = [user_tab_width, @body_class, client_class_list, *custom_diff_color_classes]
!!! 5
%html{ lang: I18n.locale, class: page_classes }
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 4e9ae7c7fd8..6a65b31a002 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -1,9 +1,9 @@
- add_page_specific_style 'page_bundles/login'
- custom_text = custom_sign_in_description
!!! 5
-%html.html-devise-layout{ lang: I18n.locale }
+%html.html-devise-layout{ class: user_application_theme, lang: I18n.locale }
= render "layouts/head", { startup_filename: 'signin' }
- %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{user_application_theme} #{client_class_list}", data: { page: body_data_page, qa_selector: 'login_page' } }
+ %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{client_class_list}", data: { page: body_data_page, qa_selector: 'login_page', testid: 'login-page' } }
= header_message
= render "layouts/init_client_detection_flags"
- if Feature.enabled?(:restyle_login_page, @project)
@@ -31,7 +31,7 @@
%h1.mb-3.gl-font-size-h2
= brand_title
.mb-3
- .gl-w-half.gl-xs-w-full.gl-ml-auto.gl-mr-auto.bar
+ .gl-w-full.gl-sm-w-half.gl-ml-auto.gl-mr-auto.bar
= yield
= render 'devise/shared/footer'
diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml
index 3e969b866a6..6816a64ac8f 100644
--- a/app/views/layouts/devise_empty.html.haml
+++ b/app/views/layouts/devise_empty.html.haml
@@ -1,8 +1,8 @@
- add_page_specific_style 'page_bundles/login'
!!! 5
-%html.html-devise-layout{ lang: I18n.locale }
+%html.html-devise-layout{ class: user_application_theme, lang: I18n.locale }
= render "layouts/head"
- %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{user_application_theme} #{client_class_list}" }
+ %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{client_class_list}" }
= header_message
= render "layouts/init_client_detection_flags"
= render "layouts/header/empty"
diff --git a/app/views/layouts/fullscreen.html.haml b/app/views/layouts/fullscreen.html.haml
index da192822902..f168c742085 100644
--- a/app/views/layouts/fullscreen.html.haml
+++ b/app/views/layouts/fullscreen.html.haml
@@ -1,8 +1,8 @@
- minimal = local_assigns.fetch(:minimal, false)
!!! 5
-%html{ lang: I18n.locale, class: page_class }
+%html{ class: [user_application_theme, page_class], lang: I18n.locale }
= render "layouts/head"
- %body{ class: "#{user_application_theme} #{user_tab_width} #{@body_class} fullscreen-layout", data: { page: body_data_page } }
+ %body{ class: "#{user_tab_width} #{@body_class} fullscreen-layout", data: { page: body_data_page } }
= render 'peek/bar'
= header_message
- unless minimal
diff --git a/app/views/layouts/minimal.html.haml b/app/views/layouts/minimal.html.haml
index 8b6a2a2f2a7..e499b9ae240 100644
--- a/app/views/layouts/minimal.html.haml
+++ b/app/views/layouts/minimal.html.haml
@@ -1,17 +1,18 @@
- page_classes = page_class.push(@html_class).flatten.compact
!!! 5
-%html{ lang: I18n.locale, class: page_classes }
+%html.gl-h-full{ lang: I18n.locale, class: page_classes }
= render "layouts/head"
- %body{ data: body_data, class: system_message_class }
+ %body.gl-h-full{ data: body_data, class: system_message_class }
= header_message
= render 'peek/bar'
= render 'layouts/published_experiments'
= render "layouts/header/empty"
- .layout-page
+ .layout-page.gl-h-full.borderless.gl-display-flex.gl-flex-wrap
.content-wrapper.gl-pt-6{ class: 'gl-md-pt-11!' }
%div{ class: container_class }
%main#content-body.content
= render "layouts/flash" unless @hide_flash
= yield
+ = yield :footer
= footer_message
diff --git a/app/views/layouts/nav/_ask_duo_button.html.haml b/app/views/layouts/nav/_ask_duo_button.html.haml
deleted file mode 100644
index e37ce50352c..00000000000
--- a/app/views/layouts/nav/_ask_duo_button.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-- if Gitlab.ee? && ::Gitlab::Llm::TanukiBot.show_breadcrumbs_entry_point_for?(user: current_user)
- - label = s_('TanukiBot|GitLab Duo Chat')
- = render Pajamas::ButtonComponent.new(variant: :confirm,
- category: :secondary,
- icon: 'tanuki-ai',
- size: 'small',
- button_options: { class: 'js-tanuki-bot-chat-toggle gl-ml-3 gl-display-none gl-md-display-inline', data: { track_action: 'click_button', track_label: 'tanuki_bot_breadcrumbs_button' }, aria: { label: label }}) do
- = label
- = render Pajamas::ButtonComponent.new(variant: :confirm,
- category: :secondary,
- icon: 'tanuki-ai',
- size: 'small',
- button_options: { class: 'js-tanuki-bot-chat-toggle has-tooltip gl-ml-3 gl-md-display-none', title: label, data: { track_action: 'click_button', track_label: 'tanuki_bot_breadcrumbs_button', placement: 'left' }, aria: { label: label }})
diff --git a/app/views/layouts/nav/_top_bar.html.haml b/app/views/layouts/nav/_top_bar.html.haml
index ef783b688e0..c938cad5c42 100644
--- a/app/views/layouts/nav/_top_bar.html.haml
+++ b/app/views/layouts/nav/_top_bar.html.haml
@@ -12,4 +12,4 @@
- elsif defined?(@left_sidebar)
= render Pajamas::ButtonComponent.new(icon: 'sidebar', category: :tertiary, button_options: { class: 'toggle-mobile-nav gl-ml-n3', data: { testid: 'toggle-mobile-nav-button' }, aria: { label: _('Open sidebar') } })
= render "layouts/nav/breadcrumbs/breadcrumbs"
- = render "layouts/nav/ask_duo_button"
+ = render_if_exists "layouts/nav/ask_duo_button"
diff --git a/app/views/layouts/signup_onboarding.html.haml b/app/views/layouts/signup_onboarding.html.haml
index a5953021671..c8e15896b97 100644
--- a/app/views/layouts/signup_onboarding.html.haml
+++ b/app/views/layouts/signup_onboarding.html.haml
@@ -1,9 +1,9 @@
- add_page_specific_style 'page_bundles/signup'
- add_page_specific_style 'page_bundles/login'
!!! 5
-%html.html-devise-layout{ lang: I18n.locale }
+%html.html-devise-layout{ class: user_application_theme, lang: I18n.locale }
= render "layouts/head"
- %body.signup-page.navless{ class: "#{system_message_class} #{user_application_theme} #{client_class_list}", data: { page: body_data_page, qa_selector: 'signup_page' } }
+ %body.signup-page.navless{ class: "#{system_message_class} #{client_class_list}", data: { page: body_data_page } }
= header_message
= render "layouts/init_client_detection_flags"
= render "layouts/header/logo_with_title"
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
index 32f00a4c0c6..09b5407ecdb 100644
--- a/app/views/layouts/terms.html.haml
+++ b/app/views/layouts/terms.html.haml
@@ -2,11 +2,10 @@
- add_page_specific_style 'page_bundles/terms'
- @hide_top_bar = true
- @hide_top_bar_padding = true
-- body_classes = [user_application_theme]
-%html{ lang: I18n.locale, class: page_class }
+%html{ lang: I18n.locale, class: [user_application_theme, page_class] }
= render "layouts/head"
- %body{ class: body_classes, data: { page: body_data_page } }
+ %body{ data: { page: body_data_page } }
.layout-page.terms{ class: page_class }
.content-wrapper.gl-pb-5
.mobile-overlay
diff --git a/app/views/notify/github_gists_import_errors_email.html.haml b/app/views/notify/github_gists_import_errors_email.html.haml
index 07b4cfca77e..903f4bf1466 100644
--- a/app/views/notify/github_gists_import_errors_email.html.haml
+++ b/app/views/notify/github_gists_import_errors_email.html.haml
@@ -11,7 +11,7 @@
%li
= s_("GithubImporter|Gist with id %{gist_id} failed due to error: %{error}.") % { gist_id: gist_id, error: error }
- if error == Gitlab::GithubGistsImport::Importer::GistImporter::FILE_COUNT_LIMIT_MESSAGE
- - import_snippets_url = help_page_url('api/import.md', anchor: 'import-github-gists-into-gitlab-snippets')
+ - import_snippets_url = help_page_url('api/import', anchor: 'import-github-gists-into-gitlab-snippets')
- import_snippets_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: import_snippets_url }
= html_escape(s_("GithubImporter|Please follow %{import_snippets_link_start}Import GitHub gists into GitLab snippets%{import_snippets_link_end} for more details.")) % { import_snippets_link_start: import_snippets_link_start, import_snippets_link_end: '</a>'.html_safe }
diff --git a/app/views/notify/pages_domain_auto_ssl_failed_email.html.haml b/app/views/notify/pages_domain_auto_ssl_failed_email.html.haml
index c0b334fba94..d053fdff624 100644
--- a/app/views/notify/pages_domain_auto_ssl_failed_email.html.haml
+++ b/app/views/notify/pages_domain_auto_ssl_failed_email.html.haml
@@ -5,7 +5,7 @@
%p
#{_('Domain')}: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)}
%p
- - docs_url = help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md', anchor: 'troubleshooting')
+ - docs_url = help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration', anchor: 'troubleshooting')
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: docs_url }
- link_end = '</a>'.html_safe
= _("Please follow the %{link_start}Let's Encrypt troubleshooting instructions%{link_end} to re-obtain your Let's Encrypt certificate.").html_safe % { link_start: link_start, link_end: link_end }
diff --git a/app/views/notify/pages_domain_auto_ssl_failed_email.text.haml b/app/views/notify/pages_domain_auto_ssl_failed_email.text.haml
index feb88d2df39..ecc466d3e74 100644
--- a/app/views/notify/pages_domain_auto_ssl_failed_email.text.haml
+++ b/app/views/notify/pages_domain_auto_ssl_failed_email.text.haml
@@ -3,5 +3,5 @@
#{_('Project')}: #{project_url(@project)}
#{_('Domain')}: #{project_pages_domain_url(@project, @domain)}
-- docs_url = help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md', anchor: 'troubleshooting')
+- docs_url = help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration', anchor: 'troubleshooting')
= _("Please follow the Let's Encrypt troubleshooting instructions to re-obtain your Let's Encrypt certificate: %{docs_url}.").html_safe % { docs_url: docs_url }
diff --git a/app/views/notify/pages_domain_disabled_email.html.haml b/app/views/notify/pages_domain_disabled_email.html.haml
index 44f85df97b9..6b4e40780aa 100644
--- a/app/views/notify/pages_domain_disabled_email.html.haml
+++ b/app/views/notify/pages_domain_disabled_email.html.haml
@@ -8,6 +8,6 @@
Domain: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)}
%p
If this domain has been disabled in error, please follow
- = link_to 'these instructions', help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: '4-verify-the-domains-ownership')
+ = link_to 'these instructions', help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: '4-verify-the-domains-ownership')
to verify and re-enable your domain.
= render 'removal_notification'
diff --git a/app/views/notify/pages_domain_disabled_email.text.haml b/app/views/notify/pages_domain_disabled_email.text.haml
index 5a0fcab72d4..12295f9aa18 100644
--- a/app/views/notify/pages_domain_disabled_email.text.haml
+++ b/app/views/notify/pages_domain_disabled_email.text.haml
@@ -7,7 +7,7 @@ Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)})
If this domain has been disabled in error, please follow these instructions
to verify and re-enable your domain:
-= help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: 'steps')
+= help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: 'steps')
If you no longer wish to use this domain with GitLab Pages, please remove it
from your GitLab project and delete any related DNS records.
diff --git a/app/views/notify/pages_domain_enabled_email.html.haml b/app/views/notify/pages_domain_enabled_email.html.haml
index 103b17a87df..64155e888b7 100644
--- a/app/views/notify/pages_domain_enabled_email.html.haml
+++ b/app/views/notify/pages_domain_enabled_email.html.haml
@@ -7,5 +7,5 @@
Domain: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)}
%p
Please visit
- = link_to 'these instructions', help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: 'steps')
+ = link_to 'these instructions', help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: 'steps')
for more information about custom domain verification.
diff --git a/app/views/notify/pages_domain_enabled_email.text.haml b/app/views/notify/pages_domain_enabled_email.text.haml
index bf8d2ac767a..df56dacf52c 100644
--- a/app/views/notify/pages_domain_enabled_email.text.haml
+++ b/app/views/notify/pages_domain_enabled_email.text.haml
@@ -5,5 +5,5 @@ Project: #{@project.human_name} (#{project_url(@project)})
Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)})
Please visit
-= help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: 'steps')
+= help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: 'steps')
for more information about custom domain verification.
diff --git a/app/views/notify/pages_domain_verification_failed_email.html.haml b/app/views/notify/pages_domain_verification_failed_email.html.haml
index a819b66f18e..4d92d8d1088 100644
--- a/app/views/notify/pages_domain_verification_failed_email.html.haml
+++ b/app/views/notify/pages_domain_verification_failed_email.html.haml
@@ -10,6 +10,6 @@
Until then, you can view your content at #{link_to @domain.url, @domain.url}
%p
Please visit
- = link_to 'these instructions', help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: 'steps')
+ = link_to 'these instructions', help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: 'steps')
for more information about custom domain verification.
= render 'removal_notification'
diff --git a/app/views/notify/pages_domain_verification_failed_email.text.haml b/app/views/notify/pages_domain_verification_failed_email.text.haml
index 85aa2d7a503..045fd5483b2 100644
--- a/app/views/notify/pages_domain_verification_failed_email.text.haml
+++ b/app/views/notify/pages_domain_verification_failed_email.text.haml
@@ -7,7 +7,7 @@ Unless you take action, it will be disabled on *#{@domain.enabled_until.strftime
Until then, you can view your content at #{@domain.url}
Please visit
-= help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: 'steps')
+= help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: 'steps')
for more information about custom domain verification.
If you no longer wish to use this domain with GitLab Pages, please remove it
diff --git a/app/views/notify/pages_domain_verification_succeeded_email.html.haml b/app/views/notify/pages_domain_verification_succeeded_email.html.haml
index 808b12948f9..aaf0dae597f 100644
--- a/app/views/notify/pages_domain_verification_succeeded_email.html.haml
+++ b/app/views/notify/pages_domain_verification_succeeded_email.html.haml
@@ -9,5 +9,5 @@
content at #{link_to @domain.url, @domain.url}
%p
Please visit
- = link_to 'these instructions', help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: 'steps')
+ = link_to 'these instructions', help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: 'steps')
for more information about custom domain verification.
diff --git a/app/views/notify/pages_domain_verification_succeeded_email.text.haml b/app/views/notify/pages_domain_verification_succeeded_email.text.haml
index 8d0694ef613..15cf9823a08 100644
--- a/app/views/notify/pages_domain_verification_succeeded_email.text.haml
+++ b/app/views/notify/pages_domain_verification_succeeded_email.text.haml
@@ -6,5 +6,5 @@ Domain: #{@domain.domain} (#{project_pages_domain_url(@project, @domain)})
No action is required on your part. You can view your content at #{@domain.url}
Please visit
-= help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: 'steps')
+= help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: 'steps')
for more information about custom domain verification.
diff --git a/app/views/organizations/organizations/users.html.haml b/app/views/organizations/organizations/users.html.haml
new file mode 100644
index 00000000000..5fb9d786e0b
--- /dev/null
+++ b/app/views/organizations/organizations/users.html.haml
@@ -0,0 +1,4 @@
+- page_title _('Users')
+
+#js-organizations-users{ data: organization_user_app_data(@organization) }
+
diff --git a/app/views/organizations/settings/general.html.haml b/app/views/organizations/settings/general.html.haml
index 94892ef9fbb..663c8fceedf 100644
--- a/app/views/organizations/settings/general.html.haml
+++ b/app/views/organizations/settings/general.html.haml
@@ -1 +1,4 @@
- page_title _("General settings")
+- add_page_specific_style 'page_bundles/settings'
+
+#js-organizations-settings-general{ data: { app_data: organization_settings_general_app_data(@organization) } }
diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml
index 982199d3d6f..031869cc60e 100644
--- a/app/views/profiles/gpg_keys/index.html.haml
+++ b/app/views/profiles/gpg_keys/index.html.haml
@@ -28,7 +28,7 @@
%h4.gl-mt-0
= _('Add a GPG key')
%p
- - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/repository/signed_commits/gpg.md') }
+ - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/repository/signed_commits/gpg') }
= _('Add a GPG key for secure access to GitLab. %{help_link_start}Learn more%{help_link_end}.').html_safe % {help_link_start: help_link_start, help_link_end: '</a>'.html_safe }
= render 'form'
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index 7ba42274f88..f80cd8cddc5 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -25,7 +25,7 @@
-# TODO: Remove this conditional when https://gitlab.com/gitlab-org/gitlab/-/issues/324764 is resolved.
- if Feature.enabled?(:disable_ssh_key_used_tracking)
= _('Unavailable')
- = link_to sprite_icon('question-o'), help_page_path('user/ssh.md', anchor: 'view-your-accounts-ssh-keys')
+ = link_to sprite_icon('question-o'), help_page_path('user/ssh', anchor: 'view-your-accounts-ssh-keys')
- else
= key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : _('Never')
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 0cd41788a53..8477d87a587 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -30,7 +30,7 @@
%h4.gl-mt-0
= _('Add an SSH key')
%p
- - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/ssh.md') }
+ - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/ssh') }
= _('Add an SSH key for secure access to GitLab. %{help_link_start}Learn more%{help_link_end}.').html_safe % {help_link_start: help_link_start, help_link_end: '</a>'.html_safe }
= render 'form'
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index c12f6907afb..0457561b283 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -35,7 +35,7 @@
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')
+ help_path: help_page_path('user/profile/personal_access_tokens', anchor: 'personal-access-token-scopes')
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json } }
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index a6534a16e86..96375412f94 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -78,6 +78,9 @@
= f.gitlab_ui_radio_component :layout, layout_choices[0][1], layout_choices[0][0], help_text: fixed_help_text
= f.gitlab_ui_radio_component :layout, layout_choices[1][1], layout_choices[1][0], help_text: fluid_help_text
+ - if Feature.enabled?(:ui_for_organizations, current_user)
+ #js-home-organization-setting{ data: { app_data: home_organization_setting_app_data } }
+
.js-listbox-input{ data: { label: s_('Preferences|Homepage'), description: s_('Preferences|Choose what content you want to see by default on your homepage.'), name: 'user[dashboard]', items: dashboard_choices.to_json, value: current_user.dashboard, block: true.to_s, toggle_class: 'gl-form-input-xl' } }
= render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific
@@ -152,6 +155,12 @@
= f.gitlab_ui_checkbox_component :time_display_relative,
s_('Preferences|Use relative times'),
help_text: s_('Preferences|For example: 30 minutes ago.')
+ .form-group
+ = f.label :time_display_format, class: 'label-bold' do
+ = s_('Preferences|Time format')
+ - time_display_format_choices.each_entry do |time_display_format_option|
+ .gl-mb-4
+ = f.gitlab_ui_radio_component :time_display_format, time_display_format_option[1], time_display_format_option[0]
.settings-section.js-preferences-form.js-search-settings-section#enabled_following
.settings-sticky-header
.settings-sticky-header-inner
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 4da48771ba3..405364b6792 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -122,6 +122,10 @@
allow_empty: true}
%small.form-text.text-gl-muted
= external_accounts_docs_link
+ - if Feature.enabled?(:mastodon_social_ui, @user)
+ .form-group.gl-form-group
+ = f.label :mastodon
+ = f.text_field :mastodon, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: "@robin@example.com"
.form-group.gl-form-group
= f.label :website_url, s_('Profiles|Website url')
@@ -152,7 +156,7 @@
%legend.col-form-label
= _('Private profile')
- private_profile_label = s_("Profiles|Don't display activity-related personal information on your profile.")
- - private_profile_help_link = link_to sprite_icon('question-o'), help_page_path('user/profile/index.md', anchor: 'make-your-user-profile-page-private')
+ - private_profile_help_link = link_to sprite_icon('question-o'), help_page_path('user/profile/index', anchor: 'make-your-user-profile-page-private')
= f.gitlab_ui_checkbox_component :private_profile, '%{private_profile_label} %{private_profile_help_link}'.html_safe % { private_profile_label: private_profile_label, private_profile_help_link: private_profile_help_link.html_safe }
%fieldset.form-group.gl-form-group
%legend.col-form-label
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index ff0b31da022..7c42053a376 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -41,7 +41,7 @@
alert_options: { class: 'gl-mb-3' },
dismissible: false) do |c|
- c.with_body do
- = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer'
- if current_password_required?
.form-group
@@ -130,7 +130,7 @@
alert_options: { class: 'gl-mb-3' },
dismissible: false) do |c|
- c.with_body do
- = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer'
.js-manage-two-factor-form{ data: { current_password_required: current_password_required?.to_s, profile_two_factor_auth_path: profile_two_factor_auth_path, profile_two_factor_auth_method: 'delete', codes_profile_two_factor_auth_path: codes_profile_two_factor_auth_path, codes_profile_two_factor_auth_method: 'post' } }
- else
%p
diff --git a/app/views/projects/_errors.html.haml b/app/views/projects/_errors.html.haml
index 2dba22d3be6..9c478f245dc 100644
--- a/app/views/projects/_errors.html.haml
+++ b/app/views/projects/_errors.html.haml
@@ -1 +1 @@
-= form_errors(@project)
+= form_errors(@project, custom_message: [:limit_reached])
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 93f4fe62568..e41a0d3d262 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -3,15 +3,17 @@
- emails_disabled = @project.emails_disabled?
.project-home-panel.js-show-on-project-root.gl-mt-4.gl-mb-5{ class: [("empty-project" if empty_repo)] }
- .gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-sm-flex-direction-column.gl-mb-3.gl-gap-5
+ .gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-flex-direction-column.gl-md-flex-direction-row.gl-mb-3.gl-gap-5
.home-panel-title-row.gl-display-flex.gl-align-items-center
%div{ class: 'avatar-container rect-avatar s64 home-panel-avatar gl-flex-shrink-0 gl-w-11 gl-h-11 gl-mr-3! float-none' }
= project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64, itemprop: 'image')
%div
%h1.home-panel-title.gl-font-size-h1.gl-mt-3.gl-mb-2.gl-display-flex.gl-word-break-word{ data: { testid: 'project-name-content' }, itemprop: 'name' }
= @project.name
- = visibility_level_content(@project, css_class: 'visibility-icon gl-text-secondary gl-ml-2', icon_css_class: 'icon')
- = render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: @project, additional_classes: 'gl-align-self-center gl-ml-2'
+ = visibility_level_content(@project, css_class: 'visibility-icon gl-text-secondary gl-mx-2', icon_css_class: 'icon')
+ = render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: @project, additional_classes: 'gl-align-self-center gl-mx-2'
+ - if @project.catalog_resource
+ = render partial: 'shared/ci_catalog_badge', locals: { href: project_ci_catalog_resource_path(@project, @project.catalog_resource), css_class: 'gl-mx-2' }
- if @project.group
= render_if_exists 'shared/tier_badge', source: @project, source_type: 'Project'
.home-panel-metadata.gl-font-sm.gl-text-secondary.gl-font-base.gl-font-weight-normal.gl-line-height-normal{ data: { testid: 'project-id-content' }, itemprop: 'identifier' }
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
index 6315c6dc52d..3e92ef25552 100644
--- a/app/views/projects/_import_project_pane.html.haml
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -17,7 +17,7 @@
= html_escape(_("Importing GitLab projects? Migrating GitLab projects when migrating groups by direct transfer is in Beta. %{link_start}Learn more.%{link_end}")) % { link_start: docs_link, link_end: '</a>'.html_safe }
.import-buttons
- if gitlab_project_import_enabled?
- .import_gitlab_project.has-tooltip{ data: { container: 'body', qa_selector: 'gitlab_import_button' } }
+ .import_gitlab_project.has-tooltip{ data: { container: 'body', testid: 'gitlab-import-button' } }
= render Pajamas::ButtonComponent.new(href: '#', icon: 'tanuki', button_options: { class: 'btn_import_gitlab_project js-import-project-btn', data: { href: new_import_gitlab_project_path, platform: 'gitlab_export', **tracking_attrs_data(track_label, 'click_button', 'gitlab_export') } }) do
= _('GitLab export')
diff --git a/app/views/projects/_invite_members_empty_project.html.haml b/app/views/projects/_invite_members_empty_project.html.haml
index d6cab06f773..14b0e82e021 100644
--- a/app/views/projects/_invite_members_empty_project.html.haml
+++ b/app/views/projects/_invite_members_empty_project.html.haml
@@ -4,6 +4,6 @@
= s_('InviteMember|Invite your team')
%p= s_('InviteMember|Add members to this project and start collaborating with your team.')
.js-invite-members-trigger{ data: { variant: 'confirm',
- classes: 'gl-mb-8 gl-xs-w-full',
+ classes: 'gl-mb-8 gl-w-full gl-sm-w-auto',
display_text: s_('InviteMember|Invite members'),
trigger_source: 'project_empty_page' } }
diff --git a/app/views/projects/_invite_members_modal.html.haml b/app/views/projects/_invite_members_modal.html.haml
index a1b0bdd6c56..8713cb4990a 100644
--- a/app/views/projects/_invite_members_modal.html.haml
+++ b/app/views/projects/_invite_members_modal.html.haml
@@ -3,4 +3,8 @@
.js-invite-members-modal{ data: { is_project: 'true',
access_levels: ProjectMember.permissible_access_level_roles(current_user, project).to_json,
reload_page_on_submit: current_path?('project_members#index').to_s,
- help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(project)).merge(users_filter_data(project.group)) }
+ help_link: help_page_url('user/permissions'),
+ is_signup_enabled: signup_enabled?.to_s,
+ new_users_url: new_admin_user_url,
+ is_current_user_admin: current_user&.admin?.to_s,
+ }.merge(common_invite_modal_dataset(project)).merge(users_filter_data(project.group)) }
diff --git a/app/views/projects/_service_desk_settings.html.haml b/app/views/projects/_service_desk_settings.html.haml
index 3dbc4c0fad7..aee61624f69 100644
--- a/app/views/projects/_service_desk_settings.html.haml
+++ b/app/views/projects/_service_desk_settings.html.haml
@@ -1,5 +1,5 @@
- expanded = expanded_by_default?
-%section.settings.js-service-desk-setting-wrapper.no-animate#js-service-desk{ class: ('expanded' if expanded), data: { qa_selector: 'service_desk_settings_content' } }
+%section.settings.js-service-desk-setting-wrapper.no-animate#js-service-desk{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Service Desk')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
@@ -18,6 +18,7 @@
selected_file_template_project_id: "#{@project.service_desk_setting&.file_template_project_id}",
outgoing_name: "#{@project.service_desk_setting&.outgoing_name}",
project_key: "#{@project.service_desk_setting&.project_key}",
+ add_external_participants_from_cc: "#{@project.service_desk_setting&.add_external_participants_from_cc}",
templates: available_service_desk_templates_for(@project),
public_project: "#{@project.public?}",
custom_email_endpoint: project_service_desk_custom_email_path(@project) } }
diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml
index e120975a8f9..19db01a2df1 100644
--- a/app/views/projects/artifacts/_tree_file.html.haml
+++ b/app/views/projects/artifacts/_tree_file.html.haml
@@ -1,6 +1,6 @@
- blob = file.blob
- external_link = blob.external_link?(@build)
-- if external_link
+- if external_link && Gitlab::CurrentSettings.enable_artifact_external_redirect_warning_page
- path_to_file = external_file_project_job_artifacts_path(@project, @build, path: file.path)
- else
- path_to_file = file_project_job_artifacts_path(@project, @build, path: file.path)
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 49a29e1dcb7..0753a021f1f 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -12,7 +12,7 @@
= render 'filepath_form', input_options: input_options
- if current_action?(:new) || current_action?(:create)
- - input_options = { id: 'file_name', name: 'file_name', value: params[:file_name] || (should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : ''), required: true, placeholder: "Filename", testid: 'file_name_field', class: 'new-file-name js-file-path-name-input' }
+ - input_options = { id: 'file_name', name: 'file_name', value: params[:file_name] || (should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : ''), required: true, placeholder: "Filename", testid: 'file-name-field', class: 'new-file-name js-file-path-name-input' }
= render 'filepath_form', input_options: input_options
- if should_suggest_gitlab_ci_yml?
.js-suggest-gitlab-ci-yml{ data: { track_label: 'suggest_gitlab_ci_yml',
@@ -37,7 +37,7 @@
= _("Soft wrap")
.file-editor.code
- .js-edit-mode-pane#editor{ data: { 'editor-loading': true, qa_selector: 'source_editor_preview_container' } }<
+ .js-edit-mode-pane#editor{ data: { 'editor-loading': true, testid: 'source-editor-preview-container' } }<
%pre.editor-loading-content= params[:content] || local_assigns[:blob_data]
- if local_assigns[:path]
.js-edit-mode-pane#preview.hide
diff --git a/app/views/projects/blob/_pipeline_tour_success.html.haml b/app/views/projects/blob/_pipeline_tour_success.html.haml
index f645d23aa1c..be2654c9b86 100644
--- a/app/views/projects/blob/_pipeline_tour_success.html.haml
+++ b/app/views/projects/blob/_pipeline_tour_success.html.haml
@@ -1,6 +1,6 @@
.js-success-pipeline-modal{ data: { 'commit-cookie': suggest_pipeline_commit_cookie_name,
'go-to-pipelines-path': project_pipelines_path(@project),
'project-merge-requests-path': project_merge_requests_path(@project),
- 'example-link': help_page_path('ci/examples/index.md'),
+ 'example-link': help_page_path('ci/examples/index'),
'code-quality-link': help_page_path('ci/testing/code_quality'),
'human-access': @project.team.human_max_access(current_user&.id) } }
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index 82f517e8a84..e8b0f2a6c6f 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -15,3 +15,6 @@
= render partial: 'pipeline_tour_success' if show_suggest_pipeline_creation_celebration?
= render 'shared/web_ide_path'
+
+-# https://gitlab.com/gitlab-org/gitlab/-/issues/408388#note_1578533983
+#js-ambiguous-ref-modal{ data: { ambiguous: @is_ambiguous_ref.to_s, ref: current_ref } }
diff --git a/app/views/projects/blob/viewers/_route_map.html.haml b/app/views/projects/blob/viewers/_route_map.html.haml
index 30182c100d5..64122b4dcd4 100644
--- a/app/views/projects/blob/viewers/_route_map.html.haml
+++ b/app/views/projects/blob/viewers/_route_map.html.haml
@@ -6,4 +6,4 @@
This Route Map is invalid:
= viewer.validation_message
-= link_to 'Learn more', help_page_path('ci/environments/index.md', anchor: 'go-from-source-files-to-public-pages')
+= link_to 'Learn more', help_page_path('ci/environments/index', anchor: 'go-from-source-files-to-public-pages')
diff --git a/app/views/projects/blob/viewers/_route_map_loading.html.haml b/app/views/projects/blob/viewers/_route_map_loading.html.haml
index d9e965246a8..0e5816a56af 100644
--- a/app/views/projects/blob/viewers/_route_map_loading.html.haml
+++ b/app/views/projects/blob/viewers/_route_map_loading.html.haml
@@ -1,4 +1,4 @@
= gl_loading_icon(inline: true, css_class: "gl-mr-1")
Validating Route Map…
-= link_to 'Learn more', help_page_path('ci/environments/index.md', anchor: 'go-from-source-files-to-public-pages')
+= link_to 'Learn more', help_page_path('ci/environments/index', anchor: 'go-from-source-files-to-public-pages')
diff --git a/app/views/projects/branch_defaults/_branch_names_fields.html.haml b/app/views/projects/branch_defaults/_branch_names_fields.html.haml
index 3e77cb51a85..982280120fa 100644
--- a/app/views/projects/branch_defaults/_branch_names_fields.html.haml
+++ b/app/views/projects/branch_defaults/_branch_names_fields.html.haml
@@ -10,6 +10,6 @@
%p.form-text.text-muted
= s_('ProjectSettings|Leave empty to use default template.')
= sprintf(s_('ProjectSettings|Maximum %{maxLength} characters.'), { maxLength: Issue::MAX_BRANCH_TEMPLATE })
- - branch_name_help_link = help_page_path('user/project/repository/branches/index.md', anchor: 'name-your-branch')
+ - branch_name_help_link = help_page_path('user/project/repository/branches/index', anchor: 'name-your-branch')
= link_to _('What variables can I use?'), branch_name_help_link, target: "_blank"
= render_if_exists 'projects/branch_defaults/branch_names_help'
diff --git a/app/views/projects/branch_defaults/_default_branch_fields.html.haml b/app/views/projects/branch_defaults/_default_branch_fields.html.haml
index 2c59e187d30..78ce43ca8c9 100644
--- a/app/views/projects/branch_defaults/_default_branch_fields.html.haml
+++ b/app/views/projects/branch_defaults/_default_branch_fields.html.haml
@@ -11,7 +11,7 @@
.form-group
- help_text = _("When merge requests and commits in the default branch close, any issues they reference also close.")
- - help_icon = link_to sprite_icon('question-o'), help_page_path('user/project/issues/managing_issues.md', anchor: 'closing-issues-automatically'), target: '_blank', rel: 'noopener noreferrer'
+ - help_icon = link_to sprite_icon('question-o'), help_page_path('user/project/issues/managing_issues', anchor: 'closing-issues-automatically'), target: '_blank', rel: 'noopener noreferrer'
= f.gitlab_ui_checkbox_component :autoclose_referenced_issues,
s_('ProjectSettings|Auto-close referenced issues on default branch'),
help_text: (help_text + "&nbsp;" + help_icon).html_safe
diff --git a/app/views/projects/branch_defaults/_show.html.haml b/app/views/projects/branch_defaults/_show.html.haml
index 5906cd34c17..521d5bb9890 100644
--- a/app/views/projects/branch_defaults/_show.html.haml
+++ b/app/views/projects/branch_defaults/_show.html.haml
@@ -14,4 +14,4 @@
%input{ name: 'update_section', type: 'hidden', value: 'js-issue-settings' }
= render 'projects/branch_defaults/default_branch_fields', f: f
= render 'projects/branch_defaults/branch_names_fields', f: f
- = f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/projects/branch_rules/_show.html.haml b/app/views/projects/branch_rules/_show.html.haml
index c16c03953c6..10cb91e35bd 100644
--- a/app/views/projects/branch_rules/_show.html.haml
+++ b/app/views/projects/branch_rules/_show.html.haml
@@ -3,7 +3,7 @@
- show_status_checks = @project.licensed_feature_available?(:external_status_checks)
- show_approvers = @project.licensed_feature_available?(:merge_request_approvers)
-%section.settings.no-animate#branch-rules{ class: ('expanded' if expanded), data: { qa_selector: 'branch_rules_content' } }
+%section.settings.no-animate#branch-rules{ class: ('expanded' if expanded), data: { testid: 'branch-rules-content' } }
.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
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 61961172eb2..3b9e8e706f9 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -4,10 +4,10 @@
- mr_status = merge_request_status(related_merge_request)
- is_default_branch = branch.name == @repository.root_ref
-%li{ class: "branch-item gl-display-flex! gl-align-items-center! js-branch-item js-branch-#{branch.name} gl-pl-5! gl-pr-2!", data: { name: branch.name, qa_selector: 'branch_container', qa_name: branch.name } }
+%li{ class: "branch-item gl-display-flex! gl-align-items-center! js-branch-item js-branch-#{branch.name} gl-pl-5! gl-pr-2!", data: { name: branch.name, testid: 'branch-container', qa_name: branch.name } }
.branch-info
.gl-display-flex.gl-align-items-center
- = link_to project_tree_path(@project, branch.name, ref_type: 'heads'), class: 'item-title str-truncated-100 ref-name', data: { qa_selector: 'branch_link' } do
+ = link_to project_tree_path(@project, branch.name, ref_type: 'heads'), class: 'item-title str-truncated-100 ref-name', data: { testid: 'branch-link' } do
= branch.name
= clipboard_button(text: branch.name, title: _("Copy branch name"))
- if is_default_branch
@@ -28,7 +28,7 @@
.pipeline-status.d-none.d-md-block<
- if commit_status
- = render 'ci/status/icon', size: 16, status: commit_status
+ = render 'ci/status/icon', status: commit_status
- elsif show_commit_status
.gl-display-inline-flex.gl-vertical-align-middle.gl-mr-3
%svg.s16
diff --git a/app/views/projects/branches/_panel.html.haml b/app/views/projects/branches/_panel.html.haml
index 8ef7d435420..8952ba75568 100644
--- a/app/views/projects/branches/_panel.html.haml
+++ b/app/views/projects/branches/_panel.html.haml
@@ -12,7 +12,7 @@
%h3.gl-new-card-title.h5
= panel_title
- c.with_body do
- %ul.content-list.branches-list.all-branches{ data: { qa_selector: 'all_branches_container' } }
+ %ul.content-list.branches-list.all-branches{ data: { testid: 'all-branches-container' } }
- branches.first(overview_max_branches).each do |branch|
= render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch), commit_status: @branch_pipeline_statuses[branch.name], show_commit_status: @branch_pipeline_statuses.any?
- if branches.size > overview_max_branches
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 4017db459a9..76d6b0a042d 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -14,7 +14,7 @@
%td.status
-# Sending 'status' prevents calling the user relation inside the presenter, generating N+1,
-# see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68743
- = render "ci/status/badge", status: status, title: job.status_title(status)
+ = render "ci/status/icon", status: status, show_status_text: true
%td
- if can?(current_user, :read_build, job)
@@ -104,10 +104,10 @@
.btn-group
- if can?(current_user, :read_job_artifacts, job) && job.artifacts?
= link_button_to nil, download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: _('Download artifacts'), icon: 'download'
+ - if can?(current_user, :cancel_build, job) && job.active?
+ = link_button_to nil, cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: _('Cancel'), icon: 'cancel'
- if can?(current_user, :update_build, job)
- - if job.active?
- = link_button_to nil, cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: _('Cancel'), icon: 'cancel'
- - elsif job.scheduled?
+ - if job.scheduled?
= render Pajamas::ButtonComponent.new(disabled: true, icon: 'planning') do
%time.js-remaining-time{ datetime: job.scheduled_at.utc.iso8601 }
= duration_in_numbers(job.execute_in)
@@ -124,7 +124,7 @@
class: 'has-tooltip',
icon: 'time-out'
- elsif allow_retry
- - if job.playable? && !admin && can?(current_user, :update_build, job)
+ - if job.playable? && !admin
= link_button_to nil, play_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Play'), icon: 'play'
- elsif job.retryable?
= link_button_to nil, retry_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Retry'), icon: 'retry'
diff --git a/app/views/projects/cleanup/_show.html.haml b/app/views/projects/cleanup/_show.html.haml
index fffa1ff36b9..1d365dbceb8 100644
--- a/app/views/projects/cleanup/_show.html.haml
+++ b/app/views/projects/cleanup/_show.html.haml
@@ -11,7 +11,7 @@
- link_end = '</a>'.html_safe
= _("Clean up after running %{link_start}git filter-repo%{link_end} on the repository.").html_safe % { link_start: link_start, link_end: link_end }
= link_to sprite_icon('question-o'),
- help_page_path('user/project/repository/reducing_the_repo_size_using_git.md'),
+ help_page_path('user/project/repository/reducing_the_repo_size_using_git'),
target: '_blank', rel: 'noopener noreferrer'
.settings-content
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index e79a91eddaf..42482a773be 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -3,7 +3,7 @@
= render partial: 'signature', object: @commit.signature
%strong
#{ s_('CommitBoxTitle|Commit') }
- %span.commit-sha{ data: { qa_selector: 'commit_sha_content' } }= @commit.short_id
+ %span.commit-sha{ data: { testid: 'commit-sha-content' } }= @commit.short_id
= clipboard_button(text: @commit.id, title: _('Copy commit SHA'))
%span.d-none.d-sm-inline= _('authored')
#{time_ago_with_tooltip(@commit.authored_date)}
@@ -19,7 +19,7 @@
#{time_ago_with_tooltip(@commit.committed_date)}
#js-commit-comments-button{ data: { comments_count: @notes_count.to_i } }
- = link_button_to _('Browse files'), project_tree_path(@project, @commit), class: 'gl-mr-3 gl-xs-w-full gl-xs-mb-3'
+ = link_button_to _('Browse files'), project_tree_path(@project, @commit), class: 'gl-mr-3 gl-w-full gl-sm-w-auto gl-xs-mb-3'
#js-commit-options-dropdown{ data: commit_options_dropdown_data(@project, @commit) }
.commit-box{ data: { project_path: project_path(@project) } }
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index 6aefc2eaa8b..d4a775728e3 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -17,17 +17,17 @@
- if signature.x509?
= render partial: "projects/commit/x509/certificate_details", locals: { signature: signature }
- = link_to(_('Learn more about X.509 signed commits'), help_page_path('user/project/repository/signed_commits/x509.md'), class: 'gl-link gl-display-block')
+ = link_to(_('Learn more about X.509 signed commits'), help_page_path('user/project/repository/signed_commits/x509'), class: 'gl-link gl-display-block')
- elsif signature.ssh?
= _('SSH key fingerprint:')
%span.gl-font-monospace= signature.key_fingerprint_sha256 || _('Unknown')
- = link_to(_('Learn about signing commits with SSH keys.'), help_page_path('user/project/repository/signed_commits/ssh.md'), class: 'gl-link gl-display-block gl-mt-3')
+ = link_to(_('Learn about signing commits with SSH keys.'), help_page_path('user/project/repository/signed_commits/ssh'), class: 'gl-link gl-display-block gl-mt-3')
- else
= _('GPG Key ID:')
%span.gl-font-monospace= signature.gpg_key_primary_keyid
- = link_to(_('Learn about signing commits'), help_page_path('user/project/repository/signed_commits/index.md'), class: 'gl-link gl-display-block gl-mt-3')
+ = link_to(_('Learn about signing commits'), help_page_path('user/project/repository/signed_commits/index'), class: 'gl-link gl-display-block gl-mt-3')
%a.signature-badge.gl-display-inline-block.gl-ml-4{ role: 'button', tabindex: 0, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
= gl_badge_tag label, variant: variant
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index c42d0fe9931..9f0c910c1c0 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -26,7 +26,7 @@
= author_avatar(commit, size: 40, has_tooltip: false)
.commit-detail.flex-list.gl-display-flex.gl-justify-content-space-between.gl-align-items-center.gl-flex-grow-1.gl-min-w-0
- .commit-content{ data: { qa_selector: 'commit_content' } }
+ .commit-content{ data: { testid: 'commit-content' } }
- if view_details && merge_request
= link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: ["commit-row-message item-title js-onboarding-commit-item", ("font-italic" if commit.message.empty?)]
- else
@@ -36,7 +36,7 @@
= commit.short_id
- if commit.description? && collapsible
= render Pajamas::ButtonComponent.new(icon: 'ellipsis_h',
- button_options: { class: 'button-ellipsis-horizontal text-expander js-toggle-button', data: { toggle: 'tooltip', container: 'body' }, :title => _("Toggle commit description"), aria: { label: _("Toggle commit description") }})
+ button_options: { class: 'button-ellipsis-horizontal text-expander js-toggle-button', data: { toggle: 'tooltip', container: 'body', collapse_title: _("Toggle commit description"), expand_title: _("Toggle commit description") }, :title => _("Toggle commit description"), aria: { label: _("Toggle commit description") }})
.committer
- commit_author_link = commit_author_link(commit, avatar: false, size: 24)
diff --git a/app/views/projects/diffs/viewers/_collapsed.html.haml b/app/views/projects/diffs/viewers/_collapsed.html.haml
index 578b0af3241..6cffae44084 100644
--- a/app/views/projects/diffs/viewers/_collapsed.html.haml
+++ b/app/views/projects/diffs/viewers/_collapsed.html.haml
@@ -1,3 +1,4 @@
.nothing-here-block.diff-collapsed{ data: { diff_for_path: collapsed_diff_url(viewer.diff_file) } }
= _("This diff is collapsed.")
- %button.click-to-expand.gl-button.btn.btn-link= _("Click to expand it.")
+ = render Pajamas::ButtonComponent.new(variant: :link, button_options: { class: 'click-to-expand' }) do
+ = _("Click to expand it.")
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 4e84a6ef7e7..fd0dc1178f7 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -5,116 +5,119 @@
- reduce_visibility_form_id = 'reduce-visibility-form'
- @force_desktop_expanded_sidebar = true
-= render Pajamas::AlertComponent.new(title: _('GitLab Pages has moved'),
+- if can?(current_user, :admin_project, @project)
+ = render Pajamas::AlertComponent.new(title: _('GitLab Pages has moved'),
alert_options: { class: 'gl-my-5', data: { feature_id: Users::CalloutsHelper::PAGES_MOVED_CALLOUT, dismiss_endpoint: callouts_path, defer_links: 'true' } }) do |c|
- - c.with_body do
- = _('To go to GitLab Pages, on the left sidebar, select %{pages_link}.').html_safe % {pages_link: link_to('Deploy > Pages', project_pages_path(@project)).html_safe}
-
-%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')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = _('Collapse')
- %p.gl-text-secondary= _('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: { testid: 'visibility-features-permissions-content' } }
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Visibility, project features, permissions')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = expanded ? _('Collapse') : _('Expand')
- %p.gl-text-secondary= _('Choose visibility level, enable/disable project features and their permissions, disable email notifications, and show default emoji reactions.')
-
- .settings-content
- = form_for @project, html: { multipart: true, class: "sharing-permissions-form", id: reduce_visibility_form_id }, authenticity_token: true do |f|
- %input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' }
- %template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project).to_json.html_safe
- .js-project-permissions-form{ data: visibility_confirm_modal_data(@project, reduce_visibility_form_id) }
-- if show_merge_request_settings_callout?(@project)
- %section.settings.expanded
- = render Pajamas::AlertComponent.new(variant: :info,
+ - c.with_body do
+ = _('To go to GitLab Pages, on the left sidebar, select %{pages_link}.').html_safe % {pages_link: link_to('Deploy > Pages', project_pages_path(@project)).html_safe}
+
+ %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')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = _('Collapse')
+ %p.gl-text-secondary= _('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: { testid: 'visibility-features-permissions-content' } }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Visibility, project features, permissions')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
+ %p.gl-text-secondary= _('Choose visibility level, enable/disable project features and their permissions, disable email notifications, and show default emoji reactions.')
+
+ .settings-content
+ = form_for @project, html: { multipart: true, class: "sharing-permissions-form", id: reduce_visibility_form_id }, authenticity_token: true do |f|
+ %input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' }
+ %template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project).to_json.html_safe
+ .js-project-permissions-form{ data: visibility_confirm_modal_data(@project, reduce_visibility_form_id) }
+ - if show_merge_request_settings_callout?(@project)
+ %section.settings.expanded
+ = render Pajamas::AlertComponent.new(variant: :info,
title: _('Merge requests and approvals settings have moved.'),
alert_options: { class: 'js-merge-request-settings-callout gl-my-5', data: { feature_id: Users::CalloutsHelper::MERGE_REQUEST_SETTINGS_MOVED_CALLOUT, dismiss_endpoint: callouts_path, defer_links: 'true' } }) do |c|
- - c.with_body do
- = _('On the left sidebar, select %{merge_requests_link} to view them.').html_safe % { merge_requests_link: link_to('Settings > Merge requests', project_settings_merge_requests_path(@project)).html_safe }
-
-%section.settings.no-animate{ class: ('expanded' if expanded), data: { testid: 'badges-settings-content' } }
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
- = s_('ProjectSettings|Badges')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = expanded ? _('Collapse') : _('Expand')
- %p.gl-text-secondary
- = s_('ProjectSettings|Customize this project\'s badges.')
- = link_to s_('ProjectSettings|What are badges?'), help_page_path('user/project/badges')
- .settings-content
- = render 'shared/badges/badge_settings'
-
-= render_if_exists 'compliance_management/compliance_framework/project_settings', expanded: expanded
-
-= render_if_exists 'projects/settings/default_issue_template'
-
-= render 'projects/service_desk_settings'
-
-%section.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded), data: { testid: 'advanced-settings-content' } }
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Advanced')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = expanded ? _('Collapse') : _('Expand')
- %p.gl-text-secondary= s_('ProjectSettings|Housekeeping, export, archive, change path, transfer, and delete.')
-
- .settings-content
- = render_if_exists 'projects/settings/restore', project: @project
-
- = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-mt-0' }, header_options: { class: 'gl-new-card-header gl-flex-direction-column' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
- - c.with_header do
- .gl-new-card-title-wrapper
- %h4.gl-new-card-title= _('Housekeeping')
- %p.gl-new-card-description
- = _('Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects.')
- = link_to _('Learn more.'), help_page_path('administration/housekeeping'), target: '_blank', rel: 'noopener noreferrer'
-
- - c.with_body do
- .gl-display-flex.gl-flex-wrap.gl-gap-3
- = render Pajamas::ButtonComponent.new(method: :post, href: housekeeping_project_path(@project)) do
- = _('Run housekeeping')
- #js-project-prune-unreachable-objects-button{ data: { prune_objects_path: housekeeping_project_path(@project, prune: true), prune_objects_doc_path: help_page_path('administration/housekeeping', anchor: 'prune-unreachable-objects') } }
-
- = render 'export', project: @project
-
- = render_if_exists 'projects/settings/archive'
-
- = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card rename-repository' }, header_options: { class: 'gl-new-card-header gl-flex-direction-column' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
- - c.with_header do
- .gl-new-card-title-wrapper
- %h4.gl-new-card-title.warning-title= _('Change path')
- %p.gl-new-card-description
- - link = link_to('', help_page_path('user/project/settings/index', anchor: 'rename-a-repository'), target: '_blank', rel: 'noopener noreferrer')
- = safe_format(_("A project’s repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}"), tag_pair(link, :link_start, :link_end))
-
- - c.with_body do
- = render 'projects/errors'
- = gitlab_ui_form_for @project do |f|
- .form-group
- %p
- %span.gl-font-weight-bold= _("Be careful. Renaming a project's repository can have unintended side effects.")
- = _('You will need to update your local repositories to point to the new location.')
- - if @project.deployment_platform.present?
- %p= _('Your deployment services will be broken, you will need to manually fix the services after renaming.')
- = f.label :path, _('Path'), class: 'label-bold'
+ - c.with_body do
+ = _('On the left sidebar, select %{merge_requests_link} to view them.').html_safe % { merge_requests_link: link_to('Settings > Merge requests', project_settings_merge_requests_path(@project)).html_safe }
+
+ %section.settings.no-animate{ class: ('expanded' if expanded), data: { testid: 'badges-settings-content' } }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
+ = s_('ProjectSettings|Badges')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
+ %p.gl-text-secondary
+ = s_('ProjectSettings|Customize this project\'s badges.')
+ = link_to s_('ProjectSettings|What are badges?'), help_page_path('user/project/badges')
+ .settings-content
+ = render 'shared/badges/badge_settings'
+
+ = render_if_exists 'compliance_management/compliance_framework/project_settings', expanded: expanded
+
+ = render_if_exists 'projects/settings/default_issue_template'
+
+ = render 'projects/service_desk_settings'
+
+ %section.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded), data: { testid: 'advanced-settings-content' } }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Advanced')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
+ %p.gl-text-secondary= s_('ProjectSettings|Housekeeping, export, archive, change path, transfer, and delete.')
+
+ .settings-content
+ = render_if_exists 'projects/settings/restore', project: @project
+
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-mt-0' }, header_options: { class: 'gl-new-card-header gl-flex-direction-column' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
+ - c.with_header do
+ .gl-new-card-title-wrapper
+ %h4.gl-new-card-title= _('Housekeeping')
+ %p.gl-new-card-description
+ = _('Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects.')
+ = link_to _('Learn more.'), help_page_path('administration/housekeeping'), target: '_blank', rel: 'noopener noreferrer'
+
+ - c.with_body do
+ .gl-display-flex.gl-flex-wrap.gl-gap-3
+ = render Pajamas::ButtonComponent.new(method: :post, href: housekeeping_project_path(@project)) do
+ = _('Run housekeeping')
+ #js-project-prune-unreachable-objects-button{ data: { prune_objects_path: housekeeping_project_path(@project, prune: true), prune_objects_doc_path: help_page_path('administration/housekeeping', anchor: 'prune-unreachable-objects') } }
+
+ = render 'export', project: @project
+
+ = render_if_exists 'projects/settings/archive'
+
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card rename-repository' }, header_options: { class: 'gl-new-card-header gl-flex-direction-column' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
+ - c.with_header do
+ .gl-new-card-title-wrapper
+ %h4.gl-new-card-title.warning-title= _('Change path')
+ %p.gl-new-card-description
+ - link = link_to('', help_page_path('user/project/settings/index', anchor: 'rename-a-repository'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(_("A project’s repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}"), tag_pair(link, :link_start, :link_end))
+
+ - c.with_body do
+ = render 'projects/errors'
+ = gitlab_ui_form_for @project do |f|
.form-group
- .input-group
- .input-group-prepend
- .input-group-text
- #{Gitlab::Utils.append_path(root_url, @project.namespace.full_path)}/
- = f.text_field :path, class: 'form-control gl-form-input-xl', data: { testid: 'project-path-field' }
- = f.submit _('Change path'), class: "btn-danger", data: { testid: 'change-path-button' }, pajamas_button: true
-
- = render 'transfer', project: @project
-
- = render 'remove_fork', project: @project
-
- = render 'remove', project: @project
+ %p
+ %span.gl-font-weight-bold= _("Be careful. Renaming a project's repository can have unintended side effects.")
+ = _('You will need to update your local repositories to point to the new location.')
+ - if @project.deployment_platform.present?
+ %p= _('Your deployment services will be broken, you will need to manually fix the services after renaming.')
+ = f.label :path, _('Path'), class: 'label-bold'
+ .form-group
+ .input-group
+ .input-group-prepend
+ .input-group-text
+ #{Gitlab::Utils.append_path(root_url, @project.namespace.full_path)}/
+ = f.text_field :path, class: 'form-control gl-form-input-xl', data: { testid: 'project-path-field' }
+ = f.submit _('Change path'), class: "btn-danger", data: { testid: 'change-path-button' }, pajamas_button: true
+
+ = render 'transfer', project: @project
+
+ = render 'remove_fork', project: @project
+
+ = render 'remove', project: @project
+- elsif can?(current_user, :archive_project, @project)
+ = render_if_exists 'projects/settings/archive'
.save-project-loader.hide
.center
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 7ddaf868a35..c2bea4bf43c 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -5,7 +5,7 @@
"can-read-environment" => can?(current_user, :read_environment, @project).to_s,
"can-create-environment" => can?(current_user, :create_environment, @project).to_s,
"new-environment-path" => new_project_environment_path(@project),
- "help-page-path" => help_page_path("ci/environments/index.md"),
+ "help-page-path" => help_page_path("ci/environments/index"),
"project-path" => @project.full_path,
"project-id" => @project.id,
"default-branch-name" => @project.default_branch_or_main,
diff --git a/app/views/projects/feature_flags/new.html.haml b/app/views/projects/feature_flags/new.html.haml
index 3a32a249d1e..1f723cb96b0 100644
--- a/app/views/projects/feature_flags/new.html.haml
+++ b/app/views/projects/feature_flags/new.html.haml
@@ -10,5 +10,5 @@
user_callout_id: Users::CalloutsHelper::FEATURE_FLAGS_NEW_VERSION,
show_user_callout: show_feature_flags_new_version?.to_s,
strategy_type_docs_page_path: help_page_path('operations/feature_flags', anchor: 'feature-flag-strategies'),
- environments_scope_docs_path: help_page_path('ci/environments/index.md', anchor: 'limit-the-environment-scope-of-a-cicd-variable'),
+ environments_scope_docs_path: help_page_path('ci/environments/index', anchor: 'limit-the-environment-scope-of-a-cicd-variable'),
project_id: @project.id } }
diff --git a/app/views/projects/feature_flags_user_lists/edit.html.haml b/app/views/projects/feature_flags_user_lists/edit.html.haml
index 417b6354ec0..70d614fc327 100644
--- a/app/views/projects/feature_flags_user_lists/edit.html.haml
+++ b/app/views/projects/feature_flags_user_lists/edit.html.haml
@@ -3,6 +3,6 @@
- breadcrumb_title s_('FeatureFlags|Edit User List')
- page_title s_('FeatureFlags|Edit User List')
-#js-edit-user-list{ data: { 'user-lists-docs-path' => help_page_path('operations/feature_flags.md', anchor: 'user-list'),
+#js-edit-user-list{ data: { 'user-lists-docs-path' => help_page_path('operations/feature_flags', anchor: 'user-list'),
'user-list-iid' => @user_list.iid,
'project-id' => @project.id } }
diff --git a/app/views/projects/feature_flags_user_lists/new.html.haml b/app/views/projects/feature_flags_user_lists/new.html.haml
index cea55c0ca2a..7f20fc4a9ec 100644
--- a/app/views/projects/feature_flags_user_lists/new.html.haml
+++ b/app/views/projects/feature_flags_user_lists/new.html.haml
@@ -4,6 +4,6 @@
- breadcrumb_title s_('FeatureFlags|New User List')
- page_title s_('FeatureFlags|New User List')
-#js-new-user-list{ data: { 'user-lists-docs-path' => help_page_path('operations/feature_flags.md', anchor: 'user-list'),
+#js-new-user-list{ data: { 'user-lists-docs-path' => help_page_path('operations/feature_flags', anchor: 'user-list'),
'feature-flags-path' => project_feature_flags_path(@project),
'project-id' => @project.id } }
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
index 0c760ab82c9..997e7b7f24d 100644
--- a/app/views/projects/find_file/show.html.haml
+++ b/app/views/projects/find_file/show.html.haml
@@ -6,7 +6,7 @@
- blob_path = project_blob_path(@project, @ref)
.file-finder-holder.tree-holder.clearfix.js-file-finder.gl-pt-4{ data: { file_find_url: "#{escape_javascript(project_files_path(@project, @ref, ref_type: @ref_type, format: :json))}", find_tree_url: escape_javascript(tree_path), blob_url_template: escape_javascript(blob_path), ref_type: @ref_type } }
.nav-block.gl-xs-mr-0
- .tree-ref-holder.gl-xs-mb-3.gl-xs-w-full.gl-max-w-26
+ .tree-ref-holder.gl-xs-mb-3.gl-max-w-26
#js-blob-ref-switcher{ data: { project_id: @project.id, ref: @ref, ref_type: @ref_type, namespace: "/-/find_file" } }
%ul.breadcrumb.repo-breadcrumb.gl-flex-nowrap
%li.breadcrumb-item.gl-white-space-nowrap
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index 49047749b71..fe7d2c9d198 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -8,7 +8,7 @@
- full_count_title = "#{@public_forks_count} public, #{@internal_forks_count} internal, and #{@private_forks_count} private"
#{pluralize(@total_forks_count, 'fork')}: #{full_count_title}
- .gl-display-flex.gl-sm-flex-direction-column.gl-md-align-items-center
+ .gl-display-flex.gl-flex-direction-column.gl-md-flex-direction-row.gl-md-align-items-center
= form_tag request.original_url, method: :get, class: 'project-filter-form gl-display-flex gl-mt-3 gl-md-mt-0', id: 'project-filter-form' do |f|
= search_field_tag :filter_projects, nil, placeholder: _('Search forks'), class: 'projects-list-filter project-filter-form-field form-control input-short gl-flex-grow-1',
spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' }
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index e9c6b3fcd22..1194a361753 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -9,6 +9,7 @@
project_id: @project.id,
project_name: @project.name,
project_path: @project.path,
+ project_default_branch: @project.default_branch,
project_description: @project.description,
project_visibility: @project.visibility,
restricted_visibility_levels: Gitlab::CurrentSettings.restricted_visibility_levels.to_json } }
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index a3569d41714..e766536f12b 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -7,7 +7,7 @@
%tr.generic-commit-status{ class: ('retried' if retried) }
%td.status
- = render 'ci/status/badge', status: generic_commit_status.detailed_status(current_user)
+ = render 'ci/status/icon', status: generic_commit_status.detailed_status(current_user), show_status_text: true
%td
= generic_commit_status.name
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 90d99d51d29..68de9c44e38 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -43,7 +43,7 @@
%li.droplab-item-ignore.gl-ml-3.gl-mr-3.gl-mt-5
- if can_create_confidential_merge_request?
- #js-forked-project{ data: { namespace_path: @project.namespace.full_path, project_path: @project.full_path, new_fork_path: new_project_fork_path(@project), help_page_path: help_page_path('user/project/merge_requests/index.md') } }
+ #js-forked-project{ data: { namespace_path: @project.namespace.full_path, project_path: @project.full_path, new_fork_path: new_project_fork_path(@project), help_page_path: help_page_path('user/project/merge_requests/index') } }
.form-group
%label{ for: 'new-branch-name' }
= _('Branch name')
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index 21f1a4d19fa..1a6edb288b5 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -16,9 +16,9 @@
%li.list-item{ class: "gl-py-0! gl-border-0!" }
.item-body.gl-display-flex.align-items-center.gl-px-3.gl-pr-2.gl-mx-n2
.item-contents.gl-display-flex.gl-align-items-center.gl-flex-wrap.gl-flex-grow-1.gl-min-h-7
- .item-title.gl-display-flex.mb-xl-0.gl-min-w-0
+ .item-title.gl-display-flex.mb-xl-0.gl-min-w-0.gl-align-items-center
- if branch[:pipeline_status].present?
- %span.related-branch-ci-status
+ %span.gl-mt-n2.gl-mb-n2.gl-mr-3
= render 'ci/status/icon', status: branch[:pipeline_status]
%span.related-branch-info
%strong
diff --git a/app/views/projects/issues/service_desk/_issue.html.haml b/app/views/projects/issues/service_desk/_issue.html.haml
index 66b2eabac9d..dbc6e613e8b 100644
--- a/app/views/projects/issues/service_desk/_issue.html.haml
+++ b/app/views/projects/issues/service_desk/_issue.html.haml
@@ -1,4 +1,4 @@
-%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id, qa_selector: 'issue_container', qa_issue_title: issue.title } }
+%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
.issuable-info-container
.issuable-main-info
.issue-title.title
diff --git a/app/views/projects/issues/service_desk/_issue_estimate.html.haml b/app/views/projects/issues/service_desk/_issue_estimate.html.haml
index c49bf626f4e..c6fa8b64dec 100644
--- a/app/views/projects/issues/service_desk/_issue_estimate.html.haml
+++ b/app/views/projects/issues/service_desk/_issue_estimate.html.haml
@@ -1,7 +1,7 @@
- issue = local_assigns.fetch(:issue)
- if issue.time_estimate > 0
- %span.issuable-estimate.d-none.d-sm-inline-block.has-tooltip{ data: { container: 'body', qa_selector: 'issuable_estimate' }, title: _('Estimate') }
+ %span.issuable-estimate.d-none.d-sm-inline-block.has-tooltip{ data: { container: 'body' }, title: _('Estimate') }
&nbsp;
= sprite_icon('timer', css_class: 'issue-estimate-icon')
= Gitlab::TimeTrackingFormatter.output(issue.time_estimate)
diff --git a/app/views/projects/jobs/_header.html.haml b/app/views/projects/jobs/_header.html.haml
index 018ff093475..a77e8f2d0b4 100644
--- a/app/views/projects/jobs/_header.html.haml
+++ b/app/views/projects/jobs/_header.html.haml
@@ -2,7 +2,7 @@
.content-block.build-header.top-area.page-content-header
.header-content
- = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false, title: @build.status_title
+ = render 'ci/status/icon', status: @build.detailed_status(current_user), show_status_text: true
%strong
Job
= link_to "##{@build.id}", project_job_path(@project, @build), class: 'js-build-id'
diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml
index 80085cc6a34..2c0a8d831e4 100644
--- a/app/views/projects/merge_requests/_nav_btns.html.haml
+++ b/app/views/projects/merge_requests/_nav_btns.html.haml
@@ -9,12 +9,9 @@
= _("New merge request")
.dropdown.gl-dropdown
- = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret has-tooltip gl-display-none! gl-md-display-inline-flex!", data: { toggle: 'dropdown', title: _('Actions') } do
- = sprite_icon "ellipsis_v", size: 16, css_class: "dropdown-icon gl-icon"
- %span.gl-sr-only
- = _('Actions')
- = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md btn-block gl-button gl-dropdown-toggle gl-md-display-none!", data: { 'toggle' => 'dropdown' } do
- %span.gl-dropdown-button-text= _('Actions')
+ = render Pajamas::ButtonComponent.new(type: :button, category: :tertiary, variant: :default, icon: 'ellipsis_v', button_options: { data: { toggle: 'dropdown' }, class: 'has-tooltip gl-display-none! gl-md-display-inline-flex!', title: _("Actions")})
+ = render Pajamas::ButtonComponent.new(type: :button, variant: :default, button_options: { data: { 'toggle' => 'dropdown' }, class: 'gl-md-display-none!'}) do
+ = _('Actions')
= sprite_icon "chevron-down", size: 16, css_class: "dropdown-icon gl-icon"
.dropdown-menu.dropdown-menu-right
.gl-dropdown-inner
diff --git a/app/views/projects/merge_requests/_page.html.haml b/app/views/projects/merge_requests/_page.html.haml
index 637980bd2f8..03a1f2f3179 100644
--- a/app/views/projects/merge_requests/_page.html.haml
+++ b/app/views/projects/merge_requests/_page.html.haml
@@ -6,7 +6,7 @@
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", _("Merge requests")
- page_description @merge_request.description_html
- page_card_attributes @merge_request.card_attributes
-- suggest_changes_help_path = help_page_path('user/project/merge_requests/reviews/suggestions.md')
+- suggest_changes_help_path = help_page_path('user/project/merge_requests/reviews/suggestions')
- mr_action = j(params[:tab].presence || 'show')
- add_page_specific_style 'page_bundles/issuable'
- add_page_specific_style 'page_bundles/design_management'
diff --git a/app/views/projects/mirrors/_authentication_method.html.haml b/app/views/projects/mirrors/_authentication_method.html.haml
index c4cf128a62a..f72b0d582b7 100644
--- a/app/views/projects/mirrors/_authentication_method.html.haml
+++ b/app/views/projects/mirrors/_authentication_method.html.haml
@@ -5,7 +5,7 @@
= f.label :auth_method, _('Authentication method'), class: 'label-bold'
= f.select :auth_method,
options_for_select(auth_options, mirror.auth_method),
- {}, { class: "custom-select gl-form-select js-mirror-auth-type gl-max-w-34 gl-display-block", data: { qa_selector: 'authentication_method_field' } }
+ {}, { class: "custom-select gl-form-select js-mirror-auth-type gl-max-w-34 gl-display-block", data: { testid: 'authentication-method-field' } }
= f.hidden_field :auth_method, value: "password", class: "js-hidden-mirror-auth-type"
.form-group
diff --git a/app/views/projects/mirrors/_branch_filter.html.haml b/app/views/projects/mirrors/_branch_filter.html.haml
index 7d90906bfe8..39e82fd5711 100644
--- a/app/views/projects/mirrors/_branch_filter.html.haml
+++ b/app/views/projects/mirrors/_branch_filter.html.haml
@@ -6,4 +6,4 @@
= _('Mirror only protected branches')
- c.with_help_text do
= _('If enabled, only protected branches will be mirrored.')
- = link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index.md', anchor: 'mirror-only-protected-branches'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index', anchor: 'mirror-only-protected-branches'), target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index 00837ce1c73..7b27062f782 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -3,14 +3,14 @@
- mirror_settings_enabled = can?(current_user, :admin_remote_mirror, @project)
- mirror_settings_class = "#{'expanded' if expanded} #{'js-mirror-settings' if mirror_settings_enabled}".strip
-%section.settings.project-mirror-settings.no-animate#js-push-remote-settings{ class: mirror_settings_class, data: { qa_selector: 'mirroring_repositories_settings_content' } }
+%section.settings.project-mirror-settings.no-animate#js-push-remote-settings{ class: mirror_settings_class, data: { testid: 'mirroring-repositories-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Mirroring repositories')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _('Set up your project to automatically push and/or pull changes to/from another repository. Branches, tags, and commits will be synced automatically.')
- = link_to _('How do I mirror repositories?'), help_page_path('user/project/repository/mirror/index.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('How do I mirror repositories?'), help_page_path('user/project/repository/mirror/index'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
@@ -35,7 +35,7 @@
%div= form_errors(@project)
.form-group.has-feedback
= label_tag :url, _('Git repository URL'), class: 'label-light'
- = text_field_tag :url, nil, class: 'form-control gl-form-input js-mirror-url js-repo-url gl-form-input-xl', placeholder: _('Input the remote repository URL'), required: true, pattern: "(#{protocols}):\/\/.+", autocomplete: 'new-password', data: { qa_selector: 'mirror_repository_url_field' }
+ = text_field_tag :url, nil, class: 'form-control gl-form-input js-mirror-url js-repo-url gl-form-input-xl', placeholder: _('Input the remote repository URL'), required: true, pattern: "(#{protocols}):\/\/.+", autocomplete: 'new-password', data: { testid: 'mirror-repository-url-field' }
= render 'projects/mirrors/instructions'
@@ -43,7 +43,7 @@
= render 'projects/mirrors/branch_filter'
- = f.submit _('Mirror repository'), class: 'js-mirror-submit', name: :update_remote_mirror, pajamas_button: true, data: { qa_selector: 'mirror_repository_button' }
+ = f.submit _('Mirror repository'), class: 'js-mirror-submit', name: :update_remote_mirror, pajamas_button: true, data: { testid: 'mirror-repository-button' }
= render Pajamas::ButtonComponent.new(button_options: { type: 'reset', class: 'gl-ml-2 js-toggle-button' }) do
= _('Cancel')
diff --git a/app/views/projects/mirrors/_mirror_repos_form.html.haml b/app/views/projects/mirrors/_mirror_repos_form.html.haml
index 8378a74311f..24cda3445de 100644
--- a/app/views/projects/mirrors/_mirror_repos_form.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_form.html.haml
@@ -1,7 +1,7 @@
.form-group
= label_tag :mirror_direction, _('Mirror direction'), class: 'label-light'
.select-wrapper
- = select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control gl-form-select select-control js-mirror-direction gl-max-w-34 gl-display-block', disabled: true, data: { qa_selector: 'mirror_direction_field' }
+ = select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control gl-form-select select-control js-mirror-direction gl-max-w-34 gl-display-block', disabled: true, data: { testid: 'mirror-direction-field' }
= sprite_icon('chevron-down', css_class: "gl-icon gl-absolute gl-top-3 gl-right-3 gl-text-gray-200")
= render partial: "projects/mirrors/mirror_repos_push", locals: { f: f }
diff --git a/app/views/projects/mirrors/_mirror_repos_list.html.haml b/app/views/projects/mirrors/_mirror_repos_list.html.haml
index 59611db941f..5e3c4889d1d 100644
--- a/app/views/projects/mirrors/_mirror_repos_list.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_list.html.haml
@@ -17,24 +17,24 @@
= render_if_exists 'projects/mirrors/table_pull_row'
- @project.remote_mirrors.each_with_index do |mirror, index|
- next if mirror.new_record?
- %tr.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?), data: { qa_selector: 'mirrored_repository_row_container' } }
- %td{ data: { qa_selector: 'mirror_repository_url_content' } }
+ %tr.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?), data: { testid: 'mirrored-repository-row-container' } }
+ %td{ data: { testid: 'mirror-repository-url-content' } }
= mirror.safe_url || _('Invalid URL')
= render_if_exists 'projects/mirrors/mirror_branches_setting_badge', record: mirror
%td= _('Push')
%td
= mirror.last_update_started_at.present? ? time_ago_with_tooltip(mirror.last_update_started_at) : _('Never')
- %td{ data: { qa_selector: 'mirror_last_update_at_content' } }= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never')
+ %td{ data: { testid: 'mirror-last-update-at-content' } }= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never')
%td
- if mirror.disabled?
= render 'projects/mirrors/disabled_mirror_badge'
- if mirror.last_error.present?
- = gl_badge_tag _('Error'), { variant: :danger }, { data: { toggle: 'tooltip', html: 'true', qa_selector: 'mirror_error_badge_content' }, title: html_escape(mirror.last_error.try(:strip)) }
+ = gl_badge_tag _('Error'), { variant: :danger }, { data: { toggle: 'tooltip', html: 'true', testid: 'mirror-error-badge-content' }, title: html_escape(mirror.last_error.try(:strip)) }
%td
- if mirror_settings_enabled
.btn-group.mirror-actions-group{ role: 'group' }
- if mirror.ssh_key_auth?
- = clipboard_button(text: mirror.ssh_public_key, variant: :default, category: :primary, size: :medium, title: _('Copy SSH public key'), testid: 'copy_public_key_button')
+ = clipboard_button(text: mirror.ssh_public_key, variant: :default, category: :primary, size: :medium, title: _('Copy SSH public key'), testid: 'copy-public-key-button')
= render 'shared/remote_mirror_update_button', remote_mirror: mirror
= render Pajamas::ButtonComponent.new(variant: :danger,
icon: 'remove',
diff --git a/app/views/projects/mirrors/_mirror_repos_push.html.haml b/app/views/projects/mirrors/_mirror_repos_push.html.haml
index 5b02d650989..7f0298191cd 100644
--- a/app/views/projects/mirrors/_mirror_repos_push.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_push.html.haml
@@ -16,4 +16,4 @@
= _('Keep divergent refs')
- c.with_help_text do
- link_opening_tag = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
- = html_escape(_('Do not force push over diverged refs. After the mirror is created, this setting can only be modified using the API. %{mirroring_docs_link_start}Learn more about this option%{link_closing_tag} and %{mirroring_api_docs_link_start}the API.%{link_closing_tag}')) % { mirroring_docs_link_start: link_opening_tag % {url: help_page_path('user/project/repository/mirror/push.md', anchor: 'keep-divergent-refs')}, mirroring_api_docs_link_start: link_opening_tag % {url: help_page_path('api/remote_mirrors')}, link_closing_tag: '</a>'.html_safe }
+ = html_escape(_('Do not force push over diverged refs. After the mirror is created, this setting can only be modified using the API. %{mirroring_docs_link_start}Learn more about this option%{link_closing_tag} and %{mirroring_api_docs_link_start}the API.%{link_closing_tag}')) % { mirroring_docs_link_start: link_opening_tag % {url: help_page_path('user/project/repository/mirror/push', anchor: 'keep-divergent-refs')}, mirroring_api_docs_link_start: link_opening_tag % {url: help_page_path('api/remote_mirrors')}, link_closing_tag: '</a>'.html_safe }
diff --git a/app/views/projects/mirrors/_ssh_host_keys.html.haml b/app/views/projects/mirrors/_ssh_host_keys.html.haml
index d367f383e5a..cd9580d15e9 100644
--- a/app/views/projects/mirrors/_ssh_host_keys.html.haml
+++ b/app/views/projects/mirrors/_ssh_host_keys.html.haml
@@ -3,13 +3,13 @@
- verified_at = mirror.ssh_known_hosts_verified_at
.form-group.js-ssh-host-keys-section{ class: ('collapse' unless mirror.ssh_mirror_url?) }
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-detect-host-keys gl-mr-3', data: { qa_selector: 'detect_host_keys' } }) do
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-detect-host-keys gl-mr-3', data: { testid: '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?) }
%label.label-bold
= _('Fingerprints')
- .fingerprints-list.js-fingerprints-list{ data: { qa_selector: 'fingerprints_list' } }
+ .fingerprints-list.js-fingerprints-list{ data: { testid: 'fingerprints-list' } }
- mirror.ssh_known_hosts_fingerprints.each do |fp|
%code= fp.fingerprint_sha256 || fp.fingerprint
- if verified_at
diff --git a/app/views/projects/ml/model_versions/show.html.haml b/app/views/projects/ml/model_versions/show.html.haml
new file mode 100644
index 00000000000..0b3d5462a89
--- /dev/null
+++ b/app/views/projects/ml/model_versions/show.html.haml
@@ -0,0 +1,6 @@
+- add_to_breadcrumbs s_('ModelRegistry|Model registry'), project_ml_models_path(@project)
+- add_to_breadcrumbs @model_version.name, project_ml_model_path(@project, @model)
+- breadcrumb_title @model_version.version
+- page_title "#{@model_version.name} / #{@model_version.version}"
+
+= render(Projects::Ml::ShowMlModelVersionComponent.new(model_version: @model_version))
diff --git a/app/views/projects/ml/models/index.html.haml b/app/views/projects/ml/models/index.html.haml
index 08f0db257ae..ffe7ee3397e 100644
--- a/app/views/projects/ml/models/index.html.haml
+++ b/app/views/projects/ml/models/index.html.haml
@@ -1,4 +1,4 @@
- breadcrumb_title s_('ModelRegistry|Model registry')
- page_title s_('ModelRegistry|Model registry')
-= render(Projects::Ml::ModelsIndexComponent.new(paginator: @paginator))
+= render(Projects::Ml::ModelsIndexComponent.new(paginator: @paginator, model_count: @model_count))
diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml
index 6eab31075d4..1e18e528665 100644
--- a/app/views/projects/pages/_access.html.haml
+++ b/app/views/projects/pages/_access.html.haml
@@ -1,7 +1,7 @@
- if @project.pages_deployed?
- pages_url = build_pages_url(@project, with_unique_domain: true)
- = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5', data: { qa_selector: 'access_page_container' } }, footer_options: { class: 'gl-alert-warning' }) do |c|
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5', data: { testid: 'access-page-container' } }, footer_options: { class: 'gl-alert-warning' }) do |c|
- c.with_header do
= s_('GitLabPages|Access pages')
- c.with_body do
diff --git a/app/views/projects/pages/_waiting.html.haml b/app/views/projects/pages/_waiting.html.haml
index 0613ffc4809..7aad6d6e0d2 100644
--- a/app/views/projects/pages/_waiting.html.haml
+++ b/app/views/projects/pages/_waiting.html.haml
@@ -5,7 +5,7 @@
.row.gl-align-items-center.gl-justify-content-center
.text-content.gl-text-center.order-md-1
%h4= s_("GitLabPages|Waiting for the Pages Pipeline to complete...")
- %p= s_("GitLabPages|Your Project has been configured for Pages. Now we have to wait for the Pipeline to succeed for the first time.")
+ %p= s_("GitLabPages|Your project is configured for GitLab Pages and the pipeline is running...")
= render Pajamas::ButtonComponent.new(variant: :confirm, href: project_pipelines_path(@project)) do
= s_("GitLabPages|Check the Pipeline Status")
= render Pajamas::ButtonComponent.new(href: new_namespace_project_pages_path) do
diff --git a/app/views/projects/pages/new.html.haml b/app/views/projects/pages/new.html.haml
index 89f8f62ea83..56dfc69d740 100644
--- a/app/views/projects/pages/new.html.haml
+++ b/app/views/projects/pages/new.html.haml
@@ -1,10 +1,5 @@
- @breadcrumb_link = project_pages_path(@project)
- page_title s_('GitLabPages|Pages')
-- if Feature.enabled?(:use_pipeline_wizard_for_pages, @project.group)
- #js-pages{ data: @pipeline_wizard_data }
-- else
- = render 'header'
-
- = render 'use'
+#js-pages{ data: @pipeline_wizard_data }
diff --git a/app/views/projects/pages_domains/_certificate.html.haml b/app/views/projects/pages_domains/_certificate.html.haml
index f80fd495695..1136abe9884 100644
--- a/app/views/projects/pages_domains/_certificate.html.haml
+++ b/app/views/projects/pages_domains/_certificate.html.haml
@@ -21,7 +21,7 @@
label_position: :hidden)
= f.hidden_field :auto_ssl_enabled?, class: "js-project-feature-toggle-input"
%p.gl-text-secondary.gl-mt-1
- - docs_link_url = help_page_path("user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md")
+ - docs_link_url = help_page_path("user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration")
- docs_link_start = "<a href=\"%{docs_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { docs_link_url: docs_link_url }
- docs_link_end = "</a>".html_safe
= _("Let's Encrypt is a free, automated, and open certificate authority (CA) that gives digital certificates in order to enable HTTPS (SSL/TLS) for websites. Learn more about Let's Encrypt configuration by following the %{docs_link_start}documentation on GitLab Pages%{docs_link_end}.").html_safe % { docs_link_url: docs_link_url, docs_link_start: docs_link_start, docs_link_end: docs_link_end }
diff --git a/app/views/projects/pages_domains/_dns.html.haml b/app/views/projects/pages_domains/_dns.html.haml
index 9ca9360199d..bec35dba147 100644
--- a/app/views/projects/pages_domains/_dns.html.haml
+++ b/app/views/projects/pages_domains/_dns.html.haml
@@ -27,5 +27,5 @@
.input-group-append
= deprecated_clipboard_button(target: '#domain_verification', class: 'btn-default d-none d-sm-block')
%p.form-text.text-muted
- - link_to_help = link_to(_('verify ownership'), help_page_path('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: '4-verify-the-domains-ownership'))
+ - link_to_help = link_to(_('verify ownership'), help_page_path('user/project/pages/custom_domains_ssl_tls_certification/index', anchor: '4-verify-the-domains-ownership'))
= _("To %{link_to_help} of your domain, add the above key to a TXT record within your DNS configuration within seven days.").html_safe % { link_to_help: link_to_help }
diff --git a/app/views/projects/pages_domains/_helper_text.html.haml b/app/views/projects/pages_domains/_helper_text.html.haml
index f29cb0609e6..4ad341c1394 100644
--- a/app/views/projects/pages_domains/_helper_text.html.haml
+++ b/app/views/projects/pages_domains/_helper_text.html.haml
@@ -1,4 +1,4 @@
-- docs_link_url = help_page_path("user/project/pages/custom_domains_ssl_tls_certification/index.md", anchor: "adding-an-ssltls-certificate-to-pages")
+- docs_link_url = help_page_path("user/project/pages/custom_domains_ssl_tls_certification/index", anchor: "adding-an-ssltls-certificate-to-pages")
- docs_link_start = "<a href=\"%{docs_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { docs_link_url: docs_link_url }
- docs_link_end = "</a>".html_safe
diff --git a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
index 8dcc59a09d0..cd49f064613 100644
--- a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
+++ b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
@@ -14,7 +14,7 @@
.create_access_levels-container
= yield :create_access_levels
- = f.submit _('Protect'), pajamas_button: true, disabled: true, data: { qa_selector: 'protect_tag_button' }
+ = f.submit _('Protect'), pajamas_button: true, disabled: true, data: { testid: 'protect-tag-button' }
= render Pajamas::ButtonComponent.new(button_options: { type: 'reset', class: 'gl-ml-2 js-toggle-button' }) do
= _('Cancel')
diff --git a/app/views/projects/protected_tags/shared/_dropdown.html.haml b/app/views/projects/protected_tags/shared/_dropdown.html.haml
index 758df7b3c1e..b1e29768be2 100644
--- a/app/views/projects/protected_tags/shared/_dropdown.html.haml
+++ b/app/views/projects/protected_tags/shared/_dropdown.html.haml
@@ -6,7 +6,7 @@
footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_tag_name],
- project_id: @project.try(:id), qa_selector: 'tags_dropdown' } }) do
+ project_id: @project.try(:id), testid: 'tags-dropdown' } }) do
%ul.dropdown-footer-list
%li
diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml
index f71ecc3a7c5..5c810b55bec 100644
--- a/app/views/projects/protected_tags/shared/_index.html.haml
+++ b/app/views/projects/protected_tags/shared/_index.html.haml
@@ -1,6 +1,6 @@
- expanded = expanded_by_default?
-%section.settings.no-animate#js-protected-tags-settings{ class: ('expanded' if expanded), data: { qa_selector: 'protected_tag_settings_content' } }
+%section.settings.no-animate#js-protected-tags-settings{ class: ('expanded' if expanded), data: { testid: 'protected-tag-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= s_("ProtectedTag|Protected tags")
diff --git a/app/views/projects/readme_templates/default.md.tt b/app/views/projects/readme_templates/default.md.tt
index 779b87336ea..7432918be21 100644
--- a/app/views/projects/readme_templates/default.md.tt
+++ b/app/views/projects/readme_templates/default.md.tt
@@ -38,7 +38,7 @@ git push -uf origin <%= params[:default_branch] %>
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
-- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
+- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
@@ -47,9 +47,10 @@ 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!). Thanks 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.
## Name
diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml
index 2d435a7ce9d..a79b73f6f61 100644
--- a/app/views/projects/runners/_group_runners.html.haml
+++ b/app/views/projects/runners/_group_runners.html.haml
@@ -1,4 +1,4 @@
-- link = link_to _('Runner API'), help_page_path('api/runners.md')
+- link = link_to _('Runner API'), help_page_path('api/runners')
%h4
= _('Group runners')
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 12432cd3484..96b87767690 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -26,7 +26,8 @@
- elsif runner.project_type?
= form_for [@project, @project.runner_projects.new] do |f|
= f.hidden_field :runner_id, value: runner.id
- = f.submit _('Enable for this project'), class: 'btn gl-button'
+ = render Pajamas::ButtonComponent.new(variant: :default, size: :small, type: :submit) do
+ = _('Enable for this project')
- if runner.description.present?
%p.gl-my-2
= runner.description
diff --git a/app/views/projects/settings/access_tokens/_form.html.haml b/app/views/projects/settings/access_tokens/_form.html.haml
index 919462a0f62..ee993962c7a 100644
--- a/app/views/projects/settings/access_tokens/_form.html.haml
+++ b/app/views/projects/settings/access_tokens/_form.html.haml
@@ -7,7 +7,7 @@
resource: @project,
token: @resource_access_token,
scopes: @scopes,
- access_levels: ProjectMember.permissible_access_level_roles(current_user, @project),
+ access_levels: ProjectMember.permissible_access_level_roles_for_project_access_token(current_user, @project),
default_access_level: Gitlab::Access::GUEST,
prefix: :resource_access_token,
description_prefix: :project_access_token,
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index fd27b125602..7011595e075 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -9,9 +9,9 @@
- 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/cicd_variables.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/cicd_variables.md', anchor: 'incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
+- help_link_continouos = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/stages', anchor: 'auto-deploy'), target: '_blank', rel: 'noopener noreferrer'
+- help_link_timed = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/cicd_variables', 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/cicd_variables', anchor: 'incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
.row
.col-lg-12
@@ -22,7 +22,7 @@
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
= render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-3' }, footer_options: { class: "js-extra-settings #{auto_devops_enabled || 'hidden'}", data: { testid: 'extra-auto-devops-settings' } }) do |c|
- c.with_body do
- - autodevops_help_link = link_to _('Learn more.'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer'
+ - autodevops_help_link = link_to _('Learn more.'), help_page_path('topics/autodevops/index'), target: '_blank', rel: 'noopener noreferrer'
- auto_devops_badge = auto_devops_enabled ? (gl_badge_tag badge_for_auto_devops_scope(@project), { variant: :info }, { class: 'js-instance-default-badge gl-ml-3 gl-mt-n1'}) : ''
= form.gitlab_ui_checkbox_component :enabled,
(s_('CICD|Default to Auto DevOps pipeline') + auto_devops_badge).html_safe,
diff --git a/app/views/projects/settings/merge_requests/_merge_request_merge_commit_template.html.haml b/app/views/projects/settings/merge_requests/_merge_request_merge_commit_template.html.haml
index da1965f549c..0a6f940e41a 100644
--- a/app/views/projects/settings/merge_requests/_merge_request_merge_commit_template.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_merge_commit_template.html.haml
@@ -9,5 +9,5 @@
%p.form-text.text-muted
= s_('ProjectSettings|Leave empty to use default template.')
= sprintf(s_('ProjectSettings|Maximum %{maxLength} characters.'), { maxLength: Project::MAX_COMMIT_TEMPLATE_LENGTH })
- - link = link_to('', help_page_path('user/project/merge_requests/commit_templates.md'), target: '_blank', rel: 'noopener noreferrer')
+ - link = link_to('', help_page_path('user/project/merge_requests/commit_templates'), target: '_blank', rel: 'noopener noreferrer')
= safe_format(s_('ProjectSettings|%{link_start}What variables can I use?%{link_end}'), tag_pair(link, :link_start, :link_end))
diff --git a/app/views/projects/settings/merge_requests/_merge_request_merge_method_settings.html.haml b/app/views/projects/settings/merge_requests/_merge_request_merge_method_settings.html.haml
index dd32d3f9d92..891bd62c0a4 100644
--- a/app/views/projects/settings/merge_requests/_merge_request_merge_method_settings.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_merge_method_settings.html.haml
@@ -12,7 +12,7 @@
- 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'
+- ffTrainsHelp = link_to s_('ProjectSettings|What are merge trains?'), help_page_path('ci/pipelines/merge_trains', anchor: 'enable-merge-trains'), target: '_blank', rel: 'noopener noreferrer'
- ffTrainsWithFastForward = (noMergeCommit + "<br />" + ffOnly + "<br />" + ffConflictRebase + "<br />" + ffTrains + " " + ffTrainsHelp).html_safe
- ffTrainsWithoutFastForward = (noMergeCommit + "<br />" + ffOnly + "<br />" + ffConflictRebase).html_safe
@@ -22,7 +22,7 @@
%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'
+ = link_to s_('ProjectSettings|How do they differ?'), help_page_path('user/project/merge_requests/methods/index'), target: '_blank', rel: 'noopener noreferrer'
= form.gitlab_ui_radio_component :merge_method,
:merge,
labelMerge,
@@ -35,4 +35,4 @@
:ff,
labelFastForward,
help_text: ffTrainsHelpFullHelpText,
- radio_options: { data: { qa_selector: 'merge_ff_radio' } }
+ radio_options: { data: { testid: 'merge-ff-radio' } }
diff --git a/app/views/projects/settings/merge_requests/_merge_request_merge_suggestions_settings.html.haml b/app/views/projects/settings/merge_requests/_merge_request_merge_suggestions_settings.html.haml
index 501288f727b..5aa7449c72f 100644
--- a/app/views/projects/settings/merge_requests/_merge_request_merge_suggestions_settings.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_merge_suggestions_settings.html.haml
@@ -9,5 +9,5 @@
%p.form-text.text-muted
= s_('ProjectSettings|Leave empty to use default template.')
= sprintf(s_('ProjectSettings|Maximum %{maxLength} characters.'), { maxLength: Project::MAX_SUGGESTIONS_TEMPLATE_LENGTH })
- - link = link_to('', help_page_path('user/project/merge_requests/reviews/suggestions.md', anchor: 'configure-the-commit-message-for-applied-suggestions'), target: '_blank', rel: 'noopener noreferrer')
+ - link = link_to('', help_page_path('user/project/merge_requests/reviews/suggestions', anchor: 'configure-the-commit-message-for-applied-suggestions'), target: '_blank', rel: 'noopener noreferrer')
= safe_format(s_('ProjectSettings|%{link_start}What variables can I use?%{link_end}'), tag_pair(link, :link_start, :link_end))
diff --git a/app/views/projects/settings/merge_requests/_merge_request_pipelines_and_threads_options.html.haml b/app/views/projects/settings/merge_requests/_merge_request_pipelines_and_threads_options.html.haml
index a9609434f15..65eb5b60cc3 100644
--- a/app/views/projects/settings/merge_requests/_merge_request_pipelines_and_threads_options.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_pipelines_and_threads_options.html.haml
@@ -9,5 +9,4 @@
help_text: s_('MergeChecks|Introduces the risk of merging changes that do not pass the pipeline.'),
checkbox_options: { class: 'gl-pl-6' }
= form.gitlab_ui_checkbox_component :only_allow_merge_if_all_discussions_are_resolved,
- s_('MergeChecks|All threads must be resolved'),
- checkbox_options: { data: { qa_selector: 'only_allow_merge_if_all_discussions_are_resolved_checkbox' } }
+ s_('MergeChecks|All threads must be resolved')
diff --git a/app/views/projects/settings/merge_requests/_merge_request_squash_commit_template.html.haml b/app/views/projects/settings/merge_requests/_merge_request_squash_commit_template.html.haml
index bc6530b927c..26b038f1bf7 100644
--- a/app/views/projects/settings/merge_requests/_merge_request_squash_commit_template.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_squash_commit_template.html.haml
@@ -9,5 +9,5 @@
%p.form-text.text-muted
= s_('ProjectSettings|Leave empty to use default template.')
= sprintf(s_('ProjectSettings|Maximum %{maxLength} characters.'), { maxLength: Project::MAX_COMMIT_TEMPLATE_LENGTH })
- - link = link_to('', help_page_path('user/project/merge_requests/commit_templates.md'), target: '_blank', rel: 'noopener noreferrer')
+ - link = link_to('', help_page_path('user/project/merge_requests/commit_templates'), target: '_blank', rel: 'noopener noreferrer')
= safe_format(s_('ProjectSettings|%{link_start}What variables can I use?%{link_end}'), tag_pair(link, :link_start, :link_end))
diff --git a/app/views/projects/settings/merge_requests/_merge_request_squash_options_settings.html.haml b/app/views/projects/settings/merge_requests/_merge_request_squash_options_settings.html.haml
index 372c0723600..120b183bf51 100644
--- a/app/views/projects/settings/merge_requests/_merge_request_squash_options_settings.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_squash_options_settings.html.haml
@@ -5,7 +5,7 @@
%b= s_('ProjectSettings|Squash commits when merging')
%p.text-secondary
= s_('ProjectSettings|Set the default behavior of this option in merge requests. Changes to this are also applied to existing merge requests.')
- = link_to s_('ProjectSettings|What is squashing?'), help_page_path('user/project/merge_requests/squash_and_merge.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to s_('ProjectSettings|What is squashing?'), help_page_path('user/project/merge_requests/squash_and_merge'), target: '_blank', rel: 'noopener noreferrer'
= settings.gitlab_ui_radio_component :squash_option,
:never,
diff --git a/app/views/projects/settings/merge_requests/show.html.haml b/app/views/projects/settings/merge_requests/show.html.haml
index e877be704a2..f48a4e5e42c 100644
--- a/app/views/projects/settings/merge_requests/show.html.haml
+++ b/app/views/projects/settings/merge_requests/show.html.haml
@@ -13,7 +13,7 @@
= gitlab_ui_form_for @project, url: project_settings_merge_requests_path(@project), html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' }
= render 'projects/settings/merge_requests/merge_request_settings', form: f
- = f.submit _('Save changes'), class: "rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' }, pajamas_button: true
+ = f.submit _('Save changes'), class: "rspec-save-merge-request-changes", data: { testid: 'save-merge-request-changes-button' }, pajamas_button: true
= render_if_exists 'projects/settings/merge_requests/merge_request_approvals_settings', expanded: true
= render_if_exists 'projects/settings/merge_requests/suggested_reviewers_settings', expanded: true
diff --git a/app/views/projects/settings/operations/_alert_management.html.haml b/app/views/projects/settings/operations/_alert_management.html.haml
index c29cedd8250..849597f6e65 100644
--- a/app/views/projects/settings/operations/_alert_management.html.haml
+++ b/app/views/projects/settings/operations/_alert_management.html.haml
@@ -11,6 +11,6 @@
= _('Expand')
%p.gl-text-secondary
= _('Display alerts from all configured monitoring tools.')
- = link_to _('Learn more.'), help_page_path('operations/incident_management/integrations.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('operations/incident_management/integrations'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
.js-alerts-settings{ data: alerts_settings_data }
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index cc49ff9e293..f04d6ab341f 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -30,7 +30,7 @@
= render partial: 'projects/commit/signature', object: tag.signature
- if commit_status
- = render 'ci/status/icon', size: 24, status: commit_status, option_css_classes: 'gl-display-inline-flex gl-vertical-align-middle gl-mr-5'
+ = render 'ci/status/icon', status: commit_status, option_css_classes: 'gl-display-inline-flex gl-vertical-align-middle gl-mr-5'
- elsif @tag_pipeline_statuses && @tag_pipeline_statuses.any?
.gl-display-inline-flex.gl-vertical-align-middle.gl-mr-5
%svg.s24
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 37f27aa7caf..bed37d9cb63 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -10,9 +10,10 @@
.tree-controls
.d-block.d-sm-flex.flex-wrap.align-items-start.gl-children-ml-sm-3.gl-first-child-ml-sm-0<
= render_if_exists 'projects/tree/lock_link'
+ = render 'projects/buttons/compare', project: @project, ref: @ref, root_ref: @repository&.root_ref
+
#js-tree-history-link{ data: { history_link: project_commits_path(@project, @ref) } }
- = render 'projects/buttons/compare', project: @project, ref: @ref, root_ref: @repository&.root_ref
= render 'projects/find_file_link'
= render 'shared/web_ide_button', blob: nil
= render 'projects/buttons/download', project: @project, ref: @ref
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 3c3f9eb7390..97b254a7b85 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -13,3 +13,6 @@
= render 'projects/last_push'
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
= render 'shared/web_ide_path'
+
+-# https://gitlab.com/gitlab-org/gitlab/-/issues/408388#note_1578533983
+#js-ambiguous-ref-modal{ data: { ambiguous: @is_ambiguous_ref.to_s, ref: current_ref } }
diff --git a/app/views/projects/usage_quotas/index.html.haml b/app/views/projects/usage_quotas/index.html.haml
index 6f2a2aacf66..039df9738ff 100644
--- a/app/views/projects/usage_quotas/index.html.haml
+++ b/app/views/projects/usage_quotas/index.html.haml
@@ -14,7 +14,7 @@
.col-sm-12
%p.gl-text-secondary
= s_('UsageQuota|Usage of project resources across the %{strong_start}%{project_name}%{strong_end} project').html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, project_name: @project.name } + '.'
- %a{ href: help_page_path('user/usage_quotas.md'), target: '_blank', rel: 'noopener noreferrer' }
+ %a{ href: help_page_path('user/usage_quotas'), target: '_blank', rel: 'noopener noreferrer' }
= s_('UsageQuota|Learn more about usage quotas') + '.'
= gl_tabs_nav({ id: 'js-project-usage-quotas-tabs' }) do
diff --git a/app/views/protected_branches/shared/_create_protected_branch.html.haml b/app/views/protected_branches/shared/_create_protected_branch.html.haml
index 96e6990b080..bb1d56dcc61 100644
--- a/app/views/protected_branches/shared/_create_protected_branch.html.haml
+++ b/app/views/protected_branches/shared/_create_protected_branch.html.haml
@@ -14,12 +14,17 @@
= render partial: "protected_branches/shared/dropdown", locals: { f: f, toggle_classes: 'gl-w-full! gl-form-input-lg' }
.form-text.text-muted
- wildcards_url = help_page_url('user/project/protected_branches', anchor: 'protect-multiple-branches-with-wildcard-rules')
- - wildcards_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: wildcards_url }
- - placeholders = { wildcards_link_start: wildcards_link_start, wildcards_link_end: '</a>', code_tag_start: '<code>', code_tag_end: '</code>' }
+ - wildcards_link_tag_pair = tag_pair(link_to('', wildcards_url, target: '_blank', rel: 'noopener noreferrer'), :wildcards_link_start, :wildcards_link_end)
+
+ - case_sensitive_url = help_page_url('user/project/protected_branches', anchor: 'branch-names-are-case-sensitive')
+ - case_sensitive_link_tag_pair = tag_pair(link_to('', case_sensitive_url, target: '_blank', rel: 'noopener noreferrer'), :case_sensitive_link_start, :case_sensitive_link_end)
+
+ - code_tag_pair = tag_pair(tag.code, :code_tag_start, :code_tag_end)
+
- if protected_branch_entity.is_a?(Group)
- = (s_("ProtectedBranch|Only %{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported.") % placeholders).html_safe
+ = safe_format(s_('ProtectedBranch|Only %{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported. %{case_sensitive_link_start}Branch names are case-sensitive.%{case_sensitive_link_end}'), wildcards_link_tag_pair, case_sensitive_link_tag_pair, code_tag_pair)
- else
- = (s_("ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported.") % placeholders).html_safe
+ = safe_format(s_('ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported. %{case_sensitive_link_start}Branch names are case-sensitive.%{case_sensitive_link_end}'), wildcards_link_tag_pair, case_sensitive_link_tag_pair, code_tag_pair)
.form-group.row
= f.label :merge_access_levels_attributes, s_("ProtectedBranch|Allowed to merge:"), class: 'col-sm-12'
.col-sm-12
@@ -38,6 +43,6 @@
- force_push_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: force_push_docs_url }
= (s_("ProtectedBranch|Allow all users with push access to %{tag_start}force push%{tag_end}.") % { tag_start: force_push_link_start, tag_end: '</a>' }).html_safe
= render_if_exists 'protected_branches/ee/code_owner_approval_form', f: f, protected_branch_entity: protected_branch_entity
- = f.submit s_('ProtectedBranch|Protect'), disabled: true, data: { qa_selector: 'protect_button' }, pajamas_button: true
+ = f.submit s_('ProtectedBranch|Protect'), disabled: true, data: { testid: 'protect-button' }, pajamas_button: true
= render Pajamas::ButtonComponent.new(button_options: { type: 'reset', class: 'gl-ml-2 js-toggle-button' }) do
= _('Cancel')
diff --git a/app/views/protected_branches/shared/_index.html.haml b/app/views/protected_branches/shared/_index.html.haml
index 8e72563182c..ce5b58ee189 100644
--- a/app/views/protected_branches/shared/_index.html.haml
+++ b/app/views/protected_branches/shared/_index.html.haml
@@ -1,7 +1,7 @@
- can_admin_entity = protected_branch_can_admin_entity?(protected_branch_entity)
- expanded = expanded_by_default?
-%section.settings.no-animate#js-protected-branches-settings{ class: ('expanded' if expanded), data: { qa_selector: 'protected_branches_settings_content' } }
+%section.settings.no-animate#js-protected-branches-settings{ class: ('expanded' if expanded), data: { testid: 'protected-branches-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= s_("ProtectedBranch|Protected branches")
diff --git a/app/views/protected_branches/shared/_protected_branch.html.haml b/app/views/protected_branches/shared/_protected_branch.html.haml
index 93c84e67d81..67c6e991a59 100644
--- a/app/views/protected_branches/shared/_protected_branch.html.haml
+++ b/app/views/protected_branches/shared/_protected_branch.html.haml
@@ -27,4 +27,14 @@
%span.has-tooltip{ data: { container: 'body' }, title: s_('ProtectedBranch|Inherited - This setting can be changed at the group level'), 'aria-hidden': 'true' }
= sprite_icon 'lock'
- else
- = link_button_to s_('ProtectedBranch|Unprotect'), [protected_branch_entity, protected_branch, { update_section: 'js-protected-branches-settings' }], disabled: local_assigns[:disabled], aria: { label: s_('ProtectedBranch|Unprotect branch') }, data: { confirm: s_('ProtectedBranch|Branch will be writable for developers. Are you sure?'), confirm_btn_variant: 'danger' }, method: :delete, variant: :danger, category: :secondary, size: :small
+ .gl-relative
+ - if local_assigns[:protected_from_deletion]
+ %span.gl-absolute.gl-display-inline-block.gl-w-full.gl-h-full{ data: { container: 'body', toggle: 'popover', placement: local_assigns[:placemet], html: 'true', triggers: 'hover', content: local_assigns[:popover_content] } }
+ = render Pajamas::ButtonComponent.new(size: :small,
+ variant: :danger,
+ href: [protected_branch_entity, protected_branch, { update_section: 'js-protected-branches-settings' }],
+ method: :delete,
+ disabled: local_assigns[:protected_from_deletion],
+ button_options: { update_section: 'js-protected-branches-settings', aria: { label: s_('ProtectedBranch|Unprotect branch') }, data: { confirm: s_('ProtectedBranch|Branch will be writable for developers. Are you sure?'), confirm_btn_variant: 'danger' } },
+ category: :secondary) do
+ = s_('ProtectedBranch|Unprotect')
diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb
index e780b13de6e..82730105a53 100644
--- a/app/views/pwa/manifest.json.erb
+++ b/app/views/pwa/manifest.json.erb
@@ -3,7 +3,7 @@
"name": "<%= appearance_pwa_name %>",
"short_name": "<%= appearance_pwa_short_name %>",
"description": "<%= appearance_pwa_description %>",
- "start_url": "<%= explore_projects_path %>",
+ "start_url": "<%= root_path %>",
"scope": "<%= root_path %>",
"display": "browser",
"orientation": "any",
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index 9c1f4c8643f..4fda5379876 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -17,9 +17,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.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-display-flex.gl-flex-wrap.gl-justify-content-end.gl-pt-6.gl-pb-5
+ = render_if_exists 'search/form_elasticsearch'
#js-search-topbar{ data: { "group-initial-json": group_attributes.to_json, "project-initial-json": project_attributes.to_json, "default-branch-name": @project&.default_branch } }
.results.gl-lg-display-flex.gl-mt-0
diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml
index 6d8d4f4cab9..3f613a1b383 100644
--- a/app/views/shared/_auto_devops_callout.html.haml
+++ b/app/views/shared/_auto_devops_callout.html.haml
@@ -12,5 +12,5 @@
%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')
+ - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index'), 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 79a9bafc4f0..0ff2ee935cc 100644
--- a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
+++ b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
@@ -9,4 +9,4 @@
= _('Container registry is not enabled on this GitLab instance. Ask an administrator to enable it in order for Auto DevOps to work.')
- c.with_actions do
= link_button_to _('Settings'), project_settings_ci_cd_path(project), class: 'alert-link', variant: :confirm
- = link_button_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank', class: 'alert-link gl-ml-3'
+ = link_button_to _('More information'), help_page_path('topics/autodevops/index'), target: '_blank', class: 'alert-link gl-ml-3'
diff --git a/app/views/shared/_ci_catalog_badge.html.haml b/app/views/shared/_ci_catalog_badge.html.haml
new file mode 100644
index 00000000000..7f8f4f6143b
--- /dev/null
+++ b/app/views/shared/_ci_catalog_badge.html.haml
@@ -0,0 +1 @@
+= render Pajamas::BadgeComponent.new(s_('CiCatalog|CI/CD catalog resource'), variant: 'info', icon: 'catalog-checkmark', class: css_class, href: href)
diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index 2b55d35cf1f..f420f176a11 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -10,7 +10,7 @@
class: 'form-control gl-form-input js-commit-message',
placeholder: local_assigns[:placeholder],
data: descriptions,
- 'data-qa-selector': 'commit_message_field',
+ 'data-testid': 'commit-message-field',
required: true, rows: (local_assigns[:rows] || 3),
id: "commit_message-#{nonce}"
- if local_assigns[:hint]
diff --git a/app/views/shared/_custom_attributes.html.haml b/app/views/shared/_custom_attributes.html.haml
index be96e77dbd4..33f3ca93b9c 100644
--- a/app/views/shared/_custom_attributes.html.haml
+++ b/app/views/shared/_custom_attributes.html.haml
@@ -2,7 +2,7 @@
= render Pajamas::CardComponent.new(body_options: { class: 'gl-py-0' }) do |c|
- c.with_header do
- = link_to(_('Custom Attributes'), help_page_path('api/custom_attributes.md'))
+ = link_to(_('Custom Attributes'), help_page_path('api/custom_attributes'))
- c.with_body do
%ul.content-list
- custom_attributes.each do |custom_attribute|
diff --git a/app/views/shared/_md_preview.html.haml b/app/views/shared/_md_preview.html.haml
index 1fd430527a1..7ac6a822420 100644
--- a/app/views/shared/_md_preview.html.haml
+++ b/app/views/shared/_md_preview.html.haml
@@ -5,7 +5,7 @@
.issuable-note-warning
= sprite_icon('lock', css_class: 'icon')
%span
- = _('This merge request is locked.')
+ = _('The discussion in this merge request is locked.')
= _('Only project members can comment.')
.md-area.position-relative
diff --git a/app/views/shared/_new_nav_announcement.html.haml b/app/views/shared/_new_nav_announcement.html.haml
deleted file mode 100644
index 8cabab09ec2..00000000000
--- a/app/views/shared/_new_nav_announcement.html.haml
+++ /dev/null
@@ -1,33 +0,0 @@
-- return unless show_new_navigation_callout?
-
-- changes_url = 'https://gitlab.com/groups/gitlab-org/-/epics/9044#whats-different'
-- vision_url = 'https://about.gitlab.com/blog/2023/05/01/gitlab-product-navigation/'
-- design_url = 'https://about.gitlab.com/blog/2023/05/15/overhauling-the-navigation-is-like-building-a-dream-home/'
-- feedback_url = 'https://gitlab.com/gitlab-org/gitlab/-/issues/409005'
-- docs_url = help_page_path('tutorials/left_sidebar/index')
-
-- changes_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: changes_url }
-- vision_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: vision_url }
-- design_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: design_url }
-- link_end = '</a>'.html_safe
-
-- welcome_text = _('For the next few releases, you can go to your avatar at any time to turn the new navigation on and off.')
-- cta_text = _('Read more about the %{changes_link_start}changes%{link_end}, the %{vision_link_start}vision%{link_end}, and the %{design_link_start}design%{link_end}.' % { changes_link_start: changes_link_start,
- vision_link_start: vision_link_start,
- design_link_start: design_link_start,
- link_end: link_end}).html_safe # rubocop:disable Gettext/StaticIdentifier
-
-= render Pajamas::AlertComponent.new(dismissible: true, title: _('Welcome to a new navigation experience'),
- alert_options: { class: 'js-new-navigation-callout', data: { feature_id: "new_navigation_callout", dismiss_endpoint: callouts_path }}) do |c|
- - c.with_body do
- %p
- = welcome_text
- = cta_text
- - c.with_actions do
- = render Pajamas::ButtonComponent.new(variant: :confirm,
- href: docs_url,
- button_options: { class: 'gl-alert-action', data: { track_action: 'click_button', track_label: 'banner_nav_learn_more' } }) do |c|
- = _('Learn more')
- = render Pajamas::ButtonComponent.new(href: feedback_url,
- button_options: { data: { track_action: 'click_button', track_label: 'banner_nav_provide_feedback' } }) do |c|
- = _('Provide feedback')
diff --git a/app/views/shared/_new_nav_for_everyone_announcement.html.haml b/app/views/shared/_new_nav_for_everyone_announcement.html.haml
new file mode 100644
index 00000000000..fa870249596
--- /dev/null
+++ b/app/views/shared/_new_nav_for_everyone_announcement.html.haml
@@ -0,0 +1,18 @@
+- return unless show_new_nav_for_everyone_callout?
+
+- blog_url = 'https://about.gitlab.com/blog/2023/08/15/navigation-research-blog-post/'
+- issues_url = 'https://about.gitlab.com/submit-feedback/#product-feedback'
+
+- blog_link_tags = tag_pair(link_to('', blog_url, rel: 'noopener noreferrer', target: '_blank'), :blog_link_start, :link_end)
+- issues_link_tags = tag_pair(link_to('', issues_url, rel: 'noopener noreferrer', target: '_blank'), :issues_link_start, :link_end)
+
+- welcome_text = safe_format(_('GitLab has redesigned the left sidebar to address customer feedback. View details in %{blog_link_start}this blog post%{link_end}. Here\'s how to %{issues_link_start}file an issue%{link_end} with the GitLab product team.'), blog_link_tags, issues_link_tags)
+
+= render Pajamas::AlertComponent.new(dismissible: true,
+ alert_options: { class: 'js-new-nav-for-everyone-callout', data: { feature_id: "new_nav_for_everyone_callout", dismiss_endpoint: callouts_path }}) do |c|
+ - c.with_body do
+ %p
+ = welcome_text
+ - c.with_actions do
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: blog_url, target: '_blank', button_options: { class: 'gl-alert-action' }) do |c|
+ = _('Learn more')
diff --git a/app/views/shared/_project_limit.html.haml b/app/views/shared/_project_limit.html.haml
index a99db32c40e..914c20fb7b0 100644
--- a/app/views/shared/_project_limit.html.haml
+++ b/app/views/shared/_project_limit.html.haml
@@ -3,7 +3,7 @@
dismissible: false,
alert_options: { class: 'project-limit-message' }) do |c|
- c.with_body do
- = _("You won't be able to create new projects because you have reached your project limit.")
+ = _("You cannot create new projects in your personal namespace because you have reached your personal project limit.")
- c.with_actions do
= link_button_to _('Remind later'), '#', class: 'alert-link hide-project-limit-message', variant: :confirm
= link_button_to _("Don't show again"), profile_path(user: {hide_project_limit: true}), method: :put, class: 'alert-link gl-ml-3'
diff --git a/app/views/shared/_registration_features_discovery_message.html.haml b/app/views/shared/_registration_features_discovery_message.html.haml
index 6e386866dfb..5fa554171aa 100644
--- a/app/views/shared/_registration_features_discovery_message.html.haml
+++ b/app/views/shared/_registration_features_discovery_message.html.haml
@@ -1,5 +1,5 @@
- feature_title = local_assigns.fetch(:feature_title, s_('RegistrationFeatures|use this feature'))
-- registration_features_docs_path = help_page_path('administration/settings/usage_statistics.md', anchor: 'registration-features-program')
+- registration_features_docs_path = help_page_path('administration/settings/usage_statistics', anchor: 'registration-features-program')
- registration_features_link_start = '<a href="%{url}" target="_blank">'.html_safe % { url: registration_features_docs_path }
%div
diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml
index fa5c862b768..ec897e59d4a 100644
--- a/app/views/shared/_remote_mirror_update_button.html.haml
+++ b/app/views/shared/_remote_mirror_update_button.html.haml
@@ -3,4 +3,4 @@
button_options: { class: 'disabled', title: _('Updating'), data: { toggle: 'tooltip', container: 'body' } },
icon_classes: 'spin')
- elsif remote_mirror.enabled?
- = link_button_to nil, update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: 'rspec-update-now-button', data: { toggle: 'tooltip', container: 'body', qa_selector: 'update_now_button' }, title: _('Update now'), icon: 'retry'
+ = link_button_to nil, update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: 'rspec-update-now-button', data: { toggle: 'tooltip', container: 'body', testid: 'update-now-button' }, title: _('Update now'), icon: 'retry'
diff --git a/app/views/shared/_service_ping_consent.html.haml b/app/views/shared/_service_ping_consent.html.haml
index cfc0afb4646..b65808bfcd2 100644
--- a/app/views/shared/_service_ping_consent.html.haml
+++ b/app/views/shared/_service_ping_consent.html.haml
@@ -1,14 +1,14 @@
- if session[:ask_for_usage_stats_consent]
= render Pajamas::AlertComponent.new(alert_options: { class: 'service-ping-consent-message' }) do |c|
- c.with_body do
- - docs_link = link_to '', help_page_path('administration/settings/usage_statistics.md'), class: 'gl-link'
+ - docs_link = link_to '', help_page_path('administration/settings/usage_statistics'), class: 'gl-link'
- settings_link = link_to '', metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), class: 'gl-link'
= safe_format s_('ServicePing|To help improve GitLab, we would like to periodically %{link_start}collect usage information%{link_end}.'), tag_pair(docs_link, :link_start, :link_end)
= safe_format s_('ServicePing|This can be changed at any time in %{link_start}your settings%{link_end}.'), tag_pair(settings_link, :link_start, :link_end)
- c.with_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 })
- = render Pajamas::ButtonComponent.new(href: send_service_data_path, method: :put, variant: :confirm, button_options: { 'data-url' => admin_application_settings_path, 'data-check-enabled': true, 'data-service-ping-enabled': true, class: 'js-service-ping-consent-action alert-link' }) do
+ = render Pajamas::ButtonComponent.new(href: send_service_data_path, method: :put, variant: :confirm, button_options: { class: 'alert-link' }) do
= _('Send service data')
- = render Pajamas::ButtonComponent.new(href: not_now_path, method: :put, button_options: { 'data-url' => admin_application_settings_path, 'data-check-enabled': false, 'data-service-ping-enabled': false, class: 'js-service-ping-consent-action alert-link gl-ml-3' }) do
+ = render Pajamas::ButtonComponent.new(href: not_now_path, method: :put, button_options: { class: 'alert-link gl-ml-3' }) do
= _("Don't send service data")
diff --git a/app/views/shared/access_tokens/_form.html.haml b/app/views/shared/access_tokens/_form.html.haml
index e46da882e83..3bf85da83b1 100644
--- a/app/views/shared/access_tokens/_form.html.haml
+++ b/app/views/shared/access_tokens/_form.html.haml
@@ -30,7 +30,7 @@
.form-group
= label_tag :access_level, s_("AccessTokens|Select a role"), class: "label-bold"
.select-wrapper.gl-form-input-md
- = select_tag :"#{prefix}[access_level]", options_for_select(access_levels, default_access_level), class: "form-control select-control", data: { qa_selector: 'access_token_access_level' }
+ = select_tag :"#{prefix}[access_level]", options_for_select(access_levels, default_access_level), class: "form-control select-control"
= sprite_icon('chevron-down', css_class: "gl-icon gl-absolute gl-top-3 gl-right-3 gl-text-gray-200")
.form-group
diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml
index bb7e0d774cc..109bd559762 100644
--- a/app/views/shared/deploy_tokens/_form.html.haml
+++ b/app/views/shared/deploy_tokens/_form.html.haml
@@ -1,17 +1,17 @@
%p
- - link = link_to('', help_page_path('user/project/deploy_tokens/index.md'), target: '_blank', rel: 'noopener noreferrer')
+ - link = link_to('', help_page_path('user/project/deploy_tokens/index'), target: '_blank', rel: 'noopener noreferrer')
= safe_format(s_('DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}'), tag_pair(link, :link_start, :link_end))
= gitlab_ui_form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post, remote: true do |f|
.form-group
= f.label :name, class: 'label-bold'
- = f.text_field :name, class: 'form-control gl-form-input', data: { qa_selector: 'deploy_token_name_field' }, required: true
+ = f.text_field :name, class: 'form-control gl-form-input', data: { testid: 'deploy-token-name-field' }, required: true
.text-secondary= s_('DeployTokens|Enter a unique name for your deploy token.')
.form-group
= f.label :expires_at, _('Expiration date (optional)'), class: 'label-bold'
- = f.gitlab_ui_datepicker :expires_at, data: { qa_selector: 'deploy_token_expires_at_field' }, value: f.object.expires_at
+ = f.gitlab_ui_datepicker :expires_at, data: { testid: 'deploy-token-expires-at-field' }, value: f.object.expires_at
.text-secondary= s_('DeployTokens|Enter an expiration date for your token. Defaults to never expire.')
.form-group
@@ -22,15 +22,15 @@
.form-group
= f.label :scopes, _('Scopes (select at least one)'), class: 'label-bold'
- = f.gitlab_ui_checkbox_component :read_repository, 'read_repository', help_text: s_('DeployTokens|Allows read-only access to the repository.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_repository_checkbox' } }
+ = f.gitlab_ui_checkbox_component :read_repository, 'read_repository', help_text: s_('DeployTokens|Allows read-only access to the repository.'), checkbox_options: { data: { testid: 'deploy-token-read-repository-checkbox' } }
- if container_registry_enabled?(group_or_project)
- = f.gitlab_ui_checkbox_component :read_registry, 'read_registry', help_text: s_('DeployTokens|Allows read-only access to registry images.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_registry_checkbox' } }
- = f.gitlab_ui_checkbox_component :write_registry, 'write_registry', help_text: s_('DeployTokens|Allows write access to registry images.'), checkbox_options: { data: { qa_selector: 'deploy_token_write_registry_checkbox' } }
+ = f.gitlab_ui_checkbox_component :read_registry, 'read_registry', help_text: s_('DeployTokens|Allows read-only access to registry images.'), checkbox_options: { data: { testid: 'deploy-token-read-registry-checkbox' } }
+ = f.gitlab_ui_checkbox_component :write_registry, 'write_registry', help_text: s_('DeployTokens|Allows write access to registry images.'), checkbox_options: { data: { testid: 'deploy-token-write-registry-checkbox' } }
- if packages_registry_enabled?(group_or_project)
- = f.gitlab_ui_checkbox_component :read_package_registry, 'read_package_registry', help_text: s_('DeployTokens|Allows read-only access to the package registry.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_package_registry_checkbox' } }
- = f.gitlab_ui_checkbox_component :write_package_registry, 'write_package_registry', help_text: s_('DeployTokens|Allows read and write access to the package registry.'), checkbox_options: { data: { qa_selector: 'deploy_token_write_package_registry_checkbox' } }
+ = f.gitlab_ui_checkbox_component :read_package_registry, 'read_package_registry', help_text: s_('DeployTokens|Allows read-only access to the package registry.'), checkbox_options: { data: { testid: 'deploy-token-read-package-registry-checkbox' } }
+ = f.gitlab_ui_checkbox_component :write_package_registry, 'write_package_registry', help_text: s_('DeployTokens|Allows read and write access to the package registry.'), checkbox_options: { data: { testid: 'deploy-token-write-package-registry-checkbox' } }
.gl-mt-3
- = f.submit s_('DeployTokens|Create deploy token'), data: { qa_selector: 'create_deploy_token_button' }, pajamas_button: true
+ = f.submit s_('DeployTokens|Create deploy token'), data: { testid: 'create-deploy-token-button' }, pajamas_button: true
diff --git a/app/views/shared/deploy_tokens/_index.html.haml b/app/views/shared/deploy_tokens/_index.html.haml
index ccffc3ec923..74de71867b8 100644
--- a/app/views/shared/deploy_tokens/_index.html.haml
+++ b/app/views/shared/deploy_tokens/_index.html.haml
@@ -1,6 +1,6 @@
- expanded = expand_deploy_tokens_section?(@new_deploy_token, @created_deploy_token)
-%section.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded), data: { qa_selector: 'deploy_tokens_settings_content' } }
+%section.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded), data: { testid: 'deploy-tokens-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= s_('DeployTokens|Deploy tokens')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
diff --git a/app/views/shared/deploy_tokens/_new_deploy_token.html.haml b/app/views/shared/deploy_tokens/_new_deploy_token.html.haml
index 25c277ea0ea..2bc2e6c5b81 100644
--- a/app/views/shared/deploy_tokens/_new_deploy_token.html.haml
+++ b/app/views/shared/deploy_tokens/_new_deploy_token.html.haml
@@ -1,21 +1,21 @@
-.created-deploy-token-container.info-well{ data: { qa_selector: 'created_deploy_token_container' } }
+.created-deploy-token-container.info-well{ data: { testid: 'created-deploy-token-container' } }
.well-segment
%h5.gl-mt-0
= s_('DeployTokens|Your new Deploy Token username')
.form-group
.input-group
- = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { qa_selector: 'deploy_token_user_field' }
+ = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { testid: 'deploy-token-user-field' }
.input-group-append
= deprecated_clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username'), placement: 'left')
%span.deploy-token-help-block.gl-mt-2.text-success
- - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/deploy_tokens/index.md') }
+ - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/deploy_tokens/index') }
- link_end = "</a>".html_safe
= s_("DeployTokens|This username supports access. %{link_start}What kind of access?%{link_end}").html_safe % { link_start: link_start, link_end: link_end }
.form-group
.input-group
- = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { qa_selector: 'deploy_token_field' }
+ = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { testid: 'deploy-token-field' }
.input-group-append
= deprecated_clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token'), placement: 'left')
%span.deploy-token-help-block.gl-mt-2.text-danger
diff --git a/app/views/shared/deploy_tokens/_table.html.haml b/app/views/shared/deploy_tokens/_table.html.haml
index 3b351387d41..0b8a97a34f2 100644
--- a/app/views/shared/deploy_tokens/_table.html.haml
+++ b/app/views/shared/deploy_tokens/_table.html.haml
@@ -16,7 +16,7 @@
packages_registry_enabled: packages_registry_enabled?(group_or_project),
create_new_token_path: create_deploy_token_path(group_or_project),
token_type: group_or_project.is_a?(Group) ? 'group' : 'project',
- deploy_tokens_help_url: help_page_path('user/project/deploy_tokens/index.md')
+ deploy_tokens_help_url: help_page_path('user/project/deploy_tokens/index')
}
}
- if active_tokens.present?
diff --git a/app/views/shared/empty_states/_snippets.html.haml b/app/views/shared/empty_states/_snippets.html.haml
index a2457fb0810..800cfe8b0d1 100644
--- a/app/views/shared/empty_states/_snippets.html.haml
+++ b/app/views/shared/empty_states/_snippets.html.haml
@@ -13,6 +13,6 @@
.gl-mt-3<
- if button_path
= link_button_to s_('SnippetsEmptyState|New snippet'), button_path, title: s_('SnippetsEmptyState|New snippet'), id: 'new_snippet_link', data: { testid: 'create-first-snippet-link' }, variant: :confirm
- = link_button_to s_('SnippetsEmptyState|Documentation'), help_page_path('user/snippets.md'), title: s_('SnippetsEmptyState|Documentation')
+ = link_button_to s_('SnippetsEmptyState|Documentation'), help_page_path('user/snippets'), title: s_('SnippetsEmptyState|Documentation')
- else
%h4.gl-text-center= s_('SnippetsEmptyState|There are no snippets to show.')
diff --git a/app/views/shared/integrations/gitlab_slack_application/_help.html.haml b/app/views/shared/integrations/gitlab_slack_application/_help.html.haml
index 0956f1183cb..2e7768e54f4 100644
--- a/app/views/shared/integrations/gitlab_slack_application/_help.html.haml
+++ b/app/views/shared/integrations/gitlab_slack_application/_help.html.haml
@@ -2,7 +2,7 @@
.well-segment
%p
= s_("SlackIntegration|This integration allows users to perform common operations on this project by entering slash commands in Slack.")
- = link_to _('Learn more'), help_page_path('user/project/integrations/gitlab_slack_application.md')
+ = link_to _('Learn more'), help_page_path('user/project/integrations/gitlab_slack_application')
%p
= s_("SlackIntegration|See the list of available commands in Slack after setting up this integration by entering")
%kbd.inline /gitlab help
diff --git a/app/views/shared/integrations/gitlab_slack_application/_slack_integration_form.html.haml b/app/views/shared/integrations/gitlab_slack_application/_slack_integration_form.html.haml
index e5d05a8a83d..57d172b41f4 100644
--- a/app/views/shared/integrations/gitlab_slack_application/_slack_integration_form.html.haml
+++ b/app/views/shared/integrations/gitlab_slack_application/_slack_integration_form.html.haml
@@ -29,7 +29,7 @@
= render Pajamas::ButtonComponent.new(href: add_to_slack_link(@project, slack_app_id)) do
= s_('SlackIntegration|Reinstall GitLab for Slack app…')
%p
- = html_escape(s_('SlackIntegration|You may need to reinstall the GitLab for Slack app when we %{linkStart}make updates or change permissions%{linkEnd}.')) % { linkStart: %(<a href="#{help_page_path('user/project/integrations/gitlab_slack_application.md', anchor: 'update-the-gitlab-for-slack-app')}">).html_safe, linkEnd: '</a>'.html_safe}
+ = html_escape(s_('SlackIntegration|You may need to reinstall the GitLab for Slack app when we %{linkStart}make updates or change permissions%{linkEnd}.')) % { linkStart: %(<a href="#{help_page_path('user/project/integrations/gitlab_slack_application', anchor: 'update-the-gitlab-for-slack-app')}">).html_safe, linkEnd: '</a>'.html_safe}
- else
= render Pajamas::ButtonComponent.new(href: add_to_slack_link(@project, slack_app_id)) do
= s_('SlackIntegration|Install GitLab for Slack app…')
diff --git a/app/views/shared/integrations/mattermost_slash_commands/_help.html.haml b/app/views/shared/integrations/mattermost_slash_commands/_help.html.haml
index 6ce1c65a8dc..e01999c2279 100644
--- a/app/views/shared/integrations/mattermost_slash_commands/_help.html.haml
+++ b/app/views/shared/integrations/mattermost_slash_commands/_help.html.haml
@@ -4,7 +4,7 @@
.well-segment
%p
= s_("MattermostService|Use this service to perform common tasks in your project by entering slash commands in Mattermost.")
- = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
+ = link_to help_page_path('user/project/integrations/mattermost_slash_commands'), target: '_blank' do
= _("How do I configure this integration?")
= sprite_icon('external-link')
%p.inline
diff --git a/app/views/shared/integrations/slack_slash_commands/_help.html.haml b/app/views/shared/integrations/slack_slash_commands/_help.html.haml
index fd30c5b0da3..0440bb13797 100644
--- a/app/views/shared/integrations/slack_slash_commands/_help.html.haml
+++ b/app/views/shared/integrations/slack_slash_commands/_help.html.haml
@@ -5,7 +5,7 @@
.well-segment
%p
= s_("SlackService|Perform common operations in this project by entering slash commands in Slack.")
- = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
+ = link_to help_page_path('user/project/integrations/slack_slash_commands'), target: '_blank' do
= _("Learn more.")
= sprite_icon('external-link')
%p.inline
@@ -40,7 +40,7 @@
.col-12.input-group
= text_field_tag :url, integration_trigger_url(integration), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#url', category: :primary, size: :medium)
+ = clipboard_button(target: '#url', category: :primary, size: :medium, title: _('Copy URL'))
.form-group
= label_tag nil, _('Method'), class: 'col-12 col-form-label label-bold'
@@ -51,7 +51,7 @@
.col-12.input-group
= text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#customize_name', category: :primary, size: :medium)
+ = clipboard_button(target: '#customize_name', category: :primary, size: :medium, title: _('Copy customize name'))
.form-group
= label_tag nil, _('Customize icon'), class: 'col-12 col-form-label label-bold'
@@ -68,21 +68,21 @@
.col-12.input-group
= text_field_tag :autocomplete_description, run_actions_text.html_safe, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#autocomplete_description', category: :primary, size: :medium)
+ = clipboard_button(target: '#autocomplete_description', category: :primary, size: :medium, title: _('Copy autocomplete description'))
.form-group
= label_tag :autocomplete_usage_hint, _('Autocomplete usage hint'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#autocomplete_usage_hint', category: :primary, size: :medium)
+ = clipboard_button(target: '#autocomplete_usage_hint', category: :primary, size: :medium, title: _('Copy autocomplete usage hint'))
.form-group
= label_tag :descriptive_label, _('Descriptive label'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :descriptive_label, _('Perform common operations on GitLab project'), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#descriptive_label', category: :primary, size: :medium)
+ = clipboard_button(target: '#descriptive_label', category: :primary, size: :medium, title: _('Copy descriptive label'))
%hr
diff --git a/app/views/shared/issuable/_assignees.html.haml b/app/views/shared/issuable/_assignees.html.haml
index 5326b26d655..1ae9ce4eecd 100644
--- a/app/views/shared/issuable/_assignees.html.haml
+++ b/app/views/shared/issuable/_assignees.html.haml
@@ -7,5 +7,5 @@
= link_to_member(@project, assignee, name: false, title: s_("MrList|Assigned to %{name}") % { name: assignee.name})
- if more_assignees_count > 0
- %span{ class: 'avatar-counter has-tooltip', data: { container: 'body', placement: 'bottom', 'line-type' => 'old', qa_selector: 'avatar_counter_content' }, title: _("+%{more_assignees_count} more assignees") % { more_assignees_count: more_assignees_count} }
+ %span{ class: 'avatar-counter has-tooltip', data: { container: 'body', placement: 'bottom', 'line-type' => 'old' }, title: _("+%{more_assignees_count} more assignees") % { more_assignees_count: more_assignees_count} }
= _("+%{more_assignees_count}") % { more_assignees_count: more_assignees_count}
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 4a33f625347..c48f51dc9bc 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -11,7 +11,7 @@
= gl_tab_link_to page_filter_path(state: 'closed'), item_active: params[:state] == 'closed', id: 'state-closed', title: _('Filter by merge requests that are currently closed and unmerged.'), data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed, display_count)}
- else
- = gl_tab_link_to page_filter_path(state: 'closed'), item_active: params[:state] == 'closed', id: 'state-closed', title: _('Filter by issues that are currently closed.'), data: { state: 'closed', qa_selector: 'closed_issues_link' } do
+ = gl_tab_link_to page_filter_path(state: 'closed'), item_active: params[:state] == 'closed', id: 'state-closed', title: _('Filter by issues that are currently closed.'), data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed, display_count)}
= render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all, display_count)
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 86aaa5128a8..52c8a4d4123 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -185,6 +185,11 @@
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value.monospace
{{title}}
+ #js-dropdown-source-branch.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
+ %li.filter-dropdown-item
+ %button.btn.btn-link.js-data-value.monospace
+ {{title}}
#js-dropdown-environment.filtered-search-input-dropdown-menu.dropdown-menu
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 1392c7ab89f..f018e4f122e 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -16,7 +16,7 @@
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { always_show_toggle: true, signed: { in: signed_in }, issuable_type: issuable_type }, class: "#{sidebar_gutter_collapsed_class(is_merge_request_with_flag)} #{'right-sidebar-merge-requests' if is_merge_request_with_flag}", 'aria-live' => 'polite', 'aria-label': issuable_type }
.issuable-sidebar{ class: "#{'is-merge-request' if is_merge_request_with_flag}" }
.issuable-sidebar-header{ class: "gl-pb-4! #{'gl-pb-2! gl-md-display-flex gl-justify-content-end gl-lg-display-none!' if is_merge_request_with_flag}" }
- = render Pajamas::ButtonComponent.new(button_options: { class: "gutter-toggle float-right js-sidebar-toggle has-tooltip gl-shadow-none! #{'gl-display-block' if moved_sidebar_enabled}", type: 'button', 'aria-label' => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }) do
+ = render Pajamas::ButtonComponent.new(button_options: { class: "gutter-toggle float-right js-sidebar-toggle has-tooltip gl-shadow-none! #{'gl-display-block' if moved_sidebar_enabled} #{'gl-mt-2' if notifications_todos_buttons_enabled?}" , type: 'button', 'aria-label' => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }) do
= sidebar_gutter_toggle_icon
- if signed_in
- if !is_merge_request_with_flag
diff --git a/app/views/shared/issuable/form/_type_selector.html.haml b/app/views/shared/issuable/form/_type_selector.html.haml
index 0bcdcb9e963..89a07444d9f 100644
--- a/app/views/shared/issuable/form/_type_selector.html.haml
+++ b/app/views/shared/issuable/form/_type_selector.html.haml
@@ -10,6 +10,6 @@
- if issuable.incident_type_issue?
%p.form-text.text-muted
- - incident_docs_url = help_page_path('operations/incident_management/incidents.md')
+ - incident_docs_url = help_page_path('operations/incident_management/incidents')
- incident_docs_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">', url: incident_docs_url)
= format(_('A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents.'), incident_docs_start: incident_docs_start, incident_docs_end: '</a>').html_safe
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 7d1e9c06966..2e2c0300ae1 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -161,11 +161,10 @@
- milestone_ref = milestone.try(:to_reference, full: true)
- if milestone_ref.present?
.block.reference
- .sidebar-collapsed-icon.js-dont-change-state
- = deprecated_clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport')
+ = clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport', class: 'sidebar-collapsed-icon js-dont-change-state')
.gl-display-flex.gl-align-items-center.gl-justify-content-space-between.gl-mb-2.hide-collapsed
%span.gl-overflow-hidden.gl-text-overflow-ellipsis.gl-white-space-nowrap
= s_('MilestoneSidebar|Reference:')
%span{ title: milestone_ref }
= milestone_ref
- = deprecated_clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport')
+ = clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport')
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index 336fdedf89b..343a8597444 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -15,12 +15,12 @@
.timeline-content.timeline-content-form
= render "shared/notes/form", view: diff_view, supports_autocomplete: autocomplete
- elsif !current_user
- .disabled-comment.text-center.gl-mt-3
+ .disabled-comment.gl-text-center.gl-text-secondary.gl-mt-3
- link_to_register = link_to(_("register"), new_user_registration_path(redirect_to_referer: 'yes'), class: 'js-register-link')
- link_to_sign_in = link_to(_("sign in"), new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-sign-in-link')
= _("Please %{link_to_register} or %{link_to_sign_in} to comment").html_safe % { link_to_register: link_to_register, link_to_sign_in: link_to_sign_in }
- elsif discussion_locked
- .disabled-comment.text-center.gl-mt-3
+ .disabled-comment.gl-text-center.gl-mt-3
%span.issuable-note-warning
= sprite_icon('lock', css_class: 'icon')
%span
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 14785870dc0..74c325383a1 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -32,6 +32,7 @@
- if any_projects?(projects)
- load_pipeline_status(projects) if pipeline_status
- load_max_project_member_accesses(projects) # Prime cache used in shared/projects/project view rendered below
+ - load_catalog_resources(projects)
%ul.projects-list.gl-text-secondary.gl-w-full.gl-my-2{ class: css_classes }
- projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 2de4a9d7780..e65dcd68f66 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -35,7 +35,10 @@
%span.project-name<
= project.name
- = visibility_level_content(project, css_class: 'gl-mr-3')
+ = visibility_level_content(project, css_class: 'gl-mr-2')
+
+ - if project.catalog_resource
+ = render partial: 'shared/ci_catalog_badge', locals: { href: project_ci_catalog_resource_path(project, project.catalog_resource), css_class: 'gl-mr-2' }
- if explore_projects_tab? && project_license_name(project)
%span.gl-display-inline-flex.gl-align-items-center.gl-mr-3
diff --git a/app/views/shared/runners/_shared_runners_description.html.haml b/app/views/shared/runners/_shared_runners_description.html.haml
index c8ddb5d5176..89da1a6fa09 100644
--- a/app/views/shared/runners/_shared_runners_description.html.haml
+++ b/app/views/shared/runners/_shared_runners_description.html.haml
@@ -1,4 +1,4 @@
-- shared_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('ci/runners/runners_scope.md', anchor: 'shared-runners') }
+- shared_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('ci/runners/runners_scope', anchor: 'shared-runners') }
%h4
= _('Shared runners')
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index 7c713e63cd7..a3dfc6eb042 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -66,7 +66,7 @@
help_text: s_('Webhooks|A release is created, updated, or deleted.')
- if Feature.enabled?(:emoji_webhooks, hook.parent)
%li.gl-pb-5
- - emoji_help_link = link_to s_('Which emoji events trigger webhooks'), help_page_path('user/project/integrations/webhook_events.md', anchor: 'emoji-events')
+ - emoji_help_link = link_to s_('Which emoji events trigger webhooks'), help_page_path('user/project/integrations/webhook_events', anchor: 'emoji-events')
= form.gitlab_ui_checkbox_component :emoji_events,
integration_webhook_event_human_name(:emoji_events),
help_text: s_('Webhooks|An emoji is awarded or revoked. %{help_link}?').html_safe % { help_link: emoji_help_link }
diff --git a/app/views/shared/wikis/_wiki_directory.html.haml b/app/views/shared/wikis/_wiki_directory.html.haml
index cce81257691..8b0b6dbd8f7 100644
--- a/app/views/shared/wikis/_wiki_directory.html.haml
+++ b/app/views/shared/wikis/_wiki_directory.html.haml
@@ -1,12 +1,12 @@
- wiki_path = wiki_page_path(@wiki, wiki_directory)
-%li{ class: active_when(params[:id] == wiki_directory.slug), data: { testid: 'wiki-directory-content' } }
+%li{ class: ['wiki-directory', active_when(params[:id] == wiki_directory.slug)], data: { testid: 'wiki-directory-content' } }
.gl-relative.gl-display-flex.gl-align-items-center.js-wiki-list-toggle.wiki-list{ data: { testid: 'wiki-list' } }<
= sprite_icon('chevron-right', css_class: 'js-wiki-list-expand-button wiki-list-expand-button gl-mr-2 gl-cursor-pointer')
= sprite_icon('chevron-down', css_class: 'js-wiki-list-collapse-button wiki-list-collapse-button gl-mr-2 gl-cursor-pointer')
= render Pajamas::ButtonComponent.new(icon: 'plus', href: "#{wiki_path}/{new_page_title}", button_options: { class: 'wiki-list-create-child-button gl-bg-transparent! gl-hover-bg-gray-50! gl-focus-bg-gray-50! gl-absolute gl-top-half gl-translate-y-n50 gl-cursor-pointer gl-right-3' })
= link_to wiki_path, data: { testid: 'wiki-dir-page-link', qa_page_name: wiki_directory.title } do
= wiki_directory.title
- %ul
+ %ul.gl-pl-8
- wiki_directory.entries.each do |entry|
= render partial: entry.to_partial_path, object: entry, locals: { context: context }
diff --git a/app/views/shared/wikis/show.html.haml b/app/views/shared/wikis/show.html.haml
index 9537d6fec15..2cd03c20080 100644
--- a/app/views/shared/wikis/show.html.haml
+++ b/app/views/shared/wikis/show.html.haml
@@ -12,8 +12,7 @@
.nav-controls.pb-md-3.pb-lg-0
= render 'shared/wikis/main_links'
- - if Feature.enabled?(:print_wiki, current_user)
- #js-export-actions{ data: { options: { target: '.js-wiki-page-content', title: @page.human_title, stylesheet: [stylesheet_path('application')] }.to_json } }
+ #js-export-actions{ data: { options: { target: '.js-wiki-page-content', title: @page.human_title, stylesheet: [stylesheet_path('application')] }.to_json } }
- if @page.historical?
= render Pajamas::AlertComponent.new(variant: :warning,
diff --git a/app/views/users/_cover_controls.html.haml b/app/views/users/_cover_controls.html.haml
deleted file mode 100644
index 899a08c8a17..00000000000
--- a/app/views/users/_cover_controls.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-.cover-controls.gl-display-flex.gl-gap-3.gl-pb-4
- = yield
diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml
index 3649f72c956..597e7c37388 100644
--- a/app/views/users/_overview.html.haml
+++ b/app/views/users/_overview.html.haml
@@ -1,4 +1,4 @@
-- activity_pane_class = Feature.enabled?(:security_auto_fix) && @user.bot? ? "col-12" : "col-md-12 col-lg-6"
+- activity_pane_class = Feature.enabled?(:security_auto_fix) && @user.bot? ? "col-12" : "col-md-12 col-lg-6 gl-align-self-start"
.row.d-none.d-sm-flex
.col-12.calendar-block.gl-my-3
@@ -33,7 +33,7 @@
%h4.gl-flex-grow-1
= Feature.enabled?(:security_auto_fix) && @user.bot? ? s_('UserProfile|Bot activity') : s_('UserProfile|Activity')
= link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
- .overview-content-list{ data: { href: user_activity_path, testid: 'user-activity-content' } }
+ .overview-content-list.user-activity-content{ data: { href: user_activity_path, testid: 'user-activity-content' } }
= gl_loading_icon(size: 'md', css_class: 'loading')
- unless Feature.enabled?(:security_auto_fix) && @user.bot?
diff --git a/app/views/users/_profile_basic_info.html.haml b/app/views/users/_profile_basic_info.html.haml
index 6de9e80008e..7dd131dbe2c 100644
--- a/app/views/users/_profile_basic_info.html.haml
+++ b/app/views/users/_profile_basic_info.html.haml
@@ -2,9 +2,5 @@
= render 'middle_dot_divider', stacking: true do
@#{@user.username}
- if can?(current_user, :read_user_profile, @user)
- - unless Feature.enabled?(:user_profile_overflow_menu_vue)
- = render 'middle_dot_divider', stacking: true do
- = s_('UserProfile|User ID: %{id}') % { id: @user.id }
- = clipboard_button(title: s_('UserProfile|Copy user ID'), text: @user.id)
= render 'middle_dot_divider', stacking: true do
= s_('Member since %{date}') % { date: l(@user.created_at.to_date, format: :long) }
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 0881c5bba54..e23555428aa 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -17,32 +17,16 @@
.user-profile
.cover-block.user-cover-block.gl-border-t.gl-border-b.gl-mt-n1
%div{ class: container_class }
- - if Feature.enabled?(:user_profile_overflow_menu_vue)
- .cover-controls.gl-display-flex.gl-gap-3.gl-pb-4
- = render 'users/follow_user'
- -# The following edit button is mutually exclusive to the follow user button, they won't be shown together
- - if @user == current_user
- = render Pajamas::ButtonComponent.new(href: profile_path,
- button_options: { class: 'gl-flex-grow-1', title: s_('UserProfile|Edit profile') }) do
- = s_("UserProfile|Edit profile")
- = render 'users/view_gpg_keys'
- = render 'users/view_user_in_admin_area'
- .js-user-profile-actions{ data: user_profile_actions_data(@user) }
- - else
- = render layout: 'users/cover_controls' do
- - if @user == current_user
- = render Pajamas::ButtonComponent.new(href: profile_path,
- icon: 'pencil',
- button_options: { class: 'gl-flex-grow-1 has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }})
- - elsif current_user
- #js-report-abuse{ data: { report_abuse_path: add_category_abuse_reports_path, reported_user_id: @user.id, reported_from_url: user_url(@user) } }
- = render 'users/view_gpg_keys'
- - if can?(current_user, :read_user_profile, @user)
- = render Pajamas::ButtonComponent.new(href: user_path(@user, rss_url_options),
- icon: 'rss',
- button_options: { class: 'gl-flex-grow-1 has-tooltip', title: s_('UserProfile|Subscribe'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }})
- = render 'users/view_user_in_admin_area'
- = render 'users/follow_user'
+ .cover-controls.gl-display-flex.gl-gap-3.gl-pb-4
+ = render 'users/follow_user'
+ -# The following edit button is mutually exclusive to the follow user button, they won't be shown together
+ - if @user == current_user
+ = render Pajamas::ButtonComponent.new(href: profile_path,
+ button_options: { class: 'gl-flex-grow-1', title: s_('UserProfile|Edit profile') }) do
+ = s_("UserProfile|Edit profile")
+ = render 'users/view_gpg_keys'
+ = render 'users/view_user_in_admin_area'
+ .js-user-profile-actions{ data: user_profile_actions_data(@user) }
.profile-header{ class: [('with-no-profile-tabs' if profile_tabs.empty?), ('gl-mb-4!' if show_super_sidebar?)] }
.gl-display-inline-block.gl-mx-8.gl-vertical-align-top
@@ -111,6 +95,10 @@
= render 'middle_dot_divider', breakpoint: 'sm' do
= link_to discord_url(@user), class: 'gl-hover-text-decoration-none', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow' do
= sprite_icon('discord', css_class: 'discord-icon')
+ - if Feature.enabled?(:mastodon_social_ui, @user) && @user.mastodon.present?
+ = render 'middle_dot_divider', breakpoint: 'sm' do
+ = link_to mastodon_url(@user), class: 'gl-hover-text-decoration-none', title: "Mastodon", target: '_blank', rel: 'noopener noreferrer nofollow' do
+ = sprite_icon('mastodon', css_class: 'mastodon-icon')
- if @user.website_url.present?
= render 'middle_dot_divider', stacking: true do
- if Feature.enabled?(:security_auto_fix) && @user.bot?
@@ -126,6 +114,8 @@
%p.profile-user-bio.gl-mb-3
= @user.bio
+ -# TODO: Remove this with the removal of the old navigation.
+ -# See https://gitlab.com/groups/gitlab-org/-/epics/11875.
- if !profile_tabs.empty? && !Feature.enabled?(:profile_tabs_vue, current_user)
.scrolling-tabs-container{ class: [('gl-display-none' if show_super_sidebar?)] }
%button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') }
@@ -169,7 +159,7 @@
= gl_badge_tag @user.followers.count, size: :sm
- if profile_tab?(:following)
%li.js-following-tab
- = link_to user_following_path, data: { target: 'div#following', action: 'following', toggle: 'tab', endpoint: user_following_path(format: :json), qa_selector: 'following_tab' } do
+ = link_to user_following_path, data: { target: 'div#following', action: 'following', toggle: 'tab', endpoint: user_following_path(format: :json) } do
= s_('UserProfile|Following')
= gl_badge_tag @user.followees.count, size: :sm
- if !profile_tabs.empty? && Feature.enabled?(:profile_tabs_vue, current_user)
@@ -183,13 +173,15 @@
- if profile_tab?(:activity)
#activity.tab-pane
- .flash-container
- - if can?(current_user, :read_cross_project)
- %h4.prepend-top-20
- = s_('UserProfile|Most Recent Activity')
- .content_list{ data: { href: user_activity_path } }
- .loading
- = gl_loading_icon(size: 'md')
+ .row
+ .col-12
+ .flash-container
+ - if can?(current_user, :read_cross_project)
+ %h4.prepend-top-20
+ = s_('UserProfile|Most Recent Activity')
+ .content_list.user-activity-content{ data: { href: user_activity_path } }
+ .loading
+ = gl_loading_icon(size: 'md')
- unless @user.bot?
- if profile_tab?(:groups)
#groups.tab-pane
diff --git a/app/workers/abuse/spam_abuse_events_worker.rb b/app/workers/abuse/spam_abuse_events_worker.rb
new file mode 100644
index 00000000000..7d86e994ae4
--- /dev/null
+++ b/app/workers/abuse/spam_abuse_events_worker.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Abuse
+ class SpamAbuseEventsWorker
+ include ApplicationWorker
+
+ data_consistency :delayed
+
+ idempotent!
+ feature_category :instance_resiliency
+ urgency :low
+
+ def perform(params)
+ params = params.with_indifferent_access
+
+ @user = User.find_by_id(params[:user_id])
+ unless @user
+ logger.info(structured_payload(message: "User not found.", user_id: params[:user_id]))
+ return
+ end
+
+ report_user(params)
+ end
+
+ private
+
+ attr_reader :user
+
+ def report_user(params)
+ category = 'spam'
+ reporter = Users::Internal.security_bot
+ report_params = { user_id: params[:user_id],
+ reporter: reporter,
+ category: category,
+ message: 'User reported for abuse based on spam verdict' }
+
+ abuse_report = AbuseReport.by_category(category).by_reporter_id(reporter.id).by_user_id(params[:user_id]).first
+
+ abuse_report = AbuseReport.create!(report_params) if abuse_report.nil?
+
+ create_abuse_event(abuse_report.id, params)
+ end
+
+ # Associate the abuse report with an abuse event
+ def create_abuse_event(abuse_report_id, params)
+ Abuse::Event.create!(
+ abuse_report_id: abuse_report_id,
+ category: :spam,
+ metadata: { noteable_type: params[:noteable_type],
+ title: params[:title],
+ description: params[:description],
+ source_ip: params[:source_ip],
+ user_agent: params[:user_agent],
+ verdict: params[:verdict] },
+ source: :spamcheck,
+ user: user
+ )
+ end
+ end
+end
diff --git a/app/workers/activity_pub/projects/releases_subscription_worker.rb b/app/workers/activity_pub/projects/releases_subscription_worker.rb
new file mode 100644
index 00000000000..c392726a469
--- /dev/null
+++ b/app/workers/activity_pub/projects/releases_subscription_worker.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ module Projects
+ class ReleasesSubscriptionWorker
+ include ApplicationWorker
+ include Gitlab::Routing.url_helpers
+
+ idempotent!
+ worker_has_external_dependencies!
+ feature_category :release_orchestration
+ data_consistency :delayed
+ queue_namespace :activity_pub
+
+ sidekiq_retries_exhausted do |msg, _ex|
+ subscription_id = msg['args'].second
+ subscription = ActivityPub::ReleasesSubscription.find_by_id(subscription_id)
+ subscription&.destroy
+ end
+
+ def perform(subscription_id)
+ subscription = ActivityPub::ReleasesSubscription.find_by_id(subscription_id)
+ return if subscription.nil?
+
+ unless subscription.project.public?
+ subscription.destroy
+ return
+ end
+
+ InboxResolverService.new(subscription).execute if needs_resolving?(subscription)
+ AcceptFollowService.new(subscription, project_releases_url(subscription.project)).execute
+ end
+
+ def needs_resolving?(subscription)
+ subscription.subscriber_inbox_url.blank? || subscription.shared_inbox_url.blank?
+ end
+ end
+ end
+end
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index e5b860ba525..0bb88efe183 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -3,6 +3,15 @@
#
# Do not edit it manually!
---
+- :name: activity_pub:activity_pub_projects_releases_subscription
+ :worker_name: ActivityPub::Projects::ReleasesSubscriptionWorker
+ :feature_category: :release_orchestration
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: authorized_project_update:authorized_project_update_project_recalculate
:worker_name: AuthorizedProjectUpdate::ProjectRecalculateWorker
:feature_category: :system_access
@@ -1461,42 +1470,6 @@
:weight: 1
:idempotent: false
:tags: []
-- :name: hashed_storage:hashed_storage_migrator
- :worker_name: HashedStorage::MigratorWorker
- :feature_category: :source_code_management
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: false
- :tags: []
-- :name: hashed_storage:hashed_storage_project_migrate
- :worker_name: HashedStorage::ProjectMigrateWorker
- :feature_category: :source_code_management
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: false
- :tags: []
-- :name: hashed_storage:hashed_storage_project_rollback
- :worker_name: HashedStorage::ProjectRollbackWorker
- :feature_category: :source_code_management
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: false
- :tags: []
-- :name: hashed_storage:hashed_storage_rollbacker
- :worker_name: HashedStorage::RollbackerWorker
- :feature_category: :source_code_management
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: false
- :tags: []
- :name: incident_management:incident_management_add_severity_system_note
:worker_name: IncidentManagement::AddSeveritySystemNoteWorker
:feature_category: :incident_management
@@ -1767,6 +1740,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: package_cleanup:packages_npm_cleanup_stale_metadata_cache
+ :worker_name: Packages::Npm::CleanupStaleMetadataCacheWorker
+ :feature_category: :package_registry
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: package_repositories:packages_debian_generate_distribution
:worker_name: Packages::Debian::GenerateDistributionWorker
:feature_category: :package_registry
@@ -2307,6 +2289,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: abuse_spam_abuse_events
+ :worker_name: Abuse::SpamAbuseEventsWorker
+ :feature_category: :instance_resiliency
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: analytics_usage_trends_counter_job
:worker_name: Analytics::UsageTrends::CounterJobWorker
:feature_category: :devops_reports
@@ -2575,7 +2566,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent: false
+ :idempotent: true
:tags: []
- :name: bulk_imports_entity
:worker_name: BulkImports::EntityWorker
@@ -2629,7 +2620,7 @@
:urgency: :low
:resource_boundary: :memory
:weight: 1
- :idempotent: false
+ :idempotent: true
:tags: []
- :name: bulk_imports_pipeline_batch
:worker_name: BulkImports::PipelineBatchWorker
@@ -2638,7 +2629,7 @@
:urgency: :low
:resource_boundary: :memory
:weight: 1
- :idempotent: false
+ :idempotent: true
:tags: []
- :name: bulk_imports_relation_batch_export
:worker_name: BulkImports::RelationBatchExportWorker
@@ -2892,6 +2883,15 @@
:weight: 2
:idempotent: false
:tags: []
+- :name: environments_auto_recover
+ :worker_name: Environments::AutoRecoverWorker
+ :feature_category: :continuous_delivery
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: environments_auto_stop
:worker_name: Environments::AutoStopWorker
:feature_category: :continuous_delivery
@@ -3567,6 +3567,15 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: projects_import_export_after_import_merge_requests
+ :worker_name: Projects::ImportExport::AfterImportMergeRequestsWorker
+ :feature_category: :importers
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: projects_import_export_create_relation_exports
:worker_name: Projects::ImportExport::CreateRelationExportsWorker
:feature_category: :importers
@@ -3837,15 +3846,6 @@
:weight: 1
:idempotent: false
:tags: []
-- :name: tasks_to_be_done_create
- :worker_name: TasksToBeDone::CreateWorker
- :feature_category: :onboarding
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :cpu
- :weight: 1
- :idempotent: true
- :tags: []
- :name: update_external_pull_requests
:worker_name: UpdateExternalPullRequestsWorker
:feature_category: :continuous_integration
diff --git a/app/workers/bulk_import_worker.rb b/app/workers/bulk_import_worker.rb
index 5b9b46081cc..70e7d82741f 100644
--- a/app/workers/bulk_import_worker.rb
+++ b/app/workers/bulk_import_worker.rb
@@ -1,11 +1,16 @@
# frozen_string_literal: true
-class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
+class BulkImportWorker
include ApplicationWorker
data_consistency :always
feature_category :importers
- sidekiq_options retry: false, dead: false
+ sidekiq_options retry: 3, dead: false
+ idempotent!
+
+ sidekiq_retries_exhausted do |msg, exception|
+ new.perform_failure(exception, msg['args'].first)
+ end
def perform(bulk_import_id)
bulk_import = BulkImport.find_by_id(bulk_import_id)
@@ -13,4 +18,12 @@ class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
BulkImports::ProcessService.new(bulk_import).execute
end
+
+ def perform_failure(exception, bulk_import_id)
+ bulk_import = BulkImport.find_by_id(bulk_import_id)
+
+ Gitlab::ErrorTracking.track_exception(exception, bulk_import_id: bulk_import.id)
+
+ bulk_import.fail_op
+ end
end
diff --git a/app/workers/bulk_imports/entity_worker.rb b/app/workers/bulk_imports/entity_worker.rb
index 9b60dcdeb8a..e510a8c0d06 100644
--- a/app/workers/bulk_imports/entity_worker.rb
+++ b/app/workers/bulk_imports/entity_worker.rb
@@ -5,12 +5,16 @@ module BulkImports
include ApplicationWorker
idempotent!
- deduplicate :until_executed
+ deduplicate :until_executed, if_deduplicated: :reschedule_once
data_consistency :always
feature_category :importers
- sidekiq_options retry: false, dead: false
+ sidekiq_options retry: 3, dead: false
worker_has_external_dependencies!
+ sidekiq_retries_exhausted do |msg, exception|
+ new.perform_failure(exception, msg['args'].first)
+ end
+
PERFORM_DELAY = 5.seconds
# Keep `_current_stage` parameter for backwards compatibility.
@@ -27,10 +31,17 @@ module BulkImports
end
re_enqueue
- rescue StandardError => e
- Gitlab::ErrorTracking.track_exception(e, log_params(message: 'Entity failed'))
+ end
+
+ def perform_failure(exception, entity_id)
+ @entity = ::BulkImports::Entity.find(entity_id)
+
+ Gitlab::ErrorTracking.track_exception(
+ exception,
+ log_params(message: "Request to export #{entity.source_type} failed")
+ )
- @entity.fail_op!
+ entity.fail_op!
end
private
@@ -68,7 +79,7 @@ module BulkImports
end
def logger
- @logger ||= Gitlab::Import::Logger.build
+ @logger ||= Logger.build
end
def log_exception(exception, payload)
@@ -88,7 +99,7 @@ module BulkImports
bulk_import_entity_type: entity.source_type,
source_full_path: entity.source_full_path,
source_version: source_version,
- importer: 'gitlab_migration'
+ importer: Logger::IMPORTER_NAME
}
defaults.merge(extra)
diff --git a/app/workers/bulk_imports/export_request_worker.rb b/app/workers/bulk_imports/export_request_worker.rb
index 44759916f99..f7456ddccb1 100644
--- a/app/workers/bulk_imports/export_request_worker.rb
+++ b/app/workers/bulk_imports/export_request_worker.rb
@@ -80,8 +80,7 @@ module BulkImports
bulk_import_id: entity.bulk_import_id,
bulk_import_entity_type: entity.source_type,
source_full_path: entity.source_full_path,
- source_version: entity.bulk_import.source_version_info.to_s,
- importer: 'gitlab_migration'
+ source_version: entity.bulk_import.source_version_info.to_s
}
)
@@ -97,7 +96,7 @@ module BulkImports
end
def logger
- @logger ||= Gitlab::Import::Logger.build
+ @logger ||= Logger.build
end
def log_exception(exception, payload)
@@ -114,8 +113,7 @@ module BulkImports
bulk_import_entity_type: entity.source_type,
source_full_path: entity.source_full_path,
message: "Request to export #{entity.source_type} failed",
- source_version: entity.bulk_import.source_version_info.to_s,
- importer: 'gitlab_migration'
+ source_version: entity.bulk_import.source_version_info.to_s
}
)
diff --git a/app/workers/bulk_imports/finish_batched_pipeline_worker.rb b/app/workers/bulk_imports/finish_batched_pipeline_worker.rb
index b1f3757e058..40d26e14dc1 100644
--- a/app/workers/bulk_imports/finish_batched_pipeline_worker.rb
+++ b/app/workers/bulk_imports/finish_batched_pipeline_worker.rb
@@ -16,22 +16,21 @@ module BulkImports
def perform(pipeline_tracker_id)
@tracker = Tracker.find(pipeline_tracker_id)
+ @context = ::BulkImports::Pipeline::Context.new(tracker)
return unless tracker.batched?
return unless tracker.started?
return re_enqueue if import_in_progress?
if tracker.stale?
+ logger.error(log_attributes(message: 'Tracker stale. Failing batches and tracker'))
tracker.batches.map(&:fail_op!)
tracker.fail_op!
else
+ tracker.pipeline_class.new(@context).on_finish
+ logger.info(log_attributes(message: 'Tracker finished'))
tracker.finish!
end
-
- ensure
- # This is needed for in-flight migrations.
- # It will be remove in https://gitlab.com/gitlab-org/gitlab/-/issues/426299
- ::BulkImports::EntityWorker.perform_async(tracker.entity.id) if job_version.nil?
end
private
@@ -45,5 +44,20 @@ module BulkImports
def import_in_progress?
tracker.batches.any? { |b| b.started? || b.created? }
end
+
+ def logger
+ @logger ||= Logger.build
+ end
+
+ def log_attributes(extra = {})
+ structured_payload(
+ {
+ tracker_id: tracker.id,
+ bulk_import_id: tracker.entity.id,
+ bulk_import_entity_id: tracker.entity.bulk_import_id,
+ pipeline_class: tracker.pipeline_name
+ }.merge(extra)
+ )
+ end
end
end
diff --git a/app/workers/bulk_imports/pipeline_batch_worker.rb b/app/workers/bulk_imports/pipeline_batch_worker.rb
index 6230d517641..1485275e616 100644
--- a/app/workers/bulk_imports/pipeline_batch_worker.rb
+++ b/app/workers/bulk_imports/pipeline_batch_worker.rb
@@ -1,26 +1,65 @@
# frozen_string_literal: true
module BulkImports
- class PipelineBatchWorker # rubocop:disable Scalability/IdempotentWorker
+ class PipelineBatchWorker
include ApplicationWorker
include ExclusiveLeaseGuard
+ DEFER_ON_HEALTH_DELAY = 5.minutes
+
data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency
feature_category :importers
- sidekiq_options retry: false, dead: false
+ sidekiq_options dead: false, retry: 3
worker_has_external_dependencies!
worker_resource_boundary :memory
+ idempotent!
+
+ sidekiq_retries_exhausted do |msg, exception|
+ new.perform_failure(msg['args'].first, exception)
+ end
+
+ defer_on_database_health_signal(:gitlab_main, [], DEFER_ON_HEALTH_DELAY) do |job_args, schema, tables|
+ batch = ::BulkImports::BatchTracker.find(job_args.first)
+ pipeline_tracker = batch.tracker
+ pipeline_schema = ::BulkImports::PipelineSchemaInfo.new(
+ pipeline_tracker.pipeline_class,
+ pipeline_tracker.entity.portable_class
+ )
+
+ if pipeline_schema.db_schema && pipeline_schema.db_table
+ schema = pipeline_schema.db_schema
+ tables = [pipeline_schema.db_table]
+ end
+
+ [schema, tables]
+ end
+
+ def self.defer_on_database_health_signal?
+ Feature.enabled?(:bulk_import_deferred_workers)
+ end
def perform(batch_id)
@batch = ::BulkImports::BatchTracker.find(batch_id)
+
@tracker = @batch.tracker
@pending_retry = false
+ return unless process_batch?
+
+ log_extra_metadata_on_done(:pipeline_class, @tracker.pipeline_name)
+
try_obtain_lease { run }
ensure
::BulkImports::FinishBatchedPipelineWorker.perform_async(tracker.id) unless pending_retry
end
+ def perform_failure(batch_id, exception)
+ @batch = ::BulkImports::BatchTracker.find(batch_id)
+ @tracker = @batch.tracker
+
+ fail_batch(exception)
+ end
+
private
attr_reader :batch, :tracker, :pending_retry
@@ -28,35 +67,31 @@ module BulkImports
def run
return batch.skip! if tracker.failed? || tracker.finished?
+ logger.info(log_attributes(message: 'Batch tracker started'))
batch.start!
tracker.pipeline_class.new(context).run
batch.finish!
+ logger.info(log_attributes(message: 'Batch tracker finished'))
rescue BulkImports::RetryPipelineError => e
@pending_retry = true
retry_batch(e)
- rescue StandardError => e
- fail_batch(e)
end
def fail_batch(exception)
batch.fail_op!
- Gitlab::ErrorTracking.track_exception(
- exception,
- batch_id: batch.id,
- tracker_id: tracker.id,
- pipeline_class: tracker.pipeline_name,
- pipeline_step: 'pipeline_batch_worker_run'
- )
+ Gitlab::ErrorTracking.track_exception(exception, log_attributes(message: 'Batch tracker failed'))
BulkImports::Failure.create(
bulk_import_entity_id: batch.tracker.entity.id,
pipeline_class: tracker.pipeline_name,
pipeline_step: 'pipeline_batch_worker_run',
exception_class: exception.class.to_s,
- exception_message: exception.message.truncate(255),
+ exception_message: exception.message,
correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id
)
+
+ ::BulkImports::FinishBatchedPipelineWorker.perform_async(tracker.id)
end
def context
@@ -78,7 +113,32 @@ module BulkImports
end
def re_enqueue(delay = FILE_EXTRACTION_PIPELINE_PERFORM_DELAY)
+ log_extra_metadata_on_done(:re_enqueue, true)
+
self.class.perform_in(delay, batch.id)
end
+
+ def process_batch?
+ batch.created? || batch.started?
+ end
+
+ def logger
+ @logger ||= Logger.build
+ end
+
+ def log_attributes(extra = {})
+ structured_payload(
+ {
+ batch_id: batch.id,
+ batch_number: batch.batch_number,
+ tracker_id: tracker.id,
+ bulk_import_id: tracker.entity.bulk_import_id,
+ bulk_import_entity_id: tracker.entity.id,
+ pipeline_class: tracker.pipeline_name,
+ pipeline_step: 'pipeline_batch_worker_run',
+ importer: Logger::IMPORTER_NAME
+ }.merge(extra)
+ )
+ end
end
end
diff --git a/app/workers/bulk_imports/pipeline_worker.rb b/app/workers/bulk_imports/pipeline_worker.rb
index 24185f43795..2c1d28b33c5 100644
--- a/app/workers/bulk_imports/pipeline_worker.rb
+++ b/app/workers/bulk_imports/pipeline_worker.rb
@@ -1,43 +1,68 @@
# frozen_string_literal: true
module BulkImports
- class PipelineWorker # rubocop:disable Scalability/IdempotentWorker
+ class PipelineWorker
include ApplicationWorker
include ExclusiveLeaseGuard
FILE_EXTRACTION_PIPELINE_PERFORM_DELAY = 10.seconds
+ DEFER_ON_HEALTH_DELAY = 5.minutes
+
data_consistency :always
feature_category :importers
- sidekiq_options retry: false, dead: false
+ sidekiq_options dead: false, retry: 3
worker_has_external_dependencies!
deduplicate :until_executing
worker_resource_boundary :memory
+ idempotent!
version 2
+ sidekiq_retries_exhausted do |msg, exception|
+ new.perform_failure(msg['args'][0], msg['args'][2], exception)
+ end
+
+ defer_on_database_health_signal(:gitlab_main, [], DEFER_ON_HEALTH_DELAY) do |job_args, schema, tables|
+ pipeline_tracker = ::BulkImports::Tracker.find(job_args.first)
+ pipeline_schema = ::BulkImports::PipelineSchemaInfo.new(
+ pipeline_tracker.pipeline_class,
+ pipeline_tracker.entity.portable_class
+ )
+
+ if pipeline_schema.db_schema && pipeline_schema.db_table
+ schema = pipeline_schema.db_schema
+ tables = [pipeline_schema.db_table]
+ end
+
+ [schema, tables]
+ end
+
+ def self.defer_on_database_health_signal?
+ Feature.enabled?(:bulk_import_deferred_workers)
+ end
+
# Keep _stage parameter for backwards compatibility.
def perform(pipeline_tracker_id, _stage, entity_id)
@entity = ::BulkImports::Entity.find(entity_id)
@pipeline_tracker = ::BulkImports::Tracker.find(pipeline_tracker_id)
+ log_extra_metadata_on_done(:pipeline_class, @pipeline_tracker.pipeline_name)
+
try_obtain_lease do
- if pipeline_tracker.enqueued?
+ if pipeline_tracker.enqueued? || pipeline_tracker.started?
logger.info(log_attributes(message: 'Pipeline starting'))
run
- else
- message = "Pipeline in #{pipeline_tracker.human_status_name} state instead of expected enqueued state"
-
- logger.error(log_attributes(message: message))
-
- fail_tracker(StandardError.new(message)) unless pipeline_tracker.finished? || pipeline_tracker.skipped?
end
end
- ensure
- # This is needed for in-flight migrations.
- # It will be remove in https://gitlab.com/gitlab-org/gitlab/-/issues/426299
- ::BulkImports::EntityWorker.perform_async(entity_id) if job_version.nil?
+ end
+
+ def perform_failure(pipeline_tracker_id, entity_id, exception)
+ @entity = ::BulkImports::Entity.find(entity_id)
+ @pipeline_tracker = ::BulkImports::Tracker.find(pipeline_tracker_id)
+
+ fail_tracker(exception)
end
private
@@ -53,20 +78,22 @@ module BulkImports
return re_enqueue if export_empty? || export_started?
if file_extraction_pipeline? && export_status.batched?
+ log_extra_metadata_on_done(:batched, true)
+
pipeline_tracker.update!(status_event: 'start', jid: jid, batched: true)
return pipeline_tracker.finish! if export_status.batches_count < 1
enqueue_batches
else
+ log_extra_metadata_on_done(:batched, false)
+
pipeline_tracker.update!(status_event: 'start', jid: jid)
pipeline_tracker.pipeline_class.new(context).run
pipeline_tracker.finish!
end
rescue BulkImports::RetryPipelineError => e
retry_tracker(e)
- rescue StandardError => e
- fail_tracker(e)
end
def source_version
@@ -85,16 +112,18 @@ module BulkImports
pipeline_class: pipeline_tracker.pipeline_name,
pipeline_step: 'pipeline_worker_run',
exception_class: exception.class.to_s,
- exception_message: exception.message.truncate(255),
+ exception_message: exception.message,
correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id
)
end
def logger
- @logger ||= Gitlab::Import::Logger.build
+ @logger ||= Logger.build
end
def re_enqueue(delay = FILE_EXTRACTION_PIPELINE_PERFORM_DELAY)
+ log_extra_metadata_on_done(:re_enqueue, true)
+
self.class.perform_in(
delay,
pipeline_tracker.id,
@@ -159,10 +188,10 @@ module BulkImports
bulk_import_entity_type: entity.source_type,
source_full_path: entity.source_full_path,
pipeline_tracker_id: pipeline_tracker.id,
- pipeline_name: pipeline_tracker.pipeline_name,
+ pipeline_class: pipeline_tracker.pipeline_name,
pipeline_tracker_state: pipeline_tracker.human_status_name,
source_version: source_version,
- importer: 'gitlab_migration'
+ importer: Logger::IMPORTER_NAME
}.merge(extra)
)
end
diff --git a/app/workers/bulk_imports/relation_batch_export_worker.rb b/app/workers/bulk_imports/relation_batch_export_worker.rb
index 4ce36929e15..87ceb775075 100644
--- a/app/workers/bulk_imports/relation_batch_export_worker.rb
+++ b/app/workers/bulk_imports/relation_batch_export_worker.rb
@@ -7,10 +7,25 @@ module BulkImports
idempotent!
data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency
feature_category :importers
- sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION
+ sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION, retry: 3
+
+ sidekiq_retries_exhausted do |job, exception|
+ batch = BulkImports::ExportBatch.find(job['args'][1])
+ portable = batch.export.portable
+
+ Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name)
+
+ batch.update!(status_event: 'fail_op', error: exception.message.truncate(255))
+ end
def perform(user_id, batch_id)
- RelationBatchExportService.new(user_id, batch_id).execute
+ @user = User.find(user_id)
+ @batch = BulkImports::ExportBatch.find(batch_id)
+
+ log_extra_metadata_on_done(:relation, @batch.export.relation)
+ log_extra_metadata_on_done(:objects_count, @batch.objects_count)
+
+ RelationBatchExportService.new(@user, @batch).execute
end
end
end
diff --git a/app/workers/bulk_imports/relation_export_worker.rb b/app/workers/bulk_imports/relation_export_worker.rb
index 531edc6c7a7..168626fee85 100644
--- a/app/workers/bulk_imports/relation_export_worker.rb
+++ b/app/workers/bulk_imports/relation_export_worker.rb
@@ -10,25 +10,37 @@ module BulkImports
loggable_arguments 2, 3
data_consistency :always
feature_category :importers
- sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION
+ sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION, retry: 3
worker_resource_boundary :memory
+ sidekiq_retries_exhausted do |job, exception|
+ _user_id, portable_id, portable_type, relation, batched = job['args']
+ portable = portable(portable_id, portable_type)
+
+ export = portable.bulk_import_exports.find_by_relation(relation)
+
+ Gitlab::ErrorTracking.track_exception(exception, portable_id: portable_id, portable_type: portable.class.name)
+
+ export.update!(status_event: 'fail_op', error: exception.message.truncate(255), batched: batched)
+ end
+
+ def self.portable(portable_id, portable_class)
+ portable_class.classify.constantize.find(portable_id)
+ end
+
def perform(user_id, portable_id, portable_class, relation, batched = false)
user = User.find(user_id)
- portable = portable(portable_id, portable_class)
+ portable = self.class.portable(portable_id, portable_class)
config = BulkImports::FileTransfer.config_for(portable)
+ log_extra_metadata_on_done(:relation, relation)
if Gitlab::Utils.to_boolean(batched) && config.batchable_relation?(relation)
+ log_extra_metadata_on_done(:batched, true)
BatchedRelationExportService.new(user, portable, relation, jid).execute
else
+ log_extra_metadata_on_done(:batched, false)
RelationExportService.new(user, portable, relation, jid).execute
end
end
-
- private
-
- def portable(portable_id, portable_class)
- portable_class.classify.constantize.find(portable_id)
- end
end
end
diff --git a/app/workers/bulk_imports/stuck_import_worker.rb b/app/workers/bulk_imports/stuck_import_worker.rb
index 3fa4221728b..6c8569b0aa0 100644
--- a/app/workers/bulk_imports/stuck_import_worker.rb
+++ b/app/workers/bulk_imports/stuck_import_worker.rb
@@ -14,18 +14,29 @@ module BulkImports
def perform
BulkImport.stale.find_each do |import|
+ logger.error(message: 'BulkImport stale', bulk_import_id: import.id)
import.cleanup_stale
end
- BulkImports::Entity.includes(:trackers).stale.find_each do |import| # rubocop: disable CodeReuse/ActiveRecord
+ BulkImports::Entity.includes(:trackers).stale.find_each do |entity| # rubocop: disable CodeReuse/ActiveRecord
ApplicationRecord.transaction do
- import.cleanup_stale
+ logger.error(
+ message: 'BulkImports::Entity stale',
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_id: entity.id
+ )
- import.trackers.find_each do |tracker|
+ entity.cleanup_stale
+
+ entity.trackers.find_each do |tracker|
tracker.cleanup_stale
end
end
end
end
+
+ def logger
+ @logger ||= Logger.build
+ end
end
end
diff --git a/app/workers/ci/cancel_pipeline_worker.rb b/app/workers/ci/cancel_pipeline_worker.rb
index 0b2c96e7ace..f099e185629 100644
--- a/app/workers/ci/cancel_pipeline_worker.rb
+++ b/app/workers/ci/cancel_pipeline_worker.rb
@@ -20,7 +20,7 @@ module Ci
pipeline: pipeline,
current_user: nil,
cascade_to_children: false,
- auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id
+ auto_canceled_by_pipeline: ::Ci::Pipeline.find_by_id(auto_canceled_by_pipeline_id)
).force_execute
end
end
diff --git a/app/workers/ci/initial_pipeline_process_worker.rb b/app/workers/ci/initial_pipeline_process_worker.rb
index 703cae8bf88..8d7a62e5b09 100644
--- a/app/workers/ci/initial_pipeline_process_worker.rb
+++ b/app/workers/ci/initial_pipeline_process_worker.rb
@@ -17,24 +17,10 @@ module Ci
def perform(pipeline_id)
Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
- create_deployments!(pipeline)
-
Ci::PipelineCreation::StartPipelineService
.new(pipeline)
.execute
end
end
-
- private
-
- def create_deployments!(pipeline)
- return if Feature.enabled?(:create_deployment_only_for_processable_jobs, pipeline.project)
-
- pipeline.stages.flat_map(&:statuses).each { |build| create_deployment(build) }
- end
-
- def create_deployment(build)
- ::Deployments::CreateForJobService.new.execute(build)
- end
end
end
diff --git a/app/workers/ci/refs/unlock_previous_pipelines_worker.rb b/app/workers/ci/refs/unlock_previous_pipelines_worker.rb
index bf595590cb1..588ec4ce1f0 100644
--- a/app/workers/ci/refs/unlock_previous_pipelines_worker.rb
+++ b/app/workers/ci/refs/unlock_previous_pipelines_worker.rb
@@ -14,7 +14,9 @@ module Ci
def perform(ref_id)
::Ci::Ref.find_by_id(ref_id).try do |ref|
- pipeline = ref.last_finished_pipeline
+ next unless ref.artifacts_locked?
+
+ pipeline = ref.last_unlockable_ci_source_pipeline
result = ::Ci::Refs::EnqueuePipelinesToUnlockService.new.execute(ref, before_pipeline: pipeline)
log_extra_metadata_on_done(:total_pending_entries, result[:total_pending_entries])
diff --git a/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb b/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
index f6feb6d1598..316d30d94da 100644
--- a/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
+++ b/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
@@ -52,8 +52,7 @@ module Gitlab
job_delay = client.rate_limit_resets_in + calculate_job_delay(enqueued_job_counter)
- self.class
- .perform_in(job_delay, project.id, hash, notify_key)
+ self.class.perform_in(job_delay, project.id, hash.deep_stringify_keys, notify_key.to_s)
end
end
end
diff --git a/app/workers/concerns/gitlab/github_import/stage_methods.rb b/app/workers/concerns/gitlab/github_import/stage_methods.rb
index 80013ff3cd9..5c63c667a03 100644
--- a/app/workers/concerns/gitlab/github_import/stage_methods.rb
+++ b/app/workers/concerns/gitlab/github_import/stage_methods.rb
@@ -5,6 +5,8 @@ module Gitlab
module StageMethods
extend ActiveSupport::Concern
+ MAX_RETRIES_AFTER_INTERRUPTION = 20
+
included do
include ApplicationWorker
@@ -18,6 +20,29 @@ module Gitlab
end
end
+ class_methods do
+ # We can increase the number of times a GitHubImport::Stage worker is retried
+ # after being interrupted if the importer it executes can restart exactly
+ # from where it left off.
+ #
+ # It is not safe to call this method if the importer loops over its data from
+ # the beginning when restarted, even if it skips data that is already imported
+ # inside the loop, as there is a possibility the importer will never reach
+ # the end of the loop.
+ #
+ # Examples of stage workers that call this method are ones that execute services that:
+ #
+ # - Continue paging an endpoint from where it left off:
+ # https://gitlab.com/gitlab-org/gitlab/-/blob/487521cc/lib/gitlab/github_import/parallel_scheduling.rb#L114-117
+ # - Continue their loop from where it left off:
+ # https://gitlab.com/gitlab-org/gitlab/-/blob/024235ec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer.rb#L15
+ def resumes_work_when_interrupted!
+ return unless Feature.enabled?(:github_importer_raise_max_interruptions)
+
+ sidekiq_options max_retries_after_interruption: MAX_RETRIES_AFTER_INTERRUPTION
+ end
+ end
+
# project_id - The ID of the GitLab project to import the data into.
def perform(project_id)
info(project_id, message: 'starting stage')
@@ -54,6 +79,8 @@ module Gitlab
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def try_import(client, project)
+ project.import_state.refresh_jid_expiration
+
import(client, project)
rescue RateLimitError
self.class.perform_in(client.rate_limit_resets_in, project.id)
diff --git a/app/workers/concerns/worker_attributes.rb b/app/workers/concerns/worker_attributes.rb
index cb09aaf1a6a..28c82a5a38e 100644
--- a/app/workers/concerns/worker_attributes.rb
+++ b/app/workers/concerns/worker_attributes.rb
@@ -201,10 +201,10 @@ module WorkerAttributes
!!get_class_attribute(:big_payload)
end
- def defer_on_database_health_signal(gitlab_schema, tables = [], delay_by = DEFAULT_DEFER_DELAY)
+ def defer_on_database_health_signal(gitlab_schema, tables = [], delay_by = DEFAULT_DEFER_DELAY, &block)
set_class_attribute(
:database_health_check_attrs,
- { gitlab_schema: gitlab_schema, tables: tables, delay_by: delay_by }
+ { gitlab_schema: gitlab_schema, tables: tables, delay_by: delay_by, block: block }
)
end
diff --git a/app/workers/environments/auto_recover_worker.rb b/app/workers/environments/auto_recover_worker.rb
new file mode 100644
index 00000000000..75e86e38f1a
--- /dev/null
+++ b/app/workers/environments/auto_recover_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Environments
+ class AutoRecoverWorker
+ include ApplicationWorker
+
+ deduplicate :until_executed
+ data_consistency :delayed
+ idempotent!
+ feature_category :continuous_delivery
+
+ def perform(environment_id, _params = {})
+ Environment.find_by_id(environment_id).try do |environment|
+ next unless environment.long_stopping?
+
+ next unless environment.stop_actions.all?(&:complete?)
+
+ environment.recover_stuck_stopping
+ end
+ end
+ end
+end
diff --git a/app/workers/environments/auto_stop_cron_worker.rb b/app/workers/environments/auto_stop_cron_worker.rb
index 4d6453a85e7..26b18c406e5 100644
--- a/app/workers/environments/auto_stop_cron_worker.rb
+++ b/app/workers/environments/auto_stop_cron_worker.rb
@@ -13,6 +13,7 @@ module Environments
def perform
AutoStopService.new.execute
+ AutoRecoverService.new.execute
end
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_attachments_worker.rb b/app/workers/gitlab/github_import/stage/import_attachments_worker.rb
index f9952f04e99..a5d085a82c0 100644
--- a/app/workers/gitlab/github_import/stage/import_attachments_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_attachments_worker.rb
@@ -11,6 +11,8 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ resumes_work_when_interrupted!
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
@@ -48,8 +50,8 @@ module Gitlab
def move_to_next_stage(project, waiters = {})
AdvanceStageWorker.perform_async(
project.id,
- waiters,
- :protected_branches
+ waiters.deep_stringify_keys,
+ 'protected_branches'
)
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_base_data_worker.rb b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
index 94cb3cb6c71..5bbe14b6528 100644
--- a/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
@@ -27,8 +27,6 @@ module Gitlab
klass.new(project, client).execute
end
- project.import_state.refresh_jid_expiration
-
ImportPullRequestsWorker.perform_async(project.id)
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_collaborators_worker.rb b/app/workers/gitlab/github_import/stage/import_collaborators_worker.rb
index 751ca92388a..037b529b866 100644
--- a/app/workers/gitlab/github_import/stage/import_collaborators_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_collaborators_worker.rb
@@ -20,7 +20,6 @@ module Gitlab
info(project.id, message: 'starting importer', importer: 'Importer::CollaboratorsImporter')
waiter = Importer::CollaboratorsImporter.new(project, client).execute
- project.import_state.refresh_jid_expiration
move_to_next_stage(project, { waiter.key => waiter.jobs_remaining })
end
@@ -44,7 +43,7 @@ module Gitlab
def move_to_next_stage(project, waiters = {})
AdvanceStageWorker.perform_async(
- project.id, waiters, :pull_requests_merged_by
+ project.id, waiters.deep_stringify_keys, 'pull_requests_merged_by'
)
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb b/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb
index c80412d941b..35779d7bfc5 100644
--- a/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb
@@ -11,6 +11,8 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ resumes_work_when_interrupted!
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
@@ -30,7 +32,7 @@ module Gitlab
end
def move_to_next_stage(project, waiters = {})
- AdvanceStageWorker.perform_async(project.id, waiters, :notes)
+ AdvanceStageWorker.perform_async(project.id, waiters.deep_stringify_keys, 'notes')
end
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
index 592b789cc94..58e1f637b6a 100644
--- a/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
@@ -11,6 +11,8 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ resumes_work_when_interrupted!
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
@@ -20,7 +22,7 @@ module Gitlab
hash[waiter.key] = waiter.jobs_remaining
end
- AdvanceStageWorker.perform_async(project.id, waiters, :issue_events)
+ AdvanceStageWorker.perform_async(project.id, waiters.deep_stringify_keys, 'issue_events')
end
# The importers to run in this stage. Issues can't be imported earlier
diff --git a/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb b/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb
index e89a850c991..8d7bd98f303 100644
--- a/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb
@@ -11,6 +11,11 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ # Importer::LfsObjectsImporter can resume work when interrupted as
+ # it uses Projects::LfsPointers::LfsObjectDownloadListService which excludes LFS objects that already exist.
+ # https://gitlab.com/gitlab-org/gitlab/-/blob/eabf0800/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb#L69-71
+ resumes_work_when_interrupted!
+
def perform(project_id)
return unless (project = find_project(project_id))
@@ -28,7 +33,7 @@ module Gitlab
AdvanceStageWorker.perform_async(
project.id,
{ waiter.key => waiter.jobs_remaining },
- :finish
+ 'finish'
)
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
index c1fdb76d03e..0459545d8e1 100644
--- a/app/workers/gitlab/github_import/stage/import_notes_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
@@ -11,6 +11,8 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ resumes_work_when_interrupted!
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
@@ -20,7 +22,7 @@ module Gitlab
hash[waiter.key] = waiter.jobs_remaining
end
- AdvanceStageWorker.perform_async(project.id, waiters, :attachments)
+ AdvanceStageWorker.perform_async(project.id, waiters.deep_stringify_keys, 'attachments')
end
def importers(project)
diff --git a/app/workers/gitlab/github_import/stage/import_protected_branches_worker.rb b/app/workers/gitlab/github_import/stage/import_protected_branches_worker.rb
index f8448094c28..e281e965f94 100644
--- a/app/workers/gitlab/github_import/stage/import_protected_branches_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_protected_branches_worker.rb
@@ -19,12 +19,10 @@ module Gitlab
.new(project, client)
.execute
- project.import_state.refresh_jid_expiration
-
AdvanceStageWorker.perform_async(
project.id,
{ waiter.key => waiter.jobs_remaining },
- :lfs_objects
+ 'lfs_objects'
)
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb
index 2e7cd28578f..2f543951bf3 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb
@@ -11,6 +11,8 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ resumes_work_when_interrupted!
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
@@ -18,12 +20,10 @@ module Gitlab
.new(project, client)
.execute
- project.import_state.refresh_jid_expiration
-
AdvanceStageWorker.perform_async(
project.id,
{ waiter.key => waiter.jobs_remaining },
- :pull_request_review_requests
+ 'pull_request_review_requests'
)
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker.rb
index 2f860349e25..db76545ae87 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker.rb
@@ -11,6 +11,8 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ resumes_work_when_interrupted!
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
@@ -18,12 +20,10 @@ module Gitlab
.new(project, client)
.execute
- project.import_state.refresh_jid_expiration
-
AdvanceStageWorker.perform_async(
project.id,
{ waiter.key => waiter.jobs_remaining },
- :pull_request_reviews
+ 'pull_request_reviews'
)
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb
index 51730033133..31b7c57a524 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb
@@ -11,6 +11,8 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ resumes_work_when_interrupted!
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
@@ -18,12 +20,10 @@ module Gitlab
.new(project, client)
.execute
- project.import_state.refresh_jid_expiration
-
AdvanceStageWorker.perform_async(
project.id,
{ waiter.key => waiter.jobs_remaining },
- :issues_and_diff_notes
+ 'issues_and_diff_notes'
)
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
index 029d38d8b93..c68b95b5111 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
@@ -11,6 +11,8 @@ module Gitlab
include GithubImport::Queue
include StageMethods
+ resumes_work_when_interrupted!
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
@@ -25,12 +27,10 @@ module Gitlab
.new(project, client)
.execute
- project.import_state.refresh_jid_expiration
-
AdvanceStageWorker.perform_async(
project.id,
{ waiter.key => waiter.jobs_remaining },
- :collaborators
+ 'collaborators'
)
end
diff --git a/app/workers/gitlab/import/advance_stage.rb b/app/workers/gitlab/import/advance_stage.rb
index 180c08905ff..782439894c0 100644
--- a/app/workers/gitlab/import/advance_stage.rb
+++ b/app/workers/gitlab/import/advance_stage.rb
@@ -19,7 +19,7 @@ module Gitlab
# completed.
# timeout_timer - Time the sidekiq worker was first initiated with the current job_count
# previous_job_count - Number of jobs remaining on last invocation of this worker
- def perform(project_id, waiters, next_stage, timeout_timer = Time.zone.now, previous_job_count = nil)
+ def perform(project_id, waiters, next_stage, timeout_timer = Time.zone.now.to_s, previous_job_count = nil)
import_state_jid = find_import_state_jid(project_id)
# If the import state is nil the project may have been deleted or the import
@@ -45,7 +45,9 @@ module Gitlab
handle_timeout(import_state_jid, next_stage, project_id, new_waiters, new_job_count)
else
- self.class.perform_in(INTERVAL, project_id, new_waiters, next_stage, timeout_timer, previous_job_count)
+ self.class.perform_in(INTERVAL,
+ project_id, new_waiters.deep_stringify_keys, next_stage.to_s, timeout_timer.to_s, previous_job_count
+ )
end
end
diff --git a/app/workers/gitlab/jira_import/stage/import_issues_worker.rb b/app/workers/gitlab/jira_import/stage/import_issues_worker.rb
index 7a5eb6c1e3a..5d890ecfe13 100644
--- a/app/workers/gitlab/jira_import/stage/import_issues_worker.rb
+++ b/app/workers/gitlab/jira_import/stage/import_issues_worker.rb
@@ -9,7 +9,14 @@ module Gitlab
private
def import(project)
- jobs_waiter = Gitlab::JiraImport::IssuesImporter.new(project).execute
+ jira_client = if Feature.enabled?(:increase_jira_import_issues_timeout)
+ project.jira_integration.client(read_timeout: 2.minutes)
+ end
+
+ jobs_waiter = Gitlab::JiraImport::IssuesImporter.new(
+ project,
+ jira_client
+ ).execute
project.latest_jira_import.refresh_jid_expiration
diff --git a/app/workers/hashed_storage/base_worker.rb b/app/workers/hashed_storage/base_worker.rb
deleted file mode 100644
index 372440996d9..00000000000
--- a/app/workers/hashed_storage/base_worker.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-module HashedStorage
- class BaseWorker # rubocop:disable Scalability/IdempotentWorker
- include ExclusiveLeaseGuard
- include WorkerAttributes
-
- feature_category :source_code_management
-
- LEASE_TIMEOUT = 30.seconds.to_i
- LEASE_KEY_SEGMENT = 'project_migrate_hashed_storage_worker'
-
- protected
-
- def lease_key
- # we share the same lease key for both migration and rollback so they don't run simultaneously
- "#{LEASE_KEY_SEGMENT}:#{project_id}"
- end
-
- def lease_timeout
- LEASE_TIMEOUT
- end
- end
-end
diff --git a/app/workers/hashed_storage/migrator_worker.rb b/app/workers/hashed_storage/migrator_worker.rb
deleted file mode 100644
index a7e7a505681..00000000000
--- a/app/workers/hashed_storage/migrator_worker.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module HashedStorage
- class MigratorWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
-
- queue_namespace :hashed_storage
- feature_category :source_code_management
-
- # @param [Integer] start initial ID of the batch
- # @param [Integer] finish last ID of the batch
- def perform(start, finish); end
- end
-end
diff --git a/app/workers/hashed_storage/project_migrate_worker.rb b/app/workers/hashed_storage/project_migrate_worker.rb
deleted file mode 100644
index e1bf71de179..00000000000
--- a/app/workers/hashed_storage/project_migrate_worker.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module HashedStorage
- class ProjectMigrateWorker < BaseWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
-
- queue_namespace :hashed_storage
- loggable_arguments 1
-
- attr_reader :project_id
-
- def perform(project_id, old_disk_path = nil); end
- end
-end
diff --git a/app/workers/hashed_storage/project_rollback_worker.rb b/app/workers/hashed_storage/project_rollback_worker.rb
deleted file mode 100644
index af4223ff354..00000000000
--- a/app/workers/hashed_storage/project_rollback_worker.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module HashedStorage
- class ProjectRollbackWorker < BaseWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
-
- queue_namespace :hashed_storage
- loggable_arguments 1
-
- attr_reader :project_id
-
- def perform(project_id, old_disk_path = nil); end
- end
-end
diff --git a/app/workers/hashed_storage/rollbacker_worker.rb b/app/workers/hashed_storage/rollbacker_worker.rb
deleted file mode 100644
index e659e65a370..00000000000
--- a/app/workers/hashed_storage/rollbacker_worker.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module HashedStorage
- class RollbackerWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
-
- queue_namespace :hashed_storage
- feature_category :source_code_management
-
- # @param [Integer] start initial ID of the batch
- # @param [Integer] finish last ID of the batch
- def perform(start, finish); end
- end
-end
diff --git a/app/workers/merge_request_cleanup_refs_worker.rb b/app/workers/merge_request_cleanup_refs_worker.rb
index 92dfe8a8cb0..db1a1e96997 100644
--- a/app/workers/merge_request_cleanup_refs_worker.rb
+++ b/app/workers/merge_request_cleanup_refs_worker.rb
@@ -18,8 +18,6 @@ class MergeRequestCleanupRefsWorker
FAILURE_THRESHOLD = 3
def perform_work
- return unless Feature.enabled?(:merge_request_refs_cleanup)
-
unless merge_request
logger.error('No existing merge request to be cleaned up.')
return
diff --git a/app/workers/merge_requests/set_reviewer_reviewed_worker.rb b/app/workers/merge_requests/set_reviewer_reviewed_worker.rb
index 2f15bf3b879..7e8bc60f6e1 100644
--- a/app/workers/merge_requests/set_reviewer_reviewed_worker.rb
+++ b/app/workers/merge_requests/set_reviewer_reviewed_worker.rb
@@ -13,18 +13,23 @@ module MergeRequests
current_user_id = event.data[:current_user_id]
merge_request_id = event.data[:merge_request_id]
current_user = User.find_by_id(current_user_id)
- merge_request = MergeRequest.find_by_id(merge_request_id)
- if !current_user
+ unless current_user
logger.info(structured_payload(message: 'Current user not found.', current_user_id: current_user_id))
- elsif !merge_request
- logger.info(structured_payload(message: 'Merge request not found.', merge_request_id: merge_request_id))
- else
- project = merge_request.source_project
+ return
+ end
+
+ merge_request = MergeRequest.find_by_id(merge_request_id)
- ::MergeRequests::MarkReviewerReviewedService.new(project: project, current_user: current_user)
- .execute(merge_request)
+ unless merge_request
+ logger.info(structured_payload(message: 'Merge request not found.', merge_request_id: merge_request_id))
+ return
end
+
+ project = merge_request.source_project
+
+ ::MergeRequests::UpdateReviewerStateService.new(project: project, current_user: current_user)
+ .execute(merge_request, "reviewed")
end
end
end
diff --git a/app/workers/packages/cleanup_package_registry_worker.rb b/app/workers/packages/cleanup_package_registry_worker.rb
index 5f14102b5a1..5b2d8bacd62 100644
--- a/app/workers/packages/cleanup_package_registry_worker.rb
+++ b/app/workers/packages/cleanup_package_registry_worker.rb
@@ -13,6 +13,7 @@ module Packages
def perform
enqueue_package_file_cleanup_job if Packages::PackageFile.pending_destruction.exists?
enqueue_cleanup_policy_jobs if Packages::Cleanup::Policy.runnable.exists?
+ enqueue_cleanup_stale_npm_metadata_cache_job if Packages::Npm::MetadataCache.pending_destruction.exists?
log_counts
end
@@ -27,6 +28,10 @@ module Packages
Packages::Cleanup::ExecutePolicyWorker.perform_with_capacity
end
+ def enqueue_cleanup_stale_npm_metadata_cache_job
+ Packages::Npm::CleanupStaleMetadataCacheWorker.perform_with_capacity
+ end
+
def log_counts
use_replica_if_available do
pending_destruction_package_files_count = Packages::PackageFile.pending_destruction.count
diff --git a/app/workers/packages/npm/cleanup_stale_metadata_cache_worker.rb b/app/workers/packages/npm/cleanup_stale_metadata_cache_worker.rb
new file mode 100644
index 00000000000..158209c28fd
--- /dev/null
+++ b/app/workers/packages/npm/cleanup_stale_metadata_cache_worker.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Packages
+ module Npm
+ class CleanupStaleMetadataCacheWorker
+ include ApplicationWorker
+ include ::Packages::CleanupArtifactWorker
+
+ MAX_CAPACITY = 2
+
+ data_consistency :sticky
+
+ queue_namespace :package_cleanup
+ feature_category :package_registry
+
+ deduplicate :until_executed
+ idempotent!
+
+ def max_running_jobs
+ MAX_CAPACITY
+ end
+
+ private
+
+ def model
+ Packages::Npm::MetadataCache
+ end
+
+ def log_metadata(npm_metadata_cache)
+ log_extra_metadata_on_done(:npm_metadata_cache_id, npm_metadata_cache.id)
+ end
+
+ def log_cleanup_item(npm_metadata_cache)
+ logger.info(
+ structured_payload(
+ npm_metadata_cache_id: npm_metadata_cache.id
+ )
+ )
+ end
+ end
+ end
+end
diff --git a/app/workers/packages/nuget/extraction_worker.rb b/app/workers/packages/nuget/extraction_worker.rb
index 55aca0beb03..33fc98cf95b 100644
--- a/app/workers/packages/nuget/extraction_worker.rb
+++ b/app/workers/packages/nuget/extraction_worker.rb
@@ -18,7 +18,7 @@ module Packages
return unless package_file
- ::Packages::Nuget::UpdatePackageFromMetadataService.new(package_file).execute
+ ::Packages::Nuget::ProcessPackageFileService.new(package_file).execute
rescue StandardError => exception
process_package_file_error(
package_file: package_file,
diff --git a/app/workers/projects/import_export/after_import_merge_requests_worker.rb b/app/workers/projects/import_export/after_import_merge_requests_worker.rb
new file mode 100644
index 00000000000..b40e0ca5f09
--- /dev/null
+++ b/app/workers/projects/import_export/after_import_merge_requests_worker.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Projects
+ module ImportExport
+ class AfterImportMergeRequestsWorker
+ include ApplicationWorker
+
+ idempotent!
+ data_consistency :delayed
+ urgency :low
+ feature_category :importers
+
+ def perform(project_id)
+ project = Project.find_by_id(project_id)
+ return unless project
+
+ project.merge_requests.set_latest_merge_request_diff_ids!
+ end
+ end
+ end
+end
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
index f1da5f37945..0bac595f0c4 100644
--- a/app/workers/remove_expired_group_links_worker.rb
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -11,7 +11,7 @@ class RemoveExpiredGroupLinksWorker # rubocop:disable Scalability/IdempotentWork
def perform
ProjectGroupLink.expired.find_each do |link|
- Projects::GroupLinks::DestroyService.new(link.project, nil).execute(link)
+ Projects::GroupLinks::DestroyService.new(link.project, nil).execute(link, skip_authorization: true)
end
GroupGroupLink.expired.find_in_batches do |link_batch|
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 5ec9ceaf004..f4a507246ac 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -2,6 +2,7 @@
class RepositoryForkWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ include Gitlab::Utils::StrongMemoize
data_consistency :always
@@ -12,10 +13,8 @@ class RepositoryForkWorker # rubocop:disable Scalability/IdempotentWorker
feature_category :source_code_management
def perform(*args)
- target_project_id = args.shift
- target_project = Project.find(target_project_id)
+ @target_project_id = args.shift
- source_project = target_project.forked_from_project
unless source_project
return target_project.import_state.mark_as_failed(_('Source project cannot be found.'))
end
@@ -25,6 +24,21 @@ class RepositoryForkWorker # rubocop:disable Scalability/IdempotentWorker
private
+ def target_project
+ Project.find(@target_project_id)
+ end
+ strong_memoize_attr :target_project
+
+ def source_project
+ @source_project ||= target_project.forked_from_project
+ end
+
+ def branch
+ return unless target_project.import_data&.data
+
+ target_project.import_data.data['fork_branch']
+ end
+
def fork_repository(target_project, source_project)
return unless start_fork(target_project)
@@ -46,7 +60,7 @@ class RepositoryForkWorker # rubocop:disable Scalability/IdempotentWorker
source_repo = source_project.repository.raw
target_repo = target_project.repository.raw
- ::Gitlab::GitalyClient::RepositoryService.new(target_repo).fork_repository(source_repo)
+ ::Gitlab::GitalyClient::RepositoryService.new(target_repo).fork_repository(source_repo, branch)
rescue GRPC::BadStatus => e
Gitlab::ErrorTracking.track_exception(e, source_project_id: source_project.id, target_project_id: target_project.id)
diff --git a/app/workers/schedule_merge_request_cleanup_refs_worker.rb b/app/workers/schedule_merge_request_cleanup_refs_worker.rb
index ced1f443ea6..2ecc95335e2 100644
--- a/app/workers/schedule_merge_request_cleanup_refs_worker.rb
+++ b/app/workers/schedule_merge_request_cleanup_refs_worker.rb
@@ -12,7 +12,6 @@ class ScheduleMergeRequestCleanupRefsWorker
def perform
return if Gitlab::Database.read_only?
- return unless Feature.enabled?(:merge_request_refs_cleanup)
MergeRequest::CleanupSchedule.stuck_retry!
MergeRequestCleanupRefsWorker.perform_with_capacity
diff --git a/app/workers/tasks_to_be_done/create_worker.rb b/app/workers/tasks_to_be_done/create_worker.rb
deleted file mode 100644
index 91046e3cfed..00000000000
--- a/app/workers/tasks_to_be_done/create_worker.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module TasksToBeDone
- class CreateWorker
- include ApplicationWorker
-
- data_consistency :always
- idempotent!
- feature_category :onboarding
- urgency :low
- worker_resource_boundary :cpu
-
- def perform(member_task_id, current_user_id, assignee_ids = [])
- # no-op removing
- # https://docs.gitlab.com/ee/development/sidekiq/compatibility_across_updates.html#removing-worker-classes
- end
- end
-end