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/images/apple-touch-icon.pngbin0 -> 7455 bytes
-rw-r--r--app/assets/images/ext_snippet_icons/logo.svg13
-rw-r--r--app/assets/images/favicon-blue.pngbin1522 -> 390 bytes
-rw-r--r--app/assets/images/favicon-yellow.pngbin1481 -> 373 bytes
-rw-r--r--app/assets/images/favicon.pngbin1611 -> 591 bytes
-rw-r--r--app/assets/images/file_icons.svg2
-rw-r--r--app/assets/images/gitlab_logo.pngbin3616 -> 1528 bytes
-rw-r--r--app/assets/images/logo.svg34
-rw-r--r--app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gifbin3654 -> 0 bytes
-rw-r--r--app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gifbin3040 -> 0 bytes
-rw-r--r--app/assets/images/mailers/gitlab_footer_logo.gifbin3654 -> 0 bytes
-rw-r--r--app/assets/images/mailers/gitlab_header_logo.gifbin3040 -> 0 bytes
-rw-r--r--app/assets/images/mailers/gitlab_header_logo.pngbin7096 -> 0 bytes
-rw-r--r--app/assets/images/mailers/gitlab_logo.pngbin0 -> 1528 bytes
-rw-r--r--app/assets/images/mailers/gitlab_logo_black_text.pngbin0 -> 1961 bytes
-rw-r--r--app/assets/images/mailers/gitlab_tanuki_2x.pngbin2545 -> 0 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/gitlab-logo-gray-rgb.pngbin42439 -> 0 bytes
-rw-r--r--app/assets/images/msapplication-tile.pngbin4328 -> 0 bytes
-rw-r--r--app/assets/images/touch-icon-ipad-retina.pngbin5662 -> 0 bytes
-rw-r--r--app/assets/images/touch-icon-ipad.pngbin2465 -> 0 bytes
-rw-r--r--app/assets/images/touch-icon-iphone-retina.pngbin3460 -> 0 bytes
-rw-r--r--app/assets/images/touch-icon-iphone.pngbin1949 -> 0 bytes
-rw-r--r--app/assets/images/twitter_card.jpgbin0 -> 133437 bytes
-rw-r--r--app/assets/javascripts/access_tokens/components/projects_token_selector.vue2
-rw-r--r--app/assets/javascripts/admin/background_migrations/components/database_listbox.vue51
-rw-r--r--app/assets/javascripts/admin/background_migrations/index.js38
-rw-r--r--app/assets/javascripts/admin/statistics_panel/components/app.vue2
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue18
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue2
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql2
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql4
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql4
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/mutations/reset_prometheus_token.mutation.graphql2
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql8
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/mutations/update_prometheus_integration.mutation.graphql6
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/queries/get_http_integration.query.graphql2
-rw-r--r--app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue33
-rw-r--r--app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue72
-rw-r--r--app/assets/javascripts/analytics/shared/constants.js36
-rw-r--r--app/assets/javascripts/analytics/usage_trends/components/users_chart.vue6
-rw-r--r--app/assets/javascripts/api.js25
-rw-r--r--app/assets/javascripts/api/integrations_api.js21
-rw-r--r--app/assets/javascripts/api/tags_api.js12
-rw-r--r--app/assets/javascripts/api/user_api.js18
-rw-r--r--app/assets/javascripts/behaviors/copy_to_clipboard.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js15
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js2
-rw-r--r--app/assets/javascripts/blob/components/blob_content.vue2
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue2
-rw-r--r--app/assets/javascripts/blob/file_template_selector.js43
-rw-r--r--app/assets/javascripts/blob/openapi/index.js1
-rw-r--r--app/assets/javascripts/blob/pipeline_tour_success_modal.vue2
-rw-r--r--app/assets/javascripts/blob/sketch/index.js2
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js29
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column_form.vue26
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue21
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue5
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue15
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue1
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue27
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue8
-rw-r--r--app/assets/javascripts/boards/components/board_top_bar.vue4
-rw-r--r--app/assets/javascripts/boards/components/config_toggle.vue4
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue7
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue2
-rw-r--r--app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/board_list_destroy.mutation.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/board_list_update.mutation.graphql4
-rw-r--r--app/assets/javascripts/boards/graphql/board_lists.query.graphql6
-rw-r--r--app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/group_board.query.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/lists_issues.query.graphql4
-rw-r--r--app/assets/javascripts/boards/graphql/project_board.query.graphql2
-rw-r--r--app/assets/javascripts/boards/index.js1
-rw-r--r--app/assets/javascripts/boards/stores/actions.js5
-rw-r--r--app/assets/javascripts/boards/stores/getters.js14
-rw-r--r--app/assets/javascripts/branches/components/delete_branch_modal.vue37
-rw-r--r--app/assets/javascripts/captcha/captcha_modal.vue4
-rw-r--r--app/assets/javascripts/ci_secure_files/components/secure_files_list.vue202
-rw-r--r--app/assets/javascripts/ci_secure_files/index.js5
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue23
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue5
-rw-r--r--app/assets/javascripts/ci_variable_list/constants.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/index.js2
-rw-r--r--app/assets/javascripts/clone_panel.js7
-rw-r--r--app/assets/javascripts/clusters/agents/components/activity_events_list.vue16
-rw-r--r--app/assets/javascripts/clusters/agents/components/revoke_token_button.vue201
-rw-r--r--app/assets/javascripts/clusters/agents/components/show.vue2
-rw-r--r--app/assets/javascripts/clusters/agents/components/token_table.vue11
-rw-r--r--app/assets/javascripts/clusters/agents/constants.js2
-rw-r--r--app/assets/javascripts/clusters/agents/graphql/cache_update.js22
-rw-r--r--app/assets/javascripts/clusters/agents/graphql/mutations/revoke_token.mutation.graphql5
-rw-r--r--app/assets/javascripts/clusters/components/new_cluster.vue8
-rw-r--r--app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue2
-rw-r--r--app/assets/javascripts/clusters/forms/components/integration_form.vue2
-rw-r--r--app/assets/javascripts/clusters/gke_cluster_namespace/index.js (renamed from app/assets/javascripts/create_cluster/gke_cluster_namespace/index.js)0
-rw-r--r--app/assets/javascripts/clusters/new_cluster.js8
-rw-r--r--app/assets/javascripts/clusters/stores/new_cluster/index.js12
-rw-r--r--app/assets/javascripts/clusters/stores/new_cluster/state.js3
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_table.vue17
-rw-r--r--app/assets/javascripts/clusters_list/components/agents.vue2
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters.vue4
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_actions.vue79
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_view_all.vue2
-rw-r--r--app/assets/javascripts/clusters_list/constants.js4
-rw-r--r--app/assets/javascripts/clusters_list/index.js4
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue1
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/project_form_group.vue6
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue (renamed from app/assets/javascripts/content_editor/components/code_block_bubble_menu.vue)94
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/formatting.vue (renamed from app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue)50
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/link.vue189
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/media.vue288
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue10
-rw-r--r--app/assets/javascripts/content_editor/components/divider.vue3
-rw-r--r--app/assets/javascripts/content_editor/components/loading_indicator.vue2
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_button.vue9
-rw-r--r--app/assets/javascripts/content_editor/components/top_toolbar.vue7
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/code_block.vue (renamed from app/assets/javascripts/content_editor/components/wrappers/frontmatter.vue)14
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/media.vue51
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue4
-rw-r--r--app/assets/javascripts/content_editor/constants/code_block_languages.js210
-rw-r--r--app/assets/javascripts/content_editor/constants/index.js (renamed from app/assets/javascripts/content_editor/constants.js)0
-rw-r--r--app/assets/javascripts/content_editor/extensions/blockquote.js2
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js17
-rw-r--r--app/assets/javascripts/content_editor/extensions/diagram.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/frontmatter.js15
-rw-r--r--app/assets/javascripts/content_editor/extensions/image.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/paste_markdown.js11
-rw-r--r--app/assets/javascripts/content_editor/extensions/playable.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/sourcemap.js48
-rw-r--r--app/assets/javascripts/content_editor/services/asset_resolver.js13
-rw-r--r--app/assets/javascripts/content_editor/services/code_block_language_loader.js246
-rw-r--r--app/assets/javascripts/content_editor/services/content_editor.js46
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js24
-rw-r--r--app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js (renamed from app/assets/javascripts/content_editor/services/markdown_deserializer.js)2
-rw-r--r--app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js475
-rw-r--r--app/assets/javascripts/content_editor/services/highlight_js_language_loader.js248
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js115
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_sourcemap.js2
-rw-r--r--app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js87
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js131
-rw-r--r--app/assets/javascripts/content_editor/services/upload_helpers.js4
-rw-r--r--app/assets/javascripts/content_editor/services/utils.js2
-rw-r--r--app/assets/javascripts/create_cluster/components/cluster_form_dropdown.vue246
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue58
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue530
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue182
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/constants.js9
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/index.js55
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js79
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/actions.js148
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/getters.js2
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/index.js49
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js19
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js66
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/state.js34
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js70
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue112
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue53
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue194
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_submit_button.vue18
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue44
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue101
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/constants.js11
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/gapi_loader.js24
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/index.js95
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/store/actions.js99
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/store/getters.js5
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/store/index.js18
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/store/mutation_types.js8
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/store/mutations.js28
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/store/state.js13
-rw-r--r--app/assets/javascripts/create_cluster/init_create_cluster.js35
-rw-r--r--app/assets/javascripts/create_cluster/store/cluster_dropdown/actions.js14
-rw-r--r--app/assets/javascripts/create_cluster/store/cluster_dropdown/getters.js0
-rw-r--r--app/assets/javascripts/create_cluster/store/cluster_dropdown/index.js13
-rw-r--r--app/assets/javascripts/create_cluster/store/cluster_dropdown/mutation_types.js3
-rw-r--r--app/assets/javascripts/create_cluster/store/cluster_dropdown/mutations.js16
-rw-r--r--app/assets/javascripts/create_cluster/store/cluster_dropdown/state.js5
-rw-r--r--app/assets/javascripts/crm/components/form.vue39
-rw-r--r--app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue58
-rw-r--r--app/assets/javascripts/crm/contacts/components/contacts_root.vue2
-rw-r--r--app/assets/javascripts/crm/organizations/components/organizations_root.vue2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/base.vue21
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_table.vue2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue57
-rw-r--r--app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue4
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql2
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql1
-rw-r--r--app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql1
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue4
-rw-r--r--app/assets/javascripts/diffs/components/app.vue13
-rw-r--r--app/assets/javascripts/diffs/components/compare_dropdown_layout.vue7
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue158
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue106
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue7
-rw-r--r--app/assets/javascripts/diffs/components/diff_row.vue8
-rw-r--r--app/assets/javascripts/diffs/components/diff_row_utils.js7
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue85
-rw-r--r--app/assets/javascripts/diffs/components/hidden_files_warning.vue2
-rw-r--r--app/assets/javascripts/diffs/components/image_diff_overlay.vue4
-rw-r--r--app/assets/javascripts/diffs/constants.js3
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js2
-rw-r--r--app/assets/javascripts/diffs/store/utils.js10
-rw-r--r--app/assets/javascripts/diffs/utils/diff_file.js27
-rw-r--r--app/assets/javascripts/diffs/utils/queue_events.js26
-rw-r--r--app/assets/javascripts/editor/components/source_editor_toolbar.vue6
-rw-r--r--app/assets/javascripts/editor/components/source_editor_toolbar_button.vue74
-rw-r--r--app/assets/javascripts/editor/components/source_editor_toolbar_graphql.js53
-rw-r--r--app/assets/javascripts/editor/constants.js5
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_extension_base.js4
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js108
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_toolbar_ext.js98
-rw-r--r--app/assets/javascripts/editor/graphql/add_items.mutation.graphql3
-rw-r--r--app/assets/javascripts/editor/graphql/get_item.query.graphql9
-rw-r--r--app/assets/javascripts/editor/graphql/remove_items.mutation.graphql3
-rw-r--r--app/assets/javascripts/editor/graphql/typedefs.graphql23
-rw-r--r--app/assets/javascripts/editor/graphql/update_item.mutation.graphql2
-rw-r--r--app/assets/javascripts/editor/schema/ci.json21
-rw-r--r--app/assets/javascripts/environments/components/enable_review_app_modal.vue13
-rw-r--r--app/assets/javascripts/environments/components/environment_folder.vue8
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue2
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue4
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue2
-rw-r--r--app/assets/javascripts/error_tracking/queries/details.query.graphql2
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/app.vue2
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js27
-rw-r--r--app/assets/javascripts/filtered_search/constants.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js4
-rw-r--r--app/assets/javascripts/filtered_search/group_runners_filtered_search_token_keys.js27
-rw-r--r--app/assets/javascripts/filtered_search/null_dropdown.js9
-rw-r--r--app/assets/javascripts/flash.js86
-rw-r--r--app/assets/javascripts/frequent_items/constants.js2
-rw-r--r--app/assets/javascripts/frequent_items/utils.js5
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js2
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/mutations/todo_mark_done.mutation.graphql2
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json4
-rw-r--r--app/assets/javascripts/group.js37
-rw-r--r--app/assets/javascripts/group_settings/components/shared_runners_form.vue128
-rw-r--r--app/assets/javascripts/group_settings/constants.js3
-rw-r--r--app/assets/javascripts/group_settings/mount_shared_runners.js20
-rw-r--r--app/assets/javascripts/header_search/components/app.vue4
-rw-r--r--app/assets/javascripts/header_search/index.js16
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue4
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue2
-rw-r--r--app/assets/javascripts/import_entities/components/import_status.vue5
-rw-r--r--app/assets/javascripts/import_entities/constants.js1
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue23
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue1
-rw-r--r--app/assets/javascripts/import_entities/import_groups/constants.js2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/index.js2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/utils.js2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue16
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue9
-rw-r--r--app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql1
-rw-r--r--app/assets/javascripts/integrations/constants.js10
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue26
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue104
-rw-r--r--app/assets/javascripts/integrations/edit/index.js2
-rw-r--r--app/assets/javascripts/invite_members/components/import_a_project_modal.vue2
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue26
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue171
-rw-r--r--app/assets/javascripts/invite_members/components/members_token_select.vue4
-rw-r--r--app/assets/javascripts/invite_members/components/user_limit_notification.vue75
-rw-r--r--app/assets/javascripts/invite_members/constants.js32
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js9
-rw-r--r--app/assets/javascripts/issuable/components/issuable_header_warnings.vue27
-rw-r--r--app/assets/javascripts/issuable/components/related_issuable_item.vue2
-rw-r--r--app/assets/javascripts/issuable/components/status_box.vue91
-rw-r--r--app/assets/javascripts/issuable/issuable_form.js3
-rw-r--r--app/assets/javascripts/issues/create_merge_request_dropdown.js9
-rw-r--r--app/assets/javascripts/issues/issue.js4
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue109
-rw-r--r--app/assets/javascripts/issues/list/constants.js29
-rw-r--r--app/assets/javascripts/issues/list/index.js18
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues.query.graphql7
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql14
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues_counts_without_crm.query.graphql136
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues_without_crm.query.graphql94
-rw-r--r--app/assets/javascripts/issues/list/queries/issue.fragment.graphql4
-rw-r--r--app/assets/javascripts/issues/list/utils.js25
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue90
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue156
-rw-r--r--app/assets/javascripts/issues/show/index.js2
-rw-r--r--app/assets/javascripts/issues/show/utils.js99
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/api.js10
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue52
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/app.vue48
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue21
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue10
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/constants.js17
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/index.js7
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in.vue65
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com.vue68
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue72
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue88
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_page.vue35
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions.vue43
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions_page.vue54
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/store/actions.js73
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/store/index.js11
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/store/mutation_types.js12
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/store/mutations.js40
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/store/state.js20
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_form.vue5
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue4
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue1
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_job_details_container.vue27
-rw-r--r--app/assets/javascripts/jobs/components/stuck_block.vue4
-rw-r--r--app/assets/javascripts/jobs/components/table/cells/actions_cell.vue13
-rw-r--r--app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql4
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue4
-rw-r--r--app/assets/javascripts/jobs/store/getters.js12
-rw-r--r--app/assets/javascripts/lib/dompurify.js1
-rw-r--r--app/assets/javascripts/lib/gfm/index.js4
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js58
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue20
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js4
-rw-r--r--app/assets/javascripts/lib/utils/cookies.js8
-rw-r--r--app/assets/javascripts/lib/utils/datetime/timeago_utility.js2
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js14
-rw-r--r--app/assets/javascripts/lib/utils/http_status.js1
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js17
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js4
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js13
-rw-r--r--app/assets/javascripts/lib/utils/users_cache.js11
-rw-r--r--app/assets/javascripts/main.js10
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue2
-rw-r--r--app/assets/javascripts/merge_request.js31
-rw-r--r--app/assets/javascripts/merge_request_tabs.js47
-rw-r--r--app/assets/javascripts/milestones/components/milestone_combobox.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue5
-rw-r--r--app/assets/javascripts/mr_notes/index.js8
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue22
-rw-r--r--app/assets/javascripts/notes/components/comment_type_dropdown.vue48
-rw-r--r--app/assets/javascripts/notes/components/diff_discussion_header.vue5
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue113
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue5
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue9
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue25
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue20
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue20
-rw-r--r--app/assets/javascripts/notes/i18n.js14
-rw-r--r--app/assets/javascripts/notes/stores/actions.js17
-rw-r--r--app/assets/javascripts/notes/stores/getters.js33
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue8
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue64
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue17
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue10
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js3
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql2
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_metadata.query.graphql2
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql2
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/utils.js8
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue90
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifest_row.vue24
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js1
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql1
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/index.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue5
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue82
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue8
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue40
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/bundle.js1
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue15
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue3
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/constants.js5
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue76
-rw-r--r--app/assets/javascripts/pages/admin/background_migrations/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/clusters/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/clusters/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/new/components/app.vue4
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js15
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue16
-rw-r--r--app/assets/javascripts/pages/projects/clusters/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/clusters/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue41
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js28
-rw-r--r--app/assets/javascripts/pages/projects/project.js20
-rw-r--r--app/assets/javascripts/pages/projects/serverless/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/services/edit/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js55
-rw-r--r--app/assets/javascripts/pages/projects/wikis/edit/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/wikis/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/wikis/show/index.js2
-rw-r--r--app/assets/javascripts/pages/registrations/new/index.js5
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js1
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue26
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue13
-rw-r--r--app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue4
-rw-r--r--app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue48
-rw-r--r--app/assets/javascripts/pipeline_editor/components/file_tree/container.vue78
-rw-r--r--app/assets/javascripts/pipeline_editor/components/file_tree/file_item.vue45
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue22
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue2
-rw-r--r--app/assets/javascripts/pipeline_editor/components/popovers/file_tree_popover.vue59
-rw-r--r--app/assets/javascripts/pipeline_editor/components/popovers/walkthrough_popover.vue (renamed from app/assets/javascripts/pipeline_editor/components/walkthrough_popover.vue)2
-rw-r--r--app/assets/javascripts/pipeline_editor/constants.js4
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql6
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js4
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue3
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue67
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue2
-rw-r--r--app/assets/javascripts/pipeline_new/index.js2
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/constants.js4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue60
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue22
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue151
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue10
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue17
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue73
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue111
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/utils.js33
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_tabs.vue40
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue24
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue37
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue220
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue14
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue29
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue10
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue16
-rw-r--r--app/assets/javascripts/pipelines/constants.js47
-rw-r--r--app/assets/javascripts/pipelines/graphql/mutations/retry_failed_job.mutation.graphql12
-rw-r--r--app/assets/javascripts/pipelines/graphql/provider.js9
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql41
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js14
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_dag.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_failed_jobs.js36
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_header.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_notification.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_tabs.js27
-rw-r--r--app/assets/javascripts/pipelines/pipelines_index.js8
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/actions.js6
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/constants.js1
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/getters.js4
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js1
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutations.js14
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/state.js1
-rw-r--r--app/assets/javascripts/pipelines/utils.js19
-rw-r--r--app/assets/javascripts/project_select_combo_button.js3
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue14
-rw-r--r--app/assets/javascripts/projects/project_new.js5
-rw-r--r--app/assets/javascripts/projects/settings/init_access_dropdown.js2
-rw-r--r--app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue2
-rw-r--r--app/assets/javascripts/projects/settings/topics/queries/project_topics_search.query.graphql1
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue24
-rw-r--r--app/assets/javascripts/prometheus_alerts/components/reset_key.vue149
-rw-r--r--app/assets/javascripts/prometheus_alerts/index.js28
-rw-r--r--app/assets/javascripts/related_issues/components/add_issuable_form.vue5
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_root.vue3
-rw-r--r--app/assets/javascripts/releases/components/app_edit_new.vue38
-rw-r--r--app/assets/javascripts/releases/components/tag_field_new.vue15
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/actions.js22
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/getters.js22
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js6
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/mutations.js18
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/state.js6
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/getters.js14
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue19
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue5
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue29
-rw-r--r--app/assets/javascripts/repository/constants.js21
-rw-r--r--app/assets/javascripts/repository/index.js8
-rw-r--r--app/assets/javascripts/rest_api.js1
-rw-r--r--app/assets/javascripts/right_sidebar.js27
-rw-r--r--app/assets/javascripts/runner/admin_runner_edit/admin_runner_edit_app.vue21
-rw-r--r--app/assets/javascripts/runner/admin_runner_edit/index.js3
-rw-r--r--app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue20
-rw-r--r--app/assets/javascripts/runner/admin_runner_show/index.js6
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue104
-rw-r--r--app/assets/javascripts/runner/admin_runners/index.js3
-rw-r--r--app/assets/javascripts/runner/components/registration/registration_dropdown.vue2
-rw-r--r--app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue8
-rw-r--r--app/assets/javascripts/runner/components/runner_delete_button.vue2
-rw-r--r--app/assets/javascripts/runner/components/runner_details.vue3
-rw-r--r--app/assets/javascripts/runner/components/runner_jobs.vue2
-rw-r--r--app/assets/javascripts/runner/components/runner_pagination.vue4
-rw-r--r--app/assets/javascripts/runner/components/runner_projects.vue2
-rw-r--r--app/assets/javascripts/runner/components/runner_update_form.vue210
-rw-r--r--app/assets/javascripts/runner/graphql/details/runner.query.graphql9
-rw-r--r--app/assets/javascripts/runner/graphql/details/runner_details.fragment.graphql5
-rw-r--r--app/assets/javascripts/runner/graphql/details/runner_details_shared.fragment.graphql35
-rw-r--r--app/assets/javascripts/runner/graphql/details/runner_update.mutation.graphql15
-rw-r--r--app/assets/javascripts/runner/graphql/edit/runner_fields.fragment.graphql5
-rw-r--r--app/assets/javascripts/runner/graphql/edit/runner_fields_shared.fragment.graphql15
-rw-r--r--app/assets/javascripts/runner/graphql/edit/runner_form.query.graphql7
-rw-r--r--app/assets/javascripts/runner/graphql/edit/runner_update.mutation.graphql13
-rw-r--r--app/assets/javascripts/runner/graphql/show/runner.query.graphql41
-rw-r--r--app/assets/javascripts/runner/graphql/show/runner_jobs.query.graphql (renamed from app/assets/javascripts/runner/graphql/details/runner_jobs.query.graphql)0
-rw-r--r--app/assets/javascripts/runner/graphql/show/runner_projects.query.graphql (renamed from app/assets/javascripts/runner/graphql/details/runner_projects.query.graphql)0
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue111
-rw-r--r--app/assets/javascripts/runner/local_storage_alert/constants.js1
-rw-r--r--app/assets/javascripts/runner/local_storage_alert/save_alert_to_local_storage.js8
-rw-r--r--app/assets/javascripts/runner/local_storage_alert/show_alert_from_local_storage.js18
-rw-r--r--app/assets/javascripts/runner/runner_search_utils.js6
-rw-r--r--app/assets/javascripts/runner/utils.js3
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue30
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js8
-rw-r--r--app/assets/javascripts/security_configuration/components/feature_card.vue6
-rw-r--r--app/assets/javascripts/security_configuration/components/section_layout.vue23
-rw-r--r--app/assets/javascripts/security_configuration/graphql/current_license.query.graphql6
-rw-r--r--app/assets/javascripts/security_configuration/graphql/security_training_vulnerability.query.graphql1
-rw-r--r--app/assets/javascripts/security_configuration/index.js1
-rw-r--r--app/assets/javascripts/serverless/components/area.vue145
-rw-r--r--app/assets/javascripts/serverless/components/empty_state.vue39
-rw-r--r--app/assets/javascripts/serverless/components/environment_row.vue65
-rw-r--r--app/assets/javascripts/serverless/components/function_details.vue94
-rw-r--r--app/assets/javascripts/serverless/components/function_row.vue77
-rw-r--r--app/assets/javascripts/serverless/components/functions.vue139
-rw-r--r--app/assets/javascripts/serverless/components/missing_prometheus.vue57
-rw-r--r--app/assets/javascripts/serverless/components/pod_box.vue36
-rw-r--r--app/assets/javascripts/serverless/components/url.vue28
-rw-r--r--app/assets/javascripts/serverless/constants.js10
-rw-r--r--app/assets/javascripts/serverless/event_hub.js3
-rw-r--r--app/assets/javascripts/serverless/serverless_bundle.js67
-rw-r--r--app/assets/javascripts/serverless/store/actions.js131
-rw-r--r--app/assets/javascripts/serverless/store/getters.js7
-rw-r--r--app/assets/javascripts/serverless/store/index.js18
-rw-r--r--app/assets/javascripts/serverless/store/mutation_types.js11
-rw-r--r--app/assets/javascripts/serverless/store/mutations.js49
-rw-r--r--app/assets/javascripts/serverless/store/state.js22
-rw-r--r--app/assets/javascripts/serverless/utils.js20
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue20
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue32
-rw-r--r--app/assets/javascripts/sidebar/components/attention_requested_toggle.vue33
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_content.vue31
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue16
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_formatted_date.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue55
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue15
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_editable_item.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue18
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/report.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue8
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js10
-rw-r--r--app/assets/javascripts/sidebar/queries/remove_attention_request.mutation.graphql7
-rw-r--r--app/assets/javascripts/sidebar/queries/request_attention.mutation.graphql5
-rw-r--r--app/assets/javascripts/sidebar/queries/reviewer_rereview.mutation.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql7
-rw-r--r--app/assets/javascripts/sidebar/queries/update_merge_request_milestone.mutation.graphql2
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js17
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js33
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js35
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue57
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue38
-rw-r--r--app/assets/javascripts/snippets/mutations/delete_snippet.mutation.graphql2
-rw-r--r--app/assets/javascripts/sortable/constants.js4
-rw-r--r--app/assets/javascripts/sortable/utils.js10
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_area.vue4
-rw-r--r--app/assets/javascripts/tracking/dispatch_snowplow_event.js9
-rw-r--r--app/assets/javascripts/tracking/tracker.js267
-rw-r--r--app/assets/javascripts/tracking/tracking.js275
-rw-r--r--app/assets/javascripts/user_popovers.js16
-rw-r--r--app/assets/javascripts/users_select/index.js18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue43
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue77
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue27
-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/states/mr_widget_auto_merge_enabled.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue57
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js30
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js84
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js45
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js6
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue33
-rw-r--r--app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/confidentiality_badge.vue39
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/deployment_instance.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue49
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue52
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/index.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue54
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue31
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue26
-rw-r--r--app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/metric_images/metric_images_table.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/navigation_tabs.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_list.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/list_item.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/registry_search.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue23
-rw-r--r--app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue68
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue90
-rw-r--r--app/assets/javascripts/vue_shared/components/user_select/user_select.vue2
-rw-r--r--app/assets/javascripts/vue_shared/constants.js14
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue19
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue15
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue6
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue42
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue8
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue2
-rw-r--r--app/assets/javascripts/vue_shared/mixins/timeago.js4
-rw-r--r--app/assets/javascripts/vue_shared/security_configuration/components/section_layout.vue34
-rw-r--r--app/assets/javascripts/vue_shared/security_configuration/components/section_loader.vue35
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/utils.js2
-rw-r--r--app/assets/javascripts/whats_new/components/feature.vue8
-rw-r--r--app/assets/javascripts/work_items/components/item_state.vue62
-rw-r--r--app/assets/javascripts/work_items/components/work_item_actions.vue48
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue53
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail_modal.vue103
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state.vue98
-rw-r--r--app/assets/javascripts/work_items/components/work_item_title.vue11
-rw-r--r--app/assets/javascripts/work_items/constants.js8
-rw-r--r--app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql9
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql6
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.query.graphql2
-rw-r--r--app/assets/javascripts/work_items/index.js3
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue23
-rw-r--r--app/assets/javascripts/work_items/pages/work_item_root.vue46
-rw-r--r--app/assets/javascripts/work_items/router/index.js2
-rw-r--r--app/assets/javascripts/work_items_hierarchy/components/app.vue2
-rw-r--r--app/assets/stylesheets/bootstrap_migration_components.scss11
-rw-r--r--app/assets/stylesheets/components/content_editor.scss9
-rw-r--r--app/assets/stylesheets/components/dashboard_skeleton.scss3
-rw-r--r--app/assets/stylesheets/components/design_management/design.scss2
-rw-r--r--app/assets/stylesheets/components/whats_new.scss6
-rw-r--r--app/assets/stylesheets/errors.scss8
-rw-r--r--app/assets/stylesheets/framework/broadcast_messages.scss7
-rw-r--r--app/assets/stylesheets/framework/buttons.scss10
-rw-r--r--app/assets/stylesheets/framework/diffs.scss40
-rw-r--r--app/assets/stylesheets/framework/files.scss13
-rw-r--r--app/assets/stylesheets/framework/filters.scss2
-rw-r--r--app/assets/stylesheets/framework/forms.scss1
-rw-r--r--app/assets/stylesheets/framework/header.scss46
-rw-r--r--app/assets/stylesheets/framework/icons.scss72
-rw-r--r--app/assets/stylesheets/framework/logo.scss101
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss6
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss60
-rw-r--r--app/assets/stylesheets/framework/tables.scss24
-rw-r--r--app/assets/stylesheets/framework/variables.scss4
-rw-r--r--app/assets/stylesheets/framework/wells.scss2
-rw-r--r--app/assets/stylesheets/highlight/common.scss22
-rw-r--r--app/assets/stylesheets/highlight/themes/dark.scss12
-rw-r--r--app/assets/stylesheets/highlight/themes/monokai.scss13
-rw-r--r--app/assets/stylesheets/highlight/themes/none.scss9
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-dark.scss13
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-light.scss13
-rw-r--r--app/assets/stylesheets/highlight/white_base.scss28
-rw-r--r--app/assets/stylesheets/mailer.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/_pipeline_mixins.scss92
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss37
-rw-r--r--app/assets/stylesheets/page_bundles/dashboard_projects.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/issues_show.scss67
-rw-r--r--app/assets/stylesheets/page_bundles/jira_connect.scss11
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss62
-rw-r--r--app/assets/stylesheets/page_bundles/milestone.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline_editor.scss22
-rw-r--r--app/assets/stylesheets/page_bundles/pipelines.scss65
-rw-r--r--app/assets/stylesheets/page_bundles/terms.scss8
-rw-r--r--app/assets/stylesheets/pages/commits.scss5
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss23
-rw-r--r--app/assets/stylesheets/pages/issuable.scss107
-rw-r--r--app/assets/stylesheets/pages/issues.scss29
-rw-r--r--app/assets/stylesheets/pages/login.scss1
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss6
-rw-r--r--app/assets/stylesheets/pages/notes.scss29
-rw-r--r--app/assets/stylesheets/pages/projects.scss25
-rw-r--r--app/assets/stylesheets/pages/search.scss46
-rw-r--r--app/assets/stylesheets/pages/tree.scss6
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss112
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss105
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss34
-rw-r--r--app/assets/stylesheets/themes/_dark.scss2
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss7
-rw-r--r--app/assets/stylesheets/themes/theme_helper.scss1
-rw-r--r--app/assets/stylesheets/themes/theme_light.scss5
-rw-r--r--app/assets/stylesheets/utilities.scss27
-rw-r--r--app/components/diffs/overflow_warning_component.html.haml6
-rw-r--r--app/components/pajamas/alert_component.html.haml12
-rw-r--r--app/components/pajamas/alert_component.rb15
-rw-r--r--app/controllers/admin/application_settings_controller.rb16
-rw-r--r--app/controllers/admin/applications_controller.rb5
-rw-r--r--app/controllers/admin/background_migrations_controller.rb22
-rw-r--r--app/controllers/admin/batched_jobs_controller.rb28
-rw-r--r--app/controllers/admin/deploy_keys_controller.rb1
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/admin/jobs_controller.rb1
-rw-r--r--app/controllers/admin/labels_controller.rb1
-rw-r--r--app/controllers/admin/requests_profiles_controller.rb21
-rw-r--r--app/controllers/admin/runner_projects_controller.rb1
-rw-r--r--app/controllers/admin/runners_controller.rb19
-rw-r--r--app/controllers/admin/sessions_controller.rb2
-rw-r--r--app/controllers/admin/topics_controller.rb3
-rw-r--r--app/controllers/admin/users_controller.rb2
-rw-r--r--app/controllers/application_controller.rb7
-rw-r--r--app/controllers/autocomplete_controller.rb5
-rw-r--r--app/controllers/boards/issues_controller.rb1
-rw-r--r--app/controllers/boards/lists_controller.rb1
-rw-r--r--app/controllers/clusters/base_controller.rb4
-rw-r--r--app/controllers/clusters/clusters_controller.rb67
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb4
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor_for_admin_mode.rb2
-rw-r--r--app/controllers/concerns/dependency_proxy/group_access.rb4
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/concerns/notes_actions.rb2
-rw-r--r--app/controllers/concerns/oauth_applications.rb2
-rw-r--r--app/controllers/concerns/product_analytics_tracking.rb2
-rw-r--r--app/controllers/concerns/send_file_upload.rb2
-rw-r--r--app/controllers/concerns/uploads_actions.rb14
-rw-r--r--app/controllers/concerns/wiki_actions.rb4
-rw-r--r--app/controllers/dashboard/groups_controller.rb2
-rw-r--r--app/controllers/dashboard/labels_controller.rb1
-rw-r--r--app/controllers/dashboard/milestones_controller.rb1
-rw-r--r--app/controllers/dashboard/projects_controller.rb1
-rw-r--r--app/controllers/dashboard/todos_controller.rb11
-rw-r--r--app/controllers/dashboard_controller.rb3
-rw-r--r--app/controllers/explore/groups_controller.rb1
-rw-r--r--app/controllers/explore/projects_controller.rb4
-rw-r--r--app/controllers/google_api/authorizations_controller.rb1
-rw-r--r--app/controllers/graphql_controller.rb5
-rw-r--r--app/controllers/groups/autocomplete_sources_controller.rb1
-rw-r--r--app/controllers/groups/boards_controller.rb5
-rw-r--r--app/controllers/groups/crm/contacts_controller.rb1
-rw-r--r--app/controllers/groups/crm/organizations_controller.rb1
-rw-r--r--app/controllers/groups/dependency_proxies_controller.rb4
-rw-r--r--app/controllers/groups/dependency_proxy/application_controller.rb6
-rw-r--r--app/controllers/groups/dependency_proxy_auth_controller.rb1
-rw-r--r--app/controllers/groups/dependency_proxy_for_containers_controller.rb1
-rw-r--r--app/controllers/groups/deploy_tokens_controller.rb1
-rw-r--r--app/controllers/groups/group_members_controller.rb1
-rw-r--r--app/controllers/groups/imports_controller.rb1
-rw-r--r--app/controllers/groups/labels_controller.rb1
-rw-r--r--app/controllers/groups/milestones_controller.rb1
-rw-r--r--app/controllers/groups/packages_controller.rb1
-rw-r--r--app/controllers/groups/registry/repositories_controller.rb3
-rw-r--r--app/controllers/groups/releases_controller.rb19
-rw-r--r--app/controllers/groups/runners_controller.rb32
-rw-r--r--app/controllers/groups/settings/applications_controller.rb6
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb22
-rw-r--r--app/controllers/groups/settings/packages_and_registries_controller.rb1
-rw-r--r--app/controllers/groups/settings/repository_controller.rb1
-rw-r--r--app/controllers/groups/shared_projects_controller.rb1
-rw-r--r--app/controllers/groups/uploads_controller.rb1
-rw-r--r--app/controllers/groups_controller.rb11
-rw-r--r--app/controllers/ide_controller.rb2
-rw-r--r--app/controllers/import/available_namespaces_controller.rb1
-rw-r--r--app/controllers/import/base_controller.rb1
-rw-r--r--app/controllers/import/bitbucket_controller.rb7
-rw-r--r--app/controllers/import/bitbucket_server_controller.rb7
-rw-r--r--app/controllers/import/bulk_imports_controller.rb3
-rw-r--r--app/controllers/import/fogbugz_controller.rb4
-rw-r--r--app/controllers/import/gitea_controller.rb6
-rw-r--r--app/controllers/import/gitlab_controller.rb3
-rw-r--r--app/controllers/import/gitlab_groups_controller.rb1
-rw-r--r--app/controllers/import/history_controller.rb1
-rw-r--r--app/controllers/import/manifest_controller.rb7
-rw-r--r--app/controllers/import/url_controller.rb1
-rw-r--r--app/controllers/jira_connect/application_controller.rb40
-rw-r--r--app/controllers/jira_connect/events_controller.rb2
-rw-r--r--app/controllers/jira_connect/subscriptions_controller.rb3
-rw-r--r--app/controllers/oauth/applications_controller.rb6
-rw-r--r--app/controllers/oauth/authorizations_controller.rb17
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb2
-rw-r--r--app/controllers/profiles/accounts_controller.rb1
-rw-r--r--app/controllers/profiles/emails_controller.rb1
-rw-r--r--app/controllers/profiles/gpg_keys_controller.rb2
-rw-r--r--app/controllers/profiles/keys_controller.rb1
-rw-r--r--app/controllers/profiles/notifications_controller.rb1
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb2
-rw-r--r--app/controllers/profiles/preferences_controller.rb3
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb8
-rw-r--r--app/controllers/profiles_controller.rb3
-rw-r--r--app/controllers/projects/alert_management_controller.rb1
-rw-r--r--app/controllers/projects/alerting/notifications_controller.rb3
-rw-r--r--app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb1
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb3
-rw-r--r--app/controllers/projects/avatars_controller.rb2
-rw-r--r--app/controllers/projects/blame_controller.rb7
-rw-r--r--app/controllers/projects/blob_controller.rb4
-rw-r--r--app/controllers/projects/boards_controller.rb5
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/builds_controller.rb3
-rw-r--r--app/controllers/projects/ci/pipeline_editor_controller.rb3
-rw-r--r--app/controllers/projects/ci/secure_files_controller.rb1
-rw-r--r--app/controllers/projects/cluster_agents_controller.rb1
-rw-r--r--app/controllers/projects/clusters_controller.rb2
-rw-r--r--app/controllers/projects/cycle_analytics/events_controller.rb1
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb1
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb1
-rw-r--r--app/controllers/projects/deploy_tokens_controller.rb1
-rw-r--r--app/controllers/projects/deployments_controller.rb1
-rw-r--r--app/controllers/projects/discussions_controller.rb1
-rw-r--r--app/controllers/projects/environments_controller.rb10
-rw-r--r--app/controllers/projects/feature_flags_clients_controller.rb1
-rw-r--r--app/controllers/projects/feature_flags_controller.rb1
-rw-r--r--app/controllers/projects/feature_flags_user_lists_controller.rb1
-rw-r--r--app/controllers/projects/graphs_controller.rb1
-rw-r--r--app/controllers/projects/imports_controller.rb1
-rw-r--r--app/controllers/projects/incident_management/pager_duty_incidents_controller.rb1
-rw-r--r--app/controllers/projects/incidents_controller.rb5
-rw-r--r--app/controllers/projects/issue_links_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb24
-rw-r--r--app/controllers/projects/jobs_controller.rb11
-rw-r--r--app/controllers/projects/labels_controller.rb1
-rw-r--r--app/controllers/projects/learn_gitlab_controller.rb1
-rw-r--r--app/controllers/projects/logs_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests_controller.rb33
-rw-r--r--app/controllers/projects/metrics_dashboard_controller.rb1
-rw-r--r--app/controllers/projects/milestones_controller.rb1
-rw-r--r--app/controllers/projects/notes_controller.rb1
-rw-r--r--app/controllers/projects/packages/infrastructure_registry_controller.rb1
-rw-r--r--app/controllers/projects/packages/packages_controller.rb1
-rw-r--r--app/controllers/projects/pipeline_schedules_controller.rb1
-rw-r--r--app/controllers/projects/pipelines/application_controller.rb1
-rw-r--r--app/controllers/projects/pipelines/tests_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_controller.rb54
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb1
-rw-r--r--app/controllers/projects/product_analytics_controller.rb2
-rw-r--r--app/controllers/projects/project_members_controller.rb1
-rw-r--r--app/controllers/projects/prometheus/alerts_controller.rb66
-rw-r--r--app/controllers/projects/prometheus/metrics_controller.rb4
-rw-r--r--app/controllers/projects/registry/application_controller.rb1
-rw-r--r--app/controllers/projects/releases/evidences_controller.rb1
-rw-r--r--app/controllers/projects/releases_controller.rb9
-rw-r--r--app/controllers/projects/runner_projects_controller.rb1
-rw-r--r--app/controllers/projects/runners_controller.rb1
-rw-r--r--app/controllers/projects/security/configuration_controller.rb2
-rw-r--r--app/controllers/projects/serverless/functions_controller.rb79
-rw-r--r--app/controllers/projects/service_ping_controller.rb3
-rw-r--r--app/controllers/projects/services_controller.rb14
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb3
-rw-r--r--app/controllers/projects/settings/operations_controller.rb9
-rw-r--r--app/controllers/projects/settings/packages_and_registries_controller.rb1
-rw-r--r--app/controllers/projects/settings/repository_controller.rb2
-rw-r--r--app/controllers/projects/starrers_controller.rb2
-rw-r--r--app/controllers/projects/tags/releases_controller.rb1
-rw-r--r--app/controllers/projects/terraform_controller.rb1
-rw-r--r--app/controllers/projects/todos_controller.rb1
-rw-r--r--app/controllers/projects/tracings_controller.rb1
-rw-r--r--app/controllers/projects/tree_controller.rb6
-rw-r--r--app/controllers/projects/triggers_controller.rb1
-rw-r--r--app/controllers/projects/uploads_controller.rb4
-rw-r--r--app/controllers/projects/work_items_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb26
-rw-r--r--app/controllers/pwa_controller.rb12
-rw-r--r--app/controllers/registrations/welcome_controller.rb2
-rw-r--r--app/controllers/repositories/lfs_storage_controller.rb14
-rw-r--r--app/controllers/runner_setup_controller.rb1
-rw-r--r--app/controllers/search_controller.rb15
-rw-r--r--app/controllers/sent_notifications_controller.rb1
-rw-r--r--app/controllers/sessions_controller.rb9
-rw-r--r--app/controllers/users_controller.rb4
-rw-r--r--app/experiments/build_ios_app_guide_email_experiment.rb6
-rw-r--r--app/finders/alert_management/alerts_finder.rb2
-rw-r--r--app/finders/error_tracking/errors_finder.rb46
-rw-r--r--app/finders/group_descendants_finder.rb2
-rw-r--r--app/finders/group_members_finder.rb4
-rw-r--r--app/finders/groups/projects_requiring_authorizations_refresh/base.rb35
-rw-r--r--app/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder.rb36
-rw-r--r--app/finders/groups/projects_requiring_authorizations_refresh/on_transfer_finder.rb17
-rw-r--r--app/finders/groups_finder.rb4
-rw-r--r--app/finders/incident_management/timeline_events_finder.rb37
-rw-r--r--app/finders/issuable_finder.rb4
-rw-r--r--app/finders/issues_finder.rb7
-rw-r--r--app/finders/issues_finder/params.rb4
-rw-r--r--app/finders/packages/build_infos_finder.rb46
-rw-r--r--app/finders/packages/build_infos_for_many_packages_finder.rb92
-rw-r--r--app/finders/personal_access_tokens_finder.rb2
-rw-r--r--app/finders/projects/serverless/functions_finder.rb153
-rw-r--r--app/finders/releases_finder.rb14
-rw-r--r--app/finders/tags_finder.rb4
-rw-r--r--app/finders/user_recent_events_finder.rb7
-rw-r--r--app/graphql/mutations/award_emojis/base.rb6
-rw-r--r--app/graphql/mutations/base_mutation.rb1
-rw-r--r--app/graphql/mutations/boards/update.rb3
-rw-r--r--app/graphql/mutations/ci/ci_cd_settings_update.rb37
-rw-r--r--app/graphql/mutations/ci/job/base.rb3
-rw-r--r--app/graphql/mutations/ci/pipeline/base.rb3
-rw-r--r--app/graphql/mutations/ci/project_ci_cd_settings_update.rb43
-rw-r--r--app/graphql/mutations/ci/runner/delete.rb4
-rw-r--r--app/graphql/mutations/ci/runner/update.rb4
-rw-r--r--app/graphql/mutations/ci/runners_registration_token/reset.rb32
-rw-r--r--app/graphql/mutations/clusters/agent_tokens/create.rb3
-rw-r--r--app/graphql/mutations/clusters/agent_tokens/delete.rb35
-rw-r--r--app/graphql/mutations/clusters/agent_tokens/revoke.rb3
-rw-r--r--app/graphql/mutations/clusters/agents/delete.rb3
-rw-r--r--app/graphql/mutations/container_expiration_policies/update.rb2
-rw-r--r--app/graphql/mutations/container_repositories/destroy_base.rb3
-rw-r--r--app/graphql/mutations/customer_relations/contacts/base.rb25
-rw-r--r--app/graphql/mutations/customer_relations/contacts/create.rb16
-rw-r--r--app/graphql/mutations/customer_relations/contacts/update.rb16
-rw-r--r--app/graphql/mutations/customer_relations/organizations/update.rb4
-rw-r--r--app/graphql/mutations/design_management/move.rb3
-rw-r--r--app/graphql/mutations/discussions/toggle_resolve.rb3
-rw-r--r--app/graphql/mutations/environments/canary_ingress/update.rb9
-rw-r--r--app/graphql/mutations/incident_management/timeline_event/base.rb29
-rw-r--r--app/graphql/mutations/incident_management/timeline_event/create.rb37
-rw-r--r--app/graphql/mutations/incident_management/timeline_event/destroy.rb24
-rw-r--r--app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb46
-rw-r--r--app/graphql/mutations/incident_management/timeline_event/update.rb33
-rw-r--r--app/graphql/mutations/issues/set_crm_contacts.rb2
-rw-r--r--app/graphql/mutations/merge_requests/remove_attention_request.rb40
-rw-r--r--app/graphql/mutations/merge_requests/request_attention.rb40
-rw-r--r--app/graphql/mutations/merge_requests/set_labels.rb3
-rw-r--r--app/graphql/mutations/merge_requests/toggle_attention_requested.rb2
-rw-r--r--app/graphql/mutations/metrics/dashboard/annotations/create.rb11
-rw-r--r--app/graphql/mutations/namespace/package_settings/update.rb2
-rw-r--r--app/graphql/mutations/notes/base.rb3
-rw-r--r--app/graphql/mutations/notes/create/base.rb3
-rw-r--r--app/graphql/mutations/notes/create/note.rb7
-rw-r--r--app/graphql/mutations/notes/reposition_image_diff_note.rb3
-rw-r--r--app/graphql/mutations/notes/update/image_diff_note.rb2
-rw-r--r--app/graphql/mutations/packages/destroy.rb3
-rw-r--r--app/graphql/mutations/packages/destroy_file.rb3
-rw-r--r--app/graphql/mutations/release_asset_links/delete.rb4
-rw-r--r--app/graphql/mutations/release_asset_links/update.rb4
-rw-r--r--app/graphql/mutations/saved_replies/base.rb6
-rw-r--r--app/graphql/mutations/timelogs/delete.rb33
-rw-r--r--app/graphql/mutations/todos/base.rb3
-rw-r--r--app/graphql/mutations/todos/create.rb3
-rw-r--r--app/graphql/mutations/todos/mark_all_done.rb2
-rw-r--r--app/graphql/mutations/todos/restore_many.rb3
-rw-r--r--app/graphql/mutations/user_preferences/update.rb2
-rw-r--r--app/graphql/mutations/work_items/create.rb3
-rw-r--r--app/graphql/mutations/work_items/create_from_task.rb2
-rw-r--r--app/graphql/mutations/work_items/delete.rb2
-rw-r--r--app/graphql/mutations/work_items/delete_task.rb69
-rw-r--r--app/graphql/mutations/work_items/update.rb2
-rw-r--r--app/graphql/queries/burndown_chart/burnup.iteration.query.graphql40
-rw-r--r--app/graphql/queries/burndown_chart/burnup.milestone.query.graphql36
-rw-r--r--app/graphql/queries/burndown_chart/burnup.query.graphql75
-rw-r--r--app/graphql/queries/design_management/get_design_list.query.graphql2
-rw-r--r--app/graphql/queries/pipelines/get_pipeline_details.query.graphql6
-rw-r--r--app/graphql/queries/snippet/snippet_blob_content.query.graphql2
-rw-r--r--app/graphql/resolvers/alert_management/http_integrations_resolver.rb3
-rw-r--r--app/graphql/resolvers/base_issues_resolver.rb2
-rw-r--r--app/graphql/resolvers/base_resolver.rb1
-rw-r--r--app/graphql/resolvers/boards_resolver.rb3
-rw-r--r--app/graphql/resolvers/ci/config_resolver.rb30
-rw-r--r--app/graphql/resolvers/ci/runner_status_resolver.rb9
-rw-r--r--app/graphql/resolvers/concerns/issue_resolver_arguments.rb6
-rw-r--r--app/graphql/resolvers/concerns/resolves_ids.rb5
-rw-r--r--app/graphql/resolvers/concerns/resolves_snippets.rb2
-rw-r--r--app/graphql/resolvers/concerns/time_frame_arguments.rb9
-rw-r--r--app/graphql/resolvers/design_management/design_at_version_resolver.rb3
-rw-r--r--app/graphql/resolvers/design_management/design_resolver.rb4
-rw-r--r--app/graphql/resolvers/design_management/designs_resolver.rb27
-rw-r--r--app/graphql/resolvers/design_management/version/design_at_version_resolver.rb5
-rw-r--r--app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb3
-rw-r--r--app/graphql/resolvers/design_management/version_in_collection_resolver.rb4
-rw-r--r--app/graphql/resolvers/design_management/version_resolver.rb4
-rw-r--r--app/graphql/resolvers/design_management/versions_resolver.rb29
-rw-r--r--app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb4
-rw-r--r--app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb4
-rw-r--r--app/graphql/resolvers/group_members_resolver.rb4
-rw-r--r--app/graphql/resolvers/group_packages_resolver.rb5
-rw-r--r--app/graphql/resolvers/incident_management/timeline_events_resolver.rb32
-rw-r--r--app/graphql/resolvers/package_details_resolver.rb3
-rw-r--r--app/graphql/resolvers/package_pipelines_resolver.rb54
-rw-r--r--app/graphql/resolvers/project_packages_resolver.rb5
-rw-r--r--app/graphql/resolvers/project_pipeline_resolver.rb5
-rw-r--r--app/graphql/resolvers/snippets_resolver.rb6
-rw-r--r--app/graphql/resolvers/timelog_resolver.rb6
-rw-r--r--app/graphql/resolvers/user_merge_requests_resolver_base.rb3
-rw-r--r--app/graphql/resolvers/work_item_resolver.rb3
-rw-r--r--app/graphql/subscriptions/issuable_updated.rb4
-rw-r--r--app/graphql/types/alert_management/domain_filter_enum.rb6
-rw-r--r--app/graphql/types/base_field.rb2
-rw-r--r--app/graphql/types/boards/board_issue_input_type.rb1
-rw-r--r--app/graphql/types/ci/config/config_type.rb2
-rw-r--r--app/graphql/types/ci/config/include_type.rb53
-rw-r--r--app/graphql/types/ci/config/include_type_enum.rb17
-rw-r--r--app/graphql/types/ci/pipeline_type.rb1
-rw-r--r--app/graphql/types/ci/runner_status_enum.rb14
-rw-r--r--app/graphql/types/ci/runner_type.rb13
-rw-r--r--app/graphql/types/ci/runner_upgrade_status_type_enum.rb14
-rw-r--r--app/graphql/types/color_type.rb25
-rw-r--r--app/graphql/types/concerns/gitlab_style_deprecations.rb4
-rw-r--r--app/graphql/types/container_expiration_policy_type.rb2
-rw-r--r--app/graphql/types/container_repository_type.rb1
-rw-r--r--app/graphql/types/current_user_todos.rb19
-rw-r--r--app/graphql/types/customer_relations/contact_type.rb5
-rw-r--r--app/graphql/types/customer_relations/organization_type.rb5
-rw-r--r--app/graphql/types/dependency_proxy/group_setting_type.rb2
-rw-r--r--app/graphql/types/dependency_proxy/image_ttl_group_policy_type.rb2
-rw-r--r--app/graphql/types/deprecated_mutations.rb3
-rw-r--r--app/graphql/types/design_management/design_fields.rb30
-rw-r--r--app/graphql/types/design_management/design_type.rb5
-rw-r--r--app/graphql/types/global_id_type.rb16
-rw-r--r--app/graphql/types/incident_management/timeline_event_type.rb72
-rw-r--r--app/graphql/types/issue_type.rb2
-rw-r--r--app/graphql/types/merge_request_type.rb8
-rw-r--r--app/graphql/types/mutation_type.rb17
-rw-r--r--app/graphql/types/namespace/package_settings_type.rb2
-rw-r--r--app/graphql/types/notes/update_diff_image_position_input_type.rb2
-rw-r--r--app/graphql/types/packages/package_base_type.rb60
-rw-r--r--app/graphql/types/packages/package_details_type.rb11
-rw-r--r--app/graphql/types/packages/package_type.rb64
-rw-r--r--app/graphql/types/permission_types/timelog.rb11
-rw-r--r--app/graphql/types/permission_types/work_item.rb12
-rw-r--r--app/graphql/types/project_statistics_type.rb2
-rw-r--r--app/graphql/types/project_type.rb13
-rw-r--r--app/graphql/types/projects/topic_type.rb4
-rw-r--r--app/graphql/types/query_type.rb23
-rw-r--r--app/graphql/types/range_input_type.rb2
-rw-r--r--app/graphql/types/root_storage_statistics_type.rb1
-rw-r--r--app/graphql/types/timelog_type.rb7
-rw-r--r--app/graphql/types/work_item_type.rb2
-rw-r--r--app/graphql/types/work_items/convert_task_input_type.rb4
-rw-r--r--app/graphql/types/work_items/deleted_task_input_type.rb19
-rw-r--r--app/helpers/appearances_helper.rb18
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/application_settings_helper.rb34
-rw-r--r--app/helpers/auth_helper.rb4
-rw-r--r--app/helpers/badges_helper.rb2
-rw-r--r--app/helpers/boards_helper.rb1
-rw-r--r--app/helpers/broadcast_messages_helper.rb2
-rw-r--r--app/helpers/ci/builds_helper.rb10
-rw-r--r--app/helpers/ci/pipeline_editor_helper.rb1
-rw-r--r--app/helpers/ci/pipelines_helper.rb4
-rw-r--r--app/helpers/ci/runners_helper.rb12
-rw-r--r--app/helpers/ci/secure_files_helper.rb10
-rw-r--r--app/helpers/clusters_helper.rb14
-rw-r--r--app/helpers/container_registry_helper.rb4
-rw-r--r--app/helpers/emails_helper.rb4
-rw-r--r--app/helpers/groups/group_members_helper.rb2
-rw-r--r--app/helpers/instance_configuration_helper.rb4
-rw-r--r--app/helpers/integrations_helper.rb1
-rw-r--r--app/helpers/invite_members_helper.rb25
-rw-r--r--app/helpers/issuables_helper.rb23
-rw-r--r--app/helpers/issues_helper.rb14
-rw-r--r--app/helpers/jira_connect_helper.rb3
-rw-r--r--app/helpers/lazy_image_tag_helper.rb26
-rw-r--r--app/helpers/learn_gitlab_helper.rb3
-rw-r--r--app/helpers/merge_requests_helper.rb43
-rw-r--r--app/helpers/namespaces_helper.rb3
-rw-r--r--app/helpers/nav_helper.rb8
-rw-r--r--app/helpers/page_layout_helper.rb2
-rw-r--r--app/helpers/personal_access_tokens_helper.rb7
-rw-r--r--app/helpers/profiles_helper.rb4
-rw-r--r--app/helpers/projects/pipeline_helper.rb1
-rw-r--r--app/helpers/projects_helper.rb5
-rw-r--r--app/helpers/releases_helper.rb9
-rw-r--r--app/helpers/routing/projects_helper.rb16
-rw-r--r--app/helpers/routing/pseudonymization_helper.rb2
-rw-r--r--app/helpers/search_helper.rb6
-rw-r--r--app/helpers/sorting_helper.rb45
-rw-r--r--app/helpers/storage_helper.rb5
-rw-r--r--app/helpers/system_note_helper.rb3
-rw-r--r--app/helpers/todos_helper.rb6
-rw-r--r--app/helpers/tracking_helper.rb9
-rw-r--r--app/helpers/users/group_callouts_helper.rb2
-rw-r--r--app/helpers/users_helper.rb2
-rw-r--r--app/helpers/workhorse_helper.rb2
-rw-r--r--app/mailers/emails/in_product_marketing.rb6
-rw-r--r--app/mailers/emails/merge_requests.rb14
-rw-r--r--app/mailers/emails/projects.rb14
-rw-r--r--app/mailers/previews/notify_preview.rb4
-rw-r--r--app/models/alert_management/alert.rb9
-rw-r--r--app/models/alert_management/metric_image.rb4
-rw-r--r--app/models/analytics/cycle_analytics/aggregation.rb8
-rw-r--r--app/models/application_setting.rb18
-rw-r--r--app/models/application_setting_implementation.rb3
-rw-r--r--app/models/broadcast_message.rb2
-rw-r--r--app/models/ci/bridge.rb17
-rw-r--r--app/models/ci/build.rb21
-rw-r--r--app/models/ci/build_metadata.rb2
-rw-r--r--app/models/ci/job_artifact.rb9
-rw-r--r--app/models/ci/namespace_settings.rb19
-rw-r--r--app/models/ci/pending_build.rb2
-rw-r--r--app/models/ci/pipeline.rb34
-rw-r--r--app/models/ci/processable.rb15
-rw-r--r--app/models/ci/runner.rb19
-rw-r--r--app/models/ci/secure_file.rb7
-rw-r--r--app/models/clusters/applications/prometheus.rb1
-rw-r--r--app/models/clusters/cluster.rb1
-rw-r--r--app/models/clusters/instance.rb6
-rw-r--r--app/models/clusters/platforms/kubernetes.rb46
-rw-r--r--app/models/concerns/bulk_member_access_load.rb5
-rw-r--r--app/models/concerns/ci/has_deployment_name.rb2
-rw-r--r--app/models/concerns/ci/has_status.rb19
-rw-r--r--app/models/concerns/cross_database_modification.rb2
-rw-r--r--app/models/concerns/deployment_platform.rb2
-rw-r--r--app/models/concerns/integrations/loggable.rb37
-rw-r--r--app/models/concerns/integrations/reset_secret_fields.rb41
-rw-r--r--app/models/concerns/integrations/slack_mattermost_notifier.rb12
-rw-r--r--app/models/concerns/issuable.rb4
-rw-r--r--app/models/concerns/limitable.rb4
-rw-r--r--app/models/concerns/merge_request_reviewer_state.rb2
-rw-r--r--app/models/concerns/packages/destructible.rb2
-rw-r--r--app/models/concerns/pg_full_text_searchable.rb14
-rw-r--r--app/models/concerns/project_services_loggable.rb28
-rw-r--r--app/models/concerns/routable.rb4
-rw-r--r--app/models/concerns/sha256_attribute.rb45
-rw-r--r--app/models/concerns/sha_attribute.rb64
-rw-r--r--app/models/container_registry/event.rb25
-rw-r--r--app/models/container_repository.rb25
-rw-r--r--app/models/deploy_token.rb10
-rw-r--r--app/models/deployment.rb9
-rw-r--r--app/models/design_management/action.rb1
-rw-r--r--app/models/environment.rb12
-rw-r--r--app/models/event.rb6
-rw-r--r--app/models/event_collection.rb47
-rw-r--r--app/models/group.rb4
-rw-r--r--app/models/group_group_link.rb2
-rw-r--r--app/models/incident_management/timeline_event.rb25
-rw-r--r--app/models/instance_configuration.rb19
-rw-r--r--app/models/integration.rb9
-rw-r--r--app/models/integrations/bamboo.rb54
-rw-r--r--app/models/integrations/base_chat_notification.rb4
-rw-r--r--app/models/integrations/base_ci.rb10
-rw-r--r--app/models/integrations/buildkite.rb31
-rw-r--r--app/models/integrations/drone_ci.rb32
-rw-r--r--app/models/integrations/field.rb1
-rw-r--r--app/models/integrations/jenkins.rb53
-rw-r--r--app/models/integrations/jira.rb41
-rw-r--r--app/models/integrations/mock_ci.rb18
-rw-r--r--app/models/integrations/packagist.rb3
-rw-r--r--app/models/integrations/prometheus.rb9
-rw-r--r--app/models/integrations/teamcity.rb46
-rw-r--r--app/models/issue.rb7
-rw-r--r--app/models/key.rb15
-rw-r--r--app/models/label.rb5
-rw-r--r--app/models/loose_foreign_keys/deleted_record.rb4
-rw-r--r--app/models/member.rb7
-rw-r--r--app/models/members_preloader.rb2
-rw-r--r--app/models/merge_request.rb30
-rw-r--r--app/models/merge_request_assignee.rb6
-rw-r--r--app/models/merge_request_reviewer.rb6
-rw-r--r--app/models/namespace.rb26
-rw-r--r--app/models/namespace_ci_cd_setting.rb9
-rw-r--r--app/models/namespaces/traversal/linear.rb12
-rw-r--r--app/models/namespaces/traversal/linear_scopes.rb14
-rw-r--r--app/models/packages/build_info.rb4
-rw-r--r--app/models/packages/cleanup.rb8
-rw-r--r--app/models/packages/cleanup/policy.rb32
-rw-r--r--app/models/pages_domain.rb5
-rw-r--r--app/models/personal_access_token.rb4
-rw-r--r--app/models/preloaders/group_root_ancestor_preloader.rb2
-rw-r--r--app/models/preloaders/user_max_access_level_in_groups_preloader.rb2
-rw-r--r--app/models/preloaders/user_max_access_level_in_projects_preloader.rb51
-rw-r--r--app/models/project.rb43
-rw-r--r--app/models/project_ci_cd_setting.rb3
-rw-r--r--app/models/project_import_state.rb4
-rw-r--r--app/models/project_pages_metadatum.rb2
-rw-r--r--app/models/project_setting.rb13
-rw-r--r--app/models/project_statistics.rb8
-rw-r--r--app/models/project_team.rb4
-rw-r--r--app/models/projects/topic.rb7
-rw-r--r--app/models/protected_branch.rb4
-rw-r--r--app/models/raw_usage_data.rb7
-rw-r--r--app/models/system_note_metadata.rb2
-rw-r--r--app/models/user.rb20
-rw-r--r--app/models/user_custom_attribute.rb33
-rw-r--r--app/models/users/callout.rb4
-rw-r--r--app/models/users/in_product_marketing_email.rb71
-rw-r--r--app/models/wiki.rb126
-rw-r--r--app/models/work_items/type.rb4
-rw-r--r--app/policies/group_policy.rb34
-rw-r--r--app/policies/incident_management/timeline_event_policy.rb7
-rw-r--r--app/policies/issuable_policy.rb8
-rw-r--r--app/policies/issue_policy.rb11
-rw-r--r--app/policies/namespace_ci_cd_setting_policy.rb5
-rw-r--r--app/policies/namespaces/user_namespace_policy.rb3
-rw-r--r--app/policies/project_policy.rb4
-rw-r--r--app/policies/timelog_policy.rb7
-rw-r--r--app/policies/work_item_policy.rb4
-rw-r--r--app/presenters/clusterable_presenter.rb16
-rw-r--r--app/presenters/clusters/cluster_presenter.rb2
-rw-r--r--app/presenters/dev_ops_report/metric_presenter.rb2
-rw-r--r--app/presenters/instance_clusterable_presenter.rb20
-rw-r--r--app/presenters/issue_presenter.rb16
-rw-r--r--app/presenters/project_presenter.rb3
-rw-r--r--app/presenters/projects/security/configuration_presenter.rb2
-rw-r--r--app/presenters/user_presenter.rb2
-rw-r--r--app/serializers/award_emoji_entity.rb1
-rw-r--r--app/serializers/build_details_entity.rb8
-rw-r--r--app/serializers/ci/job_entity.rb1
-rw-r--r--app/serializers/cluster_entity.rb10
-rw-r--r--app/serializers/environment_entity.rb3
-rw-r--r--app/serializers/environment_serializer.rb5
-rw-r--r--app/serializers/issue_board_entity.rb2
-rw-r--r--app/serializers/issue_entity.rb2
-rw-r--r--app/serializers/issue_sidebar_basic_entity.rb2
-rw-r--r--app/serializers/linked_issue_entity.rb2
-rw-r--r--app/serializers/merge_request_user_entity.rb2
-rw-r--r--app/serializers/merge_request_widget_entity.rb2
-rw-r--r--app/serializers/release_entity.rb9
-rw-r--r--app/services/alert_management/alerts/update_service.rb20
-rw-r--r--app/services/alert_management/metric_images/upload_service.rb2
-rw-r--r--app/services/authorized_project_update/project_access_changed_service.rb2
-rw-r--r--app/services/authorized_project_update/project_create_service.rb34
-rw-r--r--app/services/authorized_project_update/project_group_link_create_service.rb72
-rw-r--r--app/services/bulk_imports/file_decompression_service.rb2
-rw-r--r--app/services/ci/create_downstream_pipeline_service.rb3
-rw-r--r--app/services/ci/expire_pipeline_cache_service.rb2
-rw-r--r--app/services/ci/generate_kubeconfig_service.rb11
-rw-r--r--app/services/ci/job_artifacts/create_service.rb2
-rw-r--r--app/services/ci/job_artifacts/destroy_batch_service.rb17
-rw-r--r--app/services/ci/pipeline_creation/start_pipeline_service.rb5
-rw-r--r--app/services/ci/pipeline_trigger_service.rb2
-rw-r--r--app/services/ci/queue/build_queue_service.rb2
-rw-r--r--app/services/ci/queue/builds_table_strategy.rb2
-rw-r--r--app/services/ci/queue/pending_builds_strategy.rb4
-rw-r--r--app/services/ci/register_job_service.rb16
-rw-r--r--app/services/ci/retry_job_service.rb29
-rw-r--r--app/services/ci/runners/register_runner_service.rb2
-rw-r--r--app/services/ci/runners/reset_registration_token_service.rb2
-rw-r--r--app/services/ci/stuck_builds/drop_running_service.rb2
-rw-r--r--app/services/ci/update_build_state_service.rb4
-rw-r--r--app/services/clusters/kubernetes.rb2
-rw-r--r--app/services/clusters/kubernetes/create_or_update_service_account_service.rb32
-rw-r--r--app/services/concerns/alert_management/alert_processing.rb2
-rw-r--r--app/services/concerns/ci/downstream_pipeline_helpers.rb24
-rw-r--r--app/services/concerns/group_linkable.rb38
-rw-r--r--app/services/container_expiration_policies/cleanup_service.rb3
-rw-r--r--app/services/container_expiration_policies/update_service.rb2
-rw-r--r--app/services/container_expiration_policy_service.rb18
-rw-r--r--app/services/customer_relations/contacts/base_service.rb17
-rw-r--r--app/services/customer_relations/contacts/create_service.rb18
-rw-r--r--app/services/customer_relations/contacts/update_service.rb10
-rw-r--r--app/services/customer_relations/organizations/update_service.rb9
-rw-r--r--app/services/database/consistency_fix_service.rb48
-rw-r--r--app/services/deployments/update_environment_service.rb2
-rw-r--r--app/services/environments/stop_service.rb4
-rw-r--r--app/services/error_tracking/base_service.rb10
-rw-r--r--app/services/error_tracking/collect_error_service.rb26
-rw-r--r--app/services/error_tracking/issue_details_service.rb11
-rw-r--r--app/services/error_tracking/issue_latest_event_service.rb13
-rw-r--r--app/services/error_tracking/issue_update_service.rb6
-rw-r--r--app/services/error_tracking/list_issues_service.rb16
-rw-r--r--app/services/event_create_service.rb2
-rw-r--r--app/services/git/base_hooks_service.rb2
-rw-r--r--app/services/groups/group_links/create_service.rb51
-rw-r--r--app/services/groups/import_export/export_service.rb2
-rw-r--r--app/services/groups/import_export/import_service.rb2
-rw-r--r--app/services/groups/open_issues_count_service.rb27
-rw-r--r--app/services/groups/transfer_service.rb13
-rw-r--r--app/services/import/bitbucket_server_service.rb2
-rw-r--r--app/services/import/github_service.rb2
-rw-r--r--app/services/import/gitlab_projects/file_acquisition_strategies/remote_file.rb2
-rw-r--r--app/services/incident_management/timeline_events/base_service.rb27
-rw-r--r--app/services/incident_management/timeline_events/create_service.rb51
-rw-r--r--app/services/incident_management/timeline_events/destroy_service.rb38
-rw-r--r--app/services/incident_management/timeline_events/update_service.rb61
-rw-r--r--app/services/issuable/destroy_service.rb10
-rw-r--r--app/services/jira/requests/base.rb16
-rw-r--r--app/services/jira_connect/sync_service.rb2
-rw-r--r--app/services/jira_import/start_import_service.rb2
-rw-r--r--app/services/loose_foreign_keys/batch_cleaner_service.rb2
-rw-r--r--app/services/members/create_service.rb6
-rw-r--r--app/services/members/creator_service.rb8
-rw-r--r--app/services/members/projects/creator_service.rb15
-rw-r--r--app/services/merge_requests/approval_service.rb2
-rw-r--r--app/services/merge_requests/base_service.rb53
-rw-r--r--app/services/merge_requests/build_service.rb8
-rw-r--r--app/services/merge_requests/create_service.rb4
-rw-r--r--app/services/merge_requests/handle_assignees_change_service.rb2
-rw-r--r--app/services/merge_requests/mergeability/run_checks_service.rb2
-rw-r--r--app/services/merge_requests/post_merge_service.rb14
-rw-r--r--app/services/merge_requests/push_options_handler_service.rb5
-rw-r--r--app/services/merge_requests/remove_approval_service.rb1
-rw-r--r--app/services/merge_requests/remove_attention_requested_service.rb18
-rw-r--r--app/services/merge_requests/request_attention_service.rb60
-rw-r--r--app/services/merge_requests/update_assignees_service.rb2
-rw-r--r--app/services/merge_requests/update_service.rb4
-rw-r--r--app/services/namespaces/in_product_marketing_emails_service.rb4
-rw-r--r--app/services/namespaces/package_settings/update_service.rb2
-rw-r--r--app/services/notes/create_service.rb18
-rw-r--r--app/services/notification_service.rb30
-rw-r--r--app/services/projects/android_target_platform_detector_service.rb35
-rw-r--r--app/services/projects/blame_service.rb65
-rw-r--r--app/services/projects/branches_by_mode_service.rb2
-rw-r--r--app/services/projects/container_repository/cleanup_tags_service.rb5
-rw-r--r--app/services/projects/container_repository/delete_tags_service.rb18
-rw-r--r--app/services/projects/container_repository/gitlab/delete_tags_service.rb7
-rw-r--r--app/services/projects/create_service.rb33
-rw-r--r--app/services/projects/group_links/create_service.rb35
-rw-r--r--app/services/projects/in_product_marketing_campaign_emails_service.rb57
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_service.rb2
-rw-r--r--app/services/projects/open_issues_count_service.rb93
-rw-r--r--app/services/projects/overwrite_project_service.rb2
-rw-r--r--app/services/projects/participants_service.rb10
-rw-r--r--app/services/projects/prometheus/alerts/alert_params.rb17
-rw-r--r--app/services/projects/prometheus/alerts/create_service.rb15
-rw-r--r--app/services/projects/prometheus/alerts/destroy_service.rb13
-rw-r--r--app/services/projects/prometheus/alerts/update_service.rb15
-rw-r--r--app/services/projects/prometheus/metrics/base_service.rb30
-rw-r--r--app/services/projects/prometheus/metrics/destroy_service.rb1
-rw-r--r--app/services/projects/prometheus/metrics/update_service.rb29
-rw-r--r--app/services/projects/record_target_platforms_service.rb34
-rw-r--r--app/services/projects/update_pages_service.rb5
-rw-r--r--app/services/projects/update_remote_mirror_service.rb2
-rw-r--r--app/services/prometheus/create_default_alerts_service.rb105
-rw-r--r--app/services/quick_actions/interpret_service.rb17
-rw-r--r--app/services/service_ping/build_payload_service.rb27
-rw-r--r--app/services/service_ping/devops_report_service.rb26
-rw-r--r--app/services/service_ping/permit_data_categories_service.rb24
-rw-r--r--app/services/service_ping/service_ping_settings.rb17
-rw-r--r--app/services/service_ping/submit_service.rb49
-rw-r--r--app/services/system_note_service.rb31
-rw-r--r--app/services/system_notes/incidents_service.rb40
-rw-r--r--app/services/system_notes/time_tracking_service.rb12
-rw-r--r--app/services/timelogs/base_service.rb15
-rw-r--r--app/services/timelogs/delete_service.rb26
-rw-r--r--app/services/users/destroy_service.rb5
-rw-r--r--app/services/users/in_product_marketing_email_records.rb (renamed from app/services/namespaces/in_product_marketing_email_records.rb)5
-rw-r--r--app/services/users/validate_manual_otp_service.rb (renamed from app/services/users/validate_otp_service.rb)6
-rw-r--r--app/services/users/validate_push_otp_service.rb25
-rw-r--r--app/services/work_items/delete_task_service.rb45
-rw-r--r--app/services/work_items/task_list_reference_removal_service.rb63
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml6
-rw-r--r--app/views/admin/application_settings/_issue_limits.html.haml4
-rw-r--r--app/views/admin/application_settings/_note_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_pipeline_limits.html.haml9
-rw-r--r--app/views/admin/application_settings/_prometheus.html.haml21
-rw-r--r--app/views/admin/application_settings/_registry.html.haml1
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml14
-rw-r--r--app/views/admin/application_settings/_runner_registrars_form.html.haml18
-rw-r--r--app/views/admin/application_settings/_signin.html.haml61
-rw-r--r--app/views/admin/application_settings/_usage.html.haml4
-rw-r--r--app/views/admin/application_settings/_users_api_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml19
-rw-r--r--app/views/admin/application_settings/ci/_header.html.haml2
-rw-r--r--app/views/admin/application_settings/ci_cd.html.haml4
-rw-r--r--app/views/admin/application_settings/general.html.haml4
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml4
-rw-r--r--app/views/admin/application_settings/network.html.haml12
-rw-r--r--app/views/admin/application_settings/service_usage_data.html.haml4
-rw-r--r--app/views/admin/applications/_form.html.haml16
-rw-r--r--app/views/admin/background_migrations/_job.html.haml10
-rw-r--r--app/views/admin/background_migrations/_migration.html.haml10
-rw-r--r--app/views/admin/background_migrations/_migration_full_information.html.haml21
-rw-r--r--app/views/admin/background_migrations/index.html.haml29
-rw-r--r--app/views/admin/background_migrations/show.html.haml39
-rw-r--r--app/views/admin/batched_jobs/_job.html.haml17
-rw-r--r--app/views/admin/batched_jobs/_transition_log.html.haml13
-rw-r--r--app/views/admin/batched_jobs/show.html.haml36
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml2
-rw-r--r--app/views/admin/broadcast_messages/index.html.haml2
-rw-r--r--app/views/admin/dashboard/_security_newsletter_callout.html.haml6
-rw-r--r--app/views/admin/groups/_form.html.haml4
-rw-r--r--app/views/admin/groups/show.html.haml2
-rw-r--r--app/views/admin/projects/show.html.haml4
-rw-r--r--app/views/admin/requests_profiles/index.html.haml22
-rw-r--r--app/views/admin/runners/edit.html.haml130
-rw-r--r--app/views/admin/runners/show.html.haml2
-rw-r--r--app/views/admin/sessions/_new_base.html.haml2
-rw-r--r--app/views/admin/sessions/_signin_box.html.haml2
-rw-r--r--app/views/admin/sessions/_two_factor_otp.html.haml2
-rw-r--r--app/views/admin/sessions/new.html.haml2
-rw-r--r--app/views/admin/sessions/two_factor.html.haml4
-rw-r--r--app/views/admin/topics/_form.html.haml11
-rw-r--r--app/views/admin/topics/_topic.html.haml5
-rw-r--r--app/views/admin/users/_users.html.haml4
-rw-r--r--app/views/ci/runner/_how_to_setup_runner.html.haml4
-rw-r--r--app/views/ci/runner/_setup_runner_in_aws.html.haml2
-rw-r--r--app/views/ci/variables/_content.html.haml2
-rw-r--r--app/views/ci/variables/_index.html.haml6
-rw-r--r--app/views/clusters/clusters/_banner.html.haml8
-rw-r--r--app/views/clusters/clusters/_deprecation_alert.html.haml4
-rw-r--r--app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml6
-rw-r--r--app/views/clusters/clusters/_integrations.html.haml23
-rw-r--r--app/views/clusters/clusters/_sidebar.html.haml3
-rw-r--r--app/views/clusters/clusters/aws/_new.html.haml17
-rw-r--r--app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml8
-rw-r--r--app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml5
-rw-r--r--app/views/clusters/clusters/connect.html.haml2
-rw-r--r--app/views/clusters/clusters/gcp/_form.html.haml87
-rw-r--r--app/views/clusters/clusters/gcp/_gcp_not_configured.html.haml3
-rw-r--r--app/views/clusters/clusters/gcp/_header.html.haml14
-rw-r--r--app/views/clusters/clusters/gcp/_new.html.haml5
-rw-r--r--app/views/clusters/clusters/new.html.haml19
-rw-r--r--app/views/clusters/clusters/new_cluster_docs.html.haml5
-rw-r--r--app/views/clusters/clusters/show.html.haml8
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml6
-rw-r--r--app/views/dashboard/todos/index.html.haml2
-rw-r--r--app/views/devise/passwords/new.html.haml8
-rw-r--r--app/views/devise/sessions/_new_base.html.haml13
-rw-r--r--app/views/devise/sessions/_new_base_user_login_label.html.haml1
-rw-r--r--app/views/devise/sessions/_new_crowd.html.haml8
-rw-r--r--app/views/devise/sessions/_new_ldap.html.haml8
-rw-r--r--app/views/devise/sessions/two_factor.html.haml2
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml2
-rw-r--r--app/views/devise/shared/_tab_single.html.haml2
-rw-r--r--app/views/devise/shared/_tabs_normal.html.haml9
-rw-r--r--app/views/doorkeeper/authorizations/redirect.html.haml14
-rw-r--r--app/views/errors/request_conflict.html.haml18
-rw-r--r--app/views/events/event/_note.html.haml2
-rw-r--r--app/views/explore/projects/topic.html.haml12
-rw-r--r--app/views/groups/_home_panel.html.haml3
-rw-r--r--app/views/groups/_import_group_from_another_instance_panel.html.haml4
-rw-r--r--app/views/groups/_import_group_from_file_panel.html.haml4
-rw-r--r--app/views/groups/_invite_groups_modal.html.haml2
-rw-r--r--app/views/groups/dependency_proxies/show.html.haml2
-rw-r--r--app/views/groups/issues.html.haml2
-rw-r--r--app/views/groups/milestones/index.html.haml2
-rw-r--r--app/views/groups/new.html.haml2
-rw-r--r--app/views/groups/runners/_group_runners.html.haml29
-rw-r--r--app/views/groups/runners/_runner.html.haml80
-rw-r--r--app/views/groups/runners/_settings.html.haml133
-rw-r--r--app/views/groups/runners/_shared_runners.html.haml3
-rw-r--r--app/views/groups/runners/_sort_dropdown.html.haml3
-rw-r--r--app/views/groups/runners/edit.html.haml6
-rw-r--r--app/views/groups/runners/show.html.haml5
-rw-r--r--app/views/groups/settings/_export.html.haml8
-rw-r--r--app/views/groups/settings/_permissions.html.haml2
-rw-r--r--app/views/groups/settings/_remove_button.html.haml4
-rw-r--r--app/views/groups/settings/_transfer.html.haml4
-rw-r--r--app/views/groups/settings/packages_and_registries/show.html.haml2
-rw-r--r--app/views/help/instance_configuration.html.haml1
-rw-r--r--app/views/help/instance_configuration/_ci_cd_limits.html.haml52
-rw-r--r--app/views/help/instance_configuration/_size_limits.html.haml3
-rw-r--r--app/views/import/bulk_imports/status.html.haml1
-rw-r--r--app/views/import/shared/_errors.html.haml4
-rw-r--r--app/views/jira_connect/users/show.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml57
-rw-r--r--app/views/layouts/_header_search.html.haml2
-rw-r--r--app/views/layouts/_loading_hints.html.haml2
-rw-r--r--app/views/layouts/devise.html.haml4
-rw-r--r--app/views/layouts/header/_default.html.haml37
-rw-r--r--app/views/layouts/header/_logo_with_title.html.haml7
-rw-r--r--app/views/layouts/header/_new_dropdown.html.haml4
-rw-r--r--app/views/layouts/header/_registration_enabled_callout.html.haml6
-rw-r--r--app/views/layouts/header/_storage_enforcement_banner.html.haml4
-rw-r--r--app/views/layouts/in_product_marketing_mailer.html.haml2
-rw-r--r--app/views/layouts/mailer.html.haml2
-rw-r--r--app/views/layouts/nav/_breadcrumbs.html.haml5
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml4
-rw-r--r--app/views/layouts/terms.html.haml9
-rw-r--r--app/views/layouts/unknown_user_mailer.html.haml2
-rw-r--r--app/views/notify/approved_merge_request_email.html.haml157
-rw-r--r--app/views/notify/approved_merge_request_email.text.haml9
-rw-r--r--app/views/notify/build_ios_app_guide_email.html.haml13
-rw-r--r--app/views/notify/build_ios_app_guide_email.text.erb13
-rw-r--r--app/views/notify/inactive_project_deletion_warning_email.html.haml28
-rw-r--r--app/views/notify/inactive_project_deletion_warning_email.text.erb17
-rw-r--r--app/views/notify/merge_request_unmergeable_email.html.haml8
-rw-r--r--app/views/notify/merge_when_pipeline_succeeds_email.html.haml4
-rw-r--r--app/views/notify/merged_merge_request_email.html.haml14
-rw-r--r--app/views/notify/merged_merge_request_email.text.haml8
-rw-r--r--app/views/notify/unapproved_merge_request_email.html.haml156
-rw-r--r--app/views/notify/unapproved_merge_request_email.text.haml9
-rw-r--r--app/views/profiles/_email_settings.html.haml43
-rw-r--r--app/views/profiles/_name.html.haml10
-rw-r--r--app/views/profiles/accounts/show.html.haml8
-rw-r--r--app/views/profiles/notifications/show.html.haml4
-rw-r--r--app/views/profiles/preferences/show.html.haml7
-rw-r--r--app/views/profiles/show.html.haml120
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml6
-rw-r--r--app/views/projects/_deletion_failed.html.haml4
-rw-r--r--app/views/projects/_files.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml5
-rw-r--r--app/views/projects/_last_push.html.haml6
-rw-r--r--app/views/projects/_merge_request_merge_checks_settings.html.haml29
-rw-r--r--app/views/projects/_merge_request_merge_method_settings.html.haml2
-rw-r--r--app/views/projects/_merge_request_merge_options_settings.html.haml21
-rw-r--r--app/views/projects/_new_project_fields.html.haml4
-rw-r--r--app/views/projects/blame/show.html.haml3
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/views/projects/blob/_breadcrumb.html.haml2
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml5
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/branches/new.html.haml4
-rw-r--r--app/views/projects/buttons/_clone.html.haml14
-rw-r--r--app/views/projects/ci/pipeline_editor/show.html.haml1
-rw-r--r--app/views/projects/ci/secure_files/show.html.haml4
-rw-r--r--app/views/projects/commits/_commits.html.haml8
-rw-r--r--app/views/projects/default_branch/_show.html.haml14
-rw-r--r--app/views/projects/deployments/_deployment.html.haml1
-rw-r--r--app/views/projects/diffs/_diffs.html.haml3
-rw-r--r--app/views/projects/diffs/_text_file.html.haml2
-rw-r--r--app/views/projects/feature_flags/edit.html.haml2
-rw-r--r--app/views/projects/forks/error.html.haml8
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml5
-rw-r--r--app/views/projects/graphs/charts.html.haml2
-rw-r--r--app/views/projects/issues/_alert_moved_from_service_desk.html.haml4
-rw-r--r--app/views/projects/issues/_discussion.html.haml2
-rw-r--r--app/views/projects/issues/index.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml1
-rw-r--r--app/views/projects/jobs/index.html.haml2
-rw-r--r--app/views/projects/mattermosts/_no_teams.html.haml6
-rw-r--r--app/views/projects/mattermosts/new.html.haml26
-rw-r--r--app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml76
-rw-r--r--app/views/projects/merge_requests/_code_dropdown.html.haml39
-rw-r--r--app/views/projects/merge_requests/_mr_box.html.haml8
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml61
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml4
-rw-r--r--app/views/projects/merge_requests/invalid.html.haml4
-rw-r--r--app/views/projects/merge_requests/show.html.haml16
-rw-r--r--app/views/projects/milestones/index.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml4
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml4
-rw-r--r--app/views/projects/pages/_access.html.haml2
-rw-r--r--app/views/projects/pages/_destroy.haml2
-rw-r--r--app/views/projects/pages/_list.html.haml2
-rw-r--r--app/views/projects/pages_domains/_certificate.html.haml3
-rw-r--r--app/views/projects/pages_domains/_form.html.haml7
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml71
-rw-r--r--app/views/projects/pipelines/show.html.haml2
-rw-r--r--app/views/projects/runners/_group_runners.html.haml5
-rw-r--r--app/views/projects/serverless/functions/index.html.haml17
-rw-r--r--app/views/projects/serverless/functions/show.html.haml19
-rw-r--r--app/views/projects/services/_form.html.haml12
-rw-r--r--app/views/projects/services/prometheus/_external_alerts.html.haml8
-rw-r--r--app/views/projects/services/prometheus/_show.html.haml9
-rw-r--r--app/views/projects/services/prometheus/_top.html.haml9
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml22
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml16
-rw-r--r--app/views/projects/settings/operations/_prometheus.html.haml17
-rw-r--r--app/views/projects/settings/operations/show.html.haml7
-rw-r--r--app/views/projects/tree/_tree_header.html.haml6
-rw-r--r--app/views/projects/work_items/index.html.haml2
-rw-r--r--app/views/pwa/offline.html.haml31
-rw-r--r--app/views/registrations/welcome/show.html.haml4
-rw-r--r--app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml6
-rw-r--r--app/views/shared/_import_form.html.haml34
-rw-r--r--app/views/shared/_integration_settings.html.haml4
-rw-r--r--app/views/shared/_logo.svg17
-rw-r--r--app/views/shared/_logo_type.svg1
-rw-r--r--app/views/shared/_logo_ukraine.svg5
-rw-r--r--app/views/shared/_logo_with_black_text.svg12
-rw-r--r--app/views/shared/_logo_with_white_text.svg12
-rw-r--r--app/views/shared/_milestones_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/_new_project_item_select.html.haml7
-rw-r--r--app/views/shared/_no_password.html.haml6
-rw-r--r--app/views/shared/_no_ssh.html.haml6
-rw-r--r--app/views/shared/_outdated_browser.html.haml4
-rw-r--r--app/views/shared/_project_limit.html.haml6
-rw-r--r--app/views/shared/_registration_features_discovery_message.html.haml2
-rw-r--r--app/views/shared/_service_ping_consent.html.haml6
-rw-r--r--app/views/shared/_two_factor_auth_recovery_settings_check.html.haml6
-rw-r--r--app/views/shared/access_tokens/_table.html.haml15
-rw-r--r--app/views/shared/deploy_tokens/_form.html.haml30
-rw-r--r--app/views/shared/doorkeeper/applications/_form.html.haml6
-rw-r--r--app/views/shared/empty_states/_merge_requests.html.haml4
-rw-r--r--app/views/shared/errors/_gitaly_unavailable.html.haml4
-rw-r--r--app/views/shared/hook_logs/_content.html.haml4
-rw-r--r--app/views/shared/integrations/mattermost_slash_commands/_detailed_help.html.haml (renamed from app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml)14
-rw-r--r--app/views/shared/integrations/mattermost_slash_commands/_help.html.haml (renamed from app/views/projects/services/mattermost_slash_commands/_help.html.haml)4
-rw-r--r--app/views/shared/integrations/mattermost_slash_commands/_installation_info.html.haml (renamed from app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml)0
-rw-r--r--app/views/shared/integrations/prometheus/_custom_metrics.html.haml (renamed from app/views/projects/services/prometheus/_custom_metrics.html.haml)0
-rw-r--r--app/views/shared/integrations/prometheus/_help.html.haml (renamed from app/views/projects/services/prometheus/_help.html.haml)0
-rw-r--r--app/views/shared/integrations/prometheus/_metrics.html.haml (renamed from app/views/projects/services/prometheus/_metrics.html.haml)2
-rw-r--r--app/views/shared/integrations/prometheus/_show.html.haml7
-rw-r--r--app/views/shared/integrations/slack/_help.haml (renamed from app/views/projects/services/slack/_help.haml)0
-rw-r--r--app/views/shared/integrations/slack_slash_commands/_help.html.haml (renamed from app/views/projects/services/slack_slash_commands/_help.html.haml)0
-rw-r--r--app/views/shared/issuable/_assignees.html.haml2
-rw-r--r--app/views/shared/issuable/_form.html.haml4
-rw-r--r--app/views/shared/issuable/_reviewers.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml51
-rw-r--r--app/views/shared/issuable/_sort_dropdown.html.haml27
-rw-r--r--app/views/shared/issuable/_status_box.html.haml14
-rw-r--r--app/views/shared/issue_type/_details_header.html.haml19
-rw-r--r--app/views/shared/milestones/_milestone.html.haml10
-rw-r--r--app/views/shared/milestones/_milestone_complete_alert.html.haml6
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml6
-rw-r--r--app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml11
-rw-r--r--app/views/shared/notes/_hints.html.haml11
-rw-r--r--app/views/shared/projects/_topics.html.haml20
-rw-r--r--app/views/shared/runners/_form.html.haml18
-rw-r--r--app/views/shared/runners/_runner_type_alert.html.haml8
-rw-r--r--app/views/shared/snippets/_snippet.html.haml27
-rw-r--r--app/views/shared/topics/_topic.html.haml10
-rw-r--r--app/views/shared/web_hooks/_hook_errors.html.haml12
-rw-r--r--app/workers/all_queues.yml74
-rw-r--r--app/workers/authorized_project_update/project_create_worker.rb23
-rw-r--r--app/workers/authorized_project_update/project_group_link_create_worker.rb26
-rw-r--r--app/workers/bulk_imports/pipeline_worker.rb16
-rw-r--r--app/workers/ci/build_finished_worker.rb2
-rw-r--r--app/workers/cleanup_container_repository_worker.rb16
-rw-r--r--app/workers/concerns/git_garbage_collect_methods.rb20
-rw-r--r--app/workers/concerns/reenqueuer.rb2
-rw-r--r--app/workers/concerns/worker_attributes.rb4
-rw-r--r--app/workers/container_expiration_policies/cleanup_container_repository_worker.rb7
-rw-r--r--app/workers/container_expiration_policy_worker.rb56
-rw-r--r--app/workers/container_registry/migration/enqueuer_worker.rb91
-rw-r--r--app/workers/container_registry/migration/guard_worker.rb30
-rw-r--r--app/workers/database/batched_background_migration/ci_database_worker.rb2
-rw-r--r--app/workers/database/batched_background_migration_worker.rb2
-rw-r--r--app/workers/database/ci_namespace_mirrors_consistency_check_worker.rb12
-rw-r--r--app/workers/database/ci_project_mirrors_consistency_check_worker.rb12
-rw-r--r--app/workers/deployments/hooks_worker.rb3
-rw-r--r--app/workers/expire_build_instance_artifacts_worker.rb25
-rw-r--r--app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_notes_worker.rb2
-rw-r--r--app/workers/merge_request_cleanup_refs_worker.rb2
-rw-r--r--app/workers/merge_requests/close_issue_worker.rb52
-rw-r--r--app/workers/packages/cleanup_package_file_worker.rb2
-rw-r--r--app/workers/project_service_worker.rb13
-rw-r--r--app/workers/projects/after_import_worker.rb (renamed from app/services/projects/after_import_service.rb)15
-rw-r--r--app/workers/projects/git_garbage_collect_worker.rb8
-rw-r--r--app/workers/projects/inactive_projects_deletion_cron_worker.rb73
-rw-r--r--app/workers/projects/inactive_projects_deletion_notification_worker.rb31
-rw-r--r--app/workers/projects/record_target_platforms_worker.rb28
-rw-r--r--app/workers/prometheus/create_default_alerts_worker.rb14
-rw-r--r--app/workers/requests_profiles_worker.rb18
-rw-r--r--app/workers/schedule_merge_request_cleanup_refs_worker.rb2
-rw-r--r--app/workers/web_hooks/destroy_worker.rb2
-rw-r--r--app/workers/wikis/git_garbage_collect_worker.rb8
1669 files changed, 18131 insertions, 13156 deletions
diff --git a/app/assets/images/apple-touch-icon.png b/app/assets/images/apple-touch-icon.png
new file mode 100644
index 00000000000..90507f61099
--- /dev/null
+++ b/app/assets/images/apple-touch-icon.png
Binary files differ
diff --git a/app/assets/images/ext_snippet_icons/logo.svg b/app/assets/images/ext_snippet_icons/logo.svg
index 9cb3042213a..d76e9cb3e5c 100644
--- a/app/assets/images/ext_snippet_icons/logo.svg
+++ b/app/assets/images/ext_snippet_icons/logo.svg
@@ -1 +1,12 @@
-<svg width="100" height="32" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><path fill="#8C929D" d="M67.67 8.11h-2.06l.009 15.364h8.348v-1.9H67.68l-.01-13.465zM81.913 20.778a3.517 3.517 0 01-2.553 1.078c-1.57 0-2.203-.775-2.203-1.787 0-1.522 1.059-2.25 3.309-2.25.487.002.974.04 1.456.113v2.846h-.01zm-2.137-9.313a6.826 6.826 0 00-4.387 1.579l.728 1.267c.841-.492 1.872-.983 3.356-.983 1.693 0 2.44.87 2.44 2.326v.747a9.4 9.4 0 00-1.428-.114c-3.612 0-5.446 1.267-5.446 3.914 0 2.374 1.456 3.565 3.659 3.565 1.484 0 2.912-.68 3.404-1.787l.378 1.503h1.456v-7.866c-.01-2.487-1.087-4.151-4.16-4.151zM90.587 21.926c-.776 0-1.456-.094-1.967-.33v-7.102c.7-.586 1.57-1.011 2.676-1.011 1.995 0 2.76 1.408 2.76 3.687 0 3.234-1.238 4.756-3.47 4.756m.87-10.457a3.775 3.775 0 00-2.836 1.257V10.74l-.01-2.629h-2.013l.01 14.987c1.01.425 2.391.652 3.895.652 3.848 0 5.701-2.458 5.701-6.704-.01-3.356-1.72-5.578-4.746-5.578M45.228 9.776c1.825 0 3.006.605 3.772 1.22l.889-1.541c-1.2-1.06-2.827-1.627-4.567-1.627-4.387 0-7.46 2.676-7.46 8.075 0 5.654 3.319 7.857 7.11 7.857a12.083 12.083 0 004.577-.888L49.5 16.83v-1.9h-5.63v1.9h3.594l.047 4.586c-.473.236-1.286.425-2.392.425-3.045 0-5.087-1.92-5.087-5.957-.01-4.113 2.1-6.108 5.19-6.108M59.744 8.107H57.73l.01 2.582v8.916c0 2.487 1.078 4.15 4.15 4.15.416.002.83-.036 1.24-.113v-1.806c-.31.047-.624.07-.937.066-1.692 0-2.44-.87-2.44-2.326v-6.145h3.376v-1.683h-3.373l-.009-3.64h-.003zM52.608 23.474h2.014V11.75h-2.014zM52.608 10.133h2.014V8.119h-2.014z"/><path d="M31.864 17.907l-1.788-5.496-3.538-10.9a.612.612 0 00-1.16 0L21.84 12.406H10.085L6.547 1.512a.612.612 0 00-1.16 0L1.855 12.405.066 17.907c-.162.5.015 1.05.44 1.36L15.963 30.5l15.456-11.233a1.22 1.22 0 00.446-1.36" fill="#FC6D26"/><path d="M15.966 30.49l5.875-18.086H10.09z" fill="#E24329"/><path d="M15.962 30.49l-5.877-18.086H1.859z" fill="#FC6D26"/><path d="M1.852 12.41L.063 17.906c-.162.5.015 1.05.441 1.36L15.959 30.5 1.852 12.41z" fill="#FCA326"/><path d="M1.854 12.41h8.237L6.546 1.517a.612.612 0 00-1.16 0L1.854 12.41z" fill="#E24329"/><path d="M15.966 30.49l5.875-18.086h8.236z" fill="#FC6D26"/><path d="M30.074 12.41l1.79 5.496a1.219 1.219 0 01-.44 1.36L15.966 30.49l14.107-18.08z" fill="#FCA326"/><path d="M30.079 12.41H21.84L25.38 1.517a.612.612 0 011.16 0l3.539 10.893z" fill="#E24329"/></g></svg> \ No newline at end of file
+<svg width="111" height="24" viewBox="0 0 111 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M44.814 9.042h3.645c-.608-3.875-3.963-6.574-8.33-6.574-5.166 0-9.043 3.798-9.043 10.16 0 6.248 3.703 10.123 9.15 10.123 4.887 0 8.386-3.144 8.386-8.234v-2.37h-8.01v2.794h4.55c-.058 2.816-1.938 4.599-4.908 4.599-3.305 0-5.57-2.477-5.57-6.95 0-4.445 2.303-6.913 5.494-6.913 2.38 0 4.01 1.272 4.636 3.365Zm6.218 13.438h3.49V7.68h-3.49v14.8Zm1.76-17.151c1.109 0 2.014-.85 2.014-1.89s-.905-1.9-2.014-1.9c-1.109 0-2.024.849-2.024 1.9s.9 1.89 2.017 1.89h.007ZM64.971 7.68H62.05V4.126h-3.49v3.556h-2.1v2.699h2.1v8.233c-.018 2.786 2.007 4.16 4.628 4.079a7.089 7.089 0 0 0 2.055-.348l-.59-2.73a4.247 4.247 0 0 1-1.02.137c-.878 0-1.582-.309-1.582-1.717v-7.662h2.921V7.68Zm2.701 14.8h12.272v-2.998H71.25V2.737h-3.578V22.48Zm18.957.3c2.323 0 3.71-1.09 4.347-2.333h.115v2.033h3.36v-9.91c0-3.913-3.19-5.09-6.016-5.09-3.113 0-5.504 1.388-6.275 4.087l3.26.464c.345-1.013 1.329-1.88 3.04-1.88 1.62 0 2.506.829 2.506 2.285v.057c0 1.002-1.05 1.051-3.664 1.33-2.872.309-5.619 1.166-5.619 4.502-.01 2.912 2.12 4.455 4.946 4.455Zm1.147-2.56c-1.456 0-2.498-.666-2.498-1.948 0-1.34 1.167-1.899 2.72-2.121.917-.125 2.75-.357 3.2-.722v1.744c.01 1.643-1.321 3.042-3.422 3.042v.005Zm9.244 2.26h3.433v-2.332h.201c.551 1.08 1.698 2.593 4.244 2.593 3.489 0 6.102-2.768 6.102-7.644 0-4.936-2.69-7.616-6.112-7.616-2.613 0-3.702 1.57-4.234 2.641h-.147V2.737h-3.486V22.48Zm3.423-7.403c0-2.88 1.234-4.734 3.48-4.734 2.323 0 3.52 1.976 3.52 4.734 0 2.759-1.214 4.8-3.52 4.8-2.227 0-3.48-1.928-3.48-4.8Z"
+ fill="#171321"/>
+ <path d="m24.507 9.5-.034-.09L21.082.562a.896.896 0 0 0-1.694.091l-2.29 7.01H7.825L5.535.653a.898.898 0 0 0-1.694-.09L.451 9.411.416 9.5a6.297 6.297 0 0 0 2.09 7.278l.012.01.03.022 5.16 3.867 2.56 1.935 1.554 1.176a1.051 1.051 0 0 0 1.268 0l1.555-1.176 2.56-1.935 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5Z"
+ fill="#E24329"/>
+ <path d="m24.507 9.5-.034-.09a11.44 11.44 0 0 0-4.56 2.051l-7.447 5.632 4.742 3.584 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5Z"
+ fill="#FC6D26"/>
+ <path d="m7.707 20.677 2.56 1.935 1.555 1.176a1.051 1.051 0 0 0 1.268 0l1.555-1.176 2.56-1.935-4.743-3.584-4.755 3.584Z"
+ fill="#FCA326"/>
+ <path d="M5.01 11.461a11.43 11.43 0 0 0-4.56-2.05L.416 9.5a6.297 6.297 0 0 0 2.09 7.278l.012.01.03.022 5.16 3.867 4.745-3.584-7.444-5.632Z"
+ fill="#FC6D26"/>
+</svg>
diff --git a/app/assets/images/favicon-blue.png b/app/assets/images/favicon-blue.png
index 2229fe79462..4829a48a0ea 100644
--- a/app/assets/images/favicon-blue.png
+++ b/app/assets/images/favicon-blue.png
Binary files differ
diff --git a/app/assets/images/favicon-yellow.png b/app/assets/images/favicon-yellow.png
index a80827808fc..fa0a23d54e5 100644
--- a/app/assets/images/favicon-yellow.png
+++ b/app/assets/images/favicon-yellow.png
Binary files differ
diff --git a/app/assets/images/favicon.png b/app/assets/images/favicon.png
index 845e0ec34a5..feeff619345 100644
--- a/app/assets/images/favicon.png
+++ b/app/assets/images/favicon.png
Binary files differ
diff --git a/app/assets/images/file_icons.svg b/app/assets/images/file_icons.svg
index def87dd9163..8b19f411c7b 100644
--- a/app/assets/images/file_icons.svg
+++ b/app/assets/images/file_icons.svg
@@ -1 +1 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 24 24" id="actionscript" xmlns="http://www.w3.org/2000/svg"><text style="line-height:113.99999857%" x="5.605" y="15.892" transform="scale(.91325 1.095)" font-weight="400" font-size="42.822" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#f44336"/><path style="line-height:125%" d="M4.744 2.031c-1.157 0-1.994.31-2.51.93-.515.612-.771 1.678-.771 3.197v2.467c0 1.408-.402 2.111-1.201 2.111v2.035c.8 0 1.2.679 1.2 2.036v2.654c0 1.512.26 2.562.78 3.152.52.59 1.355.885 2.502.885V19.43c-.447 0-.77-.151-.97-.453-.195-.303-.292-.815-.292-1.538v-2.267c0-1.807-.404-2.937-1.214-3.395v-.045c.81-.464 1.214-1.581 1.214-3.351V6.025c0-1.283.42-1.925 1.262-1.925V2.03zm14.66 0V4.1c.842 0 1.262.642 1.262 1.925v2.268c0 1.843.402 2.996 1.207 3.46v.046c-.805.442-1.207 1.544-1.207 3.306v2.356c0 .715-.099 1.22-.299 1.516-.2.302-.52.453-.963.453v2.068c1.152 0 1.984-.295 2.494-.885.516-.59.772-1.663.772-3.218V14.84c0-1.379.404-2.069 1.209-2.069v-2.035c-.805 0-1.21-.696-1.21-2.09V6.113c0-1.49-.255-2.54-.77-3.152-.516-.62-1.348-.93-2.495-.93zm-3.054 4.46c-.455 0-.886.057-1.293.173a3.056 3.056 0 0 0-1.078.527c-.308.241-.551.549-.731.924-.18.37-.27.817-.27 1.336 0 .663.165 1.227.493 1.695.33.468.831.864 1.502 1.188.263.125.509.249.736.37.227.12.422.244.586.374.168.13.299.271.394.424a.963.963 0 0 1 .145.521c0 .144-.03.28-.09.405a.9.9 0 0 1-.275.318c-.12.088-.272.158-.455.21a2.34 2.34 0 0 1-.635.075c-.415 0-.825-.083-1.233-.25a3.644 3.644 0 0 1-1.13-.763v2.222a3.68 3.68 0 0 0 1.101.418c.427.093.875.139 1.346.139.459 0 .894-.05 1.305-.152a3.002 3.002 0 0 0 1.09-.5c.31-.237.556-.543.736-.918.183-.38.275-.849.275-1.405 0-.403-.052-.755-.156-1.056a2.542 2.542 0 0 0-.45-.813 3.295 3.295 0 0 0-.704-.633 6.754 6.754 0 0 0-.922-.535 12.4 12.4 0 0 1-.676-.348c-.2-.115-.37-.231-.51-.347a1.502 1.502 0 0 1-.322-.375.91.91 0 0 1-.115-.453c0-.153.033-.288.101-.408a.948.948 0 0 1 .29-.32c.123-.089.275-.156.454-.202a2.18 2.18 0 0 1 .598-.078c.16 0 .326.015.502.043.18.028.36.07.539.13.18.056.354.13.522.218.171.088.329.188.472.304V6.871a4.039 4.039 0 0 0-.957-.285 6.448 6.448 0 0 0-1.185-.096zm-8.774.165l-3.123 9.967h2.094l.605-2.217h3.053l.61 2.217h2.107L9.869 6.656H7.576zm1.072 1.78h.047c.028.347.077.646.145.896l.922 3.35H7.564l.934-3.377c.08-.288.13-.578.15-.87z" font-weight="400" font-size="51.019" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="android" xmlns="http://www.w3.org/2000/svg"><path d="M15 5h-1V4h1m-5 1H9V4h1m5.53-1.84L16.84.85c.19-.19.19-.51 0-.71a.513.513 0 0 0-.71 0l-1.48 1.48C13.85 1.23 12.95 1 12 1c-.96 0-1.86.23-2.66.63L7.85.14a.501.501 0 0 0-.7 0c-.2.2-.2.52 0 .71l1.31 1.31C6.97 3.26 6 5 6 7h12c0-2-1-3.75-2.47-4.84M20.5 8A1.5 1.5 0 0 0 19 9.5v7a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 20.5 8m-17 0A1.5 1.5 0 0 0 2 9.5v7A1.5 1.5 0 0 0 3.5 18 1.5 1.5 0 0 0 5 16.5v-7A1.5 1.5 0 0 0 3.5 8M6 18a1 1 0 0 0 1 1h1v3.5A1.5 1.5 0 0 0 9.5 24a1.5 1.5 0 0 0 1.5-1.5V19h2v3.5a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5V19h1a1 1 0 0 0 1-1V8H6v10z" fill="#c0ca33"/></symbol><symbol viewBox="0 0 24 24" id="angular" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#e53935"/></symbol><symbol viewBox="0 0 24 24" id="angular-component" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#0288d1"/></symbol><symbol viewBox="0 0 24 24" id="angular-directive" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#ab47bc"/></symbol><symbol viewBox="0 0 24 24" id="angular-guard" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-pipe" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#00897b"/></symbol><symbol viewBox="0 0 24 24" id="angular-resolver" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-routing" xmlns="http://www.w3.org/2000/svg"><path d="M11 10H5L3 8l2-2h6V3l1-1 1 1v1h6l2 2-2 2h-6v2h6l2 2-2 2h-6v6a2 2 0 0 1 2 2H9a2 2 0 0 1 2-2V10z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-service" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#ffca28"/></symbol><symbol viewBox="0 0 100 100" id="apiblueprint" xmlns="http://www.w3.org/2000/svg"><title>api-blueprint</title><path d="M50.133 7.521A16.998 16.998 0 0 0 33.135 24.52a16.998 16.998 0 0 0 4.945 11.974L24.861 57.398a16.998 16.998 0 0 0-3.175-.308A16.998 16.998 0 0 0 4.688 74.088a16.998 16.998 0 0 0 16.998 16.998 16.998 16.998 0 0 0 16.998-16.998 16.998 16.998 0 0 0-7.063-13.773l12.576-19.89a16.998 16.998 0 0 0 5.936 1.093 16.998 16.998 0 0 0 6.154-1.155l12.537 19.83a16.998 16.998 0 0 0-7.244 13.895 16.998 16.998 0 0 0 16.998 17 16.998 16.998 0 0 0 16.998-17A16.998 16.998 0 0 0 78.578 57.09a16.998 16.998 0 0 0-2.95.262L62.337 36.327A16.998 16.998 0 0 0 67.13 24.52 16.998 16.998 0 0 0 50.132 7.522z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="applescript" xmlns="http://www.w3.org/2000/svg"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" fill="#78909c"/></symbol><symbol viewBox="0 0 24 24" id="appveyor" xmlns="http://www.w3.org/2000/svg"><path d="M12 2c-.084 0-.165.008-.248.01a10 10 0 0 0-.266.01 9.952 9.952 0 0 0-.754.066 10 10 0 0 0-.148.018 9.855 9.855 0 0 0-.93.177 10 10 0 0 0-.07.02c-.196.049-.392.1-.584.16v.012a10 10 0 0 0-2 .875V3.34c-.02.012-.038.027-.059.039a10 10 0 0 0-.953.635c-.09.067-.172.142-.26.213a10 10 0 0 0-.628.546c-.109.104-.211.211-.315.319a10 10 0 0 0-.476.539c-.1.12-.201.237-.295.361a10 10 0 0 0-.52.766c-.088.143-.17.288-.252.435a10 10 0 0 0-.363.723c-.072.161-.136.327-.2.492a10 10 0 0 0-.269.778c-.02.067-.044.131-.062.199a10 10 0 0 0-.008.027c-.098.364-.166.728-.22 1.09-.012.077-.024.153-.034.23a9.85 9.85 0 0 0-.08 1.182c0 .03-.006.057-.006.086a10 10 0 0 0 .008.148c.001.094-.002.188.002.282l.011.004a10 10 0 0 0 .333 2.158l-.012-.004c.012.047.033.091.047.139a10 10 0 0 0 .322.955c.02.052.037.106.059.158a10 10 0 0 0 .503 1.035c.065.116.14.226.21.34a10 10 0 0 0 .423.64c.092.128.187.252.285.375a10 10 0 0 0 .448.52c.112.123.222.248.341.365a10 10 0 0 0 .803.719 10 10 0 0 0 .01.006c.099.078.207.146.309.22a10 10 0 0 0 .648.442c.138.085.28.163.424.242a10 10 0 0 0 .715.358c.114.051.226.106.343.154a10 10 0 0 0 1.133.389c.016.004.031.01.047.015a10 10 0 0 0 .461.098 10 10 0 0 0 .482.103 10 10 0 0 0 .418.051 10 10 0 0 0 .575.065 10 10 0 0 0 .144.005A10 10 0 0 0 12 22a10 10 0 0 0 .197-.01 10 10 0 0 0 .496-.025 10 10 0 0 0 .49-.043 10 10 0 0 0 .489-.074 10 10 0 0 0 .51-.098 10 10 0 0 0 .47-.12 10 10 0 0 0 .477-.14 10 10 0 0 0 .47-.172 10 10 0 0 0 .481-.197 10 10 0 0 0 .414-.201 10 10 0 0 0 .475-.252 10 10 0 0 0 .39-.238 10 10 0 0 0 .452-.301 10 10 0 0 0 .38-.291 10 10 0 0 0 .385-.315 10 10 0 0 0 .375-.347 10 10 0 0 0 .36-.363 10 10 0 0 0 .293-.334 10 10 0 0 0 .353-.434 10 10 0 0 0 .28-.393 10 10 0 0 0 .263-.4 10 10 0 0 0 .264-.461 10 10 0 0 0 .228-.436 10 10 0 0 0 .195-.437 10 10 0 0 0 .196-.48 10 10 0 0 0 .228-.69 10 10 0 0 0 .028-.094 10 10 0 0 0 .021-.066 10 10 0 0 0 .098-.461 10 10 0 0 0 .103-.482 10 10 0 0 0 .051-.418 10 10 0 0 0 .065-.575 10 10 0 0 0 .005-.144A10 10 0 0 0 22 12a10 10 0 0 0-.01-.197 10 10 0 0 0-.025-.496 10 10 0 0 0-.043-.49 10 10 0 0 0-.074-.489 10 10 0 0 0-.098-.51 10 10 0 0 0-.12-.47 10 10 0 0 0-.14-.477 10 10 0 0 0-.172-.47 10 10 0 0 0-.197-.481 10 10 0 0 0-.201-.414 10 10 0 0 0-.252-.475 10 10 0 0 0-.238-.39 10 10 0 0 0-.301-.452 10 10 0 0 0-.291-.38 10 10 0 0 0-.315-.385 10 10 0 0 0-.347-.375 10 10 0 0 0-.363-.36 10 10 0 0 0-.334-.293 10 10 0 0 0-.434-.353 10 10 0 0 0-.393-.28 10 10 0 0 0-.4-.263 10 10 0 0 0-.461-.264 10 10 0 0 0-.436-.228 10 10 0 0 0-.437-.196 10 10 0 0 0-.48-.195 10 10 0 0 0-.69-.228 10 10 0 0 0-.094-.028 10 10 0 0 0-.066-.021 10 10 0 0 0-.461-.098 10 10 0 0 0-.482-.103 10 10 0 0 0-.418-.051 10 10 0 0 0-.575-.065 10 10 0 0 0-.144-.005A10 10 0 0 0 12 2zm-.016 5.002a5 5 0 0 1 .262.01 5 5 0 0 1 .227.011 5 5 0 0 1 .341.05 5 5 0 0 1 .135.019 5 5 0 0 1 .014.004 5 5 0 0 1 .115.025 5 5 0 0 1 .303.076 5 5 0 0 1 .265.086 5 5 0 0 1 .2.074 5 5 0 0 1 .242.106 5 5 0 0 1 .228.11 5 5 0 0 1 .196.109 5 5 0 0 1 .244.15 5 5 0 0 1 .17.12 5 5 0 0 1 .224.171 5 5 0 0 1 .186.16 5 5 0 0 1 .176.164 5 5 0 0 1 .172.18 5 5 0 0 1 .177.203 5 5 0 0 1 .133.172 5 5 0 0 1 .16.223 5 5 0 0 1 .133.214 5 5 0 0 1 .12.21 5 5 0 0 1 .107.216 5 5 0 0 1 .109.24 5 5 0 0 1 .084.223 5 5 0 0 1 .08.242 5 5 0 0 1 .07.264 5 5 0 0 1 .047.207 5 5 0 0 1 .045.277 5 5 0 0 1 .028.227 5 5 0 0 1 .02.351 5 5 0 0 1 .003.079 5 5 0 0 1-.012.271 5 5 0 0 1-.011.227 5 5 0 0 1-.05.341 5 5 0 0 1-.019.135 5 5 0 0 1-.004.014 5 5 0 0 1-.025.115 5 5 0 0 1-.076.303 5 5 0 0 1-.086.265 5 5 0 0 1-.074.2 5 5 0 0 1-.106.242 5 5 0 0 1-.11.228 5 5 0 0 1-.109.196 5 5 0 0 1-.15.244 5 5 0 0 1-.12.17 5 5 0 0 1-.171.224 5 5 0 0 1-.16.186 5 5 0 0 1-.164.176 5 5 0 0 1-.18.172 5 5 0 0 1-.203.177l-.002.002c-.018.019-.028.035-.047.053l-3.959 5.09-3.05-.979a141.684 141.684 0 0 0 3.177-3.084 5 5 0 0 1-.103-.015 5 5 0 0 1-.149-.024 5 5 0 0 1-.115-.025 5 5 0 0 1-3.57-3.04 5.072 5.072 0 0 1-.206-.661 5 5 0 0 1-.033-.147c-.025-.118-.036-.24-.054-.36-.987.993-1.964 1.993-2.954 3.05l-.98-3.053 5.092-3.957c.043-.044.082-.07.125-.11a5 5 0 0 1 .71-.634c.18-.13.367-.25.561-.356a5 5 0 0 1 .16-.08 4.94 4.94 0 0 1 .516-.222 5 5 0 0 1 .147-.057c.211-.07.43-.123.654-.164a5 5 0 0 1 .172-.027c.236-.035.476-.058.722-.059zM12 9a3 3 0 0 0-.053.002 3 3 0 0 0-.166.01 3 3 0 0 0-.133.011 3 3 0 0 0-.17.026 3 3 0 0 0-.113.021 3 3 0 0 0-.19.05 3 3 0 0 0-.103.03 3 3 0 0 0-.16.057 3 3 0 0 0-.129.053 3 3 0 0 0-.146.072 3 3 0 0 0-.12.063 3 3 0 0 0-.132.082 3 3 0 0 0-.123.08 3 3 0 0 0-.116.088 3 3 0 0 0-.126.105 3 3 0 0 0-.1.094 3 3 0 0 0-.111.111 3 3 0 0 0-.096.107 3 3 0 0 0-.094.116 3 3 0 0 0-.098.136 3 3 0 0 0-.072.11 3 3 0 0 0-.076.133 3 3 0 0 0-.07.132 3 3 0 0 0-.063.14 3 3 0 0 0-.054.14 3 3 0 0 0-.077.228 3 3 0 0 0-.007.026 3 3 0 0 0-.03.138 3 3 0 0 0-.031.149 3 3 0 0 0-.014.11 3 3 0 0 0-.02.183 3 3 0 0 0-.001.052A3 3 0 0 0 9 12a3 3 0 0 0 .002.053 3 3 0 0 0 .01.166 3 3 0 0 0 .011.133 3 3 0 0 0 .026.17 3 3 0 0 0 .021.113 3 3 0 0 0 .05.19 3 3 0 0 0 .03.103 3 3 0 0 0 .057.16 3 3 0 0 0 .053.129 3 3 0 0 0 .072.146 3 3 0 0 0 .063.12 3 3 0 0 0 .082.132 3 3 0 0 0 .08.123 3 3 0 0 0 .088.116 3 3 0 0 0 .105.126 3 3 0 0 0 .094.1 3 3 0 0 0 .111.111 3 3 0 0 0 .107.096 3 3 0 0 0 .116.094 3 3 0 0 0 .136.098 3 3 0 0 0 .11.072 3 3 0 0 0 .133.076 3 3 0 0 0 .132.07 3 3 0 0 0 .135.06 3 3 0 0 0 .153.061 3 3 0 0 0 .216.07 3 3 0 0 0 .004.003 3 3 0 0 0 .026.007 3 3 0 0 0 .138.03 3 3 0 0 0 .149.031 3 3 0 0 0 .11.014 3 3 0 0 0 .183.02 3 3 0 0 0 .011.001 3 3 0 0 0 .041 0A3 3 0 0 0 12 15a3 3 0 0 0 .053-.002 3 3 0 0 0 .166-.01 3 3 0 0 0 .133-.011 3 3 0 0 0 .17-.026 3 3 0 0 0 .113-.021 3 3 0 0 0 .19-.05 3 3 0 0 0 .103-.03 3 3 0 0 0 .16-.057 3 3 0 0 0 .129-.053 3 3 0 0 0 .146-.072 3 3 0 0 0 .12-.063 3 3 0 0 0 .132-.082 3 3 0 0 0 .123-.08 3 3 0 0 0 .116-.088 3 3 0 0 0 .126-.105 3 3 0 0 0 .1-.094 3 3 0 0 0 .111-.111 3 3 0 0 0 .096-.107 3 3 0 0 0 .094-.116 3 3 0 0 0 .098-.136 3 3 0 0 0 .072-.11 3 3 0 0 0 .076-.133 3 3 0 0 0 .07-.132 3 3 0 0 0 .06-.135 3 3 0 0 0 .061-.153 3 3 0 0 0 .07-.216 3 3 0 0 0 .003-.004 3 3 0 0 0 .007-.026 3 3 0 0 0 .03-.138 3 3 0 0 0 .031-.149 3 3 0 0 0 .002-.008 3 3 0 0 0 .012-.101 3 3 0 0 0 .02-.184 3 3 0 0 0 .001-.011 3 3 0 0 0 0-.041A3 3 0 0 0 15 12a3 3 0 0 0-.002-.053 3 3 0 0 0-.01-.166 3 3 0 0 0-.011-.133 3 3 0 0 0-.026-.17 3 3 0 0 0-.021-.113 3 3 0 0 0-.05-.19 3 3 0 0 0-.03-.103 3 3 0 0 0-.057-.16 3 3 0 0 0-.053-.129 3 3 0 0 0-.072-.146 3 3 0 0 0-.063-.12 3 3 0 0 0-.082-.132 3 3 0 0 0-.08-.123 3 3 0 0 0-.088-.116 3 3 0 0 0-.105-.126 3 3 0 0 0-.094-.1 3 3 0 0 0-.111-.111 3 3 0 0 0-.107-.096 3 3 0 0 0-.116-.094 3 3 0 0 0-.136-.098 3 3 0 0 0-.11-.072 3 3 0 0 0-.133-.076 3 3 0 0 0-.132-.07 3 3 0 0 0-.14-.063 3 3 0 0 0-.14-.054 3 3 0 0 0-.228-.077 3 3 0 0 0-.026-.007 3 3 0 0 0-.138-.03 3 3 0 0 0-.149-.031 3 3 0 0 0-.008-.002 3 3 0 0 0-.101-.012 3 3 0 0 0-.184-.02 3 3 0 0 0-.011-.001 3 3 0 0 0-.041 0A3 3 0 0 0 12 9z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 720 720" id="arduino" xmlns="http://www.w3.org/2000/svg"><defs><symbol id="ana" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke-opacity="100%" stroke-width="60" stroke="#00979c" d="M174 30a10.5 10.1 0 0 0 0 280C364 320 344 30 544 30a10.5 10.1 0 0 1 0 280C354 320 374 30 174 30"/><path d="M528 205v-32.8h-32.5v-13.7H528V126h13.9v32.5h32.5v13.7h-32.5V205H528z" text-anchor="middle" fill="#00979c" stroke-width="20" stroke="#00979c" font-family="sans-serif" font-size="167"/><path fill="#00979c" stroke="#00979c" stroke-width="23.6" transform="matrix(1.56 0 0 .64 -366 .528)" d="M321 266v-17.4h53.3V266H321z"/></symbol></defs><title>Layer 1</title><use x="20.063" y="360.85" transform="matrix(.997 0 0 .997 -18.596 -159.19)" xlink:href="#ana"/></symbol><symbol viewBox="0 0 24 24" id="assembly" xmlns="http://www.w3.org/2000/svg"><path d="M1.746 1.566v20.905H5.13v-2.088H3.438V3.656h1.69v-2.09H1.747zm17.219 0v2.09h1.693v16.727h-1.693v2.09h3.383V1.566h-3.383zM15.196 3.988c-.5 0-.93.076-1.29.225-.359.15-.652.372-.877.671-.226.302-.39.673-.494 1.108a6.715 6.715 0 0 0-.155 1.54c0 .573.049 1.083.15 1.528.1.442.264.811.49 1.11.222.298.512.524.872.676.36.153.795.23 1.304.23.518 0 .954-.075 1.308-.224.353-.153.643-.376.869-.671.219-.29.38-.661.484-1.112.104-.454.156-.967.156-1.54 0-.573-.052-1.079-.152-1.515a2.92 2.92 0 0 0-.485-1.106 2.09 2.09 0 0 0-.868-.686c-.354-.155-.79-.234-1.312-.234zm-6.814.12a.941.941 0 0 1-.138.458.849.849 0 0 1-.356.296A1.71 1.71 0 0 1 7.385 5a5.244 5.244 0 0 1-.631.037v1.11H8.19v3.6H6.754v1.188h4.545V9.745H9.894V4.11H8.382zm6.814 1.138c.375 0 .643.176.805.527.161.348.241.933.241 1.756 0 .814-.082 1.399-.247 1.756-.164.356-.43.534-.799.534-.369 0-.636-.178-.8-.534-.165-.357-.248-.941-.248-1.749 0-.829.082-1.415.243-1.763.162-.35.43-.527.805-.527zm-6.33 7.64c-.5 0-.93.073-1.29.223-.359.15-.651.374-.877.673-.225.302-.39.67-.494 1.106a6.715 6.715 0 0 0-.155 1.54c0 .573.05 1.082.15 1.527.1.442.264.814.49 1.112.222.3.514.525.874.677.36.152.793.229 1.302.229.519 0 .954-.076 1.308-.225.354-.153.643-.376.869-.672.22-.29.38-.66.484-1.111.104-.455.156-.967.156-1.54 0-.573-.05-1.079-.15-1.515a2.923 2.923 0 0 0-.487-1.106 2.084 2.084 0 0 0-.867-.686c-.353-.156-.791-.232-1.313-.232zm5.846.119a.941.941 0 0 1-.138.457.85.85 0 0 1-.356.296 1.71 1.71 0 0 1-.503.137 5.245 5.245 0 0 1-.631.037v1.112h1.435v3.597h-1.435v1.189h4.545v-1.189h-1.405v-5.636h-1.512zm-5.846 1.137c.375 0 .643.176.805.527.162.347.241.933.241 1.756 0 .813-.08 1.399-.245 1.755-.164.357-.432.534-.8.534-.37 0-.637-.177-.802-.534-.164-.356-.245-.939-.245-1.746 0-.83.08-1.418.242-1.765.161-.35.43-.527.804-.527z" fill="#ff6e40"/></symbol><symbol viewBox="0 0 24 24" id="aurelia" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="api" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#apa"/><linearGradient id="apa" x1="-3.881" x2="2.377" y1="-1.442" y2="4.304"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#apb"/><linearGradient id="apb" x1=".729" x2="-.971" y1=".844" y2="-1.477"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="apk" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#apc"/><linearGradient id="apc" x1="-2.839" x2="2.875" y1="-6.936" y2="1.017"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apl" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#apd"/><linearGradient id="apd" x1="-8.212" x2="1.02" y1="-4.691" y2="2.882"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apm" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#ape"/><linearGradient id="ape" x1="-1.404" x2="4.19" y1="-2.309" y2="2.62"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apn" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#apf"/><linearGradient id="apf" x1="1.911" x2=".204" y1="2.539" y2=".204"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="apo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#apg"/><linearGradient id="apg" x1="-3.881" x2="2.377" y1="-1.738" y2="5.19"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="app" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#aph"/><linearGradient id="aph" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><g transform="rotate(11.282 -1.694 21.569) scale(.47102)" clip-rule="evenodd" fill="none" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#api)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#apj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#apk)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#apl)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#apm)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#apn)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#apo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#app)"/></g></symbol><symbol viewBox="0 0 24 24" id="autohotkey" xmlns="http://www.w3.org/2000/svg"><path d="M5 3c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5zm3.668 3.447a.9.9 0 0 1 .652.256.84.84 0 0 1 .262.625c0 .34-.014.852-.041 1.537-.022.68-.033 1.19-.033 1.53 0 .111-.016.326-.047.644a6.149 6.149 0 0 0-.033.68l2.578-.485c1.007-.179 1.874-.281 2.603-.308.018-.3.048-1.105.088-2.416.01-.345.115-.742.317-1.19.25-.55.533-.826.851-.826.237 0 .448.08.631.236.197.17.295.382.295.637a.775.775 0 0 1-.025.201c-.09.327-.135.612-.135.854 0 .125-.014.32-.041.584-.023.26-.033.453-.033.578 0 .425-.022 1.056-.067 1.893a38.963 38.963 0 0 0-.068 1.892c0 .327.025.816.074 1.465.05.649.074 1.136.074 1.463a.84.84 0 0 1-.261.625.893.893 0 0 1-.65.254 1 1 0 0 1-.686-.254.777.777 0 0 1-.29-.611c0-.327-.015-.818-.046-1.471a39.552 39.552 0 0 1-.041-1.47c0-.256.004-.482.013-.679-.702.032-1.57.142-2.603.33-.86.157-1.719.316-2.578.477-.01.304-.042.812-.096 1.523a22.354 22.354 0 0 0-.066 1.538.84.84 0 0 1-.262.625.893.893 0 0 1-.65.253.898.898 0 0 1-.653-.253.84.84 0 0 1-.262-.625c0-.452.038-1.128.114-2.028.08-.9.12-1.575.12-2.027 0-.573.015-1.436.042-2.586.027-1.155.04-2.017.04-2.59a.84.84 0 0 1 .263-.625.895.895 0 0 1 .65-.256z" fill="#4caf50"/></symbol><symbol viewBox="0 0 24 24" id="autoit" xmlns="http://www.w3.org/2000/svg"><defs id="ardefs8"><style id="arstyle4482">.cls-1{fill:#5d83ac}.cls-2{fill:#f0f0f0;fill-rule:evenodd}</style><style id="arstyle4510">.cls-1{fill:#5d83ac}.cls-2{fill:#f0f0f0;fill-rule:evenodd}</style></defs><g id="arg4522" transform="translate(-59.538 -26.404) scale(.0555)"><path d="M12.8 2.133A10.666 10.666 0 0 0 2.136 12.799 10.666 10.666 0 0 0 12.8 23.465a10.666 10.666 0 0 0 10.668-10.666A10.666 10.666 0 0 0 12.8 2.133zm.15 4.713c.456 0 .836.105 1.142.314.306.21.565.469.78.78l6.089 8.812H9.627l1.82-2.506h3.36c.315 0 .589.01.822.03a11.93 11.93 0 0 1-.473-.663 39.13 39.13 0 0 0-.517-.75l-1.748-2.578-4.577 6.467H4.746l6.25-8.813c.204-.281.46-.534.772-.757.31-.224.705-.336 1.181-.336z" transform="matrix(16.89188 0 0 16.89188 1072.761 475.745)" id="arcircle4514" fill="#1976d2" stroke-width=".026"/></g></symbol><symbol viewBox="0 0 213.33333 213.33333" id="babel" xmlns="http://www.w3.org/2000/svg"><path d="M50.22 199.659c-.875-.406-1.261-1.6-.857-2.652.404-1.053.12-1.914-.63-1.914s-1.615.748-1.92 1.663c-.328.983-1.27.302-2.304-1.667-.962-1.831-3.718-5.533-6.126-8.226-9.418-10.535-7.71-27.444 5.432-53.77 12.459-24.96 23.117-39.033 45.966-60.696 30.229-28.66 52.679-46.223 70.587-55.22 10.98-5.518 13.025-5.059 2.778.624-11.004 6.102-11.378 6.359-10.512 7.226.33.33 7.306-2.67 15.504-6.667 15.87-7.737 16.34-7.912 16.34-6.082 0 .652-4.95 3.738-11 6.858-13.062 6.736-12.722 6.48-10.472 7.872 1.117.69 5.428-.582 11.54-3.406 5.367-2.48 10.397-4.508 11.179-4.508 2.755 0-3.928 5.302-11.541 9.157-20.437 10.35-68.937 46.043-68.07 50.097.166.777-5.792 7.639-13.241 15.248-15.257 15.587-26.14 30.002-33.748 44.706-6.379 12.326-7.457 17.734-5.385 26.996 3.482 15.56 11.592 18.366 31.482 10.895 28.228-10.603 45.758-28.704 47.022-48.556.602-9.442-1.317-13.479-8.52-17.93-4.01-2.48-5.268-2.621-12.065-1.365-4.173.771-10.153 2.906-13.289 4.744s-6.455 3.34-7.377 3.34c-.922 0-3.216 1.336-5.096 2.968-1.88 1.633.48-1.13 5.247-6.14 6.82-7.167 7.956-8.9 5.333-8.132-5.208 1.525-10.194 4.33-15.649 8.803-2.76 2.264-.923.175 4.08-4.641 11.565-11.131 21.183-15.97 33.088-16.641 17.097-.966 27.254 5.805 31.964 21.31 2.435 8.017 2.609 10.24 1.353 17.37-1.65 9.361-7.034 21.553-15.593 35.307-4.398 7.067-8.434 11.427-15.588 16.844-9.166 6.94-15.654 11.02-15.654 9.845 0-.295 2.455-2.161 5.455-4.147 8.818-5.835 5.075-5.377-8.326 1.02-6.854 3.27-15.199 6.593-18.542 7.38-7.106 1.675-30.527 3.164-32.846 2.089zm-8.408-19.899c0-1.1-.6-2-1.333-2-.734 0-1.334.9-1.334 2s.6 2 1.333 2c.734 0 1.334-.9 1.334-2zm89.255-8.204c1.53-1.945 2.473-3.845 2.097-4.222-.377-.377-.836-.435-1.02-.13-.182.306-1.787 2.206-3.565 4.223-1.778 2.016-2.571 3.666-1.763 3.666s2.72-1.591 4.25-3.536zm-77.644-1.745c-.82-2.172-1.74-3.7-2.045-3.396-.951.952 1.088 7.345 2.343 7.345.656 0 .522-1.777-.298-3.95zm82.303-27.915c-.837-.837-3.217 2.55-3.184 4.53.012.734.896.178 1.965-1.235 1.07-1.413 1.618-2.896 1.219-3.295zm-66.238-36.904c-1.312-1.312-3.676.702-3.676 3.133 0 2.035.175 2.031 2.254-.047 1.24-1.24 1.88-2.628 1.422-3.086zm39.657.768c4.403-2.196 6.8-3.986 5.333-3.982-2.838.01-16.667 6.028-16.667 7.254 0 1.6 3.717.527 11.333-3.272zm16.667-5.333c0-.733-.9-1.333-2-1.333s-2 .6-2 1.333.9 1.333 2 1.333 2-.6 2-1.333zm-3.334-3.923l5.334-1.104-7.334-.133c-4.033-.073-8.233.45-9.333 1.16-2.539 1.64 3.572 1.682 11.333.077zm35.738-63.976c2.788-1.69 4.765-3.376 4.393-3.748-.947-.947-11.942 5.654-14.237 8.548-1.792 2.258-1.714 2.276 1.44.329a1452.76 1452.76 0 0 1 8.403-5.13z" fill="#ffca28" stroke-width="1.333"/></symbol><symbol viewBox="0 0 400 400" fill-opacity=".05" id="bithound" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.88 0 0 .88 24.121 2.895)" fill="#e53935" fill-opacity="1"><path d="M370.5 207c-1.5-14.8-4.8-29.9-9.5-44-13.5-40.3-38.6-81.6-70.3-110.1-1.4-1.2-6.7-4.4-8.7-3.3-5.2 2.9 4.6 22.8 5.8 26.4 7.4 22 12.1 45.3 6.8 68.3-7.1 30.4-30.4 51.7-61.5 54.3-17.1 1.4-34.3-.5-51.4 1.5-25.6 3-51.7 11.8-68 32.8-1.9 2.4-3.6 5.1-5.2 7.9h-.4c-6.3.7-12.6-2-15.7-3.7-.8-.5-1.6-.9-2.2-1.2-19-10.5-33-34-41.6-53.4-3.9-9-7.2-18.4-9.3-27.9-1-4.3-1.1-8.8-1.3-13.2-.1-2.7.3-6.5-1.2-8.9-3.3-5.2-7.5-.2-8.2 4-1.1 6.9-2.1 13.7-1.8 20.7.5 11.8 3.8 23.5 8 34.5 6.2 16.2 14.9 31.1 26.2 44.4 4.7 5.5 9.7 10.6 15.1 15.3 4.8 4.3 10.9 7.7 14.5 13.2 4.2 6.3 4.9 14.1 4.5 21.4-1 19.3-1.6 37.4 3.9 56.2 4.8 16.7 10.8 33.8 20.8 48.1 5 7.1 11.2 14.6 18 19.9 4.6 3.6 13.3 4 8.3-9.2-11.1-29.3-12.1-59.7 5.2-87.1 14.5-22.8 40.1-43.1 69-39.5 42.5 5.3 72.1 44.3 70 86-.6 11.7-1 21.7-4.7 32.7-1.5 4.4-2.6 10-1.5 14.6 1.8 7.8 10.5 4.9 14.3-.2 10.3-14 21.1-27.6 30.8-42 31.6-47.2 47-101.8 41.3-158.5z"/><path d="M132.4 92.1c.7 2.3 1.4 4.8 1.9 7.5.1 1.1.4 2.3 1 3.4 2.6 6.8 8.9 10.5 14.8 14 3.6 2.2 10.1 4.3 14.1 5.9 5.2 2.1 16.4-.6 21.7-1 12.2-1 23.5-5.3 34.7 1.2-57.4 67.3-3.2 82.3 38.8 49.9 48-37 2.8-124.3 2.8-124.3s-1-6.8-19.2-10.8c-1.7-.9-3.4-1.7-5.1-2.4-18-8.3-34.2 5.3-47.2 16.4-3.8 3.2-7.5 6.4-11.5 9.4-5.4 4-11.2 7.3-17.3 10.2-6.4 3-14 6.4-21.1 6.7-1 0-2.9.2-4.9.6-3.1.3-4.7 1.1-5.4 2.5-1.2 1-2 2.4-1.8 4.2.2 2.5 1.4 4.6 2.7 6.2.4.1.7.3 1 .4z"/></g></symbol><symbol viewBox="0 0 400.00001 399.99999" id="bower" xmlns="http://www.w3.org/2000/svg"><g transform="translate(12.061 33.203) scale(.81733)"><path d="M447.61 200.08c-23.139-22.234-138.85-36.114-175.36-40.154a107.137 107.137 0 0 0 4.517-12.944 146.107 146.107 0 0 1 15.905-5.901c.677 1.997 3.865 9.648 5.682 13.279 73.415 2.025 77.184-54.557 80.17-70.058 2.92-15.157 2.771-29.802 27.953-56.575-37.516-10.933-91.467 16.945-109.54 58.437-6.79-2.545-13.597-4.424-20.328-5.586-4.824-19.46-29.944-73.672-95.863-73.672-83.46 0-174.43 68.853-174.43 185.41 0 97.976 66.891 183.84 104.68 183.84 16.505 0 30.703-12.36 34.036-23.44 2.795 7.597 11.368 31.213 14.184 37.225 4.162 8.89 23.41 16.583 31.833 7.357 10.83 6.017 30.703 9.641 41.534-6.405 20.86 4.412 39.3-8.026 39.702-22.868 10.235-.546 15.256-14.918 13.021-26.363-1.647-8.426-19.248-38.66-26.113-49.098 13.59 11.054 48.013 14.183 52.194.007 21.911 17.198 56.057 8.171 58.765-5.815 26.624 6.917 57.16-8.276 52.146-26.676 42.771-2.958 37.296-48.464 25.296-59.996z" fill="#543729" stroke-width=".973"/><path d="M328.514 103.025c9.212-18.277 20.788-38.234 35.409-50.58-16.093 6.485-31.981 25.873-41.375 46.595a144.914 144.914 0 0 0-14.552-8.132c13.105-27.972 43.555-51.332 77.112-53.157-22.477 20.385-14.498 62.754-32.979 85.183-5.288-5.311-17.43-15.562-23.615-19.909zm-14.53 29.762c.01-.7.272-6.094.763-8.557-1.288-.304-9.3-1.87-13.476-1.772-.304 5.245 2.204 14.17 4.684 19.541 17.075-.358 29.408-5.471 36.667-10.172-6.18-2.88-16.726-5.442-24.745-6.974-.894 1.851-3.097 6.568-3.892 7.934z" fill="#00acee"/><g stroke-width=".973"><path d="M250.54 277.39c.004.024.015.057.018.082-2.165-4.657-4.463-10.314-7.208-17.708 10.688 15.557 44.184 7.533 42.427-6.407 16.395 12.336 50.143-2.055 42.471-19.353 16.423 7.653 35.168-7.745 30.964-14.455 28 5.4 54.832 10.783 63.256 12.938-5.595 9.123-18.339 15.566-37.549 11.089 10.38 14.14-9.773 31.105-37.844 21.76 6.18 13.883-18.814 26.38-47.22 11.91.361 13.889-35.24 15.488-49.315.143zm55.543-70.194c32.497 2.495 86.238 7.34 119.51 11.997-2.102-10.828-7.844-13.921-25.905-18.772-19.425 2.072-68.706 6.913-93.604 6.776z" fill="#2baf2b"/><path d="M285.78 253.36c16.395 12.336 50.143-2.055 42.471-19.353 16.423 7.653 35.168-7.745 30.964-14.455-33.103-6.383-67.84-12.788-75.719-13.908 4.78.254 12.702.797 22.59 1.556 24.899.137 74.18-4.704 93.604-6.775-31.452-7.975-95.666-19.613-140.01-22.48-2.055 3.003-5.833 8.097-12.413 13.51-19.403 41.053-54.557 68.34-93.454 68.34-11.335 0-24.018-1.912-38.233-6.456-8.865 9.497-46.661 16.694-77.329 1.641 24.326 56.961 80.74 94.984 143.19 94.984 52.591 0 75.912-53.704 70.808-67.914-1.238-3.45-6.145-14.889-8.891-22.283 10.689 15.556 44.185 7.532 42.429-6.408z" fill="#ffcc2f"/><path d="M253.91 145.27c4.644-2.526 20.69-12.253 35.981-15.908a67.843 67.843 0 0 1-.536-5.12c-10.032 2.403-28.945 10.51-39.784-.661 22.866 6.9 34.283-6.149 51.09-6.149 10.014 0 24.305 2.798 35.57 7.22-9.061-8.37-38.772-33.63-75.558-33.717-8.213 9.957-17.09 31.526-6.764 54.334z" fill="#cecece"/><path d="M115.58 253.33c14.215 4.544 26.898 6.457 38.233 6.457 38.896 0 74.05-27.29 93.454-68.341-14.351 11.978-39.291 22.228-78.241 22.228 34.694-7.866 64.56-25.156 79.753-50.427-10.68-16.998-22.263-54.603 7.07-84.33-4.512-14.497-26.475-52.766-75.095-52.766-84.85 0-155.17 71.001-155.17 166.15 0 22.525 4.547 43.65 12.67 62.664 30.666 15.054 68.462 7.858 77.327-1.64z" fill="#ef5734"/><path d="M141.03 108.45c0 21.644 17.546 39.191 39.19 39.191s39.192-17.548 39.192-39.191c0-21.644-17.548-39.191-39.192-39.191-21.644 0-39.19 17.547-39.19 39.191z" fill="#ffcc2f"/><path d="M156.76 108.45c0 12.958 10.507 23.463 23.463 23.463 12.96 0 23.464-10.506 23.464-23.463 0-12.959-10.504-23.464-23.464-23.464-12.957 0-23.463 10.506-23.463 23.464z" fill="#543729"/><ellipse cx="180.22" cy="98.044" rx="13.673" ry="8.501" fill="#fff"/></g></g></symbol><symbol viewBox="0 0 140 140" id="browserlist" xmlns="http://www.w3.org/2000/svg"><title>Browserslist logo</title><path d="M70.314 10.066a59.828 59.828 0 0 0-59.828 59.828 59.828 59.828 0 0 0 59.828 59.828 59.828 59.828 0 0 0 59.828-59.828 59.828 59.828 0 0 0-59.828-59.828zm-4.836 8.785c.496 4.043 1.352 7.322 2.572 10.223 4.779-4.287 10.265-7.546 16.041-9.02-.981 3.938-1.357 7.295-1.261 10.43 6.026-2.314 12.349-3.404 18.3-2.706-3.182 2.413-5.482 4.717-7.128 7.015-2.201 12.074 6.858 20.43 14.779 24.551a5.128 5.128 0 0 1 5.183-3.888 5.128 5.128 0 0 1 3.7 8.435v.002c-.487 1.055-2.002 2.343-3.497 3.219-4.075 2.39-11.172 5.736-20.914 7.39.045 1.214.077 2.453.077 3.747 0 4.817-.485 8.291-1.385 10.699-3.3 13.313-12.648 26.76-24.695 31.95.357-4.083.197-7.485-.402-10.591-5.582 3.218-11.646 5.278-17.623 5.52h-.002c1.785-3.662 2.855-6.878 3.412-9.976-6.347.996-12.727.742-18.377-1.17 2.93-2.732 5.054-5.314 6.673-7.96-6.292-1.344-12.169-3.87-16.766-7.686 3.822-1.544 6.795-3.239 9.3-5.197-5.426-3.517-10.034-7.998-12.972-13.23 4.012-.07 7.321-.568 10.3-1.453-3.786-5.215-6.468-11.032-7.333-16.951 3.861 1.405 7.196 2.133 10.36 2.355-1.662-6.22-2.081-12.605-.768-18.436 3.03 2.634 5.824 4.48 8.63 5.815.678-6.406 2.576-12.52 5.893-17.496 1.926 3.622 3.914 6.391 6.111 8.672 2.93-5.754 6.9-10.798 11.791-14.262zm26.465 19.557c-2.395 5.514-1.665 11.297-.555 18.732a2.138 2.138 0 0 0 .28-4.178 3.419 3.419 0 1 1 .092 6.704c.574 3.882 1.157 8.18 1.421 13.125a67.143 67.143 0 0 0 3.25-.649c6.616-1.487 12.258-3.801 16.871-6.506.45-.264.884-.563 1.276-.867.366-.557.333-.957.035-1.285-4.831-1.245-10.891-4.53-15.258-8.795-4.764-4.653-7.428-10.164-7.412-16.281z" fill="#ffca28" stroke-width=".855"/></symbol><symbol viewBox="0 0 140 140" id="browserlist_light" xmlns="http://www.w3.org/2000/svg"><title>Browserslist logo</title><g transform="translate(10.823 10.1)" stroke-width=".855"><circle cx="59.492" cy="59.795" r="59.828" fill="#ffca28"/><path d="M54.656 8.752c-4.89 3.464-8.862 8.508-11.791 14.262-2.198-2.28-4.185-5.05-6.111-8.672-3.318 4.976-5.216 11.09-5.893 17.496-2.807-1.335-5.6-3.18-8.63-5.814-1.314 5.83-.895 12.216.767 18.436-3.164-.223-6.498-.95-10.36-2.356.865 5.92 3.548 11.737 7.333 16.951-2.978.885-6.287 1.383-10.3 1.453 2.939 5.233 7.547 9.714 12.972 13.23-2.505 1.959-5.478 3.654-9.299 5.198 4.596 3.815 10.474 6.341 16.766 7.685-1.62 2.647-3.743 5.228-6.674 7.96 5.65 1.912 12.03 2.166 18.377 1.17-.556 3.098-1.626 6.314-3.412 9.975h.002c5.977-.24 12.042-2.3 17.623-5.52.6 3.108.76 6.51.402 10.593 12.047-5.19 21.395-18.638 24.695-31.951.9-2.408 1.385-5.881 1.385-10.7 0-1.293-.031-2.531-.076-3.745 9.742-1.655 16.839-5.001 20.914-7.39 1.494-.877 3.01-2.165 3.496-3.22v-.002a5.128 5.128 0 0 0-3.7-8.435 5.128 5.128 0 0 0-5.183 3.889c-7.92-4.122-16.98-12.477-14.779-24.551 1.646-2.299 3.947-4.603 7.13-7.016-5.952-.698-12.276.392-18.302 2.707-.095-3.135.28-6.492 1.262-10.43-5.776 1.473-11.262 4.733-16.041 9.02-1.22-2.902-2.076-6.18-2.572-10.223zm26.465 19.557c-.015 6.117 2.648 11.628 7.412 16.281 4.366 4.265 10.426 7.55 15.258 8.795.298.328.331.728-.035 1.285-.392.304-.825.603-1.275.867-4.613 2.704-10.256 5.019-16.871 6.506-1.071.24-2.154.458-3.25.649-.265-4.945-.848-9.243-1.422-13.125a3.419 3.419 0 1 0-.092-6.703 2.138 2.138 0 0 1-.28 4.177c-1.11-7.435-1.84-13.218.555-18.732z" fill="#37474f"/></g></symbol><symbol viewBox="0 0 24 24" id="bucklescript" xmlns="http://www.w3.org/2000/svg"><path d="M3 3v18h18V3H3zm14.1 8.858a5.5 5.5 0 0 1 1.26.145c.417.093.778.213 1.082.357v1.723h-.18a3.281 3.281 0 0 0-.959-.603 2.867 2.867 0 0 0-1.155-.247c-.14 0-.277.011-.416.035a1.4 1.4 0 0 0-.395.12.756.756 0 0 0-.291.231.54.54 0 0 0-.123.348c0 .198.065.35.196.456.13.104.376.2.738.288.237.057.466.11.683.164.22.054.455.128.706.222.496.188.86.444 1.095.77.238.32.357.738.357 1.253 0 .737-.271 1.336-.813 1.798-.538.46-1.27.689-2.197.689a5.447 5.447 0 0 1-1.402-.161 6.725 6.725 0 0 1-1.117-.416v-1.794h.183c.344.318.73.563 1.155.734.429.17.839.256 1.233.256.1 0 .235-.01.4-.03.166-.02.3-.055.403-.102a.97.97 0 0 0 .313-.225c.084-.09.127-.223.127-.4a.568.568 0 0 0-.183-.424c-.119-.12-.294-.213-.526-.276-.243-.067-.5-.128-.773-.185a5.523 5.523 0 0 1-.76-.227c-.544-.204-.936-.48-1.177-.828-.237-.351-.357-.786-.357-1.305 0-.697.27-1.265.81-1.703.54-.442 1.235-.663 2.083-.663zm-8.981.135h2.51c.521 0 .903.02 1.143.06.243.041.484.13.721.266.246.144.43.338.548.583.121.24.181.518.181.83 0 .36-.082.68-.247.959a1.697 1.697 0 0 1-.7.642v.04c.423.098.758.298 1.004.603.249.305.373.706.373 1.205 0 .361-.063.686-.19.97-.125.285-.296.52-.516.707a2.31 2.31 0 0 1-.845.472c-.304.094-.69.141-1.159.141H8.12v-7.478zm1.659 1.372v1.582h.262c.263 0 .486-.007.672-.017.185-.01.332-.043.44-.1.15-.077.248-.175.294-.295.046-.124.07-.266.07-.427a.91.91 0 0 0-.083-.371.518.518 0 0 0-.282-.277 1.187 1.187 0 0 0-.456-.086c-.18-.007-.433-.01-.76-.01h-.157zm0 2.873V18.1H9.9c.469 0 .804-.002 1.007-.006.202-.003.39-.046.56-.13a.712.712 0 0 0 .357-.33c.067-.142.099-.302.099-.483 0-.237-.04-.42-.121-.547-.078-.13-.214-.228-.405-.291a1.842 1.842 0 0 0-.538-.072 49.47 49.47 0 0 0-.716-.003h-.366z" fill="#26a69a" stroke-width="1.067"/></symbol><symbol viewBox="0 0 24 24" id="c" xmlns="http://www.w3.org/2000/svg"><path d="M15.45 15.97l.42 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96-1.14-1.27-1.68-2.88-1.68-4.83C6 9.9 6.68 8.13 8 6.89 9.28 5.64 10.92 5 12.9 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.6 2.49-1.04-.34c-.4-.1-.87-.15-1.4-.15-1.15-.01-2.11.36-2.86 1.1-.76.73-1.14 1.85-1.18 3.34.01 1.36.37 2.42 1.08 3.2.71.77 1.7 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.09-.32z" fill="#0277bd"/></symbol><symbol viewBox="0 0 300 300" id="cabal" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -822.52)" fill-rule="evenodd" color="#000"><rect transform="matrix(-.98339 .18149 .60192 .79856 0 0)" x="405.55" y="967.22" width="107.25" height="156.59" rx="12.306" ry="12.31" fill="#2d9bbd"/><rect transform="matrix(-.98528 .17093 -.59175 .80612 0 0)" x="-1156.5" y="1461.9" width="108.34" height="123.15" rx="10.69" ry="12.31" fill="#4a4bcd"/><path d="M52.112 965.158c-1.343 3.515-26.292 23.248-25.744 27.277.548 4.03 29.812 16.023 32.04 19.027s10.545 41.668 13.603 42.5 18.828-31.274 21.548-32.932c2.72-1.658 32.808 2.503 34.15-1.01 1.343-3.515-18.174-35.352-18.721-39.381-.548-4.03 9.732-40.12 7.502-43.125-2.229-3.005-30.06 9.427-33.118 8.594-3.059-.833-26.793-27.3-29.514-25.643-2.72 1.657-.405 41.177-1.747 44.693z" fill="#2e5bc1"/></g></symbol><symbol viewBox="0 0 24 24" id="cake" xmlns="http://www.w3.org/2000/svg"><path d="M12.254 6.621a1.807 1.807 0 0 0 1.808-1.807c0-.344-.09-.66-.262-.932l-1.546-2.684-1.546 2.684a1.72 1.72 0 0 0-.262.932 1.808 1.808 0 0 0 1.808 1.807m4.158 9.04l-.967-.976-.976.976c-1.175 1.166-3.236 1.175-4.42 0l-.959-.976-.994.976a3.134 3.134 0 0 1-3.977.353v4.167a.904.904 0 0 0 .904.904h14.463a.904.904 0 0 0 .904-.904v-4.167a3.134 3.134 0 0 1-3.977-.353m1.265-6.328h-4.52V7.525H11.35v1.808H6.83a2.712 2.712 0 0 0-2.711 2.712v1.392c0 .977.795 1.772 1.771 1.772.489 0 .94-.18 1.248-.515l1.952-1.926 1.908 1.926c.669.669 1.835.669 2.504 0l1.916-1.926 1.944 1.926c.316.334.768.515 1.247.515.976 0 1.78-.795 1.78-1.772v-1.392a2.712 2.712 0 0 0-2.711-2.712z" fill="#ff7043" stroke-width=".904"/></symbol><symbol viewBox="0 0 24 24" id="certificate" xmlns="http://www.w3.org/2000/svg"><path d="M4 3c-1.11 0-2 .89-2 2v10a2 2 0 0 0 2 2h8v5l3-3 3 3v-5h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H4m8 2l3 2 3-2v3.5l3 1.5-3 1.5V15l-3-2-3 2v-3.5L9 10l3-1.5V5M4 5h5v2H4V5m0 4h3v2H4V9m0 4h5v2H4v-2z" fill="#ff5722"/></symbol><symbol viewBox="0 0 24 24" id="changelog" xmlns="http://www.w3.org/2000/svg"><path d="M11 7v5.11l4.71 2.79.79-1.28-4-2.37V7m0-5C8.97 2 5.91 3.92 4.27 6.77L2 4.5V11h6.5L5.75 8.25C6.96 5.73 9.5 4 12.5 4a7.5 7.5 0 0 1 7.5 7.5 7.5 7.5 0 0 1-7.5 7.5c-3.27 0-6.03-2.09-7.06-5h-2.1c1.1 4.03 4.77 7 9.16 7 5.24 0 9.5-4.25 9.5-9.5A9.5 9.5 0 0 0 12.5 2z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 24 24" id="clojure" xmlns="http://www.w3.org/2000/svg"><path d="M3.355 1.78c-.845 0-1.525.68-1.525 1.525v17.441c0 .845.68 1.525 1.525 1.525h17.442c.845 0 1.525-.68 1.525-1.525V3.305c0-.845-.68-1.526-1.525-1.526H3.355zm6.168 2.572h1.963l6.368 14.931H15.93l-3.38-8.086-3.349 8.086H7.21l4.346-10.38-2.032-4.551z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="cmake" xmlns="http://www.w3.org/2000/svg"><path d="M11.99 2.965L2.977 20.999l9.874-8.47-.863-9.564z" fill="#1e88e5"/><path d="M12.007 2.963l.002.29 1.312 14.498-.001.006.023.26 7.362 2.979h.416l-.158-.311-.114-.228h-.002l-8.84-17.494z" fill="#e53935"/><path d="M8.607 16.11L2.98 20.995h17.743v-.016L8.607 16.11z" fill="#7cb342"/></symbol><symbol class="bfmain_logo__svg" viewBox="0 0 300 300.00001" id="code-climate" xmlns="http://www.w3.org/2000/svg"><path class="bfsymbol" d="M196.19 75.562l-51.846 51.561 30.766 30.766 21.08-21.08 59.252 59.537 30.481-30.766zm-61.246 60.961l-30.481-30.481-78.053 78.053-11.964 11.964 30.766 30.766 11.964-12.249 39.596-39.312 7.691-7.691 30.481 30.48 28.772 28.773 30.766-30.766-28.772-28.772z" fill="#eee" stroke-width="2.849"/></symbol><symbol class="bgmain_logo__svg" viewBox="0 0 300 300.00001" id="code-climate_light" xmlns="http://www.w3.org/2000/svg"><path class="bgsymbol" d="M196.19 75.562l-51.846 51.561 30.766 30.766 21.08-21.08 59.252 59.537 30.481-30.766zm-61.246 60.961l-30.481-30.481-78.053 78.053-11.964 11.964 30.766 30.766 11.964-12.249 39.596-39.312 7.691-7.691 30.481 30.48 28.772 28.773 30.766-30.766-28.772-28.772z" fill="#455a64" stroke-width="2.849"/></symbol><symbol viewBox="0 0 24 24" id="coffee" xmlns="http://www.w3.org/2000/svg"><path d="M2 21h18v-2H2M20 8h-2V5h2m0-2H4v10a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4v-3h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="coldfusion" xmlns="http://www.w3.org/2000/svg"><rect transform="rotate(90)" x="2.283" y="-21.86" width="19.487" height="19.487" ry="0" fill="#0d3858" stroke="#4dd0e1" stroke-width=".7"/><text x="6.653" y="16.426" fill="#4dd0e1" font-family="Calibri" font-size="29.001" font-weight="bold" letter-spacing="0" stroke-width=".725" word-spacing="0" style="line-height:1.25"><tspan x="6.653" y="16.426" font-family="'Segoe UI'" font-size="10.634" font-weight="normal">C<tspan font-size="11.844">f</tspan></tspan></text></symbol><symbol viewBox="0 0 24 24" id="conduct" xmlns="http://www.w3.org/2000/svg"><path d="M10 17l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9m-6-6a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#cddc39"/></symbol><symbol viewBox="0 0 24 24" id="console" xmlns="http://www.w3.org/2000/svg"><path d="M20 19V7H4v12h16m0-16a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16m-7 14v-2h5v2h-5m-3.42-4L5.57 9H8.4l3.3 3.3c.39.39.39 1.03 0 1.42L8.42 17H5.59z" fill="#ff7043"/></symbol><symbol viewBox="0 0 24 24" id="contributing" xmlns="http://www.w3.org/2000/svg"><path d="M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="cpp" xmlns="http://www.w3.org/2000/svg"><path d="M10.5 15.97l.41 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96C1.56 15.77 1 14.16 1 12.21c.05-2.31.72-4.08 2-5.32C4.32 5.64 5.96 5 7.94 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.58 2.49-1.06-.34c-.4-.1-.86-.15-1.39-.15-1.16-.01-2.12.36-2.87 1.1-.76.73-1.15 1.85-1.18 3.34 0 1.36.37 2.42 1.08 3.2.71.77 1.71 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.1-.32M11 11h2V9h2v2h2v2h-2v2h-2v-2h-2v-2m7 0h2V9h2v2h2v2h-2v2h-2v-2h-2v-2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="credits" xmlns="http://www.w3.org/2000/svg"><path d="M3 3h18v2H3V3m4 4h10v2H7V7m-4 4h18v2H3v-2m4 4h10v2H7v-2m-4 4h18v2H3v-2z" fill="#9ccc65"/></symbol><symbol viewBox="0 0 200 200" id="crystal" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:none}</style><path d="M179.363 121.67l-57.623 57.507c-.23.23-.576.346-.806.23l-78.713-21.09c-.346-.115-.577-.345-.577-.576L20.44 79.144c-.115-.345 0-.576.23-.806L78.294 20.83c.23-.23.576-.346.807-.23l78.713 21.09c.345.114.576.345.576.575l21.09 78.597c.23.346.115.577-.115.807zM102.148 59.09l-77.33 20.63c-.115 0-.23.23-.115.345l56.586 56.47c.115.115.346.115.346-.115l20.744-77.215c.115 0-.115-.23-.23-.116z" stroke-width="1.153" fill="#cfd8dc"/></symbol><symbol viewBox="0 0 200 200" id="crystal_light" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:none}</style><path d="M179.363 121.67l-57.623 57.507c-.23.23-.576.346-.806.23l-78.713-21.09c-.346-.115-.577-.345-.577-.576L20.44 79.144c-.115-.345 0-.576.23-.806L78.294 20.83c.23-.23.576-.346.807-.23l78.713 21.09c.345.114.576.345.576.575l21.09 78.597c.23.346.115.577-.115.807zM102.148 59.09l-77.33 20.63c-.115 0-.23.23-.115.345l56.586 56.47c.115.115.346.115.346-.115l20.744-77.215c.115 0-.115-.23-.23-.116z" fill="#37474f" stroke-width="1.153"/></symbol><symbol viewBox="0 0 24 24" id="csharp" xmlns="http://www.w3.org/2000/svg"><path d="M11.5 15.97l.41 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96C2.56 15.77 2 14.16 2 12.21c.05-2.31.72-4.08 2-5.32C5.32 5.64 6.96 5 8.94 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.58 2.49-1.06-.34c-.4-.1-.86-.15-1.39-.15-1.16-.01-2.12.36-2.87 1.1-.76.73-1.15 1.85-1.18 3.34 0 1.36.37 2.42 1.08 3.2.71.77 1.71 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.1-.32M13.89 19l.61-4H13l.34-2h1.5l.32-2h-1.5L14 9h1.5l.61-4h2l-.61 4h1l.61-4h2l-.61 4H22l-.34 2h-1.5l-.32 2h1.5L21 15h-1.5l-.61 4h-2l.61-4h-1l-.61 4h-2m2.95-6h1l.32-2h-1l-.32 2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="css" xmlns="http://www.w3.org/2000/svg"><path d="M5 3l-.65 3.34h13.59L17.5 8.5H3.92l-.66 3.33h13.59l-.76 3.81-5.48 1.81-4.75-1.81.33-1.64H2.85l-.79 4 7.85 3 9.05-3 1.2-6.03.24-1.21L21.94 3H5z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="css-map" xmlns="http://www.w3.org/2000/svg"><path d="M18 8v2h2v10H10v-2H8v4h14V8h-4z" fill="#42a5f5"/><path d="M4.676 3l-.488 2.51h10.211l-.33 1.623H3.864l-.496 2.502H13.58l-.57 2.863-4.119 1.36-3.569-1.36.248-1.232H3.06l-.593 3.005 5.898 2.254 6.8-2.254.902-4.53.18-.91L17.406 3H4.675z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 33 33" id="cucumber" xmlns="http://www.w3.org/2000/svg"><title>cucumber-mark-transparent-pips</title><g transform="translate(0 -5)" fill="none" fill-rule="evenodd"><path d="M-4-1h40v40H-4z"/><path d="M16.641 7.092c-7.028 0-12.714 5.686-12.714 12.714 0 6.187 4.435 11.327 10.288 12.471v3.64C21.824 34.77 28.561 28.73 29.063 20.8c.303-4.772-2.076-9.644-6.09-12.01a10.575 10.575 0 0 0-1.455-.728l-.243-.097c-.223-.082-.448-.175-.68-.242a12.614 12.614 0 0 0-3.954-.632zm2.62 4.707a1.387 1.387 0 0 0-1.213.485c-.233.31-.379.611-.534.923-.466 1.087-.31 2.251.388 3.105 1.087-.233 2.01-.927 2.475-2.014a2.45 2.45 0 0 0 .243-1.02c.048-.824-.634-1.404-1.359-1.479zm-5.654.073c-.708.068-1.382.63-1.382 1.407 0 .31.087.709.243 1.02.466 1.086 1.46 1.78 2.546 2.013.621-.854.782-2.018.316-3.105-.155-.311-.3-.617-.534-.85a1.364 1.364 0 0 0-1.188-.485zm-3.809 3.735c-1.224.063-1.77 1.602-.752 2.402.31.233.612.403.922.559 1.087.466 2.344.306 3.275-.316-.233-1.009-1.023-1.936-2.11-2.402-.388-.155-.703-.243-1.092-.243-.087-.009-.161-.004-.243 0zm11.961 4.708a3.551 3.551 0 0 0-2.013.582c.233 1.01 1.023 1.936 2.11 2.401.389.156.705.244 1.093.244 1.397.077 2.08-1.65.994-2.427-.31-.233-.611-.379-.922-.534a3.354 3.354 0 0 0-1.262-.266zm-10.603.072a3.376 3.376 0 0 0-1.261.267c-.389.155-.69.325-.923.558-1.009.854-.33 2.48 1.068 2.402.388 0 .782-.087 1.092-.243 1.087-.465 1.859-1.392 2.014-2.401a3.474 3.474 0 0 0-1.99-.582zm3.931 2.378c-1.087.233-2.009.927-2.475 2.014-.155.31-.243.684-.243.995-.077 1.32 1.724 2.028 2.5 1.02.233-.312.378-.613.534-.923.466-1.01.306-2.174-.316-3.106zm2.887.073c-.621.854-.781 2.019-.315 3.106.155.31.3.615.534.848.854.932 2.65.243 2.572-.921 0-.31-.088-.71-.243-1.02-.466-1.087-1.46-1.78-2.547-2.013z" fill="#4caf50" stroke-width=".776"/></g></symbol><symbol id="cuda" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><style>.bust0{fill:#76b900}</style><title>NVIDIA-Logo</title><path id="buEye_Mark" class="bust0" d="M76.362 75.199V64.116c1.095-.068 2.19-.137 3.284-.137 30.377-.958 50.286 26.135 50.286 26.135s-21.483 29.83-44.539 29.83c-3.079 0-6.089-.48-8.962-1.438v-33.66c11.836 1.436 14.23 6.636 21.277 18.471l15.804-13.273s-11.562-15.12-30.992-15.12c-2.053-.068-4.105.069-6.158.274m0-36.67v16.556l3.284-.205c42.213-1.437 69.784 34.618 69.784 34.618s-31.608 38.45-64.516 38.45c-2.873 0-5.678-.274-8.483-.753v10.262c2.326.274 4.72.48 7.046.48 30.65 0 52.817-15.668 74.3-34.14 3.558 2.874 18.13 9.784 21.14 12.794-20.388 17.104-67.937 30.856-94.893 30.856-2.6 0-5.062-.137-7.525-.41v14.436h116.44V38.532zm0 79.977v8.757C48.038 122.2 40.17 92.712 40.17 92.712s13.615-15.05 36.192-17.514v9.579h-.068c-11.836-1.437-21.14 9.646-21.14 9.646s5.268 18.678 21.209 24.082M26.077 91.481S42.839 66.714 76.43 64.115v-9.03C39.213 58.094 7.057 89.565 7.057 89.565s18.199 52.68 69.305 57.47v-9.579c-37.492-4.652-50.286-45.975-50.286-45.975z" fill="#8bc34a" stroke-width=".684"/></symbol><symbol viewBox="0 0 24 24" id="dart" xmlns="http://www.w3.org/2000/svg"><title>Dart</title><path d="M12.486 1.385a.978.978 0 0 0-.682.281l-.01.007-6.387 3.692 6.371 6.372v.004l7.659 7.659 1.46-2.63-5.265-12.64-2.456-2.457a.972.972 0 0 0-.69-.288z" fill="#00ca94"/><path d="M5.422 5.35L1.73 11.733l-.007.01a.967.967 0 0 0 .006 1.371l3.059 3.061 11.963 4.706 2.704-1.502-.073-.073-.018.002-7.5-7.512h-.01L5.423 5.35z" fill="#1565c0"/><path d="M5.405 5.353l6.518 6.525h.01l7.502 7.51 2.855-.544.005-8.449-3.016-2.955c-.66-.647-1.675-1.064-2.695-1.202l.002-.032-11.181-.853z" fill="#1565c0"/><path d="M5.414 5.361l6.521 6.522v.009l7.506 7.506-.546 2.855h-8.448l-2.954-3.017c-.647-.66-1.064-1.676-1.2-2.696l-.033.003L5.414 5.36z" fill="#00ee94"/></symbol><symbol viewBox="0 0 24 24" id="database" xmlns="http://www.w3.org/2000/svg"><path d="M12 3C7.58 3 4 4.79 4 7s3.58 4 8 4 8-1.79 8-4-3.58-4-8-4M4 9v3c0 2.21 3.58 4 8 4s8-1.79 8-4V9c0 2.21-3.58 4-8 4s-8-1.79-8-4m0 5v3c0 2.21 3.58 4 8 4s8-1.79 8-4v-3c0 2.21-3.58 4-8 4s-8-1.79-8-4z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="diff" xmlns="http://www.w3.org/2000/svg"><path d="M3 1c-1.11 0-2 .89-2 2v11c0 1.11.89 2 2 2h2v-2H3V3h11v2h2V3c0-1.11-.89-2-2-2H3m6 6c-1.11 0-2 .89-2 2v2h2V9h2V7H9m4 0v2h1v1h2V7h-3m5 0v2h2v11H9v-2H7v2c0 1.11.89 2 2 2h11c1.11 0 2-.89 2-2V9c0-1.11-.89-2-2-2h-2m-4 5v2h-2v2h2c1.11 0 2-.89 2-2v-2h-2m-7 1v3h3v-2H9v-1H7z" fill="#42a5f5"/></symbol><symbol id="docker" viewBox="0 0 41 34.5" xmlns="http://www.w3.org/2000/svg"><style id="bystyle2">.byst0{fill:#fff}.byst1{clip-path:url(#bySVGID_4_)}</style><g id="byg34" transform="translate(.292 1.9)" fill="#0087c9"><g id="byg32"><g id="byg30"><g id="byg28"><g id="byg26"><g id="byg9"><path id="bySVGID_1_" class="byst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2c1.2 0 2.1.9 2.1 2s-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></g></g></g></g></g></g></symbol><symbol viewBox="0 0 24 24" id="document" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m9 16v-2H6v2h9m3-4v-2H6v2h12z" fill="#42a5f5"/></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 200 200" id="drone" xmlns="http://www.w3.org/2000/svg"><g fill="#e0e0e0" transform="translate(9.063 22.346) scale(.71044)"><path d="M128.22.723C32.095.723.39 84.566.39 115.222h77.928S89.36 75.275 128.22 75.275s49.906 39.947 49.906 39.947h77.476c0-30.66-31.257-114.5-127.38-114.5m98.82 134.45h-48.914s-8.55 39.946-49.906 39.946c-41.355 0-49.902-39.948-49.902-39.948H30.255c0 10.25 37.727 82.708 98.443 82.708 60.714 0 98.344-59.604 98.344-82.708"/><circle cx="128" cy="126.08" r="32.768"/></g></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 200 200" id="drone_light" xmlns="http://www.w3.org/2000/svg"><g fill="#424242" transform="translate(9.063 22.346) scale(.71044)"><path d="M128.22.723C32.095.723.39 84.566.39 115.222h77.928S89.36 75.275 128.22 75.275s49.906 39.947 49.906 39.947h77.476c0-30.66-31.257-114.5-127.38-114.5m98.82 134.45h-48.914s-8.55 39.946-49.906 39.946c-41.355 0-49.902-39.948-49.902-39.948H30.255c0 10.25 37.727 82.708 98.443 82.708 60.714 0 98.344-59.604 98.344-82.708"/><circle cx="128" cy="126.08" r="32.768"/></g></symbol><symbol viewBox="0 0 3473 3473" id="editorconfig" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" xmlns="http://www.w3.org/2000/svg"><defs id="ccdefs4"><style id="ccstyle2">.ccfil2{fill:#020202}.ccfil0{fill:#e3e3f8}.ccfil5{fill:#efefef}.ccfil6{fill:#faf1f1}.ccfil3{fill:#fdf2f2}.ccfil1{fill:#fdfdfd}.ccfil4{fill:#fef3f3}</style></defs><g id="ccLayer_x0020_1" transform="matrix(.8945 0 0 .8945 138.649 275.985)"><g id="cc_631799120"><g id="ccg11"><path class="ccfil0" d="M967 1895c46-30 84-105 61-158-63 27-60 89-61 158z" id="ccpath7" fill="#e3e3f8"/><path class="ccfil0" d="M1679 2067c50-16 98-72 71-130-39 27-64 64-71 130z" id="ccpath9" fill="#e3e3f8"/></g><g id="ccg21"><path class="ccfil1" d="M280 2895c0 63 16 131 60 155 162 91 730 20 923-23 101-23 183-98 278-139 214-93 369-168 540-293 124-91 321-347 342-500l-169-38c-4 172-43 211-196 251-103 28-304 34-409 16-139-23-202-96-265-179-122-162 27-275-166-286-203 249-561 70-718 45-67 97-224 727-222 871 97-33 158 3 245 37 308 119 39 224-84 193-84-20-110-75-159-110z" id="ccpath13" fill="#fdfdfd"/><path class="ccfil1" d="M683 1458c125 24 236 76 342 129 173 86 204 74 220 194 2 22-2 34 61 54 106 33-61-26 223-25 169 1 556 69 681 148 52 33 42 75 218 70-2-207-57-516-138-706-99-230-230-265-497-351-156-50-614-105-756-17-133 83-158 182-282 356-36 51-49 90-72 148z" id="ccpath15" fill="#fdfdfd"/><path class="ccfil1" d="M1784 1883c100 41-5 306-144 242-45-127 62-199 91-256-60-9-231-36-282-17-66 25-81 166-47 232 160 314 867 247 792 3-30-99-58-115-159-149-81-27-162-55-251-55z" id="ccpath17" fill="#fdfdfd"/><path class="ccfil1" d="M527 1848c80 77 261 89 378 95 15-155 28-271 152-262 61 83 29 181-35 244 109-1 172-83 156-202-92-66-371-198-511-217-39 42-135 272-140 342z" id="ccpath19" fill="#fdfdfd"/></g><path class="ccfil2" d="M339 2838c66-6 238 44 252 100-107 13-243 3-252-100zm-59 57c49 35 75 90 159 110 123 31 392-74 84-193-87-34-148-70-245-37-2-144 155-774 222-871 157 25 515 204 718-45 193 11 44 124 166 286 63 83 126 156 265 179 105 18 306 12 409-16 153-40 192-79 196-251l169 38c-21 153-218 409-342 500-171 125-326 200-540 293-95 41-177 116-278 139-193 43-761 114-923 23-44-24-60-92-60-155zm1399-828c7-66 32-103 71-130 27 58-21 114-71 130zm105-184c89 0 170 28 251 55 101 34 129 50 159 149 75 244-632 311-792-3-34-66-19-207 47-232 51-19 222 8 282 17-29 57-136 129-91 256 139 64 244-201 144-242zm-817 12c1-69-2-131 61-158 23 53-15 128-61 158zm-440-47c5-70 101-300 140-342 140 19 419 151 511 217 16 119-47 201-156 202 64-63 96-161 35-244-124-9-137 107-152 262-117-6-298-18-378-95zm-100-80c-37-102-37-261 120-274l-80 223c-21 48-21 37-40 51zm256-310c23-58 36-97 72-148 124-174 149-273 282-356 142-88 600-33 756 17 267 86 398 121 497 351 81 190 136 499 138 706-176 5-166-37-218-70-125-79-512-147-681-148-284-1-117 58-223 25-63-20-59-32-61-54-16-120-47-108-220-194-106-53-217-105-342-129zm1770-49c-19-63 16-59 77-102 35-25 63-51 106-75 161-90 461-105 589 2 52 43 137 127 124 237-27 219-177 339-300 439-125 102-333 207-548 137-18-44-4-323-25-426-19-92-9-102 44-157 156-162 494-280 686-141 81 60 58 83 100 129 52-56-45-244-403-232-243 8-348 198-450 189zM997 840c5-139 133-427 261-527 155-120 317-233 555-98 59 33 56 50 62 132 5 79-2 108-22 172-158 510-290 217-796 338 19-166 163-314 243-391 137-133 236-219 442-191 57 95 63 155-6 266-92 148-115 139-101 240 72-18 94-88 127-158 201-420-91-471-270-394-120 51-334 287-404 429-14 28-29 64-42 95zm792 21c21-125 145-156 145-541 0-166-204-315-471-204-229 94-264 166-386 350-115 174-111 365-210 526-29 46-55 62-87 108-23 34-40 77-63 117-47 77-95 133-133 225-120 3-221 5-233 129-16 170 64 212 64 276-1 69-281 765-203 1180 22 114 97 115 217 129 289 35 664 23 923-81l470-225c119-67 319-194 408-287 63-65 96-120 150-197 74-108 76-106 92-253 98 18 281 61 342 114-7 69-41 36-41 98 39 1 104-48 120-102-41-60-84-50-143-98 47-37 132-54 197-81 140-58 379-234 438-394 47-129 12-344-64-428-80-88-266-133-418-133-181 0-368 130-514 186-56-49-60-105-101-159-47-64-353-224-499-255z" id="ccpath23" fill="#020202"/><path class="ccfil3" d="M2453 1409c102 9 207-181 450-189 358-12 455 176 403 232-42-46-19-69-100-129-192-139-530-21-686 141-53 55-63 65-44 157 21 103 7 382 25 426 215 70 423-35 548-137 123-100 273-220 300-439 13-110-72-194-124-237-128-107-428-92-589-2-43 24-71 50-106 75-61 43-96 39-77 102z" id="ccpath25" fill="#fdf2f2"/><path class="ccfil4" d="M997 840l49-87c13-31 28-67 42-95 70-142 284-378 404-429 179-77 471-26 270 394-33 70-55 140-127 158-14-101 9-92 101-240 69-111 63-171 6-266-206-28-305 58-442 191-80 77-224 225-243 391 506-121 638 172 796-338 20-64 27-93 22-172-6-82-3-99-62-132-238-135-400-22-555 98-128 100-256 388-261 527z" id="ccpath27" fill="#fef3f3"/><path class="ccfil5" d="M427 1768c19-14 19-3 40-51l80-223c-157 13-157 172-120 274z" id="ccpath29" fill="#efefef"/><path class="ccfil6" d="M591 2938c-14-56-186-106-252-100 9 103 145 113 252 100z" id="ccpath31" fill="#faf1f1"/></g></g></symbol><symbol viewBox="0 0 24 24" id="elixir" xmlns="http://www.w3.org/2000/svg"><path d="M12.431 22.383c-3.86 0-6.99-3.64-6.99-8.13 0-3.678 2.774-8.172 4.916-10.91 1.014-1.295 2.931-2.321 2.931-2.321s-.982 5.238 1.683 7.318c2.365 1.847 4.105 4.25 4.105 6.363 0 4.232-2.784 7.68-6.645 7.68z" fill="#9575cd" stroke-width="1.256"/></symbol><symbol viewBox="0 0 323.00001 322.99999" id="elm" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.8053 0 0 .8053 30.106 31.524)"><path fill="#f0ad00" d="M160.8 153.865l68.028-68.03H92.77z"/><path fill="#7fd13b" d="M160.983 5.098H12.033l68.524 68.525H229.51z"/><path fill="#7fd13b" stroke-width=".974" d="M243.906 88.021l74.136 74.137-74.474 74.475-74.137-74.137z"/><path fill="#60b5cc" d="M318.2 145.045V5.098H178.252z"/><path fill="#5a6378" d="M152.164 162.499L3.4 13.733v297.533z"/><path fill="#f0ad00" d="M252.205 245.27l65.995 65.996v-131.99z"/><path fill="#60b5cc" d="M160.8 171.134L12.034 319.899h297.53z"/></g></symbol><symbol viewBox="0 0 24 24" id="email" xmlns="http://www.w3.org/2000/svg"><path d="M20 8l-8 5-8-5V6l8 5 8-5m0-2H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 30 30" id="erlang" xmlns="http://www.w3.org/2000/svg"><path style="line-height:1.25;-inkscape-font-specification:'Wide Latin'" d="M5.217 4.367c-.048.052-.097.1-.144.153C2.697 7.182 1.51 10.798 1.51 15.366c0 4.418 1.156 7.862 3.46 10.34h19.414c2.553-1.152 4.127-3.43 4.127-3.43l-3.147-2.52-1.454 1.381c-.866.773-.845.931-2.314 1.78-1.496.674-3.04.966-4.634.966-2.516 0-4.423-.909-5.723-2.059-1.286-1.15-1.985-4.511-2.097-6.68l17.458.067-.182-1.472s-.847-7.129-2.542-9.372zm8.76.846c1.565 0 3.22.535 3.96 1.471.742.937.932 1.667.974 3.524H9.12c.111-1.955.436-2.81 1.372-3.697.937-.888 2.03-1.298 3.484-1.298z" font-weight="400" font-size="48" font-family="Wide Latin" letter-spacing="0" word-spacing="0" fill="#f44336" stroke-width=".97"/></symbol><symbol viewBox="0 0 299.99999 300.00001" id="eslint" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-2.88 18.438) scale(1.0344)"><path d="M97.021 99.016l48.432-27.962c1.212-.7 2.706-.7 3.918 0l48.433 27.962a3.92 3.92 0 0 1 1.959 3.393v55.924a3.924 3.924 0 0 1-1.959 3.394l-48.433 27.962c-1.212.7-2.706.7-3.918 0l-48.432-27.962a3.92 3.92 0 0 1-1.959-3.394v-55.924a3.922 3.922 0 0 1 1.959-3.393" fill="#7986cb"/><path d="M273.34 124.49L215.473 23.82c-2.102-3.64-5.985-6.325-10.188-6.325H89.545c-4.204 0-8.088 2.685-10.19 6.325L21.488 124.27c-2.102 3.641-2.102 8.236 0 11.877l57.867 99.847c2.102 3.64 5.986 5.501 10.19 5.501h115.74c4.203 0 8.087-1.805 10.188-5.446l57.867-100.01c2.104-3.639 2.104-7.907.001-11.547m-47.917 48.41c0 1.48-.891 2.849-2.174 3.59l-73.71 42.527a4.194 4.194 0 0 1-4.17 0l-73.767-42.527c-1.282-.741-2.179-2.109-2.179-3.59V87.847c0-1.481.884-2.849 2.167-3.59l73.707-42.527a4.185 4.185 0 0 1 4.168 0l73.772 42.527c1.283.741 2.186 2.109 2.186 3.59z" fill="#3f51b5"/></g></symbol><symbol viewBox="0 0 24 24" id="exe" xmlns="http://www.w3.org/2000/svg"><path d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h14m0 14V8H5v10h14z" fill="#e64a19"/></symbol><symbol viewBox="0 0 24 24" id="favicon" xmlns="http://www.w3.org/2000/svg"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.62L12 2 9.19 8.62 2 9.24l5.45 4.73L5.82 21 12 17.27z" fill="#ffd54f"/></symbol><symbol viewBox="0 0 24 24" id="file" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m5 2H6v16h12v-9h-7V4z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 400 400" id="firebase" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 103)"><path d="M72.55 208.77l44.456-292.29 56.209 90.445L195.49-37.57 330.6 209.28z" fill="#ffa712"/><path d="M195.7 276.73l134.9-67.45-46.5-224.83L72.55 208.77z" fill="#fcca3f"/><path d="M173.22 6.932L72.56 208.772l136.35-144.58z" fill="#f6820c"/></g></symbol><symbol viewBox="0 0 24 24" id="flash" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="cma"><stop offset="0" stop-color="#d92f3c"/><stop offset="1" stop-color="#791223"/></linearGradient><linearGradient xlink:href="#cma" id="cmb" x1="2.373" y1="12.027" x2="21.86" y2="12.027" gradientUnits="userSpaceOnUse" gradientTransform="translate(-.09 -24.144)"/></defs><rect width="19.487" height="19.487" x="2.283" y="-21.86" transform="rotate(90)" ry="0" fill="url(#cmb)"/><path style="line-height:125%" d="M16.802 5.768l-.013.002a6.43 6.43 0 0 0-1.182.192 5.062 5.062 0 0 0-1.494.718c-.428.323-.817.72-1.17 1.191-.34.48-.682 1.032-1.022 1.66-.12.228-.233.424-.35.636v.002h-.004l-1.34 2.394-.005-.002c-.238.443-.461.847-.665 1.198a4.358 4.358 0 0 1-.716.94 2.79 2.79 0 0 1-.907.594c-.072.027-.161.042-.242.063h-.989v2.414h.989v-.002a6.427 6.427 0 0 0 1.185-.192 5.062 5.062 0 0 0 1.494-.718 5.94 5.94 0 0 0 1.171-1.191c.34-.48.681-1.033 1.021-1.66.12-.228.235-.425.353-.637l.006.002.003-.005.037-.066h2.53v.002h1.124v-2.408h-.33v-.001h-1.98c.22-.407.432-.789.621-1.115.214-.37.452-.682.717-.94a2.79 2.79 0 0 1 .906-.594c.07-.027.16-.041.239-.061h.992V8.18h-.002V5.77h-.977v-.002z" font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#fff"/></symbol><symbol class="cnflow-logo" viewBox="0 0 299.99999 300" id="flow" xmlns="http://www.w3.org/2000/svg"><title>Flow logo</title><path d="M38.75 33.427l77.461 77.47H54.436l61.145 61.16H38.437l93.462 93.478v-77.158l.01-.01v-77.47h-.01V66.982h46.691l20.394 20.393H153.57v76.531h22.05l24.474 24.473h-15.806l-.01-.01v.01h-31.665l-.01-.01v.01h-.313l.313.313v77.148h109.149l-39.2-39.2v-15.806l8.465 8.466v-77.37h-15.682l.017-38.191 30.09 30.086V56.362h-64.874l-22.94-22.934H113.71z" fill="#fbc02d" fill-opacity=".976" stroke-width=".955" class="cnflow-logo-mark"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-aurelia" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="coa" x1="-388.15%" x2="237.68%" y1="-144.18%" y2="430.41%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cob" x1="72.945%" x2="-97.052%" y1="84.424%" y2="-147.7%"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="coc" x1="-283.88%" x2="287.54%" y1="-693.6%" y2="101.71%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cod" x1="-821.19%" x2="101.99%" y1="-469.05%" y2="288.24%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="coe" x1="-140.36%" x2="419.01%" y1="-230.93%" y2="261.98%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cof" x1="191.08%" x2="20.358%" y1="253.95%" y2="20.403%"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cog" x1="-388.09%" x2="237.67%" y1="-173.85%" y2="518.99%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="coi" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#coa"/><linearGradient id="coj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#cob"/><linearGradient id="cok" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#coc"/><linearGradient id="col" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#cod"/><linearGradient id="com" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#coe"/><linearGradient id="con" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#cof"/><linearGradient id="coo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cog"/><linearGradient id="cop" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#coh"/><linearGradient id="coh" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#f06292" fill-rule="nonzero"/><g transform="matrix(.31022 .0619 -.0619 .31022 11.807 7.546)" fill="none"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#coi)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#coj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#cok)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#col)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#com)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#con)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#coo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#cop)"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-aurelia-open" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="cpi" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cpa"/><linearGradient id="cpa" x1="-3.881" x2="2.377" y1="-1.442" y2="4.304"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#cpb"/><linearGradient id="cpb" x1=".729" x2="-.971" y1=".844" y2="-1.477"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cpk" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#cpc"/><linearGradient id="cpc" x1="-2.839" x2="2.875" y1="-6.936" y2="1.017"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpl" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#cpd"/><linearGradient id="cpd" x1="-8.212" x2="1.02" y1="-4.691" y2="2.882"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpm" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#cpe"/><linearGradient id="cpe" x1="-1.404" x2="4.19" y1="-2.309" y2="2.62"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpn" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#cpf"/><linearGradient id="cpf" x1="1.911" x2=".204" y1="2.539" y2=".204"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cpo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cpg"/><linearGradient id="cpg" x1="-3.881" x2="2.377" y1="-1.738" y2="5.19"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpp" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#cph"/><linearGradient id="cph" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#f06292" fill-rule="nonzero"/><g transform="matrix(.31022 .0619 -.0619 .31022 11.807 7.546)" fill="none"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#cpi)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#cpj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#cpk)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#cpl)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#cpm)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#cpn)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#cpo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#cpp)"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-components" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#cddc39" fill-rule="nonzero"/><path d="M11.185 9.613h5.346v2.9l3.782-3.775 3.775 3.775-3.775 3.782h2.9v5.346h-5.346v-5.346h2.446l-3.782-3.782v2.446h-5.346V9.613m0 6.682h5.346v5.346h-5.346z" fill="#f0f4c3" stroke-width=".668"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-components-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#cddc39"/><path d="M11.185 9.613h5.346v2.9l3.782-3.775 3.775 3.775-3.775 3.782h2.9v5.346h-5.346v-5.346h2.446l-3.782-3.782v2.446h-5.346V9.613m0 6.682h5.346v5.346h-5.346z" fill="#f0f4c3" stroke-width=".668"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-config" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00acc1" fill-rule="nonzero"/><path d="M17.293 17.786a2.308 2.308 0 0 1-2.308-2.308 2.308 2.308 0 0 1 2.308-2.307 2.308 2.308 0 0 1 2.308 2.307 2.308 2.308 0 0 1-2.308 2.308m4.899-1.668c.026-.211.046-.422.046-.64 0-.217-.02-.435-.046-.659l1.391-1.075a.333.333 0 0 0 .08-.422l-1.32-2.28c-.079-.146-.257-.205-.402-.146l-1.641.66a4.779 4.779 0 0 0-1.115-.647l-.244-1.747a.333.333 0 0 0-.33-.277h-2.637a.333.333 0 0 0-.33.277l-.243 1.747a4.78 4.78 0 0 0-1.114.646l-1.642-.659a.324.324 0 0 0-.402.145l-1.319 2.281a.325.325 0 0 0 .08.422l1.39 1.075c-.026.224-.046.442-.046.66s.02.428.046.639l-1.39 1.094a.325.325 0 0 0-.08.422l1.319 2.282c.079.145.257.197.402.145l1.642-.666c.342.264.698.488 1.114.653l.244 1.747a.333.333 0 0 0 .33.277h2.637a.333.333 0 0 0 .33-.277l.243-1.747a4.802 4.802 0 0 0 1.115-.653l1.641.666c.145.052.323 0 .403-.145l1.318-2.282a.333.333 0 0 0-.079-.422z" fill="#80deea" stroke-width=".659"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-config-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00acc1"/><path d="M17.293 17.786a2.308 2.308 0 0 1-2.308-2.308 2.308 2.308 0 0 1 2.308-2.307 2.308 2.308 0 0 1 2.308 2.307 2.308 2.308 0 0 1-2.308 2.308m4.899-1.668c.026-.211.046-.422.046-.64 0-.217-.02-.435-.046-.659l1.391-1.075a.333.333 0 0 0 .08-.422l-1.32-2.28c-.079-.146-.257-.205-.402-.146l-1.641.66a4.779 4.779 0 0 0-1.115-.647l-.244-1.747a.333.333 0 0 0-.33-.277h-2.637a.333.333 0 0 0-.33.277l-.243 1.747a4.78 4.78 0 0 0-1.114.646l-1.642-.659a.324.324 0 0 0-.402.145l-1.319 2.281a.325.325 0 0 0 .08.422l1.39 1.075c-.026.224-.046.442-.046.66s.02.428.046.639l-1.39 1.094a.325.325 0 0 0-.08.422l1.319 2.282c.079.145.257.197.402.145l1.642-.666c.342.264.698.488 1.114.653l.244 1.747a.333.333 0 0 0 .33.277h2.637a.333.333 0 0 0 .33-.277l.243-1.747a4.802 4.802 0 0 0 1.115-.653l1.641.666c.145.052.323 0 .403-.145l1.318-2.282a.333.333 0 0 0-.079-.422z" fill="#80deea" stroke-width=".659"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-css" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#42a5f5" fill-rule="nonzero"/><path d="M12.488 9.415l-.44 2.259h9.188l-.298 1.46h-9.18l-.447 2.251H20.5l-.514 2.576-3.705 1.224-3.211-1.224.223-1.109h-2.258l-.534 2.704 5.307 2.029 6.118-2.029.812-4.076.162-.818 1.041-5.247H12.488z" fill-rule="nonzero" fill="#bbdefb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-css-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#42a5f5" fill-rule="nonzero"/><path d="M12.488 9.415l-.44 2.259h9.188l-.298 1.46h-9.18l-.447 2.251H20.5l-.514 2.576-3.705 1.224-3.211-1.224.223-1.109h-2.258l-.534 2.704 5.307 2.029 6.118-2.029.812-4.076.162-.818 1.041-5.247H12.488z" fill-rule="nonzero" fill="#bbdefb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-dist" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#e57373" fill-rule="nonzero"/><path d="M18.575 11.113h-2.576V9.825h2.576m3.864 1.288h-2.576V9.825l-1.288-1.288h-2.576L14.71 9.825v1.288h-2.577c-.715 0-1.288.573-1.288 1.288v7.085a1.288 1.288 0 0 0 1.288 1.288H22.44a1.288 1.288 0 0 0 1.288-1.288V12.4c0-.715-.58-1.288-1.288-1.288z" fill="#ffcdd2" stroke-width=".644"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-dist-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#e57373"/><path d="M18.575 11.113h-2.576V9.825h2.576m3.864 1.288h-2.576V9.825l-1.288-1.288h-2.576L14.71 9.825v1.288h-2.577c-.715 0-1.288.573-1.288 1.288v7.085a1.288 1.288 0 0 0 1.288 1.288H22.44a1.288 1.288 0 0 0 1.288-1.288V12.4c0-.715-.58-1.288-1.288-1.288z" fill="#ffcdd2" fill-rule="evenodd" stroke-width=".644"/></symbol><symbol id="folder-docker" clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><defs id="cydefs10"><path id="cySVGID_2_" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></defs><path id="cypath2" d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><style id="cystyle2">.cyst0{fill:#fff}.cyst1{clip-path:url(#cySVGID_4_)}</style><g id="cyg34" transform="translate(8.319 9.626) scale(.39491)" fill="#b3e5fc"><g id="cyg32"><g id="cyg30"><title id="cytitle4">Group 3</title><g id="cyg28"><g id="cyg26"><g id="cyg9"><path id="cySVGID_1_" class="cyst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></g><g id="cyg24"><clipPath id="cySVGID_4_"><use id="cyuse14" width="100%" height="100%" xlink:href="#cySVGID_2_"/></clipPath><g id="cyg22" class="cyst1" clip-path="url(#cySVGID_4_)"><g id="cyg20"><g id="cyg18"><path id="cySVGID_3_" class="cyst0" d="M-48.8-21H1226v151.4H-48.8z"/></g></g></g></g></g></g></g></g></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docker-open" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="cza"><use width="100%" height="100%" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#SVGID_2_"/></clipPath></defs><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><g transform="matrix(.3949 0 0 .39489 8.319 9.626)" fill="#b3e5fc"><title>Group 3</title><path class="czst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/><g class="czst1" clip-path="url(#cza)"><path class="czst0" d="M-48.8-21H1226v151.4H-48.8z"/></g></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docs" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#0277bd" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m6.075 10.8v-1.35H13.85v1.35h6.075m2.025-2.7v-1.35h-8.1v1.35h8.1z" fill-rule="nonzero" fill="#b3e5fc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docs-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#0277bd" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m6.075 10.8v-1.35H13.85v1.35h6.075m2.025-2.7v-1.35h-8.1v1.35h8.1z" fill-rule="nonzero" fill="#b3e5fc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-expo" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#01579b" fill-rule="nonzero"/><style>.dcst0{fill:#1173b6}.st1{fill:#585d67}</style><path class="dcst0" d="M18.575 9.82c-.489-.745-.605-.844-1.6-.844h-.024c-.996 0-1.106.099-1.601.844-.46.699-5.024 9.058-5.024 9.291 0 .338.087.658.402 1.112.32.46.873.716 1.275.309.273-.274 3.201-5.321 4.616-7.23a.425.425 0 0 1 .693 0c1.414 1.909 4.343 6.956 4.616 7.23.402.407.955.15 1.275-.309.314-.454.402-.774.402-1.112-.006-.233-4.57-8.598-5.03-9.291z" fill="#1173b6" stroke-width=".058"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-expo-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#01579b"/><path class="ddst0" d="M18.575 9.82c-.489-.745-.605-.844-1.6-.844h-.024c-.996 0-1.106.099-1.601.844-.46.699-5.024 9.058-5.024 9.291 0 .338.087.658.402 1.112.32.46.873.716 1.275.309.273-.274 3.201-5.321 4.616-7.23a.425.425 0 0 1 .693 0c1.414 1.909 4.343 6.956 4.616 7.23.402.407.955.15 1.275-.309.314-.454.402-.774.402-1.112-.006-.233-4.57-8.598-5.03-9.291z" fill="#1173b6" stroke-width=".058" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-font" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef9a9a" fill-rule="nonzero"/><path d="M14.62 17.403l2.38-6.33 2.37 6.33m-3.37-9l-5.5 14h2.25l1.12-3h6.25l1.13 3h2.25l-5.5-14h-2z" fill="#f44336" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-font-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef9a9a" fill-rule="nonzero"/><path d="M14.62 17.403l2.38-6.33 2.37 6.33m-3.37-9l-5.5 14h2.25l1.12-3h6.25l1.13 3h2.25l-5.5-14h-2z" fill="#f44336" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-git" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ff8a65" fill-rule="nonzero"/><path d="M10.43 14.14l4.044-4.052 1.183 1.19a1.387 1.387 0 0 0 .65 1.56v3.877c-.42.238-.699.693-.699 1.21 0 .768.632 1.4 1.4 1.4.767 0 1.4-.632 1.4-1.4 0-.517-.28-.972-.7-1.21v-3.4l1.448 1.462c-.05.105-.05.224-.05.35 0 .767.633 1.399 1.4 1.399.768 0 1.4-.632 1.4-1.4 0-.767-.632-1.4-1.4-1.4-.126 0-.245 0-.35.05l-1.798-1.799a1.385 1.385 0 0 0-.805-1.637c-.3-.112-.615-.14-.895-.063l-1.19-1.183.553-.545a1.381 1.381 0 0 1 1.973 0l5.591 5.59a1.381 1.381 0 0 1 0 1.974l-5.59 5.591a1.381 1.381 0 0 1-1.974 0l-5.591-5.59a1.381 1.381 0 0 1 0-1.974z" fill="#e64a19" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-git-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ff8a65" fill-rule="nonzero"/><path d="M10.43 14.14l4.044-4.052 1.183 1.19a1.387 1.387 0 0 0 .65 1.56v3.877c-.42.238-.699.693-.699 1.21 0 .768.632 1.4 1.4 1.4.767 0 1.4-.632 1.4-1.4 0-.517-.28-.972-.7-1.21v-3.4l1.448 1.462c-.05.105-.05.224-.05.35 0 .767.633 1.399 1.4 1.399.768 0 1.4-.632 1.4-1.4 0-.767-.632-1.4-1.4-1.4-.126 0-.245 0-.35.05l-1.798-1.799a1.385 1.385 0 0 0-.805-1.637c-.3-.112-.615-.14-.895-.063l-1.19-1.183.553-.545a1.381 1.381 0 0 1 1.973 0l5.591 5.59a1.381 1.381 0 0 1 0 1.974l-5.59 5.591a1.381 1.381 0 0 1-1.974 0l-5.591-5.59a1.381 1.381 0 0 1 0-1.974z" fill="#e64a19" fill-rule="nonzero"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-global" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#5c6bc0" fill-rule="nonzero"/><path d="M21.132 18.585a1.22 1.22 0 0 0-1.156-.846h-.609v-1.825a.608.608 0 0 0-.608-.609h-3.65v-1.217h1.216a.608.608 0 0 0 .609-.608v-1.217h1.217a1.217 1.217 0 0 0 1.216-1.217v-.25a4.858 4.858 0 0 1 1.765 7.79m-4.198 1.545a4.86 4.86 0 0 1-4.26-4.826c0-.377.049-.742.128-1.089l2.915 2.915v.608a1.217 1.217 0 0 0 1.217 1.217m.608-9.735a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.085-6.085 6.085 6.085 0 0 0-6.085-6.084z" fill="#c5cae9" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-global-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#5c6bc0"/><path d="M21.133 18.585a1.22 1.22 0 0 0-1.156-.846h-.609v-1.825a.608.608 0 0 0-.608-.609h-3.65v-1.217h1.216a.608.608 0 0 0 .609-.608v-1.217h1.217a1.217 1.217 0 0 0 1.216-1.217v-.25a4.858 4.858 0 0 1 1.765 7.79m-4.198 1.545a4.86 4.86 0 0 1-4.26-4.826c0-.377.049-.742.128-1.089l2.915 2.915v.608a1.217 1.217 0 0 0 1.216 1.217m.609-9.735a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.085-6.085 6.085 6.085 0 0 0-6.085-6.084z" fill="#c5cae9" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-i18n" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#5c6bc0" fill-rule="nonzero"/><path d="M17.293 17.786l-1.53-1.512.018-.018a10.555 10.555 0 0 0 2.235-3.934h1.765v-1.205h-4.217V9.912h-1.205v1.205h-4.217v1.205h6.73a9.5 9.5 0 0 1-1.91 3.223 9.424 9.424 0 0 1-1.392-2.018h-1.205c.44.982 1.042 1.91 1.795 2.747l-3.067 3.024.856.856 3.012-3.013 1.874 1.874.458-1.229m3.392-3.054H19.48l-2.711 7.23h1.205l.674-1.808h2.862l.68 1.807h1.206l-2.711-7.23m-1.579 4.218l.976-2.609.976 2.609z" fill="#c5cae9" stroke-width=".602"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-i18n-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#5c6bc0"/><path d="M17.293 17.786l-1.53-1.512.018-.018a10.555 10.555 0 0 0 2.235-3.934h1.765v-1.205h-4.217V9.912h-1.205v1.205h-4.217v1.205h6.73a9.5 9.5 0 0 1-1.91 3.223 9.424 9.424 0 0 1-1.392-2.018h-1.205c.44.982 1.042 1.91 1.795 2.747l-3.067 3.024.856.856 3.012-3.013 1.874 1.874.458-1.229m3.392-3.054H19.48l-2.711 7.23h1.205l.674-1.808h2.862l.68 1.807h1.206l-2.711-7.23m-1.579 4.218l.976-2.609.976 2.609z" fill="#c5cae9" stroke-width=".602"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-images" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#009688" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m0 12.15h8.1v-5.4l-2.7 2.7-1.35-1.35-4.05 4.05m1.35-7.425c-.74 0-1.35.61-1.35 1.35s.61 1.35 1.35 1.35 1.35-.61 1.35-1.35-.61-1.35-1.35-1.35z" fill-rule="nonzero" fill="#b2dfdb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-images-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#009688" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m0 12.15h8.1v-5.4l-2.7 2.7-1.35-1.35-4.05 4.05m1.35-7.425c-.74 0-1.35.61-1.35 1.35s.61 1.35 1.35 1.35 1.35-.61 1.35-1.35-.61-1.35-1.35-1.35z" fill-rule="nonzero" fill="#b2dfdb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-include" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><path d="M20.788 15.981h-2.434v2.434h-1.217V15.98h-2.434v-1.217h2.434V12.33h1.217v2.434h2.434m-3.042-5.476a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.084-6.085 6.085 6.085 0 0 0-6.084-6.084z" fill="#b3e5fc" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-include-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><path d="M20.788 15.981h-2.434v2.434h-1.217V15.98h-2.434v-1.217h2.434V12.33h1.217v2.434h2.434m-3.042-5.476a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.084-6.085 6.085 6.085 0 0 0-6.084-6.084z" fill="#b3e5fc" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-javascript" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ffca28" fill-rule="nonzero"/><path d="M17.935 18.374a2.18 2.18 0 0 0 1.972 1.213c.829 0 1.354-.415 1.354-.987 0-.682-.542-.927-1.452-1.324l-.502-.216c-1.435-.613-2.404-1.378-2.404-3.005 0-1.5 1.167-2.638 2.917-2.638a2.957 2.957 0 0 1 2.842 1.599l-1.552.999a1.362 1.362 0 0 0-1.29-.858.873.873 0 0 0-.957.858c0 .583.374.84 1.226 1.213l.502.216c1.697.733 2.654 1.47 2.654 3.139 0 1.798-1.411 2.783-3.308 2.783a3.839 3.839 0 0 1-3.618-2.046zm-7.048.175c.315.583.583 1.027 1.283 1.027s1.066-.256 1.066-1.255v-6.774h1.998v6.804c0 2.064-1.214 3.01-2.982 3.01a3.104 3.104 0 0 1-2.993-1.826z" fill-rule="nonzero" fill="#ffecb3"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-javascript-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ffca28"/><path d="M17.935 18.374a2.18 2.18 0 0 0 1.972 1.213c.829 0 1.354-.415 1.354-.987 0-.682-.542-.927-1.452-1.324l-.502-.216c-1.435-.613-2.404-1.378-2.404-3.005 0-1.5 1.167-2.638 2.917-2.638a2.957 2.957 0 0 1 2.842 1.599l-1.552.999a1.362 1.362 0 0 0-1.29-.858.873.873 0 0 0-.957.858c0 .583.374.84 1.226 1.213l.502.216c1.697.733 2.654 1.47 2.654 3.139 0 1.798-1.412 2.783-3.308 2.783a3.839 3.839 0 0 1-3.618-2.046zm-7.048.175c.315.583.583 1.027 1.283 1.027s1.066-.256 1.066-1.255v-6.774h1.998v6.804c0 2.064-1.214 3.01-2.982 3.01a3.104 3.104 0 0 1-2.993-1.826z" fill="#ffecb3"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-lib" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#c0ca33" fill-rule="nonzero"/><path d="M17.39 12.544a2.05 2.05 0 0 0 2.05-2.05 2.05 2.05 0 0 0-2.05-2.052 2.05 2.05 0 0 0-2.05 2.051 2.05 2.05 0 0 0 2.05 2.051m0 2.42a8.992 8.992 0 0 0-6.152-2.42v7.52c2.392 0 4.539.923 6.152 2.42a8.992 8.992 0 0 1 6.152-2.42v-7.52c-2.392 0-4.539.923-6.152 2.42z" fill="#f0f4c3" stroke-width=".684"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-lib-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#c0ca33"/><path d="M17.391 12.543a2.05 2.05 0 0 0 2.05-2.05 2.05 2.05 0 0 0-2.05-2.052 2.05 2.05 0 0 0-2.05 2.051 2.05 2.05 0 0 0 2.05 2.051m0 2.42a8.992 8.992 0 0 0-6.152-2.42v7.52c2.392 0 4.539.923 6.152 2.42a8.992 8.992 0 0 1 6.152-2.42v-7.52c-2.392 0-4.539.923-6.152 2.42z" fill="#f0f4c3" stroke-width=".684"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-actions" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ab47bc" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#e1bee7" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-actions-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ab47bc"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#e1bee7" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-effects" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00bcd4" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#b2ebf2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-effects-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00bcd4"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#b2ebf2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef5350" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#ffcdd2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-reducer-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef5350"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#ffcdd2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-state" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#dcedc8" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-state-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#dcedc8" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-node" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.25 8.403c-.188 0-.382.048-.542.139l-5.166 2.986a1.096 1.096 0 0 0-.542.944v5.959c0 .388.208.75.542.944l1.354.778c.66.32.882.326 1.187.326.973 0 1.535-.59 1.535-1.618V12.98a.154.154 0 0 0-.153-.153h-.646c-.09 0-.16.07-.16.153v5.882c0 .458-.472.91-1.228.528l-1.424-.813a.181.181 0 0 1-.076-.145v-5.959c0-.062.027-.118.076-.146l5.167-2.979a.15.15 0 0 1 .152 0l5.167 2.98a.164.164 0 0 1 .076.145v5.959a.181.181 0 0 1-.076.145l-5.167 2.98c-.041.027-.11.027-.16 0l-1.305-.792c-.055-.021-.111-.028-.146-.007-.368.208-.437.25-.778.354-.083.028-.215.076.05.222l1.721 1.021c.167.097.348.146.542.146s.375-.049.542-.146l5.166-2.979c.334-.194.542-.556.542-.944v-5.959c0-.389-.208-.75-.542-.944l-5.166-2.986a1.103 1.103 0 0 0-.542-.14m1.389 4.272c-1.472 0-2.354.618-2.354 1.66 0 1.117.875 1.444 2.291 1.583 1.688.166 1.82.416 1.82.75 0 .576-.465.82-1.549.82-1.375 0-1.666-.341-1.77-1.022a.157.157 0 0 0-.153-.125h-.667c-.083 0-.146.063-.146.153 0 .861.472 1.903 2.736 1.903 1.632 0 2.57-.646 2.57-1.771 0-1.118-.75-1.41-2.34-1.625-1.605-.208-1.765-.32-1.765-.694 0-.313.14-.73 1.327-.73 1.042 0 1.451.23 1.611.945.014.07.076.118.146.118h.673a.134.134 0 0 0 .105-.049c.027-.028.048-.07.034-.11-.097-1.237-.916-1.806-2.57-1.806z" fill-rule="nonzero" fill="#f1f8e9"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-node-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.25 8.403c-.188 0-.382.048-.542.139l-5.166 2.986a1.096 1.096 0 0 0-.542.944v5.959c0 .388.208.75.542.944l1.354.778c.66.32.882.326 1.187.326.973 0 1.535-.59 1.535-1.618V12.98a.154.154 0 0 0-.153-.153h-.646c-.09 0-.16.07-.16.153v5.882c0 .458-.472.91-1.228.528l-1.424-.813a.181.181 0 0 1-.076-.145v-5.959c0-.062.027-.118.076-.146l5.167-2.979a.15.15 0 0 1 .152 0l5.167 2.98a.164.164 0 0 1 .076.145v5.959a.181.181 0 0 1-.076.145l-5.167 2.98c-.041.027-.11.027-.16 0l-1.305-.792c-.055-.021-.111-.028-.146-.007-.368.208-.437.25-.778.354-.083.028-.215.076.05.222l1.721 1.021c.167.097.348.146.542.146s.375-.049.542-.146l5.166-2.979c.334-.194.542-.556.542-.944v-5.959c0-.389-.208-.75-.542-.944l-5.166-2.986a1.103 1.103 0 0 0-.542-.14m1.389 4.272c-1.472 0-2.354.618-2.354 1.66 0 1.117.875 1.444 2.291 1.583 1.688.166 1.82.416 1.82.75 0 .576-.465.82-1.549.82-1.375 0-1.666-.341-1.77-1.022a.157.157 0 0 0-.153-.125h-.667c-.083 0-.146.063-.146.153 0 .861.472 1.903 2.736 1.903 1.632 0 2.57-.646 2.57-1.771 0-1.118-.75-1.41-2.34-1.625-1.605-.208-1.765-.32-1.765-.694 0-.313.14-.73 1.327-.73 1.042 0 1.451.23 1.611.945.014.07.076.118.146.118h.673a.134.134 0 0 0 .105-.049c.027-.028.048-.07.034-.11-.097-1.237-.916-1.806-2.57-1.806z" fill-rule="nonzero" fill="#f1f8e9"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-public" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><path d="M20.036 16.746c.05-.408.087-.817.087-1.237s-.037-.83-.087-1.238h2.091c.099.396.16.81.16 1.238a5.1 5.1 0 0 1-.16 1.237m-3.186 3.44c.371-.687.656-1.43.854-2.203h1.825a4.968 4.968 0 0 1-2.679 2.203m-.155-3.44h-2.895c-.062-.408-.099-.817-.099-1.237s.037-.835.1-1.238h2.894c.056.403.1.817.1 1.238s-.044.829-.1 1.237m-1.447 3.687a8.39 8.39 0 0 1-1.182-2.45h2.363a8.39 8.39 0 0 1-1.181 2.45m-2.475-7.399h-1.806a4.902 4.902 0 0 1 2.672-2.202c-.37.686-.65 1.429-.866 2.202m-1.806 4.95h1.806c.217.773.495 1.515.866 2.202a4.954 4.954 0 0 1-2.672-2.203m-.508-1.237a5.099 5.099 0 0 1-.16-1.237 5.1 5.1 0 0 1 .16-1.238h2.091c-.049.409-.086.817-.086 1.238s.037.829.086 1.237m2.698-6.168a8.425 8.425 0 0 1 1.181 2.456h-2.363a8.426 8.426 0 0 1 1.182-2.456m4.28 2.456h-1.824a9.682 9.682 0 0 0-.854-2.202 4.94 4.94 0 0 1 2.679 2.202m-4.281-3.712a6.193 6.193 0 0 0-6.187 6.187 6.186 6.186 0 0 0 6.187 6.186 6.186 6.186 0 0 0 6.186-6.186 6.186 6.186 0 0 0-6.186-6.187z" fill="#b3e5fc" stroke-width=".619"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-public-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><path d="M20.037 16.746c.05-.408.087-.817.087-1.237s-.037-.83-.087-1.238h2.091c.099.396.16.81.16 1.238a5.1 5.1 0 0 1-.16 1.237m-3.186 3.44c.371-.687.656-1.43.854-2.203h1.825a4.967 4.967 0 0 1-2.68 2.203m-.154-3.44h-2.895c-.062-.408-.099-.817-.099-1.237s.037-.835.099-1.238h2.895c.056.403.1.817.1 1.238s-.044.829-.1 1.237m-1.447 3.687a8.39 8.39 0 0 1-1.182-2.45h2.363a8.39 8.39 0 0 1-1.181 2.45m-2.475-7.399H13.06a4.902 4.902 0 0 1 2.672-2.202c-.371.686-.65 1.429-.866 2.202m-1.806 4.95h1.806c.217.773.495 1.515.866 2.202a4.954 4.954 0 0 1-2.672-2.203m-.508-1.237a5.099 5.099 0 0 1-.16-1.237 5.1 5.1 0 0 1 .16-1.238h2.091c-.05.409-.086.817-.086 1.238s.037.829.086 1.237m2.698-6.168a8.425 8.425 0 0 1 1.181 2.456h-2.363a8.426 8.426 0 0 1 1.182-2.456m4.28 2.456h-1.824a9.682 9.682 0 0 0-.854-2.202 4.941 4.941 0 0 1 2.679 2.202M17.34 9.322a6.193 6.193 0 0 0-6.187 6.187 6.186 6.186 0 0 0 6.187 6.186 6.186 6.186 0 0 0 6.186-6.186 6.186 6.186 0 0 0-6.186-6.187z" fill="#b3e5fc" stroke-width=".619"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-react-components" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00bcd4" fill-rule="nonzero"/><path d="M16.473 13.927c.723 0 1.313.59 1.313 1.327 0 .703-.59 1.3-1.313 1.3a1.318 1.318 0 0 1-1.313-1.3c0-.737.59-1.327 1.313-1.327m-3.252 6.946c.443.267 1.412-.14 2.529-1.194a17.015 17.015 0 0 1-1.06-1.335 15.945 15.945 0 0 1-1.686-.252c-.358 1.502-.225 2.535.217 2.78m.499-4.03l-.204-.359a5.558 5.558 0 0 0-.203.604c.19.042.4.078.618.113l-.211-.359m4.593-.533l.569-1.054-.569-1.053c-.21-.372-.435-.702-.639-1.032-.38-.022-.78-.022-1.2-.022-.422 0-.823 0-1.202.022-.204.33-.428.66-.639 1.032l-.569 1.053.57 1.054c.21.372.434.702.638 1.032.38.021.78.021 1.201.021.421 0 .822 0 1.201-.02.204-.331.428-.661.639-1.033m-1.84-4.72c-.133.155-.274.316-.414.506h.828c-.14-.19-.28-.351-.414-.506m0 7.332c.133-.154.274-.316.414-.505h-.828c.14.19.28.35.414.505m3.245-9.284c-.436-.267-1.405.14-2.522 1.194.366.414.724.864 1.06 1.334.577.057 1.146.14 1.686.253.359-1.503.225-2.535-.224-2.78m-.492 4.03l.204.358c.077-.203.154-.407.203-.604-.19-.042-.4-.077-.618-.112l.211.358m1.018-4.95c1.033.589 1.145 2.141.71 3.953 1.784.527 3.069 1.398 3.069 2.584 0 1.187-1.285 2.058-3.07 2.585.436 1.812.324 3.364-.709 3.954-1.025.59-2.423-.085-3.77-1.37-1.35 1.285-2.747 1.96-3.78 1.37-1.025-.59-1.137-2.142-.702-3.954-1.783-.527-3.069-1.398-3.069-2.585s1.286-2.057 3.07-2.584c-.436-1.812-.324-3.364.702-3.954 1.032-.59 2.43.084 3.778 1.37 1.348-1.286 2.746-1.96 3.771-1.37m-.203 6.538c.239.527.45 1.054.625 1.588 1.475-.443 2.303-1.075 2.303-1.588 0-.512-.828-1.144-2.303-1.587a15.81 15.81 0 0 1-.625 1.587m-7.136 0a15.806 15.806 0 0 1-.625-1.587c-1.474.443-2.303 1.075-2.303 1.587 0 .513.829 1.145 2.303 1.588.176-.534.387-1.06.625-1.588m6.321 1.588l-.21.358c.217-.035.428-.07.617-.113-.049-.196-.126-.4-.203-.604l-.204.359m-2.03 2.837c1.117 1.053 2.086 1.46 2.522 1.194.45-.246.583-1.278.224-2.781-.54.112-1.11.196-1.685.253-.337.47-.695.92-1.06 1.334m-3.477-6.012l.21-.358c-.217.035-.428.07-.617.113.049.196.126.4.203.604l.204-.359m2.03-2.837c-1.117-1.053-2.086-1.46-2.529-1.194-.442.246-.576 1.278-.217 2.781.54-.112 1.11-.196 1.685-.253.337-.47.695-.92 1.06-1.334z" fill="#b2ebf2" stroke-width=".702"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-react-components-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00bcd4"/><path d="M16.473 13.928c.723 0 1.313.59 1.313 1.327 0 .703-.59 1.3-1.313 1.3a1.318 1.318 0 0 1-1.313-1.3c0-.737.59-1.327 1.313-1.327m-3.252 6.946c.443.267 1.412-.14 2.529-1.194a16.997 16.997 0 0 1-1.06-1.335 15.945 15.945 0 0 1-1.686-.252c-.358 1.502-.225 2.535.217 2.78m.499-4.03l-.204-.359c-.077.204-.154.408-.203.604.19.042.4.078.618.113l-.211-.359m4.593-.533l.569-1.054-.57-1.053c-.21-.372-.434-.702-.638-1.032-.38-.022-.78-.022-1.201-.022-.421 0-.822 0-1.2.022-.205.33-.43.66-.64 1.032l-.569 1.053.569 1.054c.21.372.435.702.64 1.032.378.021.779.021 1.2.021.421 0 .822 0 1.2-.02.205-.33.43-.661.64-1.033m-1.84-4.72c-.133.155-.274.316-.414.506h.828c-.14-.19-.28-.351-.414-.506m0 7.332c.133-.154.274-.316.414-.505h-.828c.14.19.28.35.414.505m3.244-9.284c-.435-.267-1.404.14-2.52 1.194.364.414.723.864 1.06 1.334.575.057 1.144.14 1.685.253.358-1.503.225-2.535-.225-2.78m-.491 4.03l.203.358c.078-.203.155-.407.204-.604-.19-.042-.4-.077-.618-.112l.21.358m1.02-4.95c1.032.589 1.144 2.141.708 3.953 1.784.527 3.07 1.398 3.07 2.584 0 1.187-1.286 2.058-3.07 2.585.436 1.812.323 3.364-.709 3.954-1.025.59-2.423-.085-3.771-1.37-1.348 1.285-2.746 1.96-3.778 1.37-1.026-.59-1.138-2.142-.703-3.954-1.783-.527-3.069-1.398-3.069-2.585s1.286-2.057 3.07-2.584c-.436-1.812-.324-3.364.702-3.954 1.032-.59 2.43.084 3.778 1.37 1.348-1.286 2.746-1.96 3.771-1.37m-.204 6.538c.24.527.45 1.054.625 1.588 1.475-.443 2.304-1.075 2.304-1.588 0-.512-.829-1.144-2.304-1.587a15.81 15.81 0 0 1-.625 1.587m-7.135 0a15.808 15.808 0 0 1-.625-1.587c-1.475.443-2.303 1.075-2.303 1.587 0 .513.828 1.145 2.303 1.588.176-.534.386-1.06.625-1.588m6.32 1.588l-.21.358c.218-.035.428-.07.618-.113a5.56 5.56 0 0 0-.204-.604l-.203.359m-2.03 2.837c1.117 1.053 2.086 1.46 2.521 1.194.45-.246.583-1.278.225-2.781-.54.112-1.11.196-1.685.253-.338.47-.696.92-1.06 1.334m-3.477-6.012l.21-.358c-.217.035-.428.07-.617.112.049.197.126.4.203.604l.204-.358m2.03-2.837c-1.117-1.053-2.086-1.46-2.529-1.194-.442.246-.576 1.278-.217 2.781.54-.112 1.11-.196 1.685-.253.337-.47.695-.92 1.06-1.334z" fill="#b2ebf2" stroke-width=".702"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-actions" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ab47bc" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#e1bee7" stroke="#e1bee7" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-actions-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ab47bc"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#e1bee7" stroke="#e1bee7" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef5350" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#ffcdd2" stroke="#ffcdd2" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-reducer-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef5350"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#ffcdd2" stroke="#ffcdd2" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-store" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#dcedc8" stroke="#dcedc8" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-store-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#dcedc8" stroke="#dcedc8" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-resource" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#fbc02d" fill-rule="nonzero"/><path d="M21.598 12.059h-6.085v-1.217h6.085m-2.434 6.085h-3.65V15.71h3.65m2.434-1.217h-6.085v-1.217h6.085m.608-4.26h-7.301a1.217 1.217 0 0 0-1.217 1.218v7.301a1.217 1.217 0 0 0 1.217 1.217h7.301a1.217 1.217 0 0 0 1.217-1.217v-7.301a1.217 1.217 0 0 0-1.217-1.217m-9.735 2.434h-1.217v8.518a1.217 1.217 0 0 0 1.217 1.217h8.519v-1.217H12.47z" fill="#fff9c4" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-resource-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#fbc02d"/><path d="M21.598 12.059h-6.085v-1.217h6.085m-2.434 6.085h-3.65V15.71h3.65m2.434-1.217h-6.085v-1.217h6.085m.608-4.26h-7.301a1.217 1.217 0 0 0-1.217 1.218v7.301a1.217 1.217 0 0 0 1.217 1.217h7.301a1.217 1.217 0 0 0 1.217-1.217v-7.301a1.217 1.217 0 0 0-1.217-1.217m-9.735 2.433h-1.217v8.519a1.217 1.217 0 0 0 1.217 1.217h8.519v-1.217H12.47z" fill="#fff9c4" stroke-width=".608"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" id="folder-sass" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#f8bbd0" fill-rule="nonzero"/><path d="M23.36 10.506c-.39-1.527-2.922-2.03-5.319-1.178-1.426.507-2.97 1.302-4.08 2.34-1.32 1.235-1.53 2.31-1.444 2.759.306 1.584 2.477 2.62 3.37 3.388v.005c-.264.13-2.19 1.104-2.64 2.1-.476 1.052.075 1.806.44 1.908 1.131.315 2.292-.251 2.916-1.182.602-.897.551-2.056.29-2.633.36-.095.781-.138 1.316-.076 1.508.177 1.804 1.118 1.748 1.513-.057.394-.373.61-.48.676-.105.065-.137.088-.129.137.013.07.062.068.152.053.125-.021.792-.321.821-1.048.037-.924-.849-1.958-2.416-1.93-.646.01-1.052.072-1.345.181-.022-.024-.044-.05-.067-.073-.969-1.034-2.76-1.765-2.684-3.156.027-.505.203-1.835 3.442-3.45 2.653-1.322 4.777-.957 5.145-.151.524 1.152-1.136 3.293-3.891 3.601-1.05.118-1.603-.289-1.74-.44-.145-.16-.166-.167-.22-.137-.088.049-.033.19 0 .274.082.214.42.594.995.782.506.166 1.739.258 3.23-.319 1.669-.646 2.972-2.443 2.59-3.944zm-7.103 7.783a2.2 2.2 0 0 1-.065 1.413 2.405 2.405 0 0 1-.453.704c-.5.546-1.198.752-1.497.579-.323-.188-.161-.956.418-1.568.623-.66 1.52-1.083 1.52-1.083l-.002-.002.079-.043z" fill="#ec407a" fill-rule="nonzero" stroke="#ec407a" stroke-width=".5199012000000001"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" id="folder-sass-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#f8bbd0" fill-rule="nonzero"/><path d="M23.36 10.506c-.39-1.527-2.922-2.03-5.319-1.178-1.426.507-2.97 1.302-4.08 2.34-1.32 1.235-1.53 2.31-1.444 2.759.306 1.584 2.477 2.62 3.37 3.388v.005c-.264.13-2.19 1.104-2.64 2.1-.476 1.052.075 1.806.44 1.908 1.131.315 2.292-.251 2.916-1.182.602-.897.551-2.056.29-2.633.36-.095.781-.138 1.316-.076 1.508.177 1.804 1.118 1.748 1.513-.057.394-.373.61-.48.676-.105.065-.137.088-.129.137.013.07.062.068.152.053.125-.021.792-.321.821-1.048.037-.924-.849-1.958-2.416-1.93-.646.01-1.052.072-1.345.181-.022-.024-.044-.05-.067-.073-.969-1.034-2.76-1.765-2.684-3.156.027-.505.203-1.835 3.442-3.45 2.653-1.322 4.777-.957 5.145-.151.524 1.152-1.136 3.293-3.891 3.601-1.05.118-1.603-.289-1.74-.44-.145-.16-.166-.167-.22-.137-.088.049-.033.19 0 .274.082.214.42.594.995.782.506.166 1.739.258 3.23-.319 1.669-.646 2.972-2.443 2.59-3.944zm-7.103 7.783a2.2 2.2 0 0 1-.065 1.413 2.405 2.405 0 0 1-.453.704c-.5.546-1.198.752-1.497.579-.323-.188-.161-.956.418-1.568.623-.66 1.52-1.083 1.52-1.083l-.002-.002.079-.043z" fill="#ec407a" fill-rule="nonzero" stroke="#ec407a" stroke-width=".5199012000000001"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-scripts" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#546e7a" fill-rule="nonzero"/><path d="M18.466 20.241c.69 0 1.259-.568 1.259-1.258v-8.18H15.32a.632.632 0 0 0-.63.63v6.292h-1.887v-6.922c0-1.036.852-1.888 1.888-1.888h6.921c1.036 0 1.888.852 1.888 1.888v.63h-2.517v8.18a1.896 1.896 0 0 1-1.888 1.887h-6.292a1.896 1.896 0 0 1-1.888-1.888v-.629h6.293c0 .69.568 1.258 1.258 1.258z" fill-rule="nonzero" fill="#cfd8dc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-scripts-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#546e7a" fill-rule="nonzero"/><path d="M18.466 20.241c.69 0 1.259-.568 1.259-1.258v-8.18H15.32a.632.632 0 0 0-.63.63v6.292h-1.887v-6.922c0-1.036.852-1.888 1.888-1.888h6.921c1.036 0 1.888.852 1.888 1.888v.63h-2.517v8.18a1.896 1.896 0 0 1-1.888 1.887h-6.292a1.896 1.896 0 0 1-1.888-1.888v-.629h6.293c0 .69.568 1.258 1.258 1.258z" fill-rule="nonzero" fill="#cfd8dc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-src" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#4caf50" fill-rule="nonzero"/><g fill="#c8e6c9" transform="translate(2.065 -.225) scale(.70678)"><path d="M19.146 30.989a.902.902 0 0 1-.207-.025 1.045 1.045 0 0 1-.726-1.213l2.709-14.431c.049-.279.209-.525.444-.683a.891.891 0 0 1 .7-.122c.519.152.837.684.727 1.213L20.077 30.16a1.032 1.032 0 0 1-.442.681.895.895 0 0 1-.489.148zM24.578 28.944h-.068a.932.932 0 0 1-.668-.377 1.104 1.104 0 0 1 .1-1.419l4.658-4.553-4.638-4.239a1.105 1.105 0 0 1-.141-1.416.938.938 0 0 1 .661-.4.9.9 0 0 1 .709.237l5.47 5c.386.372.448.974.144 1.416a1.05 1.05 0 0 1-.142.163l-5.447 5.324a.913.913 0 0 1-.638.264zM16.423 28.947a.917.917 0 0 1-.639-.267l-5.452-5.327a.874.874 0 0 1-.132-.153 1.097 1.097 0 0 1 .141-1.414l5.471-5a.882.882 0 0 1 .7-.238.939.939 0 0 1 .665.4 1.104 1.104 0 0 1-.14 1.417L12.4 22.6l4.659 4.551c.377.382.42.988.1 1.419a.928.928 0 0 1-.669.377z" fill-rule="nonzero"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-src-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#4caf50"/><g fill="#c8e6c9" fill-rule="evenodd" transform="translate(2.064 -.224) scale(.70678)"><path d="M19.146 30.989a.902.902 0 0 1-.207-.025 1.045 1.045 0 0 1-.726-1.213l2.709-14.431c.049-.279.209-.525.444-.683a.891.891 0 0 1 .7-.122c.519.152.837.684.727 1.213L20.077 30.16a1.032 1.032 0 0 1-.442.681.895.895 0 0 1-.489.148zM24.578 28.944h-.068a.932.932 0 0 1-.668-.377 1.104 1.104 0 0 1 .1-1.419l4.658-4.553-4.638-4.239a1.105 1.105 0 0 1-.141-1.416.938.938 0 0 1 .661-.4.9.9 0 0 1 .709.237l5.47 5c.386.372.448.974.144 1.416a1.05 1.05 0 0 1-.142.163l-5.447 5.324a.913.913 0 0 1-.638.264zM16.423 28.947a.917.917 0 0 1-.639-.267l-5.452-5.327a.874.874 0 0 1-.132-.153 1.097 1.097 0 0 1 .141-1.414l5.471-5a.882.882 0 0 1 .7-.238.939.939 0 0 1 .665.4 1.104 1.104 0 0 1-.14 1.417L12.4 22.6l4.659 4.551c.377.382.42.988.1 1.419a.928.928 0 0 1-.669.377z" fill-rule="nonzero"/></g></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-test" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#1de9b6" fill-rule="nonzero"/><path d="M14 8.097v1.39h.695v9.732A2.794 2.794 0 0 0 17.475 22a2.794 2.794 0 0 0 2.781-2.78V9.486h.695v-1.39H14m2.78 9.732c-.417 0-.695-.278-.695-.695 0-.417.278-.695.696-.695.417 0 .695.278.695.695 0 .417-.278.695-.695.695m1.39-2.78c-.417 0-.695-.278-.695-.696 0-.417.278-.695.695-.695.417 0 .695.278.695.695 0 .418-.278.696-.695.696m.695-3.476h-2.78V9.487h2.78v2.086z" fill="#00897b" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-test-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#1de9b6" fill-rule="nonzero"/><path d="M14 8.097v1.39h.695v9.732A2.794 2.794 0 0 0 17.475 22a2.794 2.794 0 0 0 2.781-2.78V9.486h.695v-1.39H14m2.78 9.732c-.417 0-.695-.278-.695-.695 0-.417.278-.695.696-.695.417 0 .695.278.695.695 0 .417-.278.695-.695.695m1.39-2.78c-.417 0-.695-.278-.695-.696 0-.417.278-.695.695-.695.417 0 .695.278.695.695 0 .418-.278.696-.695.696m.695-3.476h-2.78V9.487h2.78v2.086z" fill="#00897b" fill-rule="nonzero"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-tools" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#1e88e5" fill-rule="nonzero"/><path d="M21.043 15.266a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141-2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-3.569 0a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141 2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m3.925-6.424a6.424 6.424 0 0 0-6.423 6.424 6.424 6.424 0 0 0 6.423 6.424 1.07 1.07 0 0 0 1.071-1.07c0-.28-.107-.53-.278-.715a1.105 1.105 0 0 1-.271-.713 1.07 1.07 0 0 1 1.07-1.071h1.263a3.569 3.569 0 0 0 3.57-3.569c0-3.154-2.877-5.71-6.425-5.71z" fill="#bbdefb" stroke-width=".714"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-tools-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#1e88e5"/><path d="M21.043 15.266a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141-2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-3.569 0a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141 2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m3.925-6.424a6.424 6.424 0 0 0-6.423 6.424 6.424 6.424 0 0 0 6.423 6.424 1.07 1.07 0 0 0 1.071-1.07c0-.28-.107-.53-.278-.715a1.105 1.105 0 0 1-.271-.713 1.07 1.07 0 0 1 1.07-1.071h1.263a3.569 3.569 0 0 0 3.57-3.569c0-3.154-2.877-5.71-6.425-5.71z" fill="#bbdefb" stroke-width=".714"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-views" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ff8a65" fill-rule="nonzero"/><path d="M12.487 21.868L11.384 9.5H23.5l-1.104 12.366-4.961 1.375-4.948-1.373zm4.464-3.2l-3.926-2.36v-.855l3.926-2.361v1.323l-2.504 1.465 2.504 1.465v1.323zm.982-.001v-1.323l2.522-1.464-2.522-1.464v-1.323l3.926 2.35v.874l-3.926 2.35z" fill="#e44d26"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-views-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ff8a65" fill-rule="nonzero"/><path d="M12.487 21.868L11.384 9.5H23.5l-1.104 12.366-4.961 1.375-4.948-1.373zm4.464-3.2l-3.926-2.36v-.855l3.926-2.361v1.323l-2.504 1.465 2.504 1.465v1.323zm.982-.001v-1.323l2.522-1.464-2.522-1.464v-1.323l3.926 2.35v.874l-3.926 2.35z" fill="#e44d26"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vscode" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#42a5f5" fill-rule="nonzero"/><path d="M20.811 8.52l-5.988 5.506-3.346-2.522-1.383.805 3.298 3.03-3.298 3.032 1.383.807 3.346-2.522 5.988 5.503 2.921-1.419V9.94zm0 3.622v6.396l-4.245-3.198z" fill="#bbdefb" stroke-width=".974"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vscode-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#42a5f5" fill-rule="nonzero"/><path d="M20.81 8.52l-5.988 5.506-3.346-2.522-1.384.805 3.3 3.03-3.3 3.032 1.384.807 3.346-2.522 5.988 5.503 2.921-1.419V9.94zm0 3.621v6.397l-4.245-3.198z" fill="#bbdefb" stroke-width=".974"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vue" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#009688" fill-rule="nonzero"/><g transform="translate(8.459 6.362) scale(.69572)"><path d="M1.821 4.15l10.21 17.618L22.239 4.235v-.084h-7.692l-2.434 4.178-2.422-4.178z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179z" fill="#35495e"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vue-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#009688"/><g transform="translate(8.458 6.362) scale(.69572)"><path d="M1.821 4.15l10.21 17.618L22.239 4.235v-.084h-7.692l-2.434 4.178-2.422-4.178z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179z" fill="#35495e"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-webpack" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#03a9f4" fill-rule="nonzero"/><g transform="translate(9.192 7.48) scale(.66328)"><path d="M19.376 15.988l-7.708 4.45-7.709-4.45v-8.9l7.709-4.451 7.708 4.45z" fill="#fff" fill-opacity=".785"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18s.41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.94v2.103h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#1c78c0"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-webpack-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#03a9f4"/><g transform="translate(9.193 7.48) scale(.66328)"><path d="M19.376 15.988l-7.708 4.45-7.709-4.45v-8.9l7.709-4.451 7.708 4.45z" fill="#fff" fill-opacity=".785"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18s.41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.94v2.103h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#1c78c0"/></g></symbol><symbol viewBox="0 0 24 24" id="font" xmlns="http://www.w3.org/2000/svg"><path d="M9.62 12L12 5.67 14.37 12M11 3L5.5 17h2.25l1.12-3h6.25l1.13 3h2.25L13 3h-2z" fill="#f44336"/></symbol><symbol viewBox="0 0 500 500" id="fsharp" xmlns="http://www.w3.org/2000/svg"><path d="M235.906 36.66L21.963 250.601l213.943 213.943v-84.36L106.209 250.487l129.697-129.696z" fill="#378bba" stroke-width="14.706"/><path d="M235.906 156.614l-93.622 93.62 93.622 93.622z" fill="#378bba" stroke-width="15.006"/><path d="M263.417 36.64L477.36 250.583 263.417 464.526v-84.36l129.696-129.697-129.696-129.696z" fill="#30b9db" stroke-width="14.706"/></symbol><symbol viewBox="0 0 152.99 160.01" id="fusebox" xmlns="http://www.w3.org/2000/svg"><defs id="fkdefs4"><style id="fkstyle2">.fkcls-1{fill:#fff}.fkcls-2{fill:#515151}.fkcls-3{fill:#1d79bf}.fkcls-4{fill:#383838}</style></defs><title id="fktitle6">Asset 3</title><g id="fkLayer_2" data-name="Layer 2" transform="matrix(.87285 0 0 .87285 10.17 10.175)"><g id="fkFuse_Box" data-name="Fuse Box"><g id="fkLOGO"><path class="fkcls-1" id="fkpolygon8" fill="#fff" d="M76.56 2.19l74.22 24.93-7.7 87.77-65.41 42.66-69.79-43.93-5.7-86.13z"/><path class="fkcls-2" d="M77.69 160L5.87 114.81 0 26 76.55 0 153 25.67l-7.94 90.4zM9.88 112.43l67.77 42.66 63.45-41.39 7.47-85.13-72-24.18L4.36 28.95z" id="fkpath10" fill="#515151"/><path class="fkcls-3" id="fkpolygon12" fill="#1d79bf" d="M76.4 148.8V61.68l66.93-29.82-5.99 78.77z"/><path id="fkF" class="fkcls-4" fill="#383838" d="M76.4 148.8l-60.35-37.39L9.63 31.8 76.4 61.68z"/><path class="fkcls-1" d="M25.58 52.73l.54 15.93 37.35 18.18.12 14.69-37-18.21 1.64 37.1-14.56-9-5.05-80.55 67.79 30.82v15.46z" id="fkpath15" fill="#fff"/><path class="fkcls-1" d="M135.91 90.77c-.08 13.12-6.33 26.59-16.77 33.12l-42.8 27.93V61.71l42.27-18.84c5.16-2.41 9.51-1.43 12.4 3.11 1.9 3 2.89 7.23 2.86 12.21A35.69 35.69 0 0 1 129.34 76c4.29 2 6.66 6.55 6.57 14.77zM123 63.76c0-4.64-2-6.93-4.92-5.45l-29 14.48L89 90l29.44-15.59c2.5-1.32 4.56-5.91 4.56-10.65zM125.15 96c0-5.71-2.42-8.24-6.55-5.93L89 106.64v19.58l29.34-17.46c4.43-2.64 6.79-7.27 6.81-12.76z" id="fkpath17" fill="#fff"/><path id="fkTOP" class="fkcls-4" fill="#383838" d="M76.4 8.82L9.71 31.77l109.77 2.38-84.02 9.21L76.4 61.68l20.76-9.25-27.73-1.37 49.78-8.46 24.12-10.74z"/></g></g></g></symbol><symbol viewBox="0 0 24 24" id="git" xmlns="http://www.w3.org/2000/svg"><path d="M2.6 10.59L8.38 4.8l1.69 1.7c-.24.85.15 1.78.93 2.23v5.54c-.6.34-1 .99-1 1.73a2 2 0 0 0 2 2 2 2 0 0 0 2-2c0-.74-.4-1.39-1-1.73V9.41l2.07 2.09c-.07.15-.07.32-.07.5a2 2 0 0 0 2 2 2 2 0 0 0 2-2 2 2 0 0 0-2-2c-.18 0-.35 0-.5.07L13.93 7.5a1.98 1.98 0 0 0-1.15-2.34c-.43-.16-.88-.2-1.28-.09L9.8 3.38l.79-.78c.78-.79 2.04-.79 2.82 0l7.99 7.99c.79.78.79 2.04 0 2.82l-7.99 7.99c-.78.79-2.04.79-2.82 0L2.6 13.41c-.79-.78-.79-2.04 0-2.82z" fill="#e64a19"/></symbol><symbol viewBox="0 0 494 455" id="gitlab" xmlns="http://www.w3.org/2000/svg"><title>logo</title><defs><path id="fma" d="M0 1173.3h2000V0H0v1173.3z"/></defs><g transform="matrix(.88256 0 0 -.88256 -286.767 742.766)" fill="none" fill-rule="evenodd"><mask id="fmb" fill="#fff"><use width="100%" height="100%" xlink:href="#fma"/></mask><g><g transform="translate(358.67 358.67)"><path d="M492.532 195.445l-27.559 84.815-54.617 168.1c-2.81 8.648-15.045 8.648-17.856 0l-54.619-168.1h-181.37l-54.62 168.1c-2.81 8.648-15.045 8.648-17.856 0l-54.617-168.1-27.557-84.815a18.775 18.775 0 0 1 6.82-20.992l238.51-173.29 238.51 173.29a18.777 18.777 0 0 1 6.82 20.992" fill="#fc6d26"/><path d="M247.2 1.16l90.684 279.1h-181.37z" fill="#e24329"/><path d="M247.201 1.16l-90.684 279.09H29.427z" fill="#fc6d26"/><path d="M29.422 280.256L1.862 195.44a18.774 18.774 0 0 1 6.822-20.991L247.194 1.16z" fill="#fca326"/><path d="M29.422 280.26h127.09l-54.619 168.1c-2.81 8.65-15.047 8.65-17.856 0z" fill="#e24329"/><path d="M247.2 1.16l90.684 279.09h127.09z" fill="#fc6d26"/><path d="M464.98 280.256l27.559-84.815a18.774 18.774 0 0 0-6.821-20.991L247.208 1.16z" fill="#fca326"/><path d="M464.97 280.26H337.88l54.619 168.1c2.81 8.65 15.047 8.65 17.856 0z" fill="#e24329"/></g></g></g></symbol><symbol viewBox="0 0 24 24" id="go" xmlns="http://www.w3.org/2000/svg"><path d="M10.575 1.695c-2.634 0-4.756 2.453-4.756 5.502v4.6l-.027-.003v4.71c0 3.05 2.123 5.502 4.757 5.502h2.286c2.634 0 4.757-2.453 4.757-5.502v-4.6a5.1 5.1 0 0 0 .026.003v-4.71c0-3.049-2.122-5.502-4.756-5.502h-2.287z" fill="#73cddc"/><rect width="2.289" height="3.335" x="-1.178" y="6.092" ry="1.125" transform="matrix(.4849 -.87457 .85979 .51065 0 0)" fill="#73cddc"/><rect width="2.297" height="3.39" x="10.261" y="-15.076" ry="1.143" transform="matrix(.44646 .8948 -.89204 .45195 0 0)" fill="#73cddc"/><circle cx="9.267" cy="5.13" r="2.054" fill="#fff" stroke="#5e5d5b" stroke-width=".1"/><circle cx="14.214" cy="5.116" r="2.054" fill="#fff" stroke="#5e5d5b" stroke-width=".1"/><ellipse cx="8.039" cy="5.051" rx=".792" ry=".901" fill="#030d18"/><path d="M11.792 9.556l.763.138a.403.689 0 0 1 .008.138.403.689 0 0 1-.402.69.403.689 0 0 1-.404-.69.403.689 0 0 1 .035-.276z" fill="#fff" stroke="#fff" stroke-width=".155"/><ellipse cx="8.51" cy="5.365" rx=".138" ry=".166" fill="#fff"/><ellipse cx="12.945" cy="5.189" rx=".792" ry=".901" fill="#030d18"/><ellipse cx="13.414" cy="5.446" rx=".138" ry=".166" fill="#fff"/><ellipse cx="-12.982" cy="-3.409" rx=".708" ry="1.026" transform="rotate(-129.403)" fill="#f6d2a1" stroke-width=".4"/><path d="M11.772 9.553l-.757.135a.4.672 0 0 0-.008.135.4.672 0 0 0 .4.672.4.672 0 0 0 .4-.672.4.672 0 0 0-.035-.27z" fill="#fff" stroke="#fff" stroke-width=".153"/><ellipse cx="1.841" cy="-21.563" rx=".707" ry="1.026" transform="scale(1 -1) rotate(50.597)" fill="#f6d2a1" stroke-width=".4"/><ellipse cx="-17.281" cy="-21.784" rx=".864" ry="1.27" transform="matrix(.3054 -.95222 -.97065 -.2405 0 0)" fill="#f6d2a1" stroke-width=".4"/><ellipse cx="22.885" cy="2.587" rx=".864" ry="1.27" transform="matrix(.22652 .974 .95652 -.29167 0 0)" fill="#f6d2a1" stroke-width=".4"/><path d="M10.708 8.392a.594.594 0 0 0-.594.597v.115c0 .331.264.598.594.598h.386a.973.772 0 0 1 .697-.235.973.772 0 0 1 .698.235h.334c.33 0 .594-.267.594-.598V8.99a.595.595 0 0 0-.594-.597h-2.115z" fill="#f6d2a1" stroke="#657075" stroke-width=".1"/><ellipse cx="11.734" cy="8.203" rx="1.208" ry=".68" fill="#030d18" stroke="#fff" stroke-width=".162"/></symbol><symbol viewBox="0 0 24 24" id="gradle" xmlns="http://www.w3.org/2000/svg"><path d="M21.718 5.503c-.731-1.315-2.04-1.708-2.963-1.727-1.133-.023-2.065.605-1.888 1.017.037.088.25.55.38.741.19.275.527.064.646 0 .353-.187.73-.248 1.16-.198.409.048.954.3 1.319 1.001.859 1.652-1.794 5.05-5.114 2.697-3.32-2.353-6.548-1.574-8.01-1.1-1.462.475-2.135.952-1.556 2.055.785 1.498.524 1.038 1.285 2.28 1.21 1.97 3.856-.908 3.856-.908-1.972 2.906-3.662 2.204-4.31 1.188a15.864 15.864 0 0 1-1.038-1.97c-4.993 1.76-3.642 9.534-3.642 9.534h2.48c.632-2.862 2.892-2.757 3.28 0h1.892c1.673-5.59 5.914 0 5.914 0h2.466c-.69-3.812 1.388-5.01 2.697-7.246 1.31-2.235 2.551-4.969 1.146-7.364zm-6.362 7.362c-1.304-.426-.837-1.723-.837-1.723s1.139.368 2.68.87c-.09.403-.856 1.175-1.843.853z" fill="#0097a7" stroke-width=".47"/></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 300 300" id="graphcool" xmlns="http://www.w3.org/2000/svg"><path d="M246.886 107.727c-12.237-6.892-27.616 2.1-30.081 3.646l-52.834 29.965c-7.8-6.196-18.914-5.933-26.412.625-7.499 6.558-9.24 17.537-4.14 26.094 5.102 8.556 15.588 12.246 24.923 8.768 9.335-3.478 14.852-13.129 13.111-22.937l52.688-29.9.321-.196c3.464-2.188 11.5-5.462 15.256-3.34 2.706 1.524 4.252 6.629 4.376 14.148h-.066v66.092a17.313 17.313 0 0 1-8.635 14.95l-75.739 43.755a17.312 17.312 0 0 1-17.261 0l-75.74-43.756a17.312 17.312 0 0 1-8.634-14.95V113.22c.01-6.165 3.3-11.86 8.634-14.95l68.549-39.562c6.522 7.482 17.451 9.25 26 4.206s12.283-15.468 8.886-24.794c-3.397-9.327-12.962-14.904-22.751-13.27-9.79 1.636-17.022 10.02-17.204 19.944L59.397 85.632a31.932 31.932 0 0 0-15.978 27.588v87.454a31.933 31.933 0 0 0 15.927 27.602l75.74 43.755a31.934 31.934 0 0 0 31.846 0l75.74-43.755a31.933 31.933 0 0 0 15.927-27.58V137.12h.05c.373-14.913-3.616-24.794-11.762-29.389z" fill="#27ae60" stroke="#27ae60" stroke-width="7.883622079999999"/></symbol><symbol viewBox="0 0 400 400" id="graphql" xmlns="http://www.w3.org/2000/svg"><path d="M67.008 293.022l-13.143-7.588L200.282 31.839l13.143 7.588z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M50.855 265.174H343.69v15.177H50.855z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M203.122 358.269L56.649 273.7l7.589-13.143 146.472 84.568zm127.24-220.407L183.889 53.293l7.589-13.143 146.472 84.568z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M64.278 137.803l-7.588-13.142 146.472-84.568 7.588 13.143z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M327.661 293.025L181.244 39.43l13.143-7.589 146.417 253.596zM62.466 114.597h15.176v169.136H62.466zm254.528 0h15.176v169.136h-15.176z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M200.538 351.845l-6.628-11.481L321.3 266.812l6.629 11.48z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M352.284 288.67c-8.777 15.268-28.342 20.48-43.61 11.703-15.268-8.777-20.48-28.342-11.703-43.61 8.777-15.268 28.342-20.48 43.61-11.703 15.36 8.869 20.57 28.342 11.703 43.61M97.574 141.567c-8.778 15.268-28.343 20.48-43.61 11.703-15.269-8.777-20.48-28.342-11.703-43.61 8.777-15.268 28.342-20.48 43.61-11.703 15.268 8.869 20.479 28.342 11.702 43.61M42.353 288.67c-8.777-15.268-3.566-34.741 11.702-43.61 15.268-8.776 34.741-3.565 43.61 11.703 8.776 15.268 3.565 34.741-11.703 43.61-15.36 8.776-34.833 3.565-43.61-11.703m254.71-147.103c-8.776-15.268-3.565-34.741 11.703-43.61 15.268-8.776 34.742-3.565 43.61 11.703 8.777 15.268 3.566 34.741-11.702 43.61-15.268 8.776-34.833 3.565-43.61-11.703m-99.745 236.608c-17.645 0-31.907-14.262-31.907-31.907s14.262-31.907 31.907-31.907 31.907 14.262 31.907 31.907c0 17.554-14.262 31.907-31.907 31.907m0-294.206c-17.645 0-31.907-14.262-31.907-31.907s14.262-31.907 31.907-31.907 31.907 14.262 31.907 31.907-14.262 31.907-31.907 31.907" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/></symbol><symbol viewBox="0 0 24 24" id="groovy" xmlns="http://www.w3.org/2000/svg"><path d="M12 1.982a10.119 10.119 0 0 0-10.12 10.12A10.119 10.119 0 0 0 12 22.22 10.119 10.119 0 0 0 22.12 12.1 10.119 10.119 0 0 0 12 1.983zm1.254 2.422c.91 0 1.647.261 2.213.78.571.518.857 1.188.857 2.013 0 .889-.319 1.673-.959 2.35-.64.677-1.376 1.015-2.207 1.015-.486 0-.89-.119-1.213-.357-.317-.238-.476-.532-.476-.88 0-.212.06-.4.181-.563.127-.164.274-.246.438-.246.159 0 .238.092.238.277 0 .164.06.29.182.38.121.09.261.136.42.136.423 0 .828-.29 1.215-.866.391-.582.587-1.202.587-1.863 0-.465-.151-.844-.453-1.135-.301-.296-.69-.445-1.166-.445-.714 0-1.406.318-2.078.953-.666.635-1.211 1.47-1.635 2.506-.417 1.031-.627 2.014-.627 2.945 0 .857.185 1.54.555 2.047.37.503.863.754 1.477.754 1.037 0 2.027-.734 2.974-2.2l1.493-.212c.185-.026.277.018.277.135 0 .053-.072.28-.215.681-.143.402-.337 1.074-.586 2.016.82-.476 1.455-1.003 1.904-1.58v.914c-.36.418-1.046.888-2.062 1.412-.212 1.407-.682 2.493-1.406 3.26-.725.772-1.54 1.16-2.444 1.16-.433 0-.775-.102-1.023-.303-.243-.2-.365-.477-.365-.832 0-.984.955-1.94 2.865-2.865.2-.714.395-1.356.586-1.928-.333.482-.817.907-1.451 1.278-.635.37-1.225.554-1.77.554-.889 0-1.628-.383-2.22-1.15-.588-.772-.881-1.748-.881-2.928 0-1.243.333-2.42 1-3.531a7.747 7.747 0 0 1 2.625-2.674c1.084-.672 2.134-1.008 3.15-1.008zM12.03 16.592c-1.375.687-2.062 1.365-2.062 2.031 0 .354.169.533.508.533.666 0 1.184-.856 1.554-2.564z" fill="#26c6da"/></symbol><symbol viewBox="0 0 24 24" id="gulp" xmlns="http://www.w3.org/2000/svg"><path d="M8.37 15.94a596.238 596.238 0 0 1-.482-4.982c.002-.042-.225-.077-.505-.077h-.508V8.95h3.966V5.198l1.871-1.124c1.14-.685 1.978-1.125 2.144-1.125.4 0 .866.506.866.939 0 .19-.057.422-.127.517-.07.095-.722.53-1.45.966l-1.321.792-.029 1.393-.028 1.393h3.972v1.932h-.98l-.495 4.983-.495 4.983H8.854l-.485-4.906z" fill="#e53935"/></symbol><symbol viewBox="0 0 24 24" id="h" xmlns="http://www.w3.org/2000/svg"><path d="M16.745 19.818h-3.007v-5.882q0-2.381-1.736-2.381-.869 0-1.438.663-.56.662-.56 1.718v5.882H6.988V4.533h3.016v6.508h.037q1.186-1.802 3.193-1.802 3.511 0 3.511 4.239z" stroke-width=".478" fill="#0277bd"/></symbol><symbol viewBox="0 0 253.6 253.6" id="hack" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-29.243 -29.515) scale(1.2301)"><path fill="#607d8b" d="M69.496 159.551v52.576l51.77-52.576zM123.507 41.523l-54.01 52.755v55.084l54.01-54.009z"/><path fill="#eceff1" d="M130.023 95.663v51.501l52.128-51.5z"/><path fill="#607d8b" d="M185.465 101.867l-55.442 55.174v55.083l55.442-55.262z"/><path fill="#ffa000" d="M73.068 154.283l50.427.09v-50.248z"/></g></symbol><symbol viewBox="0 0 300 300.00001" id="haml" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 165.6)"><path d="M78.42-132.307c-12.047-.302-26.924 5.998-26.924 5.998l49.195 99.791L74.605 85.005c23.81 20.134 50.07 10.504 50.07 10.504L136.76 9.212c1.526 1.446 3.146 2.77 4.777 3.995 5.244 3.714 10.925 6.553 16.606 8.738 5.68 2.185 11.583 3.933 17.482 5.244 3.933.874 7.645 1.53 11.578 1.967-1.748 3.933-2.84 8.083-2.621 12.672 0 .437.22.873.656 1.092h.217c4.152 2.185 8.521 3.934 13.328 5.027 4.589.874 9.615 1.312 14.422.656 5.026-.655 10.051-2.623 13.984-5.9 3.933-3.278 6.774-7.648 8.522-12.237l.219-.218v-.217l.656-5.899v-.22c2.185-1.311 4.37-2.621 6.555-4.37 2.622-2.184 5.025-4.589 6.773-7.648 1.748-3.059 2.84-6.774 2.621-10.488-.218-3.496-1.53-6.99-3.06-10.049-1.53-3.059-3.495-5.901-5.68-8.523-4.37-5.026-9.614-9.176-15.295-12.454-5.462-3.496-11.581-6.338-17.7-8.304l-2.404-.656-1.962-.655c-1.311-.437-2.406-1.092-3.498-1.53-2.185-1.31-3.717-2.622-4.809-4.37-2.185-3.278-2.403-8.301-1.31-13.545.218-1.311.656-2.623 1.093-3.934a96.064 96.064 0 0 0 1.31-4.152c.314-1.412.51-2.829.598-4.402l29.203-25.553c-2.275-8.404-27.488-17.158-27.488-17.158l-74.931 63.726-43.243-81.584c-1.553-.35-3.218-.527-4.94-.57zm107.682 73.14c-.449 2.336-.647 4.795-.647 7.258.219 3.715 1.311 7.87 3.715 11.366 2.403 3.496 5.68 6.117 8.957 7.646a29.663 29.663 0 0 0 5.027 1.967l2.623.654 2.184.438c5.68 1.53 11.142 3.714 16.168 6.554 5.025 2.84 9.833 6.337 13.766 10.27s6.992 8.959 7.43 13.984c.218 3.496-.22 6.118-1.313 8.303-1.093 2.404-2.84 4.588-4.807 6.555-.874.874-1.966 1.747-2.84 2.402a27.11 27.11 0 0 0-.654-5.898c-.219-1.093-.438-1.966-.875-3.059-.437-.874-.872-1.966-1.965-2.621-.218 0-.44-.001-.44.217-1.31 3.277-3.494 6.12-5.898 8.086-2.403 1.966-5.462 2.84-8.521 3.058-3.06.219-6.338-.436-9.616-1.31-3.277-.874-6.552-1.968-9.83-3.06l-.439-.22c-.656-.218-1.526.002-1.963.44-1.748 2.185-3.06 4.149-4.59 6.334a58.435 58.435 0 0 0-2.84 5.027c-3.933-1.53-7.649-2.841-11.582-4.37-5.462-2.186-10.925-4.37-15.95-6.991-5.245-2.404-10.268-5.246-14.638-8.524-3.15-2.363-6.062-4.845-8.185-7.681l2.404-17.172z" fill="#f4511e" stroke-width="0" stroke-linejoin="round"/></g></symbol><symbol viewBox="0 0 24 24" id="handlebars" xmlns="http://www.w3.org/2000/svg"><path d="M8.55 10.32c-2.753 0-4.202 3.48-5.793 3.48-.98 0-1.126-.677-1.126-.915 0-.332.236-.706.564-.706.59 0 .414.77.414.77s.798-.555.272-1.298c-.42-.595-1.31-.623-1.92-.17-.617.458-1.057 1.146-.853 2.287.1.551.468 1.35 1.233 1.805.764.455 1.925.566 2.335.566 2.194 0 4.342-1.633 6.639-2.322a5.513 5.513 0 0 1 1.497-.222 6.19 6.19 0 0 1 1.92.226c2.296.689 4.444 2.323 6.638 2.323.41 0 1.57-.11 2.335-.566.765-.455 1.132-1.256 1.231-1.807.204-1.14-.235-1.829-.853-2.287-.61-.453-1.497-.423-1.918.172-.526.743.27 1.297.27 1.297s-.176-.77.414-.77c.329 0 .565.373.565.705 0 .238-.147.914-1.126.914-1.592 0-3.04-3.478-5.794-3.478-2.565 0-3.076 1.177-3.462 1.718-.004.005-.005.011-.008.016-.005-.006-.007-.013-.012-.02-.386-.54-.896-1.717-3.461-1.717z" fill="#ff7043" fill-rule="evenodd" stroke-width=".3"/></symbol><symbol viewBox="0 0 300.00001 300" id="haskell" xmlns="http://www.w3.org/2000/svg"><g stroke-width="2.422"><path d="M23.928 240.5l59.94-89.852-59.94-89.855h44.955l59.94 89.855-59.94 89.852z" fill="#ef5350"/><path d="M83.869 240.5l59.94-89.852-59.94-89.855h44.955l119.88 179.71h-44.95l-37.46-56.156-37.468 56.156z" fill="#ffa726"/><path d="M228.72 188.08l-19.98-29.953h69.93v29.956h-49.95zm-29.97-44.924l-19.98-29.953h99.901v29.953z" fill="#ffee58"/></g></symbol><symbol viewBox="0 0 210 210" id="haxe" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -87)"><path fill="#f68712" stroke-width=".221" d="M42.78 191.545l63.431-63.43 63.431 63.43-63.431 63.431z"/><path d="M42.8 191.592L31.193 148.28 19.59 104.97 62.9 116.575l43.311 11.605-31.706 31.706z" fill="#fab20b" stroke-width=".266"/><path d="M105.956 128.111l-43.19-11.544-43.177-11.597 22.927.185 23.228.294 20.264 11.36z" fill="#fbc707" stroke-width=".265"/><path d="M19.59 104.97l11.596 43.176 11.545 43.19-11.303-19.948-11.36-20.263-.294-23.228z" fill="#fff200" stroke-width=".265"/><path d="M106.23 128.133l43.312-11.605 43.311-11.605-11.605 43.31-11.605 43.312-31.706-31.706z" fill="#f47216" stroke-width=".266"/><path d="M169.711 191.289l11.545-43.19 11.597-43.176-.185 22.927-.294 23.228-11.36 20.263z" fill="#f1471d" stroke-width=".265"/><path d="M192.853 104.923l-43.176 11.597-43.19 11.544 19.947-11.303 20.264-11.36 23.228-.293z" fill="#fbc707" stroke-width=".265"/><path d="M169.643 191.545l11.605 43.31 11.605 43.312-43.311-11.605-43.311-11.606 31.706-31.705z" fill="#f25c19" stroke-width=".266"/><path d="M106.487 255.025l43.19 11.544 43.176 11.598-22.927-.185-23.228-.294-20.264-11.36z" fill="#f68712" stroke-width=".265"/><path d="M192.853 278.167l-11.597-43.176-11.545-43.19 11.303 19.947 11.36 20.264.294 23.228z" fill="#f1471d" stroke-width=".265"/><path d="M106.211 254.976l-43.31 11.605-43.312 11.605 11.605-43.31L42.8 191.563l31.706 31.706z" fill="#f89c0e" stroke-width=".266"/><path d="M42.731 191.82l-11.545 43.19-11.597 43.176.185-22.927.294-23.228 11.36-20.263z" fill="#fff200" stroke-width=".265"/><path d="M19.59 278.186l43.175-11.597 43.19-11.544-19.947 11.303-20.264 11.36-23.228.293z" fill="#f25c19" stroke-width=".265"/></g></symbol><symbol viewBox="0 0 144 152" id="heroku" xmlns="http://www.w3.org/2000/svg"><path d="M118.68 13.279H26.865c-6.337 0-11.476 5.139-11.476 11.476V129.32c0 6.338 5.139 11.477 11.476 11.477h91.813c6.338 0 11.477-5.14 11.477-11.477V24.755c0-6.337-5.139-11.476-11.477-11.476zM44.08 121.669V96.165l14.346 12.752zm44.632 0v-38.08c-.063-2.976-1.496-6.551-7.97-6.551-12.966 0-27.51 6.52-27.654 6.586l-9.008 4.08V32.407h12.752v36.201c6.366-2.072 15.266-4.321 23.91-4.321 7.882 0 12.6 3.099 15.17 5.698 5.484 5.547 5.56 12.613 5.551 13.43v38.255zm3.188-68.54H79.149c5.011-6.576 8.158-13.496 9.564-20.723h12.751c-.86 7.243-3.796 14.187-9.563 20.722z" fill="#6963b9"/></symbol><symbol viewBox="0 0 24 24" id="hpp" xmlns="http://www.w3.org/2000/svg"><path d="M9.757 19.818H6.751v-5.882q0-2.381-1.737-2.381-.868 0-1.438.663-.56.662-.56 1.718v5.882H0V4.533h3.016v6.508h.037Q4.24 9.239 6.247 9.239q3.51 0 3.51 4.239z" stroke-width=".478" fill="#0277bd"/><path d="M13.073 11.448v2h-2v2h2v2h2v-2h2v-2h-2v-2zm7 0v2h-2v2h2v2h2v-2h2v-2h-2v-2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="html" xmlns="http://www.w3.org/2000/svg"><path d="M12 17.56l4.07-1.13.55-6.1H9.38L9.2 8.3h7.6l.2-1.99H7l.56 6.01h6.89l-.23 2.58-2.22.6-2.22-.6-.14-1.66h-2l.29 3.19L12 17.56M4.07 3h15.86L18.5 19.2 12 21l-6.5-1.8L4.07 3z" fill="#e44d26"/></symbol><symbol viewBox="0 0 24 24" id="http" xmlns="http://www.w3.org/2000/svg"><path d="M16.046 13.784c.074-.613.13-1.225.13-1.856s-.056-1.244-.13-1.856h3.137c.148.594.241 1.215.241 1.856a7.65 7.65 0 0 1-.241 1.856m-4.78 5.16c.557-1.03.984-2.144 1.281-3.304h2.738a7.452 7.452 0 0 1-4.019 3.304m-.232-5.16H9.828a12.314 12.314 0 0 1-.149-1.856c0-.631.056-1.253.149-1.856h4.343c.084.603.149 1.225.149 1.856 0 .63-.065 1.243-.149 1.856M12 19.315c-.77-1.113-1.393-2.348-1.773-3.675h3.545c-.38 1.327-1.002 2.562-1.773 3.675m-3.712-11.1h-2.71a7.353 7.353 0 0 1 4.01-3.304c-.557 1.03-.975 2.144-1.3 3.304m-2.71 7.425h2.71c.325 1.16.743 2.274 1.3 3.304a7.433 7.433 0 0 1-4.01-3.304m-.761-1.856a7.65 7.65 0 0 1-.241-1.856c0-.64.093-1.262.241-1.856h3.137c-.074.612-.13 1.225-.13 1.856 0 .63.056 1.243.13 1.856m4.046-9.253c.77 1.114 1.393 2.357 1.773 3.684h-3.545c.38-1.327 1.002-2.57 1.773-3.684m6.422 3.684h-2.738a14.523 14.523 0 0 0-1.28-3.304 7.412 7.412 0 0 1 4.018 3.304m-6.423-5.568c-5.132 0-9.28 4.176-9.28 9.28a9.28 9.28 0 0 0 9.28 9.282 9.28 9.28 0 0 0 9.281-9.281A9.28 9.28 0 0 0 12 2.647z" fill="#e53935" stroke-width=".928"/></symbol><symbol viewBox="0 0 24 24" id="image" xmlns="http://www.w3.org/2000/svg"><path d="M13.009 9.202h5.368l-5.368-5.368v5.368M6.177 2.37h7.808l5.856 5.856v11.711a1.952 1.952 0 0 1-1.952 1.952H6.178a1.951 1.951 0 0 1-1.952-1.952V4.322c0-1.083.868-1.952 1.952-1.952m0 17.567h11.71V12.13l-3.903 3.903-1.952-1.951-5.856 5.855M8.13 9.202a1.952 1.952 0 0 0-1.952 1.952 1.952 1.952 0 0 0 1.952 1.952 1.952 1.952 0 0 0 1.952-1.952A1.952 1.952 0 0 0 8.13 9.202z" fill="#26a69a" stroke-width=".976"/></symbol><symbol viewBox="0 0 512 512" id="ionic" xmlns="http://www.w3.org/2000/svg"><g fill="#4f8ff7"><path d="M423.592 132.804A31.855 31.855 0 0 0 429 115c0-17.675-14.33-32-32-32a31.853 31.853 0 0 0-17.805 5.409C344.709 63.015 302.11 48 256 48 141.125 48 48 141.125 48 256c0 114.877 93.125 208 208 208 114.873 0 208-93.123 208-208 0-46.111-15.016-88.71-40.408-123.196zM391.83 391.832c-17.646 17.646-38.191 31.499-61.064 41.174-23.672 10.012-48.826 15.089-74.766 15.089-25.94 0-51.095-5.077-74.767-15.089-22.873-9.675-43.417-23.527-61.064-41.174s-31.5-38.191-41.174-61.064C68.982 307.096 63.905 281.94 63.905 256c0-25.94 5.077-51.095 15.089-74.767 9.674-22.873 23.527-43.417 41.174-61.064s38.191-31.5 61.064-41.174c23.673-10.013 48.828-15.09 74.768-15.09 25.939 0 51.094 5.077 74.766 15.089a191.221 191.221 0 0 1 37.802 21.327A31.853 31.853 0 0 0 365 115c0 17.675 14.327 32 32 32 5.293 0 10.28-1.293 14.678-3.568a191.085 191.085 0 0 1 21.327 37.801c10.013 23.672 15.09 48.827 15.09 74.767 0 25.939-5.077 51.096-15.09 74.768-9.675 22.873-23.527 43.418-41.175 61.064z"/><circle cx="256.003" cy="256" r="96"/></g></symbol><symbol viewBox="0 0 24 24" id="java" xmlns="http://www.w3.org/2000/svg"><path d="M2 21h18v-2H2M20 8h-2V5h2m0-2H4v10a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4v-3h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="javascript" xmlns="http://www.w3.org/2000/svg"><path d="M3 3h18v18H3V3m4.73 15.04c.4.85 1.19 1.55 2.54 1.55 1.5 0 2.53-.8 2.53-2.55v-5.78h-1.7V17c0 .86-.35 1.08-.9 1.08-.58 0-.82-.4-1.09-.87l-1.38.83m5.98-.18c.5.98 1.51 1.73 3.09 1.73 1.6 0 2.8-.83 2.8-2.36 0-1.41-.81-2.04-2.25-2.66l-.42-.18c-.73-.31-1.04-.52-1.04-1.02 0-.41.31-.73.81-.73.48 0 .8.21 1.09.73l1.31-.87c-.55-.96-1.33-1.33-2.4-1.33-1.51 0-2.48.96-2.48 2.23 0 1.38.81 2.03 2.03 2.55l.42.18c.78.34 1.24.55 1.24 1.13 0 .48-.45.83-1.15.83-.83 0-1.31-.43-1.67-1.03l-1.38.8z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="javascript-map" xmlns="http://www.w3.org/2000/svg"><path d="M18 8v2h2v10H10v-2H8v4h14V8h-4z" fill="#ffca28"/><path d="M2.444 2.506h14.135v14.136H2.444V2.506m3.714 11.811c.315.668.935 1.218 1.995 1.218 1.178 0 1.987-.629 1.987-2.003V8.993H8.805v4.508c0 .675-.275.848-.707.848-.455 0-.644-.314-.856-.683l-1.084.651m4.697-.14c.392.769 1.185 1.358 2.426 1.358 1.257 0 2.199-.652 2.199-1.854 0-1.107-.636-1.602-1.767-2.089l-.33-.141c-.573-.243-.816-.408-.816-.801 0-.322.243-.573.636-.573.377 0 .628.165.856.573l1.028-.683c-.432-.754-1.044-1.045-1.884-1.045-1.186 0-1.948.754-1.948 1.752 0 1.083.636 1.594 1.594 2.002l.33.141c.613.267.974.432.974.888 0 .377-.354.652-.903.652-.652 0-1.029-.338-1.312-.81l-1.083.63z" fill="#ffca28"/></symbol><symbol viewBox="0 0 180 180" id="jenkins" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="gia"><path transform="scale(1 -1)" fill="#37474f" d="M.899-144.42h144.42V0H.899z"/></clipPath></defs><g transform="matrix(1.0691 0 0 -1.0691 9.4 166.143)" clip-path="url(#gia)"><g fill-rule="evenodd"><path d="M107.96 30.661l-12.506-1.876-16.883-1.876-10.943-.312-10.629.312-8.13 2.502-7.19 7.815-5.628 15.945-1.25 3.44-7.504 2.5-4.377 7.191-3.126 10.317 3.44 9.067 8.128 2.814 6.565-3.127 3.127-6.878 3.752.626 1.25 1.563-1.25 7.19-.313 9.068 1.876 12.505-.074 7.143 5.701 9.114 10.005 7.19 17.508 7.504 19.383-2.814 16.883-12.193 7.817-12.505 5.002-9.067 1.25-22.51-3.752-19.384-6.877-17.195-6.566-9.066" fill="#f0d6b7"/><path d="M97.334-23.425l-44.709-1.876v-7.503l3.752-26.262-1.876-2.19-31.264 10.63-2.19 3.752-3.126 35.328-7.19 21.26-1.563 5.002 25.01 17.195 7.818 3.127 6.877-8.441 5.94-5.315 6.88-2.188 3.125-.938L68.57 1.899l2.814-3.44 7.19 2.502-5.002-9.693 27.2-12.818-3.439-1.876" fill="#335061"/><path d="M23.238 85.687l8.128 2.814 6.566-3.127 3.127-6.878 3.751.626.938 3.751-1.876 7.19 1.876 17.197-1.563 9.379 5.627 6.565 12.193 9.692-3.44 4.69-17.194-8.442-7.191-5.627-4.064-8.754-6.253-8.442-1.876-10.005 1.251-10.63" fill="#6d6b6d"/><path d="M36.055 115.07s4.69 11.567 23.448 17.195c18.759 5.628.938 4.065.938 4.065l-20.321-7.817-7.817-7.816-3.438-6.253 7.19.626M26.676 87.875s-6.566 21.886 18.446 25.012l-.938 3.752-17.195-4.065-5.003-16.257 1.251-10.63 3.439 2.188" fill="#dcd9d8"/></g><g fill="#f7e4cd"><path d="M36.681 58.799l4.094 3.966s1.847-.214 2.16-2.402c.312-2.19 1.25-21.886 14.693-32.516 1.227-.97-10.004 1.564-10.004 1.564L37.62 45.042M94.209 64.739s.729 9.477 3.28 8.748c2.553-.729 2.553-3.28 2.553-3.28s-6.198-4.01-5.833-5.468" fill-rule="evenodd"/><path d="M120.16 99.442s-5.153-1.088-5.628-5.628c-.474-4.54 5.628-.938 6.566-.625M82.327 99.129s-6.879-.938-6.879-5.314c0-4.378 7.817-4.065 10.005-2.19"/><g fill-rule="evenodd"><path d="M39.807 78.808s-11.881 7.191-13.131.312c-1.25-6.877-4.065-11.88 1.876-19.07l-4.064 1.25-3.752 9.691-1.25 9.38 7.19 7.504 8.129-.626 4.69-3.751.312-4.69M45.435 98.504s5.315 27.512 32.203 32.827c22.136 4.375 33.765-.938 38.142-5.94 0 0-19.696 23.447-38.455 16.257-18.759-7.191-32.514-20.322-32.202-28.762.532-14.377.313-14.382.313-14.382M117.97 122.27s-9.066.312-9.38-7.817c0 0 0-1.25.625-2.5 0 0 7.192 8.129 11.568 3.751"/><path d="M78.268 111.1s-1.56 12.477-12.199 5.223c-6.878-4.69-6.252-11.255-5.002-12.505s.91-3.77 1.862-2.04c.952 1.728.638 7.356 4.078 8.918 3.439 1.564 9.077 3.31 11.26.404"/></g></g><g fill="#49728b" fill-rule="evenodd"><path d="M48.874 26.597L19.486 13.466s12.193-48.46 5.94-63.467l-4.377 1.563-.313 18.446-8.128 35.015-3.44 9.692 30.639 20.633 9.067-8.753M51.896-.206l4.17-5.087v-18.76h-5.003s-.625 13.132-.625 14.696c0 1.563.624 7.19.624 7.19M52-26.866l-14.069-.625 4.065-2.813L52-31.868"/></g><g fill-rule="evenodd"><path d="M100.15-23.739l11.567.313 2.814-28.764-11.881-1.563-2.5 30.014" fill="#335061"/><path d="M103.27-23.739l17.508.938s7.19 18.133 7.19 19.07c0 .939 6.253 26.263 6.253 26.263l-14.069 14.694-2.813 2.501-7.504-7.503V3.148l-6.565-26.887" fill="#335061"/><path d="M111.09-21.55l-10.942-2.188 1.563-8.755c4.064-1.876 10.943 3.127 10.943 3.127M111.4 33.162l21.885-16.257.626 7.503-16.57 15.32-5.94-6.566" fill="#49728b"/><path d="M62.85-85.332l-6.473 26.266-3.22 19.38-.531 14.385 29.296 1.56 18.226.003-1.658-32.83 2.814-25.324-.312-4.69-23.76-1.876-14.382 3.126" fill="#fff"/><path d="M96.083-23.426s-1.563-32.515 3.127-55.65c0 0-9.38-5.94-23.136-7.503l26.262.938 3.126 1.875-3.752 51.273-.938 10.944" fill="#dcd9d8"/><path d="M115.06-49.691l12.193 3.44 23.135 1.25 3.44 10.629-6.254 18.446-7.19.938-10.005-3.127-9.599-4.686-5.095.935-3.972-1.56" fill="#fff"/><path d="M114.84-43.435s8.128 3.751 9.38 3.438L120.78-22.8l4.065 1.563s2.814-16.257 2.814-18.133c0 0 17.507-.938 19.07-.938 0 0 3.752 7.191 2.814 14.694l3.44-10.005.312-5.628-5.002-7.503-5.627-1.25-9.38.312-3.126 4.064-10.943-1.563-3.44-1.25" fill="#dcd9d8"/></g><path d="M102.56-21.241L95.682-3.733l-7.19 10.317s1.562 4.377 3.75 4.377h7.192l6.878-2.501-.625-11.568-3.127-18.134" fill="#fff"/><path d="M103.9-15.297S95.145 1.585 95.145 4.086c0 0 1.563 3.752 3.752 2.814 2.19-.938 6.879-3.439 6.879-3.439v5.94l-10.63 2.19-7.19-.939 12.193-28.763 2.5-.313" fill="#dcd9d8" fill-rule="evenodd"/><path d="M65.664 25.968l-8.661.942-8.13 2.501v-2.814l3.972-4.38 12.506-5.627" fill="#fff"/><path d="M51.689 25.031s9.693-4.065 12.819-3.127l.311-3.748-8.752 1.872-5.316 3.752.938 1.251" fill="#dcd9d8" fill-rule="evenodd"/><path d="M115.03 9.897c-5.305.156-10.098.786-14.294 1.97.285 1.72-.249 3.408.18 4.647 1.17.843 3.13.83 4.898 1.027-1.529.752-3.677 1.049-5.44.615-.042 1.194-.578 1.934-.902 2.868 2.982 1.064 10.024 8.044 13.984 5.732 1.887-1.099 2.689-7.377 2.835-10.43.122-2.533-.23-5.088-1.261-6.43" fill="#d33833" fill-rule="evenodd"/><path d="M115.03 9.897c-5.305.156-10.098.786-14.294 1.97.285 1.72-.249 3.408.18 4.647 1.17.843 3.13.83 4.898 1.027-1.529.752-3.677 1.049-5.44.615-.042 1.194-.578 1.934-.902 2.868 2.982 1.064 10.024 8.044 13.984 5.732 1.887-1.099 2.689-7.377 2.835-10.43.122-2.533-.23-5.088-1.261-6.43z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M89.66 18.569c-.014-.401-.03-.806-.047-1.21-1.656-1.089-4.33-1.076-6.148-1.99 2.68-.117 4.79-.763 6.614-1.672l-.118-3.033c-3.036-2.078-5.81-5.173-9.384-7.122-1.69-.922-7.622-3.294-9.42-2.875-1.017.236-1.109 1.499-1.516 2.689-.866 2.548-2.861 6.605-3.035 10.44-.222 4.846-.71 12.967 4.51 11.969 4.213-.804 9.113-2.745 12.375-4.527 1.993-1.09 3.146-2.436 6.17-2.669" fill="#d33833" fill-rule="evenodd"/><path d="M89.66 18.569c-.014-.401-.03-.806-.047-1.21-1.656-1.089-4.33-1.076-6.148-1.99 2.68-.117 4.79-.763 6.614-1.672l-.118-3.033c-3.036-2.078-5.81-5.173-9.384-7.122-1.69-.922-7.622-3.294-9.42-2.875-1.017.236-1.109 1.499-1.516 2.689-.866 2.548-2.861 6.605-3.035 10.44-.222 4.846-.71 12.967 4.51 11.969 4.213-.804 9.113-2.745 12.375-4.527 1.993-1.09 3.146-2.436 6.17-2.669z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M92.675 12.788c-.463 2.64-.999 3.393-.792 5.695 7.04 4.693 8.361-8.061.792-5.695" fill="#d33833" fill-rule="evenodd"/><path d="M92.675 12.788c-.463 2.64-.999 3.393-.792 5.695 7.04 4.693 8.361-8.061.792-5.695z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M102.87 10.649s-2.19 3.127-.626 4.065c1.564.938 3.127 0 4.065 1.563s0 2.501.313 4.377 1.877 2.189 3.44 2.501c1.562.313 5.94.938 6.565-.625l-1.876 5.627-3.752 1.25-11.88-6.877-.626-3.44v-6.877M70.041.331c-.376 4.88-.773 9.752-1.215 14.626-.662 7.279 1.748 6.009 8.057 6.009.964 0 5.933-1.15 6.289-1.876 1.705-3.483-2.851-2.709 1.964-5.335 4.065-2.216 11.246 1.346 9.603 6.273-.919 1.095-4.789.341-6.176 1.06l-7.327 3.8c-3.108 1.612-10.29 3.962-13.603 1.709-8.395-5.71.53-19.974 3.524-25.93" fill="#ef3d3a" fill-rule="evenodd"/><g fill="#231f20" fill-rule="evenodd"><path d="M78.268 111.1c-8.521 1.985-12.755-3.566-15.338-9.323-2.306.559-1.389 3.695-.806 5.294 1.525 4.194 7.672 9.778 12.694 9.02 2.161-.325 5.086-2.301 3.45-4.99M119.79 101.4l.404-.016c1.926-4 3.593-8.238 6.022-11.769-1.628-3.79-12.322-7.144-12.157-.338 2.313 1.01 6.305.206 8.356 1.497-1.186 3.254-2.897 6.024-2.625 10.626M82.63 101.29c1.827-3.35 2.422-6.868 5.019-9.4 1.17-1.14 3.444-2.529 2.316-5.698-.263-.747-2.189-2.414-3.3-2.741-4.06-1.2-13.521-.248-10.317 4.814 3.358-.157 7.871-2.18 10.38.257-1.927 3.081-5.363 9.177-4.098 12.768M118.26 67.253c-6.113-3.927-12.93-8.197-22.947-7.207-2.14 1.86-2.956 6.002-.877 8.737 1.082-1.861.402-5.284 3.419-5.799 5.684-.972 12.299 3.477 16.387 5.032 2.535 4.275-.219 5.847-2.503 8.597-4.675 5.636-10.947 12.622-10.72 21.06 1.89 1.37 2.053-2.092 2.325-2.722 2.44-5.714 8.585-13.021 13.07-17.912 1.1-1.205 2.914-2.36 3.115-3.157.582-2.315-1.513-5.09-1.27-6.63M37.668 71.387c-1.916 1.094-2.372 5.91-4.622 6.048-3.215.195-2.629-6.25-2.616-10.018-2.213 2.009-2.602 8.194-.976 11.37-1.853.91-2.68-1.003-3.708-1.677 1.32 9.595 14.036 4.45 11.922-5.723M122.15 63.257c-2.846-5.417-6.871-11.382-15.222-11.555-.17 1.75-.3 4.411.009 5.464 6.384.614 10.325 3.863 15.212 6.091M82.149 59.745c5.326-2.8 15.114-3.102 22.353-2.89.388-1.586.379-3.545.394-5.48-9.305-.463-20.307 1.84-22.747 8.37M81.136 54.523c3.683-9.247 16.341-8.182 27.016-7.927-.47-1.2-1.489-2.62-2.755-3.132-3.42-1.392-12.855-2.448-17.604.074-3.011 1.601-4.946 5.219-6.596 7.34-.797 1.024-4.765 3.64-.06 3.645"/></g><path d="M117.82 3.516c-4.322-7.402-8.457-15.005-13.585-21.534 2.15 6.32 3.07 16.9 3.394 24.965 4.498 2.105 8.349-.474 10.191-3.43" fill="#81b0c4" fill-rule="evenodd"/><g fill="#231f20" fill-rule="evenodd"><path d="M141.07-23.089c-4.839-.969-8.239-5.671-12.959-5.37 2.594 3.658 7.14 5.2 12.959 5.37M143.21-30.661c-3.944-.417-8.576-1.055-12.577-.726 1.894 2.892 9.19 1.894 12.577.726M144.58-37.19c-4.433-.096-9.942-.008-14.155.346 2.492 2.677 11.28.993 14.155-.346"/></g><g fill-rule="evenodd"><path d="M109.48-55.057c.636-5.567 2.843-11.207 2.566-17.304-2.45-.827-3.858-1.55-7.142-1.545-.232 5.181-.925 13.102-.718 18.041 1.615-.107 3.997 1.154 5.294.808" fill="#dcd9d8"/><path d="M102.33 26.985c-2.226-1.453-4.121-3.267-6.259-4.818-4.74-.235-7.327.328-10.81 3.05.057.219.407.121.42.39 5.075-2.262 11.524.92 16.648 1.378" fill="#f0d6b7"/><path d="M75.694-7.603c1.394 6.04 6.857 9.17 11.817 12.497 5.12-6.498 8.234-14.855 11.663-22.92-8.102 2.443-16.38 6.406-23.481 10.423" fill="#81b0c4"/><path d="M104.18-55.865c-.207-4.94.486-12.86.718-18.041 3.283-.004 4.691.718 7.142 1.545.276 6.096-1.93 11.737-2.566 17.304-1.298.346-3.679-.914-5.294-.808zm-51.13 28.09c2.165-19.906 5.301-36.639 11.054-54.266 12.766-3.876 28.157-4.214 39.441-.716-2.072 9.948-1.167 22.06-2.378 32.677-.912 7.98-.447 16.009-1.698 24.15-13.673 2.844-33 .665-46.418-1.845zm49.651 1.72c-.115-8.549.383-16.982 1.036-25.542 3.282.493 5.51.822 8.56 1.49-.99 8.241-.869 17.514-2.886 24.804-2.332-.023-4.385.027-6.71-.752zm16.653 1.378c-1.558.357-3.372.014-4.86-.015.7-6.969 2.397-14.659 2.995-21.974 2.342-.073 3.593 1.032 5.52 1.403.102 6.421-.562 15.268-3.655 20.586zm25.215-23.038c4.882 1.186 7.952 7.165 6.586 13.305-.916 4.127-2.548 11.898-4.295 14.538-1.29 1.953-4.79 4.51-7.584 2.72-4.545-2.91-12.552-3.755-15.867-7.278 1.662-5.534 2.178-13.135 2.864-20.146 5.678-.354 12.665 1.562 17.387-.471-3.297-1.068-7.575-1.077-10.423-2.633 2.328-1.125 7.778-.897 11.332-.035zM99.17-18.025c-3.43 8.063-6.543 16.42-11.663 22.918-4.96-3.327-10.423-6.456-11.817-12.497 7.1-4.017 15.379-7.98 23.481-10.422zm8.453 24.971c-.325-8.065-1.245-18.644-3.395-24.965 5.128 6.53 9.263 14.132 13.585 21.534-1.842 2.957-5.693 5.536-10.19 3.431zm-9.582 3.405c-1.943.21-3.592-2.233-6.117-1.177-.58-.64-1.105-1.333-1.695-1.958 5.579-6.723 8.114-16.262 12.423-24.163 2.312 7.59 2.045 15.904 2.555 24.188-3.177-.201-4.94 2.873-7.166 3.11zm-6.161 8.132c-.208-2.303.328-3.056.791-5.695 7.57-2.367 6.248 10.388-.791 5.695zm-8.394 2.755c-3.261 1.782-8.161 3.723-12.374 4.527-5.222.999-4.732-7.123-4.51-11.968.173-3.836 2.168-7.893 3.035-10.441.406-1.19.498-2.453 1.515-2.69 1.798-.418 7.73 1.954 9.42 2.875 3.575 1.95 6.348 5.045 9.384 7.123.04 1.011.078 2.021.119 3.032-1.826.91-3.935 1.555-6.615 1.673 1.818.914 4.492.901 6.148 1.989.016.405.033.81.047 1.21-3.024.234-4.176 1.58-6.17 2.67zm-31.152 5.659c-2.707-2.748 7.592-6.494 10.871-6.696-.018 1.739.991 3.378.788 4.626-3.895.684-9.013.232-11.66 2.07zm33.345-1.29c-.013-.27-.363-.172-.42-.39 3.482-2.722 6.07-3.285 10.81-3.05 2.137 1.551 4.033 3.365 6.259 4.818-5.124-.458-11.574-3.64-16.648-1.379zm30.606-9.282c-.146 3.053-.948 9.332-2.835 10.431-3.961 2.312-11.002-4.668-13.984-5.732.324-.934.86-1.674.901-2.868 1.764.434 3.912.137 5.44-.615-1.767-.198-3.727-.185-4.897-1.027-.429-1.239.105-2.927-.18-4.647 4.196-1.184 8.989-1.814 14.294-1.97 1.032 1.341 1.383 3.896 1.261 6.429zM47.777 24.24c-.85.606-6.6 8.087-7.388 7.777-10.405-4.103-20.134-11.199-28.828-17.91 8.29-17.787 11.635-39.579 12.227-60.582 9.496-4.441 17.836-10.844 30.722-11.512-1.491 10.55-2.852 19.962-3.699 29.895-3.237 1.365-7.882-.062-10.913.423-.025 3.651 4.628 1.6 5.015 4.054.292 1.858-2.56 1.998-1.631 4.923 2.368-.861 3.612-2.763 6.138-3.477 2.309 5.05-.032 13.985.3 18.205.064.792.397 4.39 2.172 3.759 1.57-.559-.09-9.569.082-13.563.157-3.68-.444-7.242 1.046-9.552a355.817 355.817 0 0 0 38.576 3.16c-2.964 1.272-6.485 2.475-10.345 4.651-2.093 1.18-8.69 3.635-9.293 5.622-.964 3.167 2.528 4.855 3.125 7.57-6.285-3.428-7.511 3.286-8.998 8.042-1.347 4.308-2.114 7.526-2.445 10.01-5.414 2.581-11.203 5.195-15.863 8.505zm63.009 6.872c8.67 4.204 10.232-15.711 6.834-22.127.525-1.914 2.331-2.646 3.069-4.366-4.838-8.667-10.211-16.756-15.148-25.32 3.672 2.286 8.917.409 13.238 2.12 1.58.624 2.722 4.24 3.918 7.133 3.29 7.958 6.743 17.99 8.28 25.586.346 1.73 1.292 5.5 1.08 7.04-.378 2.758-4.12 4.803-6.022 6.508-3.506 3.15-5.714 5.921-9.371 8.866-1.483-2.189-4.666-3.66-5.878-5.44zM27.95 107.99c-4.13-4.545-3.266-13.062-2.766-19.121 7.467 4.697 17.377-.372 17.284-8.36 3.565.094 1.332 4.452.687 7.259-2.107 9.169 3.55 19.13.256 27.516-6.395-.485-11.649-3.097-15.46-7.294zm29.558 26.38c-9.352-2.65-21.337-9.446-25.18-17.847 2.976.432 5.041 1.933 7.977 2.119 1.11.072 2.563-.466 3.838-.148 2.54.63 4.685 6.327 6.602 8.447 1.868 2.07 4.114 2.954 5.651 4.841.988.477 2.448.444 2.504 1.927-.428.457-.879.806-1.392.66zm48.681-2.493c-9.707 5.477-26.136 9.596-36.462 4.449-8.331-4.155-19.593-11.027-23.433-19.737 3.587-8.405-1.062-16.106-1.36-24.64-.157-4.54 2.139-8.504 2.315-13.446-1.228-2.025-4.978-2.275-7.574-2.136-.873 4.372-2.403 9.287-6.906 9.78-6.371.697-11.03-4.576-11.319-10.085-.342-6.48 4.978-17.22 12.517-16.475 2.913.287 3.629 3.207 6.802 3.177 1.72-3.432-2.653-4.51-3.103-6.964-.117-.634.363-3.112.642-4.274 1.37-5.658 4.422-12.982 7.427-17.29 3.814-5.464 11.307-6.288 19.37-6.823 1.44 3.101 6.743 2.846 10.2 2.035-4.143 1.64-7.993 5.617-11.185 9.137-3.665 4.039-7.378 8.371-7.566 13.65 6.927-9.61 12.65-18.003 25.246-22.23 9.53-3.196 20.662 1.465 27.986 6.608 3.039 2.137 4.853 5.529 7.013 8.634 8.082 11.626 11.854 28.219 11.024 44.303-.342 6.633-.327 13.244-2.552 17.706-2.326 4.666-10.193 8.84-14.8 4.62-.853 4.537 3.83 7.344 9.331 5.71-3.922 5.063-8.039 11.145-13.614 14.29zm18.084-149.66c7.585 3.77 21.757 10.149 26.512-.014 1.755-3.746 3.814-10.079 4.723-13.946 1.284-5.456-1.392-16.923-7-18.754-4.953-1.617-10.733-1.518-16.7-.32-.702.585-1.484 1.603-2.03 2.665-4.261.165-8.25-.229-11.615-1.98.319-3.15-1.812-3.656-3.81-4.305-1.48-5.872 2.963-13.541 1.9-18.896-.76-3.815-5.453-4.405-8.902-5.118-.113-2.12.15-3.89.386-5.683-.789-2.907-4.327-4.561-7.679-4.967-11.029-1.326-27.775-1.922-38.384 1.893-2.96 7.261-5.292 16.093-7.758 24.384-10.346-1.105-18.715 4.464-26.603 8.113-2.731 1.266-6.51 1.964-7.53 4.138-.99 2.105-.584 6.14-.83 9.95-.625 9.733-1.16 19.12-3.73 29.086-1.154 4.472-3.165 8.418-4.568 12.727C9.358 5.184 7.092 10.12 6.5 14.1c-.877 5.903 4.681 6.232 8.235 8.79 5.494 3.954 9.806 6.142 15.756 9.711 1.762 1.057 7.077 3.733 7.681 4.966 1.202 2.443-2.062 5.888-2.935 7.803-1.38 3.03-2.1 5.602-2.298 8.59-4.992.789-8.775 3.76-11.06 7.109-3.781 5.543-6.403 15.798-3.132 23.599.257.614 1.536 1.822 1.725 2.765.372 1.858-.7 4.329-.768 6.305-.343 10.14 1.716 18.875 8.541 21.932 2.771 11.038 12.688 14.71 22.032 20.195 3.493 2.05 7.343 3.36 11.32 4.824 14.263 5.25 36.15 4.261 47.987-4.692 5.02-3.797 13.044-11.813 15.914-17.617 7.58-15.323 7.042-40.931 1.74-59.571-.712-2.503-1.746-6.181-3.19-9.187-1.006-2.1-4.134-6.3-3.754-8.153.391-1.916 7.132-7.034 8.577-8.428 2.603-2.51 7.548-5.843 7.948-9.012.43-3.372-1.485-7.984-2.456-11.238-3.245-10.858-6.412-20.895-10.091-30.576" fill="#231f20"/><path d="M73.674 57.38c.411.548 2.674 1.38 5.84-.144 0 0-3.752-.626-3.44-6.881l-1.564.313s-1.615 5.672-.836 6.712" fill="#f7e4cd"/><path d="M101.09 3.617a1.72 1.72 0 1 0-3.44.001 1.72 1.72 0 0 0 3.44-.001M102.81-4.355a1.72 1.72 0 1 0-3.44 0 1.72 1.72 0 0 0 3.44 0" fill="#1d1919"/></g><g><rect transform="matrix(.8 0 0 -.8 0 144)" x="16.854" y="177.38" width="70.412" height="4.12" rx=".983" ry=".983"/><rect transform="scale(1 -1)" x="78.502" y="-2.097" width="50.037" height="3.296" rx=".786" ry=".786"/><rect transform="scale(1 -1)" x="13.483" y="-3.697" width="54.831" height="3.296" rx=".786" ry=".786"/><rect transform="scale(1 -1)" x="83.296" y="-3.697" width="45.243" height="3.296" rx=".786" ry=".786"/></g></g></symbol><symbol viewBox="0 0 24 24" id="json" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#fbc02d"/></symbol><symbol viewBox="0 0 50 50" id="julia" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -247)" stroke-width="5.673"><circle cx="13.497" cy="281.632" r="9.555" fill="#bc342d"/><circle cx="36.081" cy="281.632" r="9.555" fill="#864e9f"/><circle cx="24.722" cy="262.389" r="9.555" fill="#328a22"/></g></symbol><symbol viewBox="0 0 64 64" id="karma" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -233)"><path d="M38.556 288.413l-20.29-26.687 9.532-7.246 20.29 26.686h-.001.002l5.527 7.247z" fill="#359b8b" stroke-width=".173"/><path d="M35.681 241.172L24.92 255.327v-14.13H12.947v13.817l7.84 33.235h4.132v-13.147l.003.003 20.29-26.686-.008-.006 5.504-7.24H35.84v.12z" fill="#3cbeae" stroke-width=".206"/></g></symbol><symbol viewBox="0 0 24 24" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7 14a2 2 0 0 1-2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2 2 2 0 0 1-2 2m5.65-4A5.99 5.99 0 0 0 7 6a6 6 0 0 0-6 6 6 6 0 0 0 6 6 5.99 5.99 0 0 0 5.65-4H17v4h4v-4h2v-4H12.65z" fill="#26a69a"/></symbol><symbol viewBox="0 0 24 24" id="kivy" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.89 0 0 1.89 -12.157 -11.429)" fill="#90a4ae"><path d="M7.026 8.63v4.474l1.928-1.928a.437.437 0 0 0 0-.619zM9.38 16.072v-4.474l-1.927 1.927a.437.437 0 0 0 0 .62zM18.576 10.412l-5.346.564-.017.018 2.39 2.39zM9.922 8.502s.023 3.304-.003 4.452c-.02.856.371 1.114.746 1.507.538.564 1.599 1.57 1.599 1.57a.53.53 0 0 0 .75 0l1.843-1.844a.53.53 0 0 0 0-.75z"/></g></symbol><symbol viewBox="0 0 24 24" id="kl" xmlns="http://www.w3.org/2000/svg"><defs><style>.a{fill:#3aaae1}.b{fill:#fdfeff}</style></defs><title>kl</title><path d="M12.033 1.737c-.25-.003-.5.11-.729.337C8.225 5.15 5.15 8.227 2.078 11.31c-.144.144-.229.346-.341.521v.41c.16.223.294.474.485.666a3259.51 3259.51 0 0 0 8.936 8.937c.193.192.443.325.666.486h.41c.205-.142.436-.256.609-.428 3.046-3.041 6.09-6.083 9.133-9.127.47-.47.472-1.005.006-1.472l-9.218-9.217c-.23-.23-.48-.347-.731-.35zm-1.062 4.545l1.386.832c.702.422 1.403.846 2.109 1.262a.544.544 0 0 1 .04.026l.016.013.017.013c.061.056.089.123.088.224a510.281 510.281 0 0 0 0 3.794.463.463 0 0 1-.007.094c-.015.069-.054.103-.142.109a.464.464 0 0 1-.044.002c-.045-.002-.09-.002-.136-.003-.323-.006-.648-.001-.998-.001v-.527-1.34-.671-.003l.004-.668c0-.147-.039-.231-.17-.308-.893-.528-1.78-1.066-2.67-1.6-.051-.03-.101-.065-.173-.111l.001-.003h-.001zm.362 3.39c.068-.003.119.043.173.138.085.148.174.293.264.44l.015.025c.096.154.194.31.292.47l-1.915 1.176c-.337.207-.673.417-1.014.617-.113.067-.154.143-.154.277.01.977.01 1.955.014 2.932V16H7.7V16h-.002c-.004-.053-.014-.112-.014-.17-.005-1.25-.006-2.501-.015-3.751 0-.142.045-.222.164-.294a467.13 467.13 0 0 0 3.353-2.054l.016-.01a.606.606 0 0 1 .032-.017l.016-.008a.308.308 0 0 1 .033-.013l.012-.004a.157.157 0 0 1 .028-.005l.01-.001zm5.677 3.126l.314.54.346.594v.001c-.158.094-.298.178-.438.259l-3.097 1.798c-.106.062-.189.071-.3.01l-.893-.496-1.524-.843-.895-.493c-.035-.02-.068-.044-.129-.085h.001l.137-.25.495-.902 1.446.795c.442.243.886.483 1.323.734.121.07.212.072.334 0 .894-.525 1.792-1.043 2.689-1.563.057-.034.118-.061.191-.1z" fill="#29b6f6" stroke-width=".041"/></symbol><symbol viewBox="0 0 500 500" id="kotlin" xmlns="http://www.w3.org/2000/svg"><path d="M500 500H0V0h500L250 250z" fill="#7F52FF"></path></symbol><symbol viewBox="0 0 240 240" id="laravel" xmlns="http://www.w3.org/2000/svg"><path d="M216.05 119.036c-1.433.343-24.945 6.673-24.945 6.673l-19.227-28.622c-.537-.828-.99-1.656.359-1.849 1.345-.196 23.195-4.477 24.182-4.723.99-.245 1.837-.536 3.053 1.267 1.21 1.8 17.836 24.626 18.464 25.506.627.877-.447 1.41-1.883 1.748m-4.101 49.326c.588 1.003 1.176 1.64-.67 2.367-1.843.73-62.243 22.847-63.418 23.39-1.173.546-2.092.73-3.607-1.637-1.51-2.362-21.16-39.264-21.16-39.264l64.03-18.075c1.876-.644 2.317-.405 3.103.822 1.074 1.68 21.143 31.403 21.726 32.4m-103.7-21.087c-.78.202-37.566 9.733-39.525 10.22-1.965.485-1.965.246-2.188-.49-.226-.727-43.728-98.053-44.333-99.271-.605-1.214-.574-2.177 0-2.177.571 0 34.734-3.313 35.944-3.383 1.207-.07 1.08.205 1.526 1.033l49.025 91.818c.84 1.58 1.239 1.81-.452 2.248m94.588-59.77c-3.5-4.58-5.2-3.751-7.357-3.41-2.154.336-27.277 4.915-30.194 5.449-2.918.536-4.758 1.803-2.963 4.53 1.597 2.422 18.113 27.824 21.751 33.42l-65.663 17.066L66.18 49.832c-2.075-3.342-2.507-4.514-7.236-4.28-4.735.23-40.969 3.495-43.55 3.731-2.58.233-5.416 1.479-2.835 8.09 2.583 6.612 43.734 102.82 44.88 105.62 1.149 2.803 4.128 7.345 11.11 5.527 7.157-1.871 31.969-8.894 45.52-12.742 7.163 14.07 21.77 42.619 24.473 46.707 3.607 5.459 6.089 4.56 11.626 2.738 4.325-1.42 67.65-26.129 70.502-27.4 2.855-1.273 4.613-2.184 2.685-5.275-1.419-2.28-18.124-26.558-26.876-39.26 5.993-1.733 27.305-7.888 29.575-8.557 2.646-.779 3.008-2.19 1.572-3.94-1.436-1.755-21.293-28.72-24.79-33.296z" fill="#ff5722" stroke="#ff5722" stroke-width="8.852" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" id="less" xmlns="http://www.w3.org/2000/svg"><path d="M13.696 2.999V5h2.002v5a2 2 0 0 0 1.999 2 2 2 0 0 0-2 2v5h-2v2h2a2 2 0 0 0 2-2v-4a2 2 0 0 1 2-2h1V11h-1a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2.001zm-.03 12.766v.47a1 1 0 0 0 .03-.236 1 1 0 0 0-.03-.234zM10.566 21v-2.001H8.565v-5a2 2 0 0 0-2-2 2 2 0 0 0 2-2V5h2.001v-2H8.565a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-.999V13h1a2 2 0 0 1 2 2v3.999A2 2 0 0 0 8.564 21zm.03-12.766v-.47a1 1 0 0 0-.03.236 1 1 0 0 0 .03.234z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="lib" xmlns="http://www.w3.org/2000/svg"><path d="M19 7H9V5h10m-4 10H9v-2h6m4-2H9V9h10m1-7H8a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2M4 6H2v14a2 2 0 0 0 2 2h14v-2H4V6z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 40 40" id="livescript" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -257)" fill="#317eac"><path stroke-width="3.299" d="M5.419 260.18h3.685v34.207H5.419z"/><path stroke-width="3.299" d="M37.074 288.197v3.685H2.867v-3.685z"/><path stroke-width="2.894" d="M29.612 265.658l2.004 2.005L7.428 291.85l-2.004-2.005z"/><path stroke-width="2.325" d="M10.73 262.471h2.835v22.08H10.73z"/><path stroke-width="2.063" d="M15.36 262.519h2.835v17.382H15.36z"/><path stroke-width="1.77" d="M19.99 262.471h2.835v12.802H19.99z"/><path stroke-width="1.422" d="M24.526 262.491h2.835v8.254h-2.835z"/><path stroke-width="1.128" d="M28.783 262.463h2.835v5.197h-2.835z"/><path stroke-width="2.325" d="M34.801 286.545v-2.835h-22.08v2.835z"/><path stroke-width="2.063" d="M34.753 281.914v-2.835H17.371v2.835z"/><path stroke-width="1.77" d="M34.801 277.284v-2.835H21.999v2.835z"/><path stroke-width="1.422" d="M34.781 272.749v-2.835h-8.254v2.835z"/><path stroke-width="1.128" d="M34.809 268.492v-2.835h-5.197v2.835z"/></g></symbol><symbol viewBox="0 0 24 24" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z" fill="#ffd54f"/></symbol><symbol viewBox="0 0 24 24" id="lua" xmlns="http://www.w3.org/2000/svg"><circle cx="12.203" cy="12.102" r="10.322" fill="none" stroke="#42a5f5"/><path d="M12.33 5.746a6.483 6.381 0 0 0-6.482 6.381 6.483 6.381 0 0 0 6.482 6.38 6.483 6.381 0 0 0 6.484-6.38 6.483 6.381 0 0 0-6.484-6.38zm1.86 1.916a2.329 2.292 0 0 1 2.33 2.293 2.329 2.292 0 0 1-2.33 2.291 2.329 2.292 0 0 1-2.329-2.29 2.329 2.292 0 0 1 2.328-2.294z" fill="#42a5f5" fill-rule="evenodd"/><ellipse cy="4.615" cx="19.631" rx="2.329" ry="2.292" fill="#42a5f5" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" id="markdown" xmlns="http://www.w3.org/2000/svg"><path d="M2 16V8h2l3 3 3-3h2v8h-2v-5.17l-3 3-3-3V16H2m14-8h3v4h2.5l-4 4.5-4-4.5H16V8z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" preserveAspectRatio="xMidYMid" id="markojs" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -120.96)" stroke-width=".984"><path d="M4.002 126.482c-.655 1.07-1.32 2.14-1.976 3.21-.655 1.06-1.308 2.142-1.963 3.212l.002.002-.002.002c.655 1.07 1.308 2.15 1.963 3.211.655 1.07 1.32 2.141 1.976 3.211h3.33c-.664-1.07-1.318-2.14-1.974-3.21-.653-1.069-1.307-2.145-1.961-3.214.654-1.068 1.308-2.146 1.961-3.215a601.93 601.93 0 0 1 1.974-3.209z" fill="#2196f3"/><path d="M3.999 126.482l-.002.002c.655 1.07 1.31 2.15 1.964 3.212.655 1.07 1.32 2.14 1.974 3.21h3.331c-.664-1.07-1.319-2.14-1.974-3.21-.653-1.068-1.306-2.146-1.96-3.214z" fill="#26a69a"/><path d="M15.203 126.482l.002.002c-.655 1.07-1.31 2.15-1.965 3.212-.655 1.07-1.319 2.14-1.974 3.21h-3.33c.664-1.07 1.318-2.14 1.973-3.21.654-1.069 1.307-2.146 1.961-3.214z" fill="#8bc34a"/><path d="M11.874 126.484c.664 1.07 1.318 2.14 1.974 3.21.653 1.068 1.307 2.146 1.961 3.214-.654 1.069-1.308 2.145-1.961 3.213-.656 1.07-1.31 2.14-1.974 3.21h3.33c.655-1.07 1.319-2.14 1.974-3.21.655-1.06 1.31-2.14 1.966-3.21l-.002-.003.002-.002c-.656-1.07-1.311-2.152-1.966-3.213-.655-1.07-1.319-2.138-1.974-3.209z" fill="#ffc107"/><path d="M16.74 126.482c.665 1.07 1.319 2.14 1.974 3.21.654 1.068 1.306 2.146 1.96 3.214-.654 1.069-1.306 2.145-1.96 3.213-.655 1.07-1.31 2.141-1.974 3.211h3.33c.656-1.07 1.32-2.14 1.974-3.21.655-1.062 1.31-2.141 1.966-3.212l-.002-.002.002-.002c-.655-1.07-1.31-2.152-1.966-3.213-.655-1.07-1.318-2.138-1.973-3.209z" fill="#f44336"/></g></symbol><symbol viewBox="0 0 23 24" id="mathematica" xmlns="http://www.w3.org/2000/svg"><path d="M11.512 1.523l-.073.025-.46.794-.454.763-1.217 2.09H9.29L5.435 3.5l-.1-.047h-.018v.092l.025.163v.086l.132 1.226v.082l.032.252v.082l.22 2.137v.075l.018.082v.06l-2.348.507-.04.015-.457.1-.025.01h-.042l-1.096.244-.04.007-.17.036v.082l.018.01 1.859 2.086.053.052.114.132.804.909v.005l-.053.05-.22.257-2.564 2.875-.01.007v.082l.071.006.295.075 1.697.366v.006l2.139.472h.015v.047l-.036.252v.08l-.046.412v.082l-.036.244v.082l-.045.412v.08l-.05.41v.08l-.036.244v.082l-.046.412v.082l-.05.407v.082l-.032.248V20l-.05.407v.104h.037l3.642-1.6.294-.134h.018l.177.312.539.911.015.032.854 1.465.16.262.404.695.007.022h.092l.005-.022.017-.025.56-.947.014-.042.6-1.033.316-.539.644-1.091.05.013 3.906 1.721h.035v-.085l-.138-1.32v-.082l-.032-.244v-.082l-.035-.245v-.085l-.033-.244v-.081l-.032-.245v-.082l-.032-.244v-.085l-.035-.245v-.082l-.032-.245v-.082l-.033-.244v-.085l-.025-.17v-.053l1.632-.354.043-.008.458-.107h.028v-.01l.23-.05.03-.01h.042l.382-.09.025-.01h.043l.194-.05h.033l1.015-.23.07-.007v-.064l-.015-.013-1.19-1.342-.028-.028-.197-.22-1.428-1.604v-.006l.295-.323.4-.457 2.148-2.408.015-.01v-.065l-.035-.008-1.288-.28-.372-.084-.047-.01-2.481-.544v-.045l.432-4.265v-.02h-.042l-.302.135-.01.014h-.025l-3.307 1.45-.297.135h-.015l-2.028-3.483-.099-.145-.014-.045zm-.001 1.114l1.365 2.323.34.592-.008.025-1.18 1.511-.517.66-.012-.01-.258-.335-.04-.05-1.397-1.787.03-.063 1.378-2.365.287-.491zm4.908 2.039l-.007.025-.168.225-.538.066zm-9.817.004l.053.02.677.3h-.499l-.224-.3zM16.947 5l-.123 1.248-.113-.928.226-.307zm-9.26.156l.053.024.705.309-.757-.175zm7.388.116l.02.168-1.318.403.003-.003.16-.071 1.015-.444zM9.669 6.388l.944 1.204v.01L9.483 7.2zm3.55.172l.21.682-.234.084-.089.022-.702.255.008-.022.776-.982zm-5 .836l.986.356.898.312.048.02 1.054.373.011 3.086-.362-.117-.67-.224-.081-.038-.735-.245-.77-.256-.29-.1-.011-.255-.032-1.195-.01-.287-.015-.894-.013-.297zm6.583 0l-.011.227-.028.9-.008.303-.032 1.475-.01.262-.337.117-.734.245-.77.256-.712.245-.355.117.01-3.086 1.632-.578zm.585.437l.09.735.79-.097-.915 1.302-.018.006.01-.183.018-.877zm-9.451.536l.152.22 1.447 2.049-2.607.968-.05.015-1.972-2.214-.28-.312.003-.01.115-.018.424-.1.14-.021.337-.078.042-.01zm11.146.003l3.284.713.029.01-.022.025-1.954 2.192-.277.312-.092-.036-2.564-.95.475-.681.152-.216zM6.787 8.52h.86l.036 1.258-.013-.006-.763-1.078zm1.358 2.625l.152.06.77.252.712.245.746.247.49.167-.065.092-1.723 2.334-1.015-.302-.082-.017-.035-.015-1.902-.56.938-1.22.981-1.277zm6.73 0l.033.006 1.787 2.327.132.17-.128.036-.032.014-2.196.642-.105.032-.564.17-.018-.003-1.053-1.44-.174-.239-.547-.726-.007-.018.469-.16.769-.254.713-.245.77-.252zm-7.766.305l-.007.02-.405.523-.291-.291.657-.245zm8.802 0l.043.007.578.212.714.27-.661.394-.375-.479-.03-.042-.262-.342zm-10.843.75l-.67.668.355-.397.207-.23zm12.911.016l.068.025.045.042.554.627.042.043.204.228-.255.135zm-6.473.265l.022.015 1.38 1.872.032.05.343.465.008.031-.088.117-.422.629-.047.074-.245.343-.97 1.43-.013.007-1.18-1.72-.096-.16-.493-.708-.008-.037 1.618-2.191.007-.01zm7.827 1.194l.565.633.063.082-.272-.093-.037-.013zm-15.785.148l.297.299-.637.218-.152.05.038-.058zm13.224.47l-.855.448.346.66-.185-.058-.27-.088-1.092-.348.012-.01zm-9.687.255l1.222.356-.006.007-.458.145-.443.135-.032.01-.49.157zm-2.765.048l.318.32 2.007.517-.567.18-.055.004-2.103-.469-.744-.156.007-.006zm14.966.205l.548.188v.003l-.457.1-.043.014-1.069.23zm-10.23.507l.007.227.01.347.025 1.363.025.691-.007.255-.24.107-2.863 1.255.032-.372.033-.255.017-.227.031-.256.037-.407.045-.42.018-.23.032-.251.032-.412.05-.414.013-.14 1.455-.457.003-.014.301-.098zm4.908 0l1.245.39v.014l.312.1 1.146.362.022.23.03.255.043.408.04.42.017.23.033.251.032.412.042.325.078.848-.078-.04-3.025-1.322-.004-.305.06-2.368zm-4.295.617l.015.007.067.107.6.875-.64.531-.034-1.438zm3.671 0h.008l-.005.06-.02.678-.005.214-.479-.223zm-2.888 3.605l.763.915.001.37-.017-.006-.025-.05-.464-.791-.012-.018zm1.53.61l.184.083-.343.586-.018.007.002-.532z" fill="#f44336" fill-rule="evenodd" stroke="#f44336" stroke-width=".7747499999999999" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 720 720" id="matlab" xmlns="http://www.w3.org/2000/svg"><title>Layer 1</title><path d="M209.247 329.98L52.368 387.638l121.325 85.822 96.752-95.804-61.198-47.674z" fill="#4db6ac" fill-rule="evenodd" stroke-width=".3"/><path d="M480.193 71.446c-13.123 1.784-9.565 1.013-28.4 16.09-18.008 14.418-69.925 100.347-97.673 129.256-24.688 25.722-34.46 12.199-60.102 33.661-25.68 21.494-65.273 64.464-65.273 64.464l63.978 47.32L394.15 222.754c23.948-32.932 23.694-37.266 36.744-71.82 6.384-16.907 17.76-29.9 27.756-45.809 12.488-19.874 30.186-34.855 21.543-33.68z" fill="#00897b" fill-rule="evenodd" stroke-width=".3"/><path d="M478.206 69.796c-31.268-.189-62.068 137.245-115.56 242.691-54.543 107.519-162.235 176.82-162.235 176.82 18.156 8.243 34.681 4.91 54.236 23.394 13.375 16.164 52.09 95.976 75.174 146.117 0 0 18.964-10.297 42.994-27.695 24.03-17.397 53.124-41.896 73.384-70.3 26.883-37.692 47.897-61.043 65.703-75.271 17.806-14.23 32.404-19.336 46.458-20.54 50.238-4.305 124.582 85.792 124.582 85.792S527.267 70.09 478.206 69.796z" fill="#ffb74d" fill-rule="evenodd" stroke-width=".3"/></symbol><symbol viewBox="0 0 24 24" id="merlin" xmlns="http://www.w3.org/2000/svg"><text style="line-height:1.25;-inkscape-font-specification:'Century Gothic Bold'" x="1.953" y="21.178" transform="scale(.99582 1.0042)" font-weight="700" font-size="30.255" font-family="Century Gothic" letter-spacing="0" word-spacing="0" fill="#42a5f5" stroke-width=".756"><tspan x="1.953" y="21.178" style="-inkscape-font-specification:'Century Gothic Bold'" font-size="22.745">M</tspan></text></symbol><symbol viewBox="0 0 192 191.99999" id="mocha" xmlns="http://www.w3.org/2000/svg"><title>Mocha Logo</title><g transform="translate(-354.75 -262.42) scale(4.835)" fill="#a1887f"><path d="M103.6 69.6c0-.5-.4-1-1-1H83.8c-.5 0-1 .4-1 1 0 3.4.5 15.1 5.5 20.8.2.2.4.3.7.3h8.4c.3 0 .5-.1.7-.3 5-5.6 5.5-17.3 5.5-20.8zm-7.4 18.2h-5.9c-.3 0-.5-.1-.7-.3-3.4-4-3.8-12-3.9-14.8 0-.5.4-1 1-1h13.2c.5 0 1 .4 1 1 0 2.8-.5 10.7-3.9 14.8-.3.2-.5.3-.8.3zM95.1 66.6s3.6-2.1 1.4-5.9c-1.3-2-1.9-3.7-1.4-4.4-1.3 1.6-3.5 3.3-1.1 6.9.8.9 1.2 2.8 1.1 3.4zM91.1 66.9s2.4-1.4.9-4c-.9-1.3-1.3-2.5-.9-2.9-.9 1.1-2.3 2.2-.7 4.7.5.5.7 1.8.7 2.2z"/><path d="M99.3 78.5c-.4 2.7-1.2 5.8-2.9 7.8-.2.2-.4.3-.6.3h-5c-.2 0-.5-.1-.6-.3-1.2-1.5-2-3.5-2.5-5.6 0 0 5.8.8 9.1-.4 2.4-.9 2.5-1.8 2.5-1.8z"/></g></symbol><symbol viewBox="0 0 24 24" id="movie" xmlns="http://www.w3.org/2000/svg"><path d="M18 4l2 4h-3l-2-4h-2l2 4h-3l-2-4H8l2 4H7L5 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V4h-4z" fill="#ff9800"/></symbol><symbol viewBox="0 0 24 24" id="music" xmlns="http://www.w3.org/2000/svg"><path d="M16 9V7h-4v5.5c-.42-.31-.93-.5-1.5-.5A2.5 2.5 0 0 0 8 14.5a2.5 2.5 0 0 0 2.5 2.5 2.5 2.5 0 0 0 2.5-2.5V9h3m-4-7a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z" fill="#ef5350"/></symbol><symbol viewBox="0 0 24 24" id="mxml" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m.12 13.5l3.74 3.74 1.42-1.41-2.33-2.33 2.33-2.33-1.42-1.41-3.74 3.74m11.16 0l-3.74-3.74-1.42 1.41 2.33 2.33-2.33 2.33 1.42 1.41 3.74-3.74z" fill="#ffa726"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-actions" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#ab47bc" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-effects" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#26c6da" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#e53935" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-state" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#9ccc65" stroke-width="12.914"/></symbol><symbol viewBox="0 0 24 24" id="nim" xmlns="http://www.w3.org/2000/svg"><path d="M4.464 15.75L2.288 3.78l5.985 7.617L12.08 3.78l3.809 7.617 5.985-7.617-2.177 11.97H4.464m15.234 3.264a1.088 1.088 0 0 1-1.088 1.088H5.553a1.088 1.088 0 0 1-1.089-1.088v-1.089h15.234z" stroke-width="1.088" fill="#ffca28"/></symbol><symbol viewBox="0 0 500 500" id="nix" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-1.965 36.302)" stroke-width=".395"><path d="M135.59 415.7c0-.295-2.752-5.283-6.116-11.084-3.364-5.801-6.116-10.776-6.116-11.055s9.514-16.889 21.143-36.912c11.629-20.022 21.323-36.798 21.542-37.279.346-.76-1.608-4.363-14.896-27.466-8.412-14.625-15.294-26.785-15.294-27.023 0-.5 24.46-43.501 25.206-44.31.414-.45.592-.384 1.078.395.32.513 16.876 29.256 36.791 63.87 62.62 108.85 74.852 130.01 75.41 130.46.3.242.544.554.544.694 0 .14-11.836.21-26.302.154-23.023-.09-26.313-.175-26.393-.694-.11-.714-27.662-48.825-28.86-50.392-.746-.978-.906-1.035-1.426-.51-.688.696-28.954 49.323-29.49 50.733l-.365.96h-13.229c-10.896 0-13.229-.095-13.229-.538zm167.58-125.61c-.134-.216 1.188-2.863 2.938-5.882 6.924-11.944 84.291-145.75 96.491-166.88 7.143-12.371 13.142-22.465 13.333-22.433.363.062 25.861 43.105 25.861 43.655 0 .174-6.761 11.952-15.026 26.173-8.46 14.557-14.932 26.104-14.81 26.421.185.483 4.564.564 30.213.564h29.996l.958 1.48c.526.814 3.296 5.547 6.155 10.518 2.859 4.971 5.45 9.29 5.756 9.597.706.705.704.724-.16 1.572-.395.388-3.36 5.323-6.587 10.965-3.228 5.643-6.056 10.387-6.285 10.543-.23.156-19.695.171-43.256.034l-42.84-.249-.804 1.15c-.441.632-7.504 12.736-15.696 26.897l-14.892 25.747H339.03c-8.517 0-20.015.116-25.55.259-6.55.168-10.15.121-10.309-.135zM169.42 132.23c-56.373-.055-102.5-.182-102.5-.282 0-.1 5.617-10.132 12.481-22.294l12.481-22.112h30.332c27.113 0 30.332-.065 30.332-.611 0-.336-6.659-12.228-14.797-26.427-8.139-14.199-14.797-25.917-14.797-26.04 0-.123 2.682-4.853 5.96-10.51s6.003-10.578 6.055-10.934c.086-.586 1.376-.648 13.572-.648 7.413 0 13.463.143 13.446.317-.017.174.222.707.531 1.184.31.476 9.763 16.937 21.007 36.578 11.244 19.64 20.71 36.022 21.036 36.4.554.647 2.549.691 31.428.691h30.837l12.896 22.145c7.093 12.18 12.8 22.301 12.682 22.492-.118.19-4.776.303-10.352.249-5.575-.054-56.26-.143-112.63-.198z" fill="#5075c1"/><path d="M25.289 203.14c-6.098 10.563-6.69 11.711-6.225 12.078.283.224 3.18 5.044 6.44 10.712 3.261 5.668 6.017 10.355 6.124 10.417.106.061 13.585.153 29.95.204 16.367.052 29.994.23 30.285.399.472.273-1.08 3.094-14.637 26.574L62.06 289.793l12.907 21.865c7.1 12.026 12.982 21.906 13.068 21.956.086.05 23.257-39.831 51.492-88.624 11.352-19.617 21.214-36.64 30.37-52.442 23.308-40.452 30.68-53.468 30.73-54.132-1.097-.11-6.141-.187-13.006-.216-3.945-.01-7.82-.02-12.75-.002l-25.341.092-15.42 26.706c-14.256 24.693-15.445 26.663-16.278 26.86l-.024.037c-.011.003-1.62-.001-1.825 0-4.29.062-20.453.063-40.226-.01-22.632-.082-41.615-.125-42.183-.096-.568.03-1.147-.03-1.29-.132-.142-.102-3.29 5.066-6.996 11.485zm205.16-190.3c-.123.149 5.62 10.392 12.761 22.763 12.199 21.131 89.393 155.03 96.276 167 1.502 2.613 2.92 4.803 3.443 5.348.9-1.249 3.531-5.63 7.954-13.219a1342.88 1342.88 0 0 1 10.049-17.76l6.606-11.443c.692-1.403.754-1.818.653-2.117-.162-.48-6.904-12.332-14.982-26.337-8.078-14.005-14.824-25.849-14.991-26.32a.73.73 0 0 1-.009-.366l-.426-.913L359.42 72.5c3.69-6.307 6.425-11.042 9.47-16.29 9.159-15.948 12.037-21.189 11.896-21.55-.126-.324-2.7-4.83-5.72-10.017-3.021-5.185-5.845-10.148-6.275-11.026-.483-.987-.734-1.364-1.1-1.456-.054.014-.083.018-.145.035-.42.112-5.454.195-11.189.185-5.734-.01-11.22.024-12.188.073l-1.76.089-14.997 25.978c-12.824 22.212-15.084 25.964-15.595 25.883-.024-.004-.15-.189-.235-.301-.109.066-.2.09-.272.05-.255-.148-7.143-11.902-15.306-26.119l-14.36-25.016c-.115-.186-.444-.744-.457-.752-.477-.275-50.502.287-50.737.57zm-18.646 283.09c-.047.109-.026.262.042.48.329 1.05 25.338 43.735 25.772 43.985.207.119 14.178.239 31.05.266 26.651.044 30.75.152 31.234.832.308.43 9.988 17.214 21.513 37.296s21.152 36.627 21.394 36.767c.242.14 5.927.243 12.633.23 6.706-.013 12.401.099 12.657.246.132.076.382-.141.852-.795l6.008-10.406c5.234-9.065 6.62-11.684 6.294-11.888-.575-.36-15.597-26.643-23.859-41.482-3.09-5.45-5.37-9.516-5.441-9.774-.195-.712-.065-.822 1.156-.98 1.956-.252 57.397-.057 58.07.205.238.092.79-.569 2.594-3.497 1.866-3.067 5.03-8.524 11-18.866 7.22-12.505 13.044-22.784 12.942-22.843-.102-.059-.771-.051-1.489.016l-.046.001c-4.452.204-33.918.203-149.74.025-38.96-.06-69.786-.09-71.912-.072-1.121.01-2.095.076-2.66.172a.25.25 0 0 0-.062.083z" fill="#7db7e1"/></g></symbol><symbol viewBox="0 0 24 24" id="nodejs" xmlns="http://www.w3.org/2000/svg"><path d="M12 1.85c-.27 0-.55.07-.78.2l-7.44 4.3c-.48.28-.78.8-.78 1.36v8.58c0 .56.3 1.08.78 1.36l1.95 1.12c.95.46 1.27.47 1.71.47 1.4 0 2.21-.85 2.21-2.33V8.44c0-.12-.1-.22-.22-.22H8.5c-.13 0-.23.1-.23.22v8.47c0 .66-.68 1.31-1.77.76L4.45 16.5a.26.26 0 0 1-.11-.21V7.71c0-.09.04-.17.11-.21l7.44-4.29c.06-.04.16-.04.22 0l7.44 4.29c.07.04.11.12.11.21v8.58c0 .08-.04.16-.11.21l-7.44 4.29c-.06.04-.16.04-.23 0L10 19.65c-.08-.03-.16-.04-.21-.01-.53.3-.63.36-1.12.51-.12.04-.31.11.07.32l2.48 1.47c.24.14.5.21.78.21s.54-.07.78-.21l7.44-4.29c.48-.28.78-.8.78-1.36V7.71c0-.56-.3-1.08-.78-1.36l-7.44-4.3c-.23-.13-.5-.2-.78-.2M14 8c-2.12 0-3.39.89-3.39 2.39 0 1.61 1.26 2.08 3.3 2.28 2.43.24 2.62.6 2.62 1.08 0 .83-.67 1.18-2.23 1.18-1.98 0-2.4-.49-2.55-1.47a.226.226 0 0 0-.22-.18h-.96c-.12 0-.21.09-.21.22 0 1.24.68 2.74 3.94 2.74 2.35 0 3.7-.93 3.7-2.55 0-1.61-1.08-2.03-3.37-2.34-2.31-.3-2.54-.46-2.54-1 0-.45.2-1.05 1.91-1.05 1.5 0 2.09.33 2.32 1.36.02.1.11.17.21.17h.97c.05 0 .11-.02.15-.07.04-.04.07-.1.05-.16C17.56 8.82 16.38 8 14 8z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 300 300" id="nodemon" xmlns="http://www.w3.org/2000/svg"><title>nodemon</title><path d="M149.868 20.62c-2.124 0-4.25.55-6.154 1.648L41.899 81.083a12.306 12.306 0 0 0-6.15 10.652v117.633a12.29 12.29 0 0 0 6.152 10.646l101.815 58.766h.001a12.282 12.282 0 0 0 12.291 0l101.84-58.766a12.29 12.29 0 0 0 6.153-10.652V91.738a12.31 12.31 0 0 0-6.146-10.652L156.015 22.27a12.302 12.302 0 0 0-6.153-1.648zM83.303 70.93s11.789 33.031 35.477 31.934l27.74-15.961a7.348 7.348 0 0 1 3.414-.99h.641a7.233 7.233 0 0 1 3.404.99l27.738 15.961c23.69 1.094 35.475-31.934 35.475-31.934 5.233 23.154 1.06 38.641-5.924 48.942l4.541 2.614h.002c2.321 1.327 3.734 3.795 3.737 6.49l-.12 95.811a3.724 3.724 0 0 1-1.855 3.227 3.624 3.624 0 0 1-3.735 0L177.1 206.971c-2.311-1.363-3.742-3.818-3.742-6.48v-44.763a7.44 7.44 0 0 0-3.737-6.465l-15.642-9.01a7.28 7.28 0 0 0-3.715-1.01 7.378 7.378 0 0 0-3.742 1.01l-15.648 9.01c-2.316 1.323-3.729 3.798-3.729 6.467v44.762c0 2.663-1.413 5.1-3.738 6.48l-36.748 21.041a3.571 3.571 0 0 1-3.71 0c-1.173-.65-1.864-1.887-1.864-3.224l-.137-95.812a7.483 7.483 0 0 1 3.74-6.49l4.541-2.615c-6.982-10.302-11.16-25.79-5.925-48.942z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 990 990" id="npm" xmlns="http://www.w3.org/2000/svg"><defs><style>.hncls-1{fill:#cb3837}.cls-2{fill:#fff}</style></defs><title>n</title><path class="hncls-1" d="M113.26 876.74V113.27h763.47v763.47zm143.59-620.4v476.18h240.61V355.63h140.21v376.96h95.457V256.34z" fill="#e53935" stroke-width=".771"/></symbol><symbol id="nunjucks" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.host0{fill:#388e3c}</style><path class="host0" d="M11.2 21.1H8.1l-2.3-7.9v7.9H2.7V2.9h3.1l2.3 7.4V2.9h3.1zM21.3 19.2c0 1-.8 1.9-1.9 1.9h-4.8c-1 0-1.9-.8-1.9-1.9v-3.8l3.2-.7V18h2.3V7.2h3.1v12z"/></symbol><symbol viewBox="0 0 150 150.00001" id="ocaml" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.76136 0 0 .76136 11.616 19.98)"><path d="M83.02 101.645l.023-.062c-.035-.159-.047-.195-.024.062z" fill="none" stroke-width="1.028"/><linearGradient id="hpa" gradientUnits="userSpaceOnUse" x1="-696.735" y1="97.7" x2="-696.735" y2="142.997" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M82.313 138.79c-.471-1.004-1.904-3.621-2.624-4.46-1.562-1.828-1.927-1.966-2.386-4.275-.799-4.02-2.913-11.31-5.405-16.341-1.286-2.596-3.426-4.777-5.385-6.66-1.71-1.652-5.565-4.431-6.237-4.294-6.296 1.257-8.249 7.432-11.21 12.323-1.638 2.705-3.374 5.007-4.665 7.885-1.192 2.646-1.087 5.577-3.128 7.849-2.093 2.333-3.454 4.814-4.48 7.829-.194.574-.747 6.596-1.348 8.015l9.357-.659c8.719.594 6.2 3.936 19.81 3.208l21.487-.665c-.666-1.97-1.584-4.25-1.938-4.991-.599-1.248-1.352-3.69-1.848-4.763z" fill="url(#hpa)" stroke-width="1.028"/><linearGradient id="hpb" gradientUnits="userSpaceOnUse" x1="-666.972" y1="142.12" x2="-666.972" y2="142.12" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><linearGradient id="hpc" gradientUnits="userSpaceOnUse" x1="-675.228" y1="-1.28" x2="-675.228" y2="142.967" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M109.553 94.296c-1.652 1.193-4.88 4.06-11.902 5.145-3.152.487-6.1.527-9.335.365-1.584-.076-3.077-.157-4.665-.177-.936-.008-4.074-.107-3.919.193l-.349.871c.054.287.169 1.004.2 1.177.129.704.165 1.265.192 1.912.048 1.331-.11 2.719-.043 4.062.141 2.787 1.175 5.326 1.306 8.137.143 3.13 1.69 6.442 3.188 8.998.569.973 1.434 1.084 1.811 2.283.442 1.373.024 2.83.239 4.293.842 5.675 2.477 11.606 5.032 16.728.018.043.038.09.06.128 3.156-.53 6.318-1.665 10.418-2.271 7.517-1.115 17.972-.54 24.688-1.17 16.993-1.597 26.216 6.97 41.478 3.459V22.459c0-11.84-9.594-21.438-21.435-21.438H19.239C7.4 1.021-2.197 10.62-2.197 22.458v46.774c3.067-1.11 7.479-7.635 8.861-9.222 2.419-2.775 2.858-6.315 4.062-8.544 2.743-5.078 3.215-8.57 9.451-8.57 2.907 0 4.061.67 6.027 3.31 1.368 1.834 3.731 5.224 4.837 7.49 1.277 2.615 3.357 6.153 4.272 6.867.677.53 1.35.928 1.976 1.163 1.012.38 1.848-.316 2.525-.855.863-.687 1.235-2.088 2.035-3.957 1.152-2.696 2.408-5.926 3.122-7.054 1.237-1.949 1.658-4.261 2.993-5.381 1.97-1.652 4.54-1.768 5.246-1.908 3.957-.781 5.755 1.906 7.704 3.645 1.276 1.138 3.019 3.432 4.256 6.507.967 2.4 2.199 4.622 2.714 6.008.497 1.339 1.725 3.484 2.453 6.055.661 2.336 2.43 4.125 3.102 5.235 0 0 1.029 2.882 7.285 5.516 1.357.572 4.1 1.501 5.736 2.096 2.718.988 5.351.86 8.704.458 2.391 0 3.686-3.462 4.772-6.234.643-1.639 1.259-6.334 1.678-7.667.406-1.297-.544-2.3.265-3.437.946-1.327 1.508-1.399 2.054-3.129 1.172-3.704 7.95-3.89 11.761-3.89 3.176 0 2.772 3.083 8.16 2.028 3.086-.605 6.059.398 9.335 1.265 2.758.732 5.352 1.566 6.906 3.385 1.005 1.178 3.5 7.08.958 7.331.244.3.423.84.88 1.135-.566 2.226-3.03.64-4.4.355-1.845-.383-3.147.057-4.952.856-3.085 1.374-7.598 1.214-10.286 3.452-2.281 1.898-2.277 6.133-3.34 8.507-.002-.001-2.955 7.6-9.402 12.248z" fill="url(#hpc)" stroke-width="1.028"/><linearGradient id="hpd" gradientUnits="userSpaceOnUse" x1="-735.137" y1="90.833" x2="-735.137" y2="141.967" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M38.247 105.09c-1.467-.15-2.83-.317-4.256-.605-2.662-.536-5.57-1.06-8.193-1.688-1.592-.385-6.895-2.263-8.048-2.792-2.702-1.246-4.496-4.63-6.609-4.282-1.348.22-2.662.682-3.5 2.042-.685 1.11-.917 3.016-1.391 4.294-.55 1.485-1.5 2.87-2.331 4.284-1.53 2.595-4.282 4.941-5.468 7.469-.239.52-.45 1.101-.649 1.708V144.415a48.57 48.57 0 0 1 4.45.96c11.955 3.19 14.872 3.46 26.598 2.119l1.1-.146c.897-1.867 1.59-8.227 2.171-10.195.454-1.51 1.077-2.712 1.313-4.253.223-1.463-.02-2.858-.146-4.188-.329-3.332 2.427-4.522 3.742-7.384 1.186-2.589 1.871-5.535 2.853-8.181.941-2.54 2.41-6.13 4.918-7.408-.305-.355-5.237-.518-6.554-.65z" fill="url(#hpd)" stroke-width="1.028"/></g></symbol><symbol viewBox="0 0 24 24" id="pdf" xmlns="http://www.w3.org/2000/svg"><path d="M14 9h5.5L14 3.5V9M7 2h8l6 6v12a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m4.93 10.44c.41.9.93 1.64 1.53 2.15l.41.32c-.87.16-2.07.44-3.34.93l-.11.04.5-1.04c.45-.87.78-1.66 1.01-2.4m6.48 3.81c.18-.18.27-.41.28-.66.03-.2-.02-.39-.12-.55-.29-.47-1.04-.69-2.28-.69l-1.29.07-.87-.58c-.63-.52-1.2-1.43-1.6-2.56l.04-.14c.33-1.33.64-2.94-.02-3.6a.853.853 0 0 0-.61-.24h-.24c-.37 0-.7.39-.79.77-.37 1.33-.15 2.06.22 3.27v.01c-.25.88-.57 1.9-1.08 2.93l-.96 1.8-.89.49c-1.2.75-1.77 1.59-1.88 2.12-.04.19-.02.36.05.54l.03.05.48.31.44.11c.81 0 1.73-.95 2.97-3.07l.18-.07c1.03-.33 2.31-.56 4.03-.75 1.03.51 2.24.74 3 .74.44 0 .74-.11.91-.3m-.41-.71l.09.11c-.01.1-.04.11-.09.13h-.04l-.19.02c-.46 0-1.17-.19-1.9-.51.09-.1.13-.1.23-.1 1.4 0 1.8.25 1.9.35M8.83 17c-.65 1.19-1.24 1.85-1.69 2 .05-.38.5-1.04 1.21-1.69l.48-.31m3.02-6.91c-.23-.9-.24-1.63-.07-2.05l.07-.12.15.05c.17.24.19.56.09 1.1l-.03.16-.16.82-.05.04z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="perl" xmlns="http://www.w3.org/2000/svg"><path d="M12 14c-1 0-3 1-3 2 0 2 3 2 3 2v-1a1 1 0 0 1-1-1 1 1 0 0 1 1-1v-1m0 5s-4-.5-4-2.5c0-3 3-3.75 4-3.75V11.5c-1 0-5 1.5-5 4.5 0 4 5 4 5 4v-1M10.07 7.03l1.19.53c.43-2.44 1.58-4.06 1.58-4.06-.43 1.03-.71 1.88-.89 2.55C13.16 3.55 15.61 2 15.61 2a15.916 15.916 0 0 0-2.64 3.53c1.58-1.68 3.77-2.78 3.77-2.78-2.69 1.72-3.9 4.45-4.2 5.21l.55.08c0 .52 0 1 .25 1.38C14.1 11.31 18 11.47 18 16s-4.03 6-6.17 6C9.69 22 5 21.03 5 16s4.95-5.07 5.83-7.08c.12-.38-.76-1.89-.76-1.89z" fill="#9575cd"/></symbol><symbol viewBox="0 0 24 24" id="php" xmlns="http://www.w3.org/2000/svg"><path d="M12 18.08c-6.63 0-12-2.72-12-6.08s5.37-6.08 12-6.08S24 8.64 24 12s-5.37 6.08-12 6.08m-5.19-7.95c.54 0 .91.1 1.09.31.18.2.22.56.13 1.03-.1.53-.29.87-.58 1.09-.28.22-.71.33-1.29.33h-.87l.53-2.76h.99m-3.5 5.55h1.44l.34-1.75h1.23c.54 0 .98-.06 1.33-.17.35-.12.67-.31.96-.58.24-.22.43-.46.58-.73.15-.26.26-.56.31-.88.16-.78.05-1.39-.33-1.82-.39-.44-.99-.65-1.82-.65H4.59l-1.28 6.58m7.25-8.33l-1.28 6.58h1.42l.74-3.77h1.14c.36 0 .6.06.71.18.11.12.13.34.07.66l-.57 2.93h1.45l.59-3.07c.13-.62.03-1.07-.27-1.36-.3-.27-.85-.4-1.65-.4h-1.27L12 7.35h-1.44M18 10.13c.55 0 .91.1 1.09.31.18.2.22.56.13 1.03-.1.53-.29.87-.57 1.09-.29.22-.72.33-1.3.33h-.85l.5-2.76h1m-3.5 5.55h1.44l.34-1.75h1.22c.55 0 1-.06 1.35-.17.35-.12.65-.31.95-.58.24-.22.44-.46.58-.73.15-.26.26-.56.32-.88.15-.78.04-1.39-.34-1.82-.36-.44-.99-.65-1.82-.65h-2.75l-1.29 6.58z" fill="#1E88E5"/></symbol><symbol viewBox="0 0 79 78" id="postcss" xmlns="http://www.w3.org/2000/svg"><title>postcss-logo-symbol</title><g transform="translate(5.48 5.52) scale(.85425)" fill="#e53935" fill-rule="evenodd" stroke="#e53935" stroke-width="1.519"><path d="M15.447 32.623c.106.08.29.132.106.29-.132.184-.29.342-.395.553-.105.185-.184.237-.342.106.21-.343.42-.66.63-.95zM68.342 60.24c0 .078.026.13.026.21.053-.105.053-.158.08-.21zm0 .236v-.026zm-5.368 10.277l-4.58-25.402c-.078-.025-.183-.077-.368-.13.053.105.08.184.106.263.13-.026.184-.026.236-.052 0-.026 0-.052.027-.08l4.58 25.404zm-4.737-31.12c-.026.078-.026.158-.026.237 0-.08 0-.16.028-.238zm.026.526c-.026 0-.026 0-.052-.028v.026c.028.026.028.026.054 0zm-.052.21v-.185c-.077.026-.156.026-.262.053.132.05.264.078.264.13z"/><path d="M78.71 33.967c-.052-1.028-.078-2.056-.184-3.083-.184-1.397-.368-2.82-.684-4.19-.237-1.133-.63-2.214-1.026-3.294-.5-1.265-1-2.556-1.632-3.768-1.026-1.95-2.368-3.69-3.605-5.508-.818-1.16-1.87-2.108-2.66-3.294-.447-.685-1.105-1.264-1.763-1.79-1.053-.845-2.158-1.61-3.263-2.347a32.525 32.525 0 0 0-2.58-1.634c-.71-.397-1.473-.713-2.21-1.056-.842-.395-1.658-.87-2.605-1.054-.238-.05-.448-.13-.685-.21-.605-.21-1.184-.447-1.79-.632-.92-.29-1.815-.632-2.763-.87C50.342 1 49.394.843 48.446.71 47.394.555 46.316.5 45.262.397a26.83 26.83 0 0 0-2.026-.184C42.236.16 41.21.16 40.21.134c-.5-.027-1.026-.08-1.526-.053-.763.026-1.526.105-2.29.21-.736.08-1.473.21-2.183.317-.867.105-1.735.158-2.604.264-.816.106-1.658.264-2.473.396-.29.053-.58.158-.87.21-.63.132-1.288.185-1.92.396-1.13.344-2.263.74-3.368 1.16-1.027.422-2.027.87-3 1.397-1 .552-1.948 1.21-2.895 1.844a45.325 45.325 0 0 0-2.66 1.923c-.84.66-1.63 1.397-2.394 2.135-.42.42-.763.922-1.158 1.396-.657.765-1.315 1.502-1.947 2.293-.524.66-1 1.344-1.5 2.03-.893 1.21-1.656 2.502-2.366 3.794-.29.527-.553 1.054-.816 1.58-.395.79-.816 1.555-1.184 2.372-.264.554-.474 1.16-.632 1.766-.367 1.292-.736 2.61-1.078 3.9-.316 1.16-.395 2.372-.42 3.558-.027 1.054.078 2.082.183 3.136.027.264-.13.58.184.79-.105.29-.026.45.13.5-.182.29.08.476-.024.74-.027.052.08.157.13.236 0 .08-.025.185 0 .264.028.237.133.474.133.738 0 .184.157.395.21.58.026.078 0 .21-.053.263-.158.184-.132.342.105.448.133.342.08.5.054.66.052.236-.027.315 0 .368.21.422.29.896.315 1.37 0 .106.053.212.106.343.026 0 0 .5 0 .5.13-.078.237-.104.368-.157.08.342.158.66.263.95.132.21.132.314.08.34.105.474.157.922.34 1.37 0-.5-.05-1-.13-1.475.368.132.684.263.895.263.027-.08.053-.184.08-.237-.158-.157-.29-.394-.448-.552.053.21 0 .29 0 .37-.105-.054-.237-.107-.368-.16.105-.13.21-.263.368-.42 0-.238-.13-.45-.5-.423.158-.052.316-.13.5-.184.29-.157-.026-.447-.026-.816.026-.447-.237-.895-.316-1.37-.132-.737-.105-1.844-.184-2.582-.158-.132-.29.21-.316.237.08.632.158 1.264.21 1.897-.157-.527-.263-1.107-.394-1.74-.027.185-.053.264-.053.37-.13.13-.026.29.053.474-.184-.08-.395-.052-.395-.052v.738c-.262-.264-.34-.474-.473-.66-.052-.21-.08-.42-.13-.63.05-.133 0-.212 0-.29a15.968 15.968 0 0 1-.08-.634c.026-.026-.026-.42-.026-.42.21.025.343.05.474.05-.263-.34-.08-.552.027-.763.053-.106.237-.13.29-.238.21-.395.553-.71.553-1.212 0-.237.08-.5.105-.738.053-.448.105-.896.13-1.344.054-.58 0-1.16.133-1.713.212-.92.475-1.843.764-2.766.21-.66.448-1.29.71-1.95.395-1.028.764-2.056 1.264-3.03.71-1.424 1.526-2.794 2.316-4.19.5-.87 1.026-1.687 1.58-2.53.525-.817 1.05-1.66 1.657-2.425a21.452 21.452 0 0 1 2.79-2.978c1.053-.948 2.053-1.923 3.184-2.793a32.218 32.218 0 0 1 4.685-3.005c1.343-.71 2.737-1.266 4.132-1.793.895-.342 1.868-.5 2.79-.79 1.052-.343 2.105-.5 3.21-.527.71-.027 1.395-.106 2.105-.185.632-.05 1.263-.104 1.948-.183-.08.105-.106.158-.132.21-.288.422-.604.844-.894 1.265-.237.343-.5.712-.737 1.054-.422.555-.87 1.108-1.264 1.688-.605.87-1.158 1.766-1.79 2.635-.63.843-1.315 1.634-1.973 2.45-.868 1.134-1.684 2.293-2.552 3.426-.79 1.08-1.63 2.11-2.394 3.19-.684.947-1.29 1.95-1.948 2.923-.973 1.45-1.947 2.872-2.92 4.322a271.93 271.93 0 0 1-2.316 3.294c-.053.08-.132.104-.21.157-.21.342-.21.527-.29.685-.21.395-.42.79-.658 1.16-.132.21-.316.394-.474.605-.026-.316.42-.474.21-.87-.13.212-.263.396-.394.607l-.316.63c.105.08.29.133.105.29-.08.133-.158.29-.237.423a.954.954 0 0 0 .29-.264c0 .29-.158.526-.29.763-.105.21-.368.37-.552.527.026.027.21.106.237.132.237-.08.316-.21.343-.132.08-.105.158-.184.184-.263.104-.264.262-.474.525-.58.106-.053.184-.132.263-.21.79-.818 1.606-1.608 2.316-2.478 1.106-1.345 2.106-2.74 3.16-4.11.446-.58.973-1.16 1.446-1.714.078.606.026 1.185 0 1.74-.08.974-.132 1.95-.21 2.95-.027.395 0 .79-.027 1.186 0 .105-.08.184-.08.29 0 .263.08.553.08.817-.08.975-.186 1.923-.265 2.898-.027.21.078.422.13.607-.13 1.422.16 2.925-.078 4.427.184-.29.237-.474.237-.658.025-.158 0-.316 0-.5v-.264c.025-.475.13-.975.078-1.45-.053-.527-.053-1.027.053-1.528.053-.21-.026-.474.106-.738v.395c-.026 1.5.027 3.003-.183 4.505-.027.132.08.37-.21.343-.238.474.052.817-.21 1.08-.054.053.05.29.077.448-.106.317-.106.317.052.343.026.58.08 1.106.105 1.66.42-1 .21-2.03.396-3.058.026.422.053.844.026 1.29 0 .687-.026 1.345-.052 2.03 0 .132-.027.264-.053.396-.08.37-.105.738-.237 1.08-.105.264-.052.66-.052.975v1.003c.105.448-.027.685.052.948-.08.265-.105.344-.08.423l.08.395c.527-.053.29.343.5.553-.158.212-.105.29-.105.397 0 .237-.025.448-.052.685 0 .606-.026 1.212-.026 1.792 0 .08.026.157.026.236 0 .054-.026.74-.026.74.053.078 0 .157-.08.236-.025 0-.104-3.347-.104-3.347h-.395c-.052 1.58.08 3.003-.21 4.48-.316.025-.42.078-.764.078-.816 0-1.632 0-2.448.026-.974 0-1.92.026-2.895.026-.472 0-.972.054-1.446.054-.632 0-1.29-.08-1.92-.08-.975 0-1.922.08-2.896.106-.71.026-1.42.026-2.13.053-.475.025-.95.05-1.422.104-.21.026-.395.105-.658.184-.08 0-.263-.026-.42 0-.265.053-.5.21-.765.264-.395.08-.5.184-.448.58v.263c-.026.052.58-.08.58-.08-.054 0-.08.158-.16.29.212-.08.343-.132.475-.184.395.185.737.08 1.052.16 1.026.262 2.078.37 3.13.473.685.053 1.343.08 2.027.105.973.053 1.947.106 2.92.106.816 0 1.606-.08 2.42-.08 1.13 0 2.264.052 3.395.08.237 0 .5-.028.763-.028h1.92c1.712-.052 3.422-.08 5.133-.13.975-.028 1.975-.08 2.948-.107l3-.08c1.158-.026 2.316-.026 3.448-.05.868 0 1.71-.03 2.58-.055.972-.026 1.972-.105 2.946-.157.527-.027 1.054-.08 1.58-.132.632-.052 1.29-.13 1.92-.157.948-.054 1.922-.08 2.87-.133 1.184-.078 2.368-.183 3.578-.21 1.106-.052 2.237-.026 3.343-.052.974-.027 1.948-.08 2.948-.106l1.66-.08s1.104-.026 1.657-.08c.947-.052 1.894-.157 2.842-.183.604-.027 1.21 0 1.815-.027.973-.026 1.973-.08 2.947-.08.367 0 .762.054 1.236.08-.21.185-.342.29-.5.422.105.026.21.08.316.132a.71.71 0 0 1-.42.13c-.054.133-.107.186-.16.45h.474c-.184 0-.342.237-.526.395-.21-.054-.395 0-.5.29.184.104.158.183.132.29-.316.104-.553.21-.42.552-.107.052-.238.105-.37.184-.13.21-.368.263-.316.553.106.025.21.08.29.104-.132.053-.263.132-.395.184-.473.29-.262.422-.157.554-.08.053-.158.105-.237.132.052.237.13.29.157.29a9.3 9.3 0 0 0-.395.316c-.08.237-.185.342-.29.5s-.158.37-.29.527c-.552.607-.947 1.32-1.657 1.793-.264.185-.5.422-.737.66-.474.447-.895.948-1.395 1.37a29.595 29.595 0 0 1-2.052 1.554 151.56 151.56 0 0 1-2.604 1.792c-.474.315-1 .552-1.5.842s-.974.554-1.474.843c-.316.21-.606.5-.948.66-.868.37-1.79.685-2.684 1.028-.87.37-1.5.685-2.158.922-.605.21-1.237.37-1.868.5-.21.054-.448 0-.685.027-.448.08-.895.186-1.343.238-1.158.158-2.316.264-3.473.422-.685.08-1.343.21-2.027.29-.473.026-.973-.026-1.447-.026-.342 0-.71.08-1.053.027-.552-.08-1.105-.21-1.658-.316-.13-.026-.316-.08-.42-.026-.21.106-.396-.052-.607 0-.13.027-.262-.08-.394-.08-.106-.025-.238.028-.37 0-.29-.078-.552-.183-.87-.157-.313.026-.63-.132-.97-.21-.475-.106-.92-.21-1.396-.317a2.38 2.38 0 0 1-.525-.237c-.685 0-1.133-.026-1.554-.185-.368-.13-.71-.315-1.105-.262-.104.026-.183-.026-.29-.026-.08-.106-.157-.317-.235-.317-.526.027-.842-.42-1.29-.553-.236-.08-.42-.343-.657-.422-.58-.237-1.052-.737-1.71-.816-.21-.027-.42-.132-.658-.21.08.104.13.183.21.262-.763-.37-1.473-.79-2.184-1.186-.104-.026-.183-.13-.262-.184l-.71-.474c-.395.08-.553-.08-.66-.132-.71-.5-1.525-.817-2.21-1.37-.29-.238-.63-.396-.84-.686-.37-.448-.817-.764-1.317-1.027-.394-.21-.762-.448-1.13-.685-.185-.132-.37-.29-.37-.58 0-.185-.078-.37-.315-.264-.105-.158-.21-.342-.342-.395-.316-.13-.526-.37-.763-.58s-.42-.5-.71-.605c-.527-.21-.843-.658-1.158-1.027-.738-.87-1.396-1.82-2.08-2.74-.053-.08-.158-.133-.237-.212.105.29.237.527.368.79-.262-.105-.446-.29-.604-.474-.027.027 1.815 3.057 1.815 3.057.16.237.29.475.448.712a.813.813 0 0 1-.79-.422c-.236-.42-.5-.684-1.026-.63a4.588 4.588 0 0 1-.13-.58c-.107 0-.185 0-.37-.027.37.58.685 1.08 1.027 1.66-.133-.08-.21-.132-.265-.158.473.5.815 1.133 1.42 1.45.132.605.816.895.974 1.475-.13-.027-.238-.053-.37-.08-.21-.263-.447-.526-.683-.816.052.184.13.342.236.474.316.395.606.79.974 1.133.132.134.316.187.316.424.21.105.29.13.368.13.054.16-.025.397.29.344.21.395.42.395.71.264.343.343.528.37.764.16 0 .13.026.262.026.368.105-.053.08-.132.08-.264.13.105.21.158.262.21.263.37.5.712.868 1.002.5.422.948.87 1.42 1.265.922.765 1.95 1.398 2.975 1.977 1.264.712 2.475 1.476 3.764 2.16 1.552.818 3.21 1.372 4.92 1.767.632.132 1.237.263 1.87.42.55.16 1.104.397 1.657.528.842.185 1.71.343 2.552.5.183.027.37.054.58.08.235.053.524-.053.577.027.132.21.237.104.395.078.184-.053.395-.053.605-.053.737.026 1.447.184 2.184.132.16 0 .396-.133.528.13.236-.105.368-.105.473-.13.028.236 0 .236-.05.262-.054.026-.133.053-.238.132.947.184 1.842.21 2.63 0 1.37.105 2.554-.053 3.686-.448.105.132.184.316.342.053.052-.08.184-.107.29-.133.236-.053.526-.158.736-.08.238.08.317-.13.5-.13.317 0 .606-.027.896-.08.158-.026.316-.105.5-.158a1.285 1.285 0 0 0-.58-.133c.317-.158.606-.29.896-.42-.053.078-.106.183-.21.183h.367c-.08 0-.185.237-.316.395.946-.237 1.814-.448 2.657-.66-.29-.552.315-.367.526-.684-.263.08-.526.158-.79.21.895-.447 1.816-.842 2.71-1.237-.13.158-.29.237-.525.37.158.025.263.025.342.05.42.133.316-.262.447-.5.5 0 .71-.078.947-.158.263-.08.526-.158.79-.263.42-.184.815-.42 1.236-.63.08-.028.21 0 .316 0 .29-.186.394-.344.473-.318.37.053.63-.08.736-.42.184-.133.316-.238.447-.318.578-.316 1.13-.632 1.71-.948.21 0 .316 0 .368-.027.344-.16.66-.342.975-.527a2.258 2.258 0 0 1-.263-.13c.262-.054.34-.08.5-.133.63-.74 1.5-1.24 2.157-1.82.29-.026.29-.105.29-.157.104-.132.21-.29.34-.396.58-.527 1.21-.975 1.737-1.528a37.16 37.16 0 0 0 2.184-2.374c.63-.738 1.264-1.475 1.79-2.292.737-1.133 1.368-2.293 2.026-3.48.474-.842.895-1.685 1.37-2.528.05-.08.157-.185.236-.185.71-.08 1.422-.13 2.106-.21.158-.026.342-.13.5-.21-.08-.132-.132-.29-.21-.422-.106-.16-.264-.29-.37-.45-.104-.13-.183-.29-.262-.447-.08-.13-.158-.236-.237-.37a9.7 9.7 0 0 1-.45-.894c-.026-.08-.08-.21-.052-.29.474-1.027.658-2.134 1.105-3.162.447-1.054.58-2.24.79-3.373.184-1.08.29-2.16.42-3.24.08-.764.185-1.502.21-2.266.16-1.212.106-2.346.08-3.48-.026-1-.08-2.028-.13-3.03zM12.685 66.405c-.184-.21-.342-.448-.526-.658l.08-.08c.287.317.577.633.866.976-.158-.08-.342-.132-.42-.238zm.42.238c.08-.027.16-.027.238-.053.08.132.132.29.21.448-.368-.027-.552-.185-.447-.395zm27.37 10.883v-.08c.5-.052.973-.105 1.473-.157v.077c-.5.08-.973.13-1.473.158zm6.63-.685c-.367.08-.762.133-1.13.186-.132.026-.29.158-.342-.08-.053.027-.106.027-.158.054.13.394.447.078.71.236-.58.08-1.13.132-1.684.21v-.052c.16-.026.343-.053.5-.08v-.078a7.743 7.743 0 0 0-.79-.053c-.077 0-.183.106-.262.132-.105.026-.21.053-.342.053-.447.026-.894.026-1.316.052-.027 0-.08-.026-.106-.026v-.08c1.763-.236 3.5-.473 5.263-.71.027.052.027.105.053.157-.158 0-.263.055-.395.08zm.396-.262c.606-.08 1.16-.132 1.738-.21-1.21.342-1.605.394-1.737.21zM24.58 23.374c.84-1.16 1.71-2.32 2.552-3.505.263-.345.473-.714.736-1.056.08-.106.185-.158.316-.264l-.026-.05c.105-.133.21-.24.263-.344.134-.21.213-.448.318-.685a.385.385 0 0 1 .105-.103c.37.184.37-.21.5-.343.237-.264.474-.553.684-.817.158-.21.316-.395.448-.632.026-.08-.053-.21-.08-.317h-.078c.08-.052.158-.13.237-.184.026 0 .026 0 .052-.026.158-.238.316-.475.474-.686.315-.42.657-.842 1.025-1.21-.052.13-.105.263-.158.368.027 0 .027.027.053.027.316-.422.658-.817.974-1.24-.027-.025-.053-.052-.08-.052-.13.132-.236.264-.368.396-.026-.027-.052-.053-.08-.053.265-.343.528-.685.79-1.08.053.08.106.184.21.395.107-.263.212-.447.29-.632-.078.08-.183.158-.262.238l-.08-.08.474-.71c.5-.712 1-1.45 1.5-2.162.185-.263.42-.474.58-.738.5-1 1.29-1.792 1.894-2.714.132-.184.316-.342.474-.5.13-.16.237-.106.342.026.71.896 1.42 1.818 2.13 2.714.528.66 1.054 1.29 1.554 1.976.605.844 1.184 1.687 1.79 2.53.684.975 1.368 1.95 2.026 2.95 1 1.477 1.947 2.953 2.947 4.428.737 1.08 1.474 2.135 2.184 3.215h-1.344c-1.236-.025-2.5-.13-3.736-.078-1.684.08-3.394.264-5.078.396-2.132.185-4.29.21-6.42.21-.765 0-1.528.107-2.29.16-.922.052-1.817.105-2.738.13-1.08.054-2.13.08-3.21.107-.606.026-1.237 0-1.895 0zm30.183 12.12v.238c-.026 0-.052.027-.105.027-.105-.37-.21-.766-.342-1.135-.263-.765-.553-1.53-1.027-2.214-.528-.737-1-1.5-1.528-2.265-.13-.185-.316-.343-.474-.5-.553-.607-1.106-1.24-1.816-1.687a21.485 21.485 0 0 0-3.29-1.688 7.374 7.374 0 0 1-.92-.474h.63l4.5-.08c.974-.025 1.922-.025 2.895-.078.236 0 .368.08.5.29.236.395.473.79.736 1.186.027.052.08.13.08.21 0 .58 0 1.186.026 1.766.025.606.08 1.186.104 1.792 0 .606-.053 1.238-.026 1.87.027.897.053 1.82.053 2.74zM26.447 26.67c1.237-.053 2.42-.132 3.632-.185.945-.053 1.92-.08 2.866-.132.395-.025.764-.05 1.158 0-.42.212-.842.423-1.21.686-.474.316-.92.737-1.395 1.08-.475.342-.896.764-1.29 1.212-.5.605-1.053 1.132-1.58 1.712-.37.422-.79.817-1.105 1.265-.447.58-.842 1.21-1.263 1.87.132-2.504.29-4.98.184-7.51zm17.185 25.35c-.843.21-1.71.448-2.58.553-.736.106-1.5.08-2.263.08a25.42 25.42 0 0 1-2.028-.08c-.763-.078-1.526-.157-2.263-.5-.633-.29-1.29-.553-1.92-.87-.634-.316-1.265-.684-1.74-1.264-.34-.423-.815-.765-1.236-1.134.08.316.263.58.553.764-.132.158-.316.08-.58-.343-.078.053-.157.08-.21.106.08-.185.158-.37.237-.527-.105-.21-.237-.448-.342-.66-.21-.342-.42-.71-.605-1.053-.053-.08-.053-.158-.105-.237a5.893 5.893 0 0 1-.37-.475c-.21-.315-.394-.657-.657-.974 0 .08.027.158.027.264-.027 0-.053.026-.053.026l-.554-1.344c-.026 0-.026 0-.052.026l.473 1.74c-.026 0-.052.025-.08.025-.077-.104-.156-.21-.21-.34-.052-.212-.21-.212-.34-.133-.08.053-.133.237-.106.316.185.448.395.896.606 1.344.052.158.105.29.184.448.027.053.106.105.106.184.106.21.185.42.316.606.237.316.5.632.737.948.235.316.445.66.656.975.026.053.105.053.13.08.133.395.58.684.896.526.08.606.737.817 1 1.397a11.957 11.957 0 0 1-.763-.343c-.027.026-.027.052-.054.105.316.158.632.316.92.5.265.16.528.317.765.5.316.29.685.45 1.13.554a.282.282 0 0 0-.05-.107c.736.343 1.5.712 2.078 1-2.737.054-5.658.107-8.685.16 0-.5-.026-.975-.026-1.476 0-.21.052-.395.025-.606-.08-1.21-.08-2.424-.237-3.61-.157-1.264-.157-2.503-.13-3.77.025-.683-.027-1.394-.054-2.08 0-.922 0-1.82.028-2.74 0-.132.053-.237.106-.37h.08c.025.054 0 .133.05.16.08.08.212.21.265.184.157-.106.394-.21.447-.37.13-.315.184-.658.184-.974 0-.236.106-.394.21-.553.054-.08.08-.158.133-.263-.105-.08-.21-.132-.342-.237.106-.29.08-.633.475-.79.052-.027.052-.16.08-.238.025-.213.05-.45.078-.66.052.08.08.105.13.157a.42.42 0 0 1 .054-.08c0-.104-.026-.315 0-.315.316-.053.184-.395.342-.553.025-.028-.027-.107-.027-.16 0-.052 0-.13.026-.13.367-.08.315-.475.552-.66.08-.053.105-.13.21-.263.21.368-.158.553-.184.816.446-.263.578-.895.315-1.08.105-.08.21-.184.29-.29.29-.316.604-.606.868-.922.185-.236.29-.526.474-.763.106-.132.316-.237.474-.317.474-.262.92-.552 1.21-1 .053-.053.132-.105.21-.158.08-.053.238-.053.264-.132.027-.052-.052-.184-.105-.263.104-.053.21-.158.42-.264-.08.158-.105.264-.158.37l.13.13c.238-.184.606-.394.843-.552 0-.025-.132-.13-.132-.13-.157.08-.394.21-.63.316.05-.08.05-.132.08-.158.367-.237.735-.474 1.13-.66.92-.42 1.842-.842 2.763-1.237.158-.08.37-.026.553-.026.078 0 .13 0 .21-.026.42-.132.842-.264 1.263-.37.183-.052.393-.078.58-.078.787.025 1.577.025 2.366.078.342.026.658.105.974.21a9.88 9.88 0 0 1 1.184.5c.447.24.868.502 1.29.792.763.5 1.473 1.054 2.236 1.502.737.448 1.316 1.054 1.79 1.74.58.816 1.237 1.554 1.5 2.555l.394 1.74c.08.316.264.632.185 1-.133.66-.238 1.345-.343 2.004-.052.265-.105.53-.078.79.05.82-.265 1.53-.58 2.268-.106.237-.264.475-.395.738a.798.798 0 0 0 .21.106l.237-.474c.027 0 .027 0 .053.027-.132.368-.237.764-.37 1.133-.314.817-.63 1.66-1.025 2.45-.21.448-.58.817-.842 1.24-.262.368-.473.763-.736 1.106-.237.29-.473.58-.79.79-.71.527-1.447 1.054-2.21 1.476-.473.29-1.026.448-1.552.58zm-14.027-1.4l-.026.027c-.055-.026-.134-.052-.186-.105l-.632-.95c-.052-.078-.08-.157-.052-.262.29.448.58.87.895 1.29zm16.37 3.61c1.183-.5 2.157-1.21 3.05-2.028.133-.132.264-.263.422-.37 1.106-.684 1.92-1.633 2.658-2.687.842-1.212 1.395-2.582 2.08-3.873a2.73 2.73 0 0 1 .157-.29c-.053 3.004.29 5.955.684 8.933-2.973.105-6 .21-9.052.316zm26.683-.79c-.026.053-.08.106-.105.16-.027-.054-.027-.133-.053-.24-.158.423-.5.212-.737.212-1.42.027-2.868.027-4.29.027-1.368 0-2.762 0-4.13.024-.448 0-.922.105-1.37.132-1.078.052-2.157.08-3.236.105-.08 0-.158-.13-.29-.236a1.81 1.81 0 0 1-.158.237c-.028-.052-.08-.104-.133-.183-.026.08-.053.158-.08.21H58c-.053-.368-.158-.71-.158-1.08 0-.79.08-1.58.105-2.372.027-.368 0-.71 0-1.054.106.08.185.133.29.21.052-.103.105-.182.158-.26 0 0-.053-.028-.106-.08.05-.027.104-.08.104-.106.026-.08.08-.158.08-.21 0-.185-.054-.343-.08-.5.026 0 .052 0 .08-.028l.157.79h.08c-.106-.183.236-.342-.053-.552-.026-.027.026-.185.026-.264-.08-.157-.13-.315-.21-.526.026-.026.105-.053.184-.08-.105-.052-.184-.104-.263-.13.263-.238.263-.37.026-.633.054-.025.106-.025.106-.05 0-.238 0-.475-.052-.71-.053-.266.08-.58-.316-.74a.79.79 0 0 0 .105.21s-.08.027-.158.08c-.342-.317-.13-.74-.21-1.213.184.053.316.106.447.16-.053-.186-.184-.397-.263-.634h-.107v-1.74c0 .027.184.027.29.054 0-.027.025-.053.025-.08-.08-.105-.185-.21-.29-.342l.053-.053c-.21-.262-.105-.63-.105-.71V39.4c.264.264-.13.606.264.764v-.263h-.027c-.026-.395-.026-.79-.052-1.186h-.052c-.027.054-.027.08-.054.133h-.052l.158-6.298c.263.342.552.66.736 1 .606 1.108 1.395 2.057 2.132 3.058.632.87 1.21 1.818 1.79 2.714.71 1.08 1.394 2.16 2.105 3.24a81.41 81.41 0 0 0 1.63 2.426c.5.71 1.028 1.396 1.554 2.082.446.606.92 1.212 1.367 1.818.527.738 1.053 1.475 1.58 2.187.262.368.552.737.84 1.106.16.21.396.37.554.5-.025 0-.052 0-.104-.026.08.105.13.184.184.237.29.158.316.316.158.554zM74 46.854v-.185c0 .052.026.13 0 .184zm.895-11.62c-.027 0-.184-.16-.21-.186-.027.08 0 .158-.053.264-.027-.078-.21-.052-.21-.13-.027.368.157.737.13 1.106.08-.053.395-.08.474-.158.027.026.08.052.106.052-.527.396-.395.79-.158 1.24.052.104.21.315.052.526-.052.053.027.21.053.343h.077v.05l-.237.08c-.052-.08-.367-.236-.367-.37v1.346c.263.08.263.448.368.633a.768.768 0 0 0 .107-.21l.027.024c-.027.158-.053.316-.106.475-.052.236-.105.447-.13.684 0 .026.05.08.05.105-.288.66-.13 1.396-.235 2.08-.08.5 0 1.03-.053 1.556-.054.448-.16.922-.264 1.37-.027.08-.08.105-.21.158.052-.316.026-.527-.027-.817-.028 0-.37-.184-.397-.184 0 .37.21.87.29 1.29-.08-.026-.395-.21-.42-.21-.054.316-.054.738-.08 1.08-.027.264-.263.5-.29.79 0 .16.184.264.158.528h.21c0-.526.238-1 .238-1.554h.078c.027.053.106.106.08.132-.053.29-.16.606-.132.896 0 .158.13.316.08.5-.054.16-.08.317-.107.554-.027-.132-.053-.184-.053-.263-.026 0-.263-.027-.29-.027-.026.158.185.316.158.448-.026.026-.052.026-.105.053l-.868-1.266c-.686-1-1.37-2.003-2.054-3.03a6.312 6.312 0 0 1-.475-.79 37.09 37.09 0 0 0-2.71-4.033c-.762-.974-1.37-2.03-2.08-3.055-.656-.975-1.314-1.924-1.972-2.9-.237-.315-.526-.605-.737-.948-.683-1.08-1.29-2.187-1.972-3.267-.58-.897-1.21-1.767-1.816-2.636-.21-.29-.42-.607-.632-.923a.37.37 0 0 1-.052-.182c-.053-.58-.106-1.16-.132-1.713 0-.527.053-1.054.053-1.608v-.474c0-.132.025-.237.025-.37.025-.025.052-.078.078-.104-.763 0-1.553-.028-2.316 0-.5.025-.763-.186-1.105-.555-1-1.133-1.737-2.424-2.605-3.636a162.42 162.42 0 0 0-2.5-3.427c-.685-.922-1.37-1.818-2.053-2.74-.764-1.054-1.5-2.108-2.29-3.162a381.983 381.983 0 0 0-2.895-3.794c-.45-.58-.95-1.133-1.45-1.74.343.054.66.106.975.133l1.264.08c.947.077 1.894.13 2.84.26.79.107 1.58.265 2.396.396 1.738.29 3.448.765 5.106 1.318.974.316 1.92.738 2.87 1.133 2.13.87 4.157 1.924 6.157 3.03.63.343 1 .896 1.472 1.397.685.712 1.37 1.423 2.027 2.16.762.87 1.472 1.766 2.21 2.662.657.79 1.34 1.58 2 2.372.21.237.37.527.552.79.42.633.895 1.24 1.263 1.924.262.502.42 1.082.604 1.635.262.817.526 1.607.79 2.424.183.606.34 1.24.472 1.87.106.423.08.87.21 1.29.16.556 0 1.16.16 1.715.025.053.05.132.078.185.105.104.184.21.026.368-.025.026-.025.13 0 .21.054-.052.08-.105.133-.184 0 .053.025.08.025.105 0 .104-.027.21 0 .315 0 .052.052.13.078.184.053-.054.105-.08.21-.16.237.897.264 1.793.264 2.715 0 .87.157 1.74-.21 2.583.078-.29-.106-.555-.027-.818z"/><path d="M58.08 45.482c.025 0 .052.027.052.027l-.027-.03c0-.025 0-.025-.026 0zm4.157 26.036c-.29.21-.58.395-.948.474-.028-.026-.028-.053-.054-.08.29-.184.605-.368.895-.553.027.05.08.104.106.157zM12.895 35.81c.29-.367.58-.736.894-1.105.025.026.235.08.262.105-.29.37-.685.87-.974 1.265-.054-.053-.133-.237-.185-.264zM5.42 48.725c-.21-.448-.42-.923-.63-1.37a.91.91 0 0 1 .236-.106c.29.42.42.92.632 1.37 0 0-.21.105-.237.105zm6.712-12.65c-.158.238-.316.502-.474.74-.026-.028-.316.104-.342.078.158-.237.552-.66.71-.896.027.026.053.053.106.08zM59.422 72.6c.025 0 .025-.026.052-.026.184.026.394.052.605.052-.344.237-.555.21-.66-.026zm-47.24-35.418c.028-.08.08-.158.133-.237.052 0 .13-.027.13-.027.107-.184.107-.316.212-.474-.026-.026-.053-.026-.08-.053-.157.108-.315.24-.473.345.053.052.053.08.053.132-.21-.027-.29.08-.395.368-.026.08-.158.106-.29.21-.026.054-.052.186-.105.317l.027.028c-.053.053-.132.08-.132.08-.158.157-.342.29-.5.447-.026.08-.052.158-.052.237.185-.184.5-.527.737-.738l.027.027c.105-.158.184-.316.29-.474.025.026.025.052.052.08-.08.21-.158.446-.237.657-.055.026-.134.08-.134.053-.105.08-.184.184-.29.263l-.473.316c-.263.237-.526.447-.816.685-.184.29-.368.553-.58.896.317-.08.396.053.37.317.368.052.395-.237.5-.448.026-.054.053-.16.105-.186.237-.21.5-.394.763-.605.053-.053.053-.16.053-.238 0-.026-.133-.026-.212-.053.237-.264.58-.71.816-1 .132-.08.263-.186.263-.265-.026-.29.158-.368.37-.474-.106-.08-.133-.157-.133-.183z"/><path d="M12.71 36.892c-.105.184-.21.342-.315.527l-.158-.08c-.105.605-.474 1.132-.842 1.237.105.053.21.106.29.08.078-.027.13-.16.183-.238l.71-1.028.238-.396-.105-.105zM3.948 48.46c.132 0 .264.026.42.026 0-.105.133-.08.133-.184h.08c0 .132.026.237.026.37h-.552c-.027-.027-.132-.186-.106-.212zm-.21-1.212c-.08-.08-.21-.158-.21-.237-.027-.104.052-.235.13-.367.054.184.08.342.132.527-.027.025-.053.052-.053.078zm.658-1.687c.105.266.21.556.316.82a.798.798 0 0 0-.21.105c-.105-.264-.237-.554-.342-.817a.652.652 0 0 1 .237-.106zm58.58 25.194c.13-.052.288-.08.5-.13-.238.183-.422.315-.58.473-.027-.026-.053-.053-.08-.053.053-.105.106-.184.16-.29zM30.63 15.074c.157-.106.29-.185.447-.29l.052.052c-.16.21-.29.42-.475.685-.026-.183-.026-.29-.053-.42-.026 0 0 0 .027-.026zm7.71 13.333c.237-.106.474-.21.763-.343-.026.158-.026.264-.026.37a.927.927 0 0 0-.264-.054c-.158.027-.448.238-.58.264-.025 0 .106-.21.106-.237zm19.74 22.346c.052.263.552.395.052.658.08.055.157.08.236.134a.2.2 0 0 1-.052.106c-.053.025-.158.078-.21.05-.027 0-.08-.104-.08-.157 0-.237.027-.474.053-.79z"/></g></symbol><symbol viewBox="0 0 24 24" id="powerpoint" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5M8 11v2h1v6H8v1h4v-1h-1v-2h2a3 3 0 0 0 3-3 3 3 0 0 0-3-3H8m5 2a1 1 0 0 1 1 1 1 1 0 0 1-1 1h-2v-2h2z" fill="#d14524"/></symbol><symbol viewBox="0 0 67.47 70" id="powershell" xmlns="http://www.w3.org/2000/svg"><path d="M18.545 12.4c-3.014 0-6.08 2.34-6.873 5.248L1.91 53.438c-.793 2.908.996 5.248 4.01 5.248h42.887c3.014 0 6.08-2.34 6.873-5.248l9.761-35.79c.794-2.908-.993-5.248-4.007-5.248h-42.89zm4.848 6.243c.652.04 1.29.33 1.76.86l7.96 9.013-3.957 3.246 3.957-3.244 4.832 5.47c.037.042.06.088.094.131.026.034.057.06.082.096.02.028.032.057.05.086.057.087.105.176.15.267.028.06.055.117.08.178a2.546 2.546 0 0 1 .171.764c.005.073.01.146.008.219-.002.09-.01.178-.021.267a2.53 2.53 0 0 1-.036.217 2.56 2.56 0 0 1-.07.252c-.024.076-.048.15-.08.224a2.547 2.547 0 0 1-.111.22 2.503 2.503 0 0 1-.133.218 2.546 2.546 0 0 1-.147.187c-.058.07-.118.137-.185.202-.027.026-.048.057-.076.082-.037.032-.077.054-.116.084-.038.03-.07.065-.11.093L16.8 52.271a2.552 2.552 0 0 1-3.563-.626 2.553 2.553 0 0 1 .63-3.563l18.349-12.853-3.06-3.467-7.839-8.873a2.549 2.549 0 0 1 .225-3.608 2.546 2.546 0 0 1 1.85-.638zm22.441 28.214c1.377 0 2.255 1.083 1.969 2.43-.287 1.347-1.627 2.433-3.004 2.434l-9.957.006c-1.378 0-2.256-1.083-1.969-2.43.287-1.347 1.626-2.433 3.004-2.434l9.957-.006z" fill="#03a9f4" stroke-width="5.342" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 210 210" id="prettier" xmlns="http://www.w3.org/2000/svg"><title>prettier-icon-dark</title><g transform="matrix(.9 0 0 .9 10.5 10.5)" fill="none" fill-rule="evenodd"><rect fill="#56B3B4" x="165" y="40" width="20" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="200" width="60" height="10" rx="5"/><rect fill="#BF85BF" x="135" y="120" width="40" height="10" rx="5"/><rect fill="#EA5E5E" x="75" y="120" width="50" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="120" width="50" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="160" width="60" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="80" width="60" height="10" rx="5"/><rect fill="#F7BA3E" x="65" y="20" width="110" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="20" width="40" height="10" rx="5"/><rect fill="#F7BA3E" x="55" y="180" width="20" height="10" rx="5"/><rect fill="#56B3B4" x="55" y="60" width="20" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="180" width="30" height="10" rx="5"/><rect fill="#F7BA3E" x="15" y="60" width="30" height="10" rx="5"/><rect fill="#56B3B4" x="95" y="100" width="90" height="10" rx="5"/><rect fill="#F7BA3E" x="45" y="100" width="40" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="100" width="20" height="10" rx="5"/><rect fill="#BF85BF" x="105" y="40" width="50" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="40" width="80" height="10" rx="5"/><rect fill="#F7BA3E" x="45" y="140" width="100" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="140" width="20" height="10" rx="5"/><rect fill="#EA5E5E" x="135" y="60" width="60" height="10" rx="5"/><rect fill="#F7BA3E" x="135" y="80" width="60" height="10" rx="5"/><rect fill="#56B3B4" x="15" width="130" height="10" rx="5"/></g></symbol><symbol viewBox="0 0 80 80" id="protractor" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="hxa"><path transform="scale(1 -1)" fill="#564b55" stroke-width="27.224" d="M-2.983-69.251h69.412v67.108H-2.983z"/></clipPath></defs><g transform="matrix(1.13039 0 0 -1.13039 5.714 82.137)" clip-path="url(#hxa)"><g transform="scale(.1)"><path d="M1180.54 92.324c-5.53 0-9.93-1.797-13.23-5.39-3.29-3.614-5.22-8.594-5.81-14.97h36.02c0 6.583-1.47 11.622-4.4 15.126-2.93 3.496-7.12 5.234-12.58 5.234zm2.84-62.656c-10.19 0-18.22 3.086-24.11 9.297-5.88 6.21-8.83 14.824-8.83 25.84 0 11.101 2.73 19.922 8.21 26.464 5.45 6.524 12.81 9.805 22.02 9.805 8.63 0 15.46-2.851 20.48-8.523 5.03-5.676 7.55-13.157 7.55-22.461v-6.613h-47.45c.21-8.086 2.26-14.22 6.12-18.418 3.89-4.18 9.34-6.29 16.38-6.29 7.42 0 14.76 1.563 22 4.669V34.14c-3.68-1.602-7.18-2.746-10.48-3.438-3.28-.684-7.24-1.035-11.89-1.035M1272.34 30.918v44.57c0 5.606-1.28 9.805-3.82 12.559-2.56 2.773-6.56 4.16-12.02 4.16-7.2 0-12.49-1.953-15.84-5.851-3.34-3.895-5.03-10.32-5.03-19.286V30.918h-10.42v68.887h8.47l1.71-9.422h.5c2.14 3.387 5.14 6.023 8.99 7.887 3.85 1.867 8.15 2.804 12.88 2.804 8.29 0 14.54-2.011 18.73-6.015 4.19-3.985 6.28-10.391 6.28-19.192V30.918h-10.43M1328.96 38.406c7.1 0 12.27 1.938 15.48 5.813 3.22 3.879 4.81 10.129 4.81 18.758v2.199c0 9.765-1.62 16.726-4.87 20.898-3.25 4.18-8.44 6.25-15.56 6.25-6.11 0-10.79-2.383-14.04-7.129-3.26-4.746-4.88-11.472-4.88-20.136 0-8.797 1.61-15.45 4.84-19.93 3.23-4.484 7.97-6.723 14.22-6.723zm20.85 1.762h-.56c-4.83-7.004-12.02-10.5-21.62-10.5-9.01 0-16.03 3.066-21.04 9.238-5 6.153-7.5 14.922-7.5 26.27 0 11.355 2.51 20.176 7.54 26.465 5.03 6.289 12.03 9.433 21 9.433 9.34 0 16.5-3.398 21.49-10.195h.81l-.43 4.96-.25 4.845v28.039h10.43V30.918h-8.49l-1.38 9.25M1434.91 38.27c1.85 0 3.63.136 5.34.421 1.72.274 3.09.547 4.1.84v-7.976c-1.15-.559-2.81-.996-5.01-1.36-2.18-.351-4.17-.527-5.94-.527-13.32 0-19.97 7.012-19.97 21.055V91.71h-9.88v5.027l9.88 4.336 4.38 14.707h6.04V99.805h20V91.71h-20V51.16c0-4.15.98-7.333 2.96-9.56 1.97-2.206 4.67-3.331 8.1-3.331M1463.81 65.43c0-8.809 1.76-15.508 5.27-20.118 3.53-4.609 8.69-6.906 15.53-6.906s12.01 2.297 15.56 6.875c3.53 4.602 5.3 11.301 5.3 20.149 0 8.75-1.77 15.41-5.3 19.953-3.55 4.539-8.77 6.824-15.69 6.824-6.82 0-11.99-2.246-15.47-6.73-3.46-4.48-5.2-11.16-5.2-20.047zm52.47 0c0-11.23-2.83-20-8.48-26.309-5.66-6.309-13.47-9.453-23.44-9.453-6.17 0-11.64 1.445-16.42 4.336-4.78 2.89-8.46 7.031-11.06 12.45-2.59 5.401-3.88 11.73-3.88 18.976 0 11.23 2.8 19.968 8.41 26.242 5.61 6.258 13.4 9.402 23.38 9.402 9.64 0 17.3-3.222 22.97-9.62 5.69-6.415 8.52-15.087 8.52-26.024M1591.71 92.324c-5.54 0-9.94-1.797-13.23-5.39-3.3-3.614-5.24-8.594-5.81-14.97h36c0 6.583-1.46 11.622-4.39 15.126-2.93 3.496-7.13 5.234-12.57 5.234zm2.83-62.656c-10.19 0-18.22 3.086-24.11 9.297-5.89 6.21-8.83 14.824-8.83 25.84 0 11.101 2.74 19.922 8.2 26.464 5.46 6.524 12.81 9.805 22.04 9.805 8.62 0 15.45-2.851 20.48-8.523 5.03-5.676 7.54-13.157 7.54-22.461v-6.613h-47.45c.21-8.086 2.25-14.22 6.13-18.418 3.87-4.18 9.33-6.29 16.36-6.29 7.43 0 14.77 1.563 22.01 4.669V34.14c-3.69-1.602-7.17-2.746-10.46-3.438-3.3-.684-7.27-1.035-11.91-1.035M1683.5 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12.01 4.16-7.2 0-12.48-1.953-15.83-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.48l1.69-9.422h.51c2.14 3.387 5.14 6.023 8.99 7.887 3.84 1.867 8.15 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M1740.11 38.406c7.12 0 12.28 1.938 15.49 5.813 3.21 3.879 4.81 10.129 4.81 18.758v2.199c0 9.765-1.62 16.726-4.87 20.898-3.25 4.18-8.43 6.25-15.56 6.25-6.12 0-10.8-2.383-14.05-7.129-3.24-4.746-4.88-11.472-4.88-20.136 0-8.797 1.64-15.45 4.85-19.93 3.22-4.484 7.96-6.723 14.21-6.723zm20.87 1.762h-.57c-4.82-7.004-12.03-10.5-21.62-10.5-9.01 0-16.02 3.066-21.03 9.238-5 6.153-7.52 14.922-7.52 26.27 0 11.355 2.52 20.176 7.55 26.465 5.02 6.289 12.02 9.433 21 9.433 9.34 0 16.5-3.398 21.48-10.195h.83l-.44 4.96-.25 4.845v28.039h10.43V30.918h-8.49l-1.37 9.25M1846.07 38.27c1.85 0 3.64.136 5.36.421 1.7.274 3.07.547 4.08.84v-7.976c-1.13-.559-2.8-.996-5-1.36-2.2-.351-4.18-.527-5.94-.527-13.33 0-19.99 7.012-19.99 21.055V91.71h-9.86v5.027l9.86 4.336 4.4 14.707h6.04V99.805H1855V91.71h-19.98V51.16c0-4.15.98-7.333 2.95-9.56 1.97-2.206 4.68-3.331 8.1-3.331M1894.26 92.324c-5.53 0-9.94-1.797-13.22-5.39-3.31-3.614-5.25-8.594-5.83-14.97h36.01c0 6.583-1.45 11.622-4.38 15.126-2.95 3.496-7.13 5.234-12.58 5.234zm2.83-62.656c-10.19 0-18.22 3.086-24.1 9.297-5.9 6.21-8.84 14.824-8.84 25.84 0 11.101 2.73 19.922 8.2 26.464 5.47 6.524 12.81 9.805 22.03 9.805 8.63 0 15.46-2.851 20.49-8.523 5.03-5.676 7.55-13.157 7.55-22.461v-6.613h-47.46c.22-8.086 2.26-14.22 6.13-18.418 3.87-4.18 9.33-6.29 16.37-6.29 7.42 0 14.75 1.563 22 4.669V34.14c-3.7-1.602-7.17-2.746-10.47-3.438-3.28-.684-7.25-1.035-11.9-1.035M1983.36 49.727c0-6.426-2.4-11.368-7.18-14.844-4.77-3.477-11.47-5.215-20.11-5.215-9.13 0-16.26 1.445-21.37 4.336v9.687a51.32 51.32 0 0 1 10.65-3.964c3.79-.977 7.45-1.457 10.97-1.457 5.46 0 9.64.87 12.57 2.609 2.95 1.738 4.41 4.394 4.41 7.95 0 2.694-1.17 4.98-3.5 6.894-2.32 1.914-6.85 4.152-13.6 6.757-6.41 2.383-10.97 4.473-13.67 6.25-2.71 1.778-4.72 3.81-6.04 6.067-1.31 2.254-1.98 4.96-1.98 8.113 0 5.606 2.29 10.04 6.86 13.281 4.57 3.25 10.84 4.883 18.79 4.883 7.42 0 14.66-1.515 21.74-4.531l-3.71-8.496c-6.9 2.851-13.17 4.277-18.79 4.277-4.94 0-8.67-.77-11.18-2.324-2.52-1.543-3.78-3.691-3.78-6.406 0-1.844.48-3.418 1.42-4.707.95-1.309 2.46-2.54 4.56-3.711 2.09-1.184 6.11-2.871 12.07-5.086 8.16-2.98 13.69-5.98 16.55-8.996 2.87-3.02 4.32-6.809 4.32-11.367M2021.28 38.27c1.85 0 3.64.136 5.35.421 1.71.274 3.09.547 4.09.84v-7.976c-1.14-.559-2.81-.996-5.01-1.36-2.18-.351-4.18-.527-5.93-.527-13.33 0-19.99 7.012-19.99 21.055V91.71h-9.87v5.027l9.87 4.336 4.4 14.707h6.02V99.805h20V91.71h-20V51.16c0-4.15 1-7.333 2.97-9.56 1.98-2.206 4.67-3.331 8.1-3.331M2053.61 30.918h-10.42v68.887h10.42zm-11.31 87.559c0 2.39.59 4.14 1.76 5.253 1.18 1.106 2.65 1.661 4.42 1.661 1.67 0 3.1-.567 4.32-1.7 1.22-1.132 1.82-2.871 1.82-5.214 0-2.344-.6-4.09-1.82-5.247-1.22-1.16-2.65-1.726-4.32-1.726-1.77 0-3.24.566-4.42 1.726-1.17 1.157-1.76 2.903-1.76 5.247M2121.59 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12 4.16-7.21 0-12.49-1.953-15.84-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.49l1.69-9.422h.5c2.15 3.387 5.14 6.023 8.99 7.887 3.85 1.867 8.16 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M2159.29 77.742c0-4.812 1.35-8.465 4.08-10.926 2.72-2.48 6.51-3.71 11.37-3.71 10.19 0 15.28 4.953 15.28 14.831 0 10.344-5.16 15.532-15.47 15.532-4.9 0-8.67-1.32-11.31-3.965-2.63-2.649-3.95-6.555-3.95-11.762zm-5.67-58.387c0-3.73 1.58-6.55 4.72-8.488 3.14-1.922 7.65-2.879 13.52-2.879 8.75 0 15.24 1.309 19.45 3.926 4.21 2.617 6.31 6.172 6.31 10.652 0 3.723-1.15 6.32-3.45 7.754-2.31 1.457-6.65 2.168-13.01 2.168h-12.51c-4.74 0-8.43-1.12-11.06-3.386-2.65-2.266-3.97-5.508-3.97-9.747zm54.94 80.45v-6.582l-12.76-1.512c1.18-1.477 2.23-3.39 3.15-5.754.91-2.371 1.37-5.039 1.37-8.02 0-6.746-2.29-12.128-6.91-16.152-4.61-4.012-10.93-6.023-18.98-6.023-2.05 0-3.98.156-5.78.5-4.45-2.356-6.67-5.305-6.67-8.871 0-1.883.77-3.282 2.34-4.176 1.54-.902 4.21-1.36 7.97-1.36h12.2c7.46 0 13.19-1.574 17.19-4.707 4-3.144 6-7.714 6-13.71 0-7.618-3.06-13.426-9.17-17.43C2192.38 2.004 2183.46 0 2171.72 0c-9 0-15.95 1.68-20.82 5.027-4.88 3.352-7.34 8.079-7.34 14.211 0 4.18 1.35 7.813 4.03 10.88 2.68 3.046 6.45 5.116 11.32 6.21-1.77.8-3.24 2.031-4.44 3.711-1.19 1.68-1.78 3.633-1.78 5.84 0 2.52.66 4.707 2.01 6.602 1.34 1.882 3.44 3.71 6.34 5.468-3.56 1.465-6.46 3.953-8.71 7.48-2.23 3.516-3.35 7.54-3.35 12.06 0 7.55 2.26 13.37 6.79 17.452 4.52 4.082 10.93 6.133 19.22 6.133 3.6 0 6.86-.429 9.75-1.27h23.82M2284.61 91.71h-17.54V30.919h-10.43v60.793h-12.31v4.707l12.31 3.766v3.839c0 16.922 7.4 25.391 22.19 25.391 3.65 0 7.93-.73 12.82-2.195l-2.7-8.364c-4.03 1.301-7.46 1.946-10.31 1.946-3.93 0-6.85-1.309-8.73-3.926-1.89-2.617-2.84-6.816-2.84-12.598v-4.472h17.54V91.71M2302.87 65.43c0-8.809 1.76-15.508 5.28-20.118 3.52-4.609 8.7-6.906 15.52-6.906 6.84 0 12.02 2.297 15.57 6.875 3.54 4.602 5.3 11.301 5.3 20.149 0 8.75-1.76 15.41-5.3 19.953-3.55 4.539-8.78 6.824-15.69 6.824-6.83 0-11.99-2.246-15.46-6.73-3.48-4.48-5.22-11.16-5.22-20.047zm52.48 0c0-11.23-2.82-20-8.47-26.309-5.67-6.309-13.48-9.453-23.46-9.453-6.15 0-11.62 1.445-16.4 4.336-4.77 2.89-8.47 7.031-11.06 12.45-2.59 5.401-3.9 11.73-3.9 18.976 0 11.23 2.81 19.968 8.43 26.242 5.6 6.258 13.4 9.402 23.38 9.402 9.63 0 17.28-3.222 22.97-9.62 5.68-6.415 8.51-15.087 8.51-26.024M2403.79 101.074c3.07 0 5.8-.254 8.22-.761l-1.43-9.676c-2.86.633-5.37.933-7.55.933-5.58 0-10.33-2.261-14.3-6.785-3.95-4.531-5.94-10.156-5.94-16.902V30.918h-10.43v68.887h8.62l1.19-12.754h.5c2.56 4.48 5.63 7.949 9.23 10.37 3.61 2.423 7.56 3.653 11.89 3.653M2500.33 69.766l-10.68 28.476c-1.39 3.594-2.81 8.028-4.28 13.262-.93-4.024-2.24-8.438-3.96-13.262l-10.81-28.476zm14.77-38.848l-11.44 29.227h-36.83l-11.32-29.227h-10.81l36.34 92.273h8.98l36.13-92.273h-11.05M2583.07 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12 4.16-7.21 0-12.49-1.953-15.84-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.48l1.69-9.422h.51c2.14 3.387 5.14 6.023 8.99 7.887 3.84 1.867 8.15 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M2620.76 77.742c0-4.812 1.36-8.465 4.08-10.926 2.73-2.48 6.53-3.71 11.37-3.71 10.2 0 15.28 4.953 15.28 14.831 0 10.344-5.15 15.532-15.45 15.532-4.91 0-8.68-1.32-11.32-3.965-2.64-2.649-3.96-6.555-3.96-11.762zm-5.66-58.387c0-3.73 1.57-6.55 4.71-8.488 3.15-1.922 7.65-2.879 13.53-2.879 8.75 0 15.23 1.309 19.44 3.926 4.21 2.617 6.31 6.172 6.31 10.652 0 3.723-1.14 6.32-3.45 7.754-2.31 1.457-6.64 2.168-13 2.168h-12.51c-4.74 0-8.43-1.12-11.07-3.386-2.63-2.266-3.96-5.508-3.96-9.747zm54.94 80.45v-6.582l-12.76-1.512c1.18-1.477 2.22-3.39 3.14-5.754.92-2.371 1.38-5.039 1.38-8.02 0-6.746-2.3-12.128-6.92-16.152-4.61-4.012-10.92-6.023-18.97-6.023-2.05 0-3.99.156-5.78.5-4.46-2.356-6.67-5.305-6.67-8.871 0-1.883.78-3.282 2.33-4.176 1.55-.902 4.21-1.36 7.98-1.36h12.2c7.46 0 13.18-1.574 17.18-4.707 4.01-3.144 6-7.714 6-13.71 0-7.618-3.06-13.426-9.17-17.43C2653.87 2.004 2644.94 0 2633.2 0c-9 0-15.95 1.68-20.83 5.027-4.88 3.352-7.33 8.079-7.33 14.211 0 4.18 1.35 7.813 4.02 10.88 2.69 3.046 6.47 5.116 11.32 6.21-1.77.8-3.23 2.031-4.43 3.711-1.19 1.68-1.79 3.633-1.79 5.84 0 2.52.66 4.707 2.01 6.602 1.35 1.882 3.45 3.71 6.35 5.468-3.56 1.465-6.47 3.953-8.71 7.48-2.23 3.516-3.35 7.54-3.35 12.06 0 7.55 2.25 13.37 6.79 17.452 4.52 4.082 10.92 6.133 19.21 6.133 3.62 0 6.86-.429 9.75-1.27h23.83M2692.7 99.805V55.117c0-5.605 1.27-9.805 3.83-12.566 2.56-2.766 6.57-4.145 12.01-4.145 7.2 0 12.47 1.965 15.81 5.903 3.33 3.945 4.99 10.379 4.99 19.304v36.192h10.44V30.918h-8.62l-1.5 9.25h-.58c-2.13-3.41-5.1-5.988-8.88-7.793-3.8-1.809-8.13-2.707-12.99-2.707-8.37 0-14.65 1.992-18.81 5.977-4.18 3.964-6.26 10.351-6.26 19.101v45.059h10.56M2760.61 30.918h10.43v97.805h-10.43zM2810.67 38.27c6.5 0 11.6 1.789 15.31 5.343 3.71 3.575 5.56 8.555 5.56 14.961v6.23l-10.44-.448c-8.3-.286-14.27-1.583-17.94-3.868-3.66-2.273-5.5-5.82-5.5-10.644 0-3.781 1.14-6.64 3.42-8.613 2.29-1.973 5.48-2.961 9.59-2.961zm23.57-7.352l-2.07 9.805h-.51c-3.44-4.305-6.86-7.227-10.27-8.77-3.42-1.523-7.68-2.285-12.8-2.285-6.83 0-12.17 1.758-16.05 5.273-3.87 3.528-5.81 8.536-5.81 15.032 0 13.906 11.12 21.199 33.37 21.875l11.7.359v4.277c0 5.418-1.17 9.395-3.5 11.985-2.32 2.566-6.03 3.855-11.15 3.855-5.74 0-12.24-1.758-19.49-5.273l-3.21 7.988c3.4 1.836 7.11 3.281 11.16 4.324a47.81 47.81 0 0 0 12.16 1.575c8.23 0 14.3-1.817 18.27-5.461 3.96-3.66 5.93-9.5 5.93-17.54V30.919h-7.73M2893.6 101.074c3.07 0 5.8-.254 8.25-.761l-1.46-9.676c-2.84.633-5.35.933-7.54.933-5.56 0-10.33-2.261-14.3-6.785-3.96-4.531-5.93-10.156-5.93-16.902V30.918h-10.44v68.887h8.61l1.19-12.754h.5c2.57 4.48 5.65 7.949 9.25 10.37 3.6 2.423 7.56 3.653 11.87 3.653M2901.63 6.727c-3.94 0-7.04.558-9.31 1.691v9.121c2.97-.84 6.08-1.25 9.31-1.25 4.14 0 7.3 1.25 9.45 3.77 2.16 2.507 3.24 6.132 3.24 10.859v91.895h10.69V31.797c0-7.95-2.01-14.121-6.04-18.496-4.02-4.383-9.8-6.574-17.34-6.574M2999.96 55.371c0-8.086-2.93-14.394-8.8-18.918-5.87-4.52-13.83-6.785-23.88-6.785-10.9 0-19.27 1.406-25.14 4.219v10.3c3.77-1.59 7.88-2.847 12.31-3.765 4.45-.93 8.85-1.399 13.21-1.399 7.12 0 12.49 1.36 16.09 4.063 3.59 2.695 5.4 6.465 5.4 11.277 0 3.196-.63 5.805-1.91 7.832-1.29 2.024-3.42 3.907-6.42 5.625-2.99 1.711-7.56 3.664-13.67 5.84-8.55 3.059-14.66 6.692-18.32 10.871-3.66 4.2-5.51 9.668-5.51 16.407 0 7.089 2.68 12.714 7.99 16.914 5.32 4.191 12.36 6.289 21.12 6.289 9.13 0 17.54-1.68 25.2-5.032l-3.32-9.304c-7.59 3.183-14.96 4.785-22.13 4.785-5.66 0-10.07-1.223-13.26-3.652-3.19-2.43-4.78-5.809-4.78-10.118 0-3.191.59-5.8 1.76-7.832 1.17-2.031 3.14-3.886 5.95-5.597 2.78-1.688 7.04-3.563 12.79-5.625 9.63-3.426 16.26-7.118 19.89-11.063 3.62-3.937 5.43-9.043 5.43-15.332M741.648 375.406h30c28.965 0 50.227 5.039 63.774 15.117 13.531 10.079 20.32 25.821 20.32 47.247 0 19.832-6.074 34.628-18.191 44.402-12.141 9.758-31.028 14.641-56.692 14.641h-39.211zm172.192 64.246c0-36.062-11.809-63.691-35.434-82.898-23.621-19.219-57.234-28.82-100.847-28.82h-35.911V198.73h-56.445v345.329h99.438c43.14 0 75.457-8.829 96.961-26.465 21.496-17.637 32.238-43.614 32.238-77.942M1099.26 464.691c11.17 0 20.39-.789 27.63-2.371l-5.43-51.718c-7.88 1.894-16.07 2.832-24.57 2.832-22.2 0-40.19-7.246-53.97-21.731-13.78-14.48-20.66-33.301-20.66-56.453V198.73h-55.514v261.227h43.464l7.32-46.055h2.83c8.66 15.594 19.96 27.95 33.9 37.09 13.93 9.141 28.93 13.699 45 13.699M1206.88 329.82c0-60.308 22.28-90.465 66.85-90.465 44.08 0 66.13 30.157 66.13 90.465 0 59.688-22.21 89.512-66.61 89.512-23.31 0-40.2-7.707-50.67-23.144-10.47-15.43-15.7-37.54-15.7-66.368zm190.13 0c0-42.672-10.95-75.972-32.83-99.898-21.89-23.945-52.35-35.918-91.41-35.918-24.41 0-45.97 5.508-64.7 16.543-18.75 11.016-33.16 26.836-43.23 47.48-10.08 20.625-15.11 44.551-15.11 71.793 0 42.364 10.86 75.43 32.58 99.2 21.73 23.777 52.36 35.671 91.89 35.671 37.79 0 67.7-12.156 89.75-36.492 22.05-24.328 33.06-57.121 33.06-98.379M1558.11 238.887c13.54 0 27.07 2.129 40.62 6.386v-41.816c-6.13-2.676-14.05-4.922-23.73-6.738-9.69-1.797-19.73-2.715-30.12-2.715-52.59 0-78.88 27.715-78.88 83.144v140.778h-35.68v24.558l38.26 20.325 18.9 55.261h34.26v-58.113h74.39v-42.031h-74.39v-139.84c0-13.379 3.34-23.242 10.03-29.629 6.69-6.387 15.48-9.57 26.34-9.57M1783.44 464.691c11.17 0 20.38-.789 27.62-2.371l-5.43-51.718c-7.88 1.894-16.06 2.832-24.56 2.832-22.2 0-40.2-7.246-53.97-21.731-13.78-14.48-20.66-33.301-20.66-56.453V198.73h-55.52v261.227h43.46l7.34-46.055h2.82c8.66 15.594 19.95 27.95 33.9 37.09 13.92 9.141 28.93 13.699 45 13.699M1925.05 236.523c20.15 0 36.32 5.625 48.52 16.895 12.21 11.25 18.31 27.051 18.31 47.344v22.676l-33.54-1.407c-26.13-.937-45.16-5.312-57.04-13.105-11.89-7.793-17.82-19.727-17.82-35.781 0-11.661 3.45-20.665 10.39-27.051 6.91-6.387 17.32-9.571 31.18-9.571zm82.66-37.793l-11.11 36.387h-1.87c-12.62-15.918-25.29-26.738-38.04-32.48-12.74-5.742-29.13-8.633-49.13-8.633-25.67 0-45.7 6.934-60.1 20.801-14.41 13.847-21.62 33.457-21.62 58.808 0 26.934 10 47.246 30 60.934 19.99 13.691 50.45 21.172 91.41 22.441l45.09 1.414v13.938c0 16.699-3.88 29.16-11.68 37.441-7.79 8.262-19.88 12.383-36.25 12.383-13.39 0-26.23-1.953-38.5-5.891a294.638 294.638 0 0 1-35.44-13.933l-17.94 39.668c14.17 7.41 29.68 13.035 46.52 16.894 16.85 3.868 32.77 5.789 47.72 5.789 33.22 0 58.31-7.246 75.22-21.726 16.94-14.492 25.4-37.246 25.4-68.262V198.73h-39.68M2220.04 194.004c-39.52 0-69.55 11.543-90.1 34.609-20.55 23.067-30.82 56.172-30.82 99.321 0 43.925 10.74 77.707 32.23 101.339 21.5 23.614 52.56 35.418 93.18 35.418 27.56 0 52.35-5.117 74.41-15.359l-16.78-44.641c-23.46 9.133-42.82 13.704-58.1 13.704-45.19 0-67.79-29.993-67.79-89.981 0-29.293 5.63-51.305 16.89-66.031 11.26-14.707 27.76-22.09 49.48-22.09 24.72 0 48.11 6.152 70.15 18.437v-48.417c-9.92-5.84-20.5-10-31.76-12.52-11.26-2.52-24.93-3.789-40.99-3.789M2451.52 238.887c13.54 0 27.08 2.129 40.63 6.386v-41.816c-6.15-2.676-14.05-4.922-23.73-6.738-9.69-1.797-19.73-2.715-30.12-2.715-52.6 0-78.9 27.715-78.9 83.144v140.778h-35.66v24.558l38.26 20.325 18.9 55.261h34.26v-58.113h74.39v-42.031h-74.39v-139.84c0-13.379 3.34-23.242 10.03-29.629 6.69-6.387 15.47-9.57 26.33-9.57M2585.92 329.82c0-60.308 22.28-90.465 66.84-90.465 44.09 0 66.15 30.157 66.15 90.465 0 59.688-22.22 89.512-66.62 89.512-23.31 0-40.2-7.707-50.67-23.144-10.47-15.43-15.7-37.54-15.7-66.368zm190.13 0c0-42.672-10.94-75.972-32.83-99.898-21.89-23.945-52.36-35.918-91.4-35.918-24.42 0-45.98 5.508-64.72 16.543-18.74 11.016-33.14 26.836-43.22 47.48-10.07 20.625-15.12 44.551-15.12 71.793 0 42.364 10.87 75.43 32.59 99.2 21.74 23.777 52.36 35.671 91.89 35.671 37.79 0 67.7-12.156 89.75-36.492 22.04-24.328 33.06-57.121 33.06-98.379M2972.33 464.691c11.18 0 20.38-.789 27.63-2.371l-5.43-51.718c-7.87 1.894-16.05 2.832-24.57 2.832-22.2 0-40.19-7.246-53.96-21.731-13.78-14.48-20.67-33.301-20.67-56.453V198.73h-55.51v261.227h43.46l7.33-46.055h2.83c8.66 15.594 19.96 27.95 33.89 37.09 13.94 9.141 28.94 13.699 45 13.699" fill="#100f0d"/><path d="M610.11 372.83c0-170.584-138.257-308.862-308.846-308.862-170.602 0-308.846 138.278-308.846 308.863 0 170.576 138.244 308.846 308.846 308.846 170.59 0 308.846-138.27 308.846-308.846" fill="#e53935" stroke-width="1.029"/><path d="M460.694 521.792l-105.04.958-61.415 61.415-72.096-47.883 12.445-12.438-29.207.26-99.129-166.817H67.357l24.39-24.402-24.57-41.363L294.66 64.049c2.192-.04 4.399-.08 6.603-.08 170.416 0 308.585 138.055 308.846 308.408L460.694 521.792" fill="#d51c2f" stroke-width="1.029"/><path d="M149.093 350.258c0 84.048 68.13 152.151 152.171 152.151 84.028 0 152.139-68.103 152.139-152.151zm342.063-7.017v14.046h44.015c-1.75 59.337-25.556 113.104-63.54 153.419L438.75 477.81l-9.925 9.94 32.875 32.887c-40.314 37.983-94.081 61.79-153.41 63.527l-.015-44.003h-14.035v44.003c-59.34-1.737-113.096-25.556-153.41-63.527l32.887-32.887-9.945-9.92-32.883 32.875c-37.975-40.315-61.781-94.082-63.53-153.419h44.002l-.008-14.034H67.176v-51.511h468.176v51.5h-44.196" fill="#f5f5f5" stroke-width="1.029"/></g></g></symbol><symbol id="pug" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:#c1272d}.hyst1{fill:#efcca3}.st2{fill:#ed1c24}.hyst3{fill:#ccac8d}.hyst4{fill:#fff}.st5{fill:#ff931e}.st6{fill:#ffb81e}.hyst7{fill:#56332b}.hyst8{fill:#442823}.hyst9{fill:#7f4a41}.hyst10{fill:#331712}.st11{fill:#fc6}.st12{fill:#ccc}.st13{fill:#b3b3b3}.st14{fill:#989898}.st15{fill:#323232}.st16{fill:#1e1e1e}.st17{fill:#4c4c4c}.st18{fill:#e6e6e6}.st19{fill:#606060}</style><path class="hyst1" d="M107.4 50.9c-.2-4.4.4-8.3-1.6-11.6-4.8-8.2-16.8-13-40.8-13v.7h-.5.5v-.7c-24 0-36.6 4.8-41.4 13.1-1.9 3.4-1.7 7.2-2 11.6-.2 3.5-1.8 7.2-1.1 11.2.8 5.2 1.1 10.4 1.9 15.2.6 3.9 6 7.2 6.5 10.9 1.4 10.2 12 14.9 36 14.9v.8h-.6.7v-.8c24 0 34.2-4.7 35.5-14.9.5-3.8 5.5-7 6.1-10.9.8-4.8 1.1-10 1.9-15.2.7-4-.9-7.8-1.1-11.3z"/><path class="hyst3" d="M64.6 54.5c4.3.1 7.3 2.8 10.1 5.3 3.3 2.9 8.9 4.9 11.2 7.4 2.3 2.5 5.3 5 6.4 8.9 1.1 3.9 1.4 8.9 1.4 10.2 0 1.3.7 1 2.7 0 4.7-2.3 9.9-8.5 9.9-8.5-.6 3.9-5.7 7.4-6.2 11.1C98.9 99.1 89 104 64.5 104h-.1.6"/><path class="hyst3" d="M80.4 46.7c.9 3.1 4.1 13.6-2.1 10.1 0 0 2.6 1.5 4.2 7.2 1.7 5.7 5.8 6.4 5.8 6.4s6.7 1.3 11.7-3c4.2-3.6 4.9-10 3.1-14.9-1.8-4.8-5-6.3-9.7-7.3-4.7-1.1-14.1-2-13 1.5z"/><circle cx="92.3" cy="58.1" r="8.8"/><circle class="hyst4" cx="90" cy="54.2" r="2.3"/><path class="hyst1" d="M78.9 57.7s7.9 5.4 12.2 10.7c4.3 5.3 4.2 6.3 4.2 6.3l-3.1 1.4s-4.4-8.3-9.8-11.4c-5.5-3.1-6.1-5.7-6.1-5.7l2.6-1.3z"/><path class="hyst3" d="M64.9 54.5c-4.3.1-7.5 2.8-10.4 5.3-3.3 2.9-9.1 4.9-11.4 7.4-2.3 2.5-5.4 5-6.5 8.9-1.1 3.9-1.5 8.9-1.5 10.2 0 1.3.2 1.4-2.7 0-4.7-2.2-9.9-8.5-9.9-8.5.6 3.9 5.7 7.4 6.2 11.1C30.1 99.1 40 104 64.5 104h.5"/><path class="hyst7" d="M88.1 71.4C83.3 65.5 75.6 60 64.9 60h-.1c-10.7 0-18.4 5.5-23.2 11.4-5 6.1-4.6 8.5-4.6 14.3 0 21 7.4 15 12.3 17.6 5 2.5 10.2 1.7 15.5 1.7h.1c5.4 0 10.5.7 15.5-1.8 4.9-2.5 12.3 3.7 12.3-17.3.1-5.8.4-8.4-4.6-14.5z"/><path class="hyst8" d="M64.4 65.2s-.7 9.7-2.1 11.6l2.6-.6-.5-11z"/><path class="hyst8" d="M65.1 65.2s.7 9.7 2.1 11.6l-2.6-.6.5-11z"/><path class="hyst7" d="M56.7 62.9c-1-2.3 2.6-6 8.3-6.1 5.7 0 9.3 3.7 8.3 6.1-1 2.4-4.6 3.1-8.3 3.2-3.6-.1-7.3-.8-8.3-3.2z"/><path d="M65 65.2c0-.4 3.4-.5 5.2-1.7 0 0-3.7 1.2-4.5.7-.8-.4-1-1.6-1-1.6s-.3 1.2-.9 1.6c-.7.4-4.9-.7-4.9-.7s5.6 1.4 5.6 1.7c0 .3-.1 1.3-.1 2 0 2.5 0 8.7.4 9.2.6.9.4-6.7.4-9.2-.1-.8-.1-1.6-.2-2z"/><path class="hyst9" d="M65.2 78.6c1.7 0 4.7 1.2 7.4 3.1-2.6-2.9-5.7-4.9-7.4-4.9-1.8 0-5.6 2.2-8.3 5.4 2.8-2.2 6.4-3.6 8.3-3.6z"/><path class="hyst8" d="M64.5 96.3c-3.8 0-7.5-1.2-10.9-2.1-.7-.2-1.4.3-2.1.1-6.3-2-11.4-5.4-14.5-9.7v1c0 21 7.4 15.1 12.3 17.6 5 2.5 10.2 1.7 15.5 1.7h.1c5.4 0 10.5.7 15.5-1.8 4.9-2.5 12.3 3.6 12.3-17.4 0-.8 0-1.6.1-2.3-2.9 4.7-8.2 8.4-14.8 10.6-.6.2-2-.3-2.6-.2-3.6 1.2-6.8 2.5-10.9 2.5z"/><path class="hyst8" d="M55 85s-2.5 7.5-.8 10.8l-2.3-1s1.7-7.6 3.1-9.8zM74.8 85s2.5 7.5.8 10.8l2.3-1s-1.8-7.6-3.1-9.8z"/><path class="hyst3" d="M48.6 46.7c-.9 3.1-4.1 13.6 2.1 10.1 0 0-2.6 1.5-4.2 7.2s-5.8 6.4-5.8 6.4-6.7 1.3-11.7-3c-4.2-3.6-4.9-10-3.1-14.9s5-6.3 9.7-7.3c4.7-1.1 14-2 13 1.5z"/><path d="M64.9 76.8c2.7 0 11.1 5.8 11.2 12.9v-.4c0-7.4-6.8-13.3-11.2-13.3-4.4 0-11.2 6-11.2 13.3v.4c.1-7.1 8.5-12.9 11.2-12.9z"/><ellipse transform="rotate(-14.465 66.712 61.468)" class="hyst10" cx="66.7" cy="61.5" rx=".8" ry="1.5"/><ellipse transform="rotate(17.235 62.371 61.462)" class="hyst10" cx="62.4" cy="61.5" rx=".8" ry="1.5"/><circle cx="37.2" cy="58.1" r="8.8"/><circle class="hyst4" cx="39.5" cy="54.2" r="2.3"/><path class="hyst9" d="M67.5 58.2c0-.1-2.3 1-2.9 1.1-.6-.1-2.9-1.2-2.9-1.1h5.8z"/><path class="hyst1" d="M50 57.7s-7.9 5.4-12.2 10.7c-4.3 5.3-4.2 6.3-4.2 6.3l3.1 1.4s4.4-8.3 9.8-11.4 6.1-5.7 6.1-5.7L50 57.7z"/><path class="hyst3" d="M32.7 41.7S30 49.1 24 52.2c0 0 9.4-1.1 8.7-10.5zM95.8 41.7s2.7 7.4 8.7 10.5c0 0-9.4-1.1-8.7-10.5zM78.7 55.5s-5.9-6.2-13.8-6.4h.1.1c-8 .2-13.8 6.4-13.8 6.4 6.9-4.8 12.8-4.7 13.8-4.7-.1 0 6.7-.1 13.6 4.7zM71.8 42.5s-3-4.2-7-4.3h.2c-3 .1-6.9 4.3-6.9 4.3 3.4-3.3 6.9-3.2 6.9-3.2s3.3-.1 6.8 3.2zM37.2 73.2s-4.7 2.3-8.1.9H29c-3-1.7-4.5-6.8-4.5-6.8s3 9 12.7 5.9zM92 73.2s4.7 2.3 8.1.9c4-1.7 4.6-6.8 4.6-6.8s-3 9-12.7 5.9z"/><path class="hyst3" d="M42.6 41.2c2.6-.5 6.9-.6 10.3.5 4.3 1.5.8 7 1.7 7.3.9.3 2.1-3.8 10.1-3.4 8.1.4 9 4 10.1 3.4s-1.1-10 11-7.8c0 0-12.7-3.4-12.1 5.8 0 0-7.3-5.6-17.5-.6.1 0 2.7-8.6-13.6-5.2zM86.9 41.2c.2 0 .3.1.4.1.1 0-.1-.1-.4-.1zM86.9 41.2zM39.1 28.9S28.3 42.5 26.7 47.7c-1.6 5.3-2.8 27-4.2 30.1l-5-21.4 9.2-22.3 12.4-5.2zM89.9 28.9s10.8 13.6 12.4 18.8c1.6 5.3 2.8 27 4.2 30.1l5-21.4-9.2-22.3-12.4-5.2z"/><path class="hyst7" d="M89.4 28.9s11.6 9.7 15 20.9c3.4 11.2 2 24.8 4.6 26.5 3.7 2.4 7.9-11.9 9.3-13.4 2.2-2.4 9.5-8.5 10-9.6.5-1.1-14.8-17.8-21.5-21.1-8.1-3.8-18.1-4.1-17.4-3.3z"/><path class="hyst8" d="M99.3 34.9s13.7 17.5 13.5 39.3l5.5-11.2c-.1 0-4.9-14.3-19-28.1z"/><path class="hyst7" d="M39.1 28.9s-11.6 9.7-15 20.9-2 24.8-4.6 26.5c-3.7 2.4-7.9-11.9-9.3-13.4C8 60.5.7 54.4.2 53.3-.3 52.2 15 35.5 21.7 32.2c8.1-3.8 18.1-4.1 17.4-3.3z"/><path class="hyst8" d="M29.2 34.9S15.5 52.4 15.7 74.2L10.3 63s4.8-14.3 18.9-28.1z"/><path class="hyst3" d="M21.8 74.6s1 5.4 2.6 7.1.5-1.3.5-1.3-1.7-.9-1.4-7.8-1.7 2-1.7 2zM107.1 74.6s-1 5.4-2.6 7.1-.5-1.3-.5-1.3 1.7-.9 1.4-7.8 1.7 2 1.7 2z"/><g><circle class="hyst8" cx="54.5" cy="70.5" r=".8"/><circle class="hyst8" cx="49.9" cy="75.3" r=".8"/><circle class="hyst8" cx="48.4" cy="70.5" r=".8"/></g><g><circle class="hyst8" cx="74" cy="70.5" r=".8"/><circle class="hyst8" cx="78.6" cy="75.3" r=".8"/><circle class="hyst8" cx="80.1" cy="70.5" r=".8"/></g></symbol><symbol viewBox="0 0 50 50" id="puppet" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -247)" fill="#fbc02d"><path stroke-width=".283" d="M11.559 249.467h13.587v13.587H11.559zM27.435 265.056h13.587v13.587H27.435zM11.559 281.074h13.587v13.587H11.559z"/><path stroke-width=".256" d="M16.62 251.615l18.305 18.305-3.236 3.236-18.305-18.305z"/><path stroke-width=".256" d="M37.834 271.331L19.53 289.636l-3.237-3.237 18.305-18.304z"/></g></symbol><symbol viewBox="0 0 100 99.999997" id="purescript" xmlns="http://www.w3.org/2000/svg"><path clip-path="url(#SVGID_2_)" d="M98.079 38.548L79.22 19.68l-5.087 5.088L90.447 41.09 74.134 57.41l5.087 5.087 18.858-18.86a3.59 3.59 0 0 0 1.055-2.55 3.578 3.578 0 0 0-1.055-2.54M25.483 42.794l-5.09-5.089L1.53 56.568a3.566 3.566 0 0 0-1.05 2.545c0 .961.373 1.863 1.05 2.542L20.394 80.52l5.089-5.086L9.162 59.113z" fill="#42a5f5" stroke-width="1.192"/><path clip-path="url(#SVGID_2_)" transform="matrix(1.19175 0 0 1.19175 -306.84 -629.047)" fill="#42a5f5" d="M281.841 551.736l6.461 6.037h28.379l-6.461-6.037zM288.302 566.861l-6.463 6.035h28.381l6.463-6.035zM281.838 581.982l6.464 6.035h28.381l-6.463-6.035z"/></symbol><symbol viewBox="0 0 24 24" id="python" xmlns="http://www.w3.org/2000/svg"><path d="M19.14 7.5A2.86 2.86 0 0 1 22 10.36v3.78A2.86 2.86 0 0 1 19.14 17H12c0 .39.32.96.71.96H17v1.68a2.86 2.86 0 0 1-2.86 2.86H9.86A2.86 2.86 0 0 1 7 19.64v-3.75a2.85 2.85 0 0 1 2.86-2.85h5.25a2.85 2.85 0 0 0 2.85-2.86V7.5h1.18m-4.28 11.79c-.4 0-.72.3-.72.89 0 .59.32.71.72.71a.71.71 0 0 0 .71-.71c0-.59-.32-.89-.71-.89m-10-1.79A2.86 2.86 0 0 1 2 14.64v-3.78A2.86 2.86 0 0 1 4.86 8H12c0-.39-.32-.96-.71-.96H7V5.36A2.86 2.86 0 0 1 9.86 2.5h4.28A2.86 2.86 0 0 1 17 5.36v3.75a2.85 2.85 0 0 1-2.86 2.85H8.89a2.85 2.85 0 0 0-2.85 2.86v2.68H4.86M9.14 5.71c.4 0 .72-.3.72-.89 0-.59-.32-.71-.72-.71-.39 0-.71.12-.71.71s.32.89.71.89z"/><path d="M9.264 22.379c-.895-.24-1.581-.799-1.947-1.582-.228-.489-.237-.606-.238-2.957-.001-2.745.057-3.074.666-3.785.193-.226.568-.517.833-.648.47-.23.579-.239 3.839-.288 3.131-.048 3.386-.065 3.814-.264.626-.291 1.07-.687 1.4-1.247.27-.46.278-.522.311-2.29l.034-1.82.932.051c1.075.058 1.504.211 2.098.748.853.77.869.841.869 3.957 0 2.434-.02 2.783-.18 3.075a3.365 3.365 0 0 1-1.337 1.33l-.517.273-3.95.031-3.951.031.068.274c.037.151.164.377.282.503.209.224.262.229 2.433.229h2.22v1.05c0 1.653-.394 2.437-1.54 3.072l-.545.302-2.644.018c-1.455.01-2.782-.018-2.95-.063zm6.12-1.692c.22-.222.253-.325.206-.675-.07-.523-.278-.73-.732-.73-.467 0-.672.217-.735.78-.042.372-.012.496.163.672.3.3.77.28 1.097-.047z" fill="#fc0" stroke="#fc0" stroke-width=".102"/><path d="M9.349 22.38c-.911-.15-1.936-1.074-2.176-1.963-.073-.273-.101-1.279-.079-2.868.033-2.317.047-2.473.27-2.926.13-.263.401-.623.603-.8.674-.592.87-.63 3.484-.675 4.399-.076 4.927-.166 5.705-.967.642-.662.706-.9.774-2.883l.061-1.784.951.055c.523.031 1.11.122 1.304.204.54.225 1.358 1.042 1.472 1.47.153.572.243 3.18.16 4.617-.071 1.23-.093 1.327-.395 1.78-.193.288-.577.647-.966.903l-.647.425-3.922.008c-2.157.004-3.942.028-3.966.052-.115.115.354.82.587.883.14.038 1.181.073 2.314.079l2.06.01v.91c0 1.739-.326 2.446-1.454 3.162l-.631.4-2.543-.011c-1.398-.007-2.733-.043-2.966-.081zm5.98-1.718c.285-.256.313-.328.251-.658-.09-.483-.301-.682-.722-.682-.436 0-.625.193-.715.73-.065.384-.044.453.2.663.358.308.595.295.985-.053z" fill="#fdd835" stroke-width=".102"/><path d="M4.281 17.396c-.88-.215-1.714-.935-2.024-1.747-.149-.389-.168-.804-.142-3.041.027-2.26.054-2.638.215-2.962.259-.519.851-1.092 1.392-1.346.437-.206.632-.217 4.408-.245l3.95-.03-.067-.275a1.367 1.367 0 0 0-.282-.504c-.21-.224-.263-.23-2.433-.23h-2.22l.002-1.143c.003-1.338.157-1.795.84-2.493.746-.763 1.103-.838 4.025-.838 2.961 0 3.28.06 4.067.768.37.333.572.621.728 1.037.201.539.213.735.183 3.072-.035 2.777-.045 2.824-.78 3.598-.787.829-.76.824-4.59.883-3.812.06-3.797.057-4.61.806-.765.706-.917 1.2-.964 3.133l-.04 1.653-.677-.01c-.371-.007-.813-.045-.98-.086zM9.59 5.551c.237-.204.286-.326.286-.72 0-.547-.201-.763-.71-.763-.502 0-.765.248-.765.724 0 .492.141.782.439.902.345.14.444.12.75-.143z" fill="#3c78aa"/></symbol><symbol viewBox="0 0 24 24" id="r" xmlns="http://www.w3.org/2000/svg"><path d="M11.956 4.05c-5.694 0-10.354 3.106-10.354 6.947 0 3.396 3.686 6.212 8.531 6.813v2.205h3.53V17.82c.88-.093 1.699-.259 2.475-.497l1.43 2.692h3.996l-2.402-4.048c1.936-1.263 3.147-3.034 3.147-4.97 0-3.841-4.659-6.947-10.354-6.947m1.584 2.712c4.349 0 7.558 1.45 7.558 4.753 0 1.77-.952 3.013-2.505 3.779a1.081 1.081 0 0 1-.228-.156c-.373-.165-.994-.352-.994-.352s3.085-.227 3.085-3.302-3.23-3.127-3.23-3.127h-7.092v7.413c-2.64-.766-4.462-2.392-4.462-4.255 0-2.63 3.52-4.753 7.868-4.753m.156 4.12h2.143s.983-.05.983.974c0 1.004-.983 1.004-.983 1.004h-2.143v-1.977m-.031 4.566h.952c.186 0 .28.052.445.207.135.103.28.3.404.476-.57.073-1.17.104-1.801.104z" fill="#1976d2" stroke-width="1.035"/></symbol><symbol viewBox="0 0 24 24" id="raml" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="razor" xmlns="http://www.w3.org/2000/svg"><path d="M15.45 11.91c-.11-2.21-1.75-3.54-3.73-3.54h-.08c-2.29 0-3.55 1.8-3.55 3.84 0 2.29 1.53 3.74 3.54 3.74 2.25 0 3.72-1.65 3.83-3.59m-3.81-5.97c1.53 0 2.97.68 4.02 1.74 0-.51.33-.89.83-.89h.11c.74 0 .89.7.89.92v7.9c-.04.52.54.78.87.44 1.27-1.29 2.78-6.69-.79-9.81-3.33-2.92-7.8-2.44-10.18-.8-2.52 1.74-4.14 5.61-2.57 9.22 1.71 3.95 6.61 5.13 9.52 3.95 1.48-.59 2.15 1.4.65 2.05-2.34.99-8.77.89-11.78-4.32-2.03-3.52-1.93-9.71 3.46-12.92C10.81 1.42 16.24 2.1 19.5 5.5c3.45 3.6 3.25 10.3-.1 12.91-1.51 1.18-3.76.03-3.74-1.7l-.02-.56a5.611 5.611 0 0 1-3.99 1.66C8.63 17.81 6 15.15 6 12.13c0-3.05 2.63-5.74 5.65-5.74z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="react" xmlns="http://www.w3.org/2000/svg"><path d="M12 10.11c1.03 0 1.87.84 1.87 1.89 0 1-.84 1.85-1.87 1.85-1.03 0-1.87-.85-1.87-1.85 0-1.05.84-1.89 1.87-1.89M7.37 20c.63.38 2.01-.2 3.6-1.7-.52-.59-1.03-1.23-1.51-1.9a22.7 22.7 0 0 1-2.4-.36c-.51 2.14-.32 3.61.31 3.96m.71-5.74l-.29-.51c-.11.29-.22.58-.29.86.27.06.57.11.88.16l-.3-.51m6.54-.76l.81-1.5-.81-1.5c-.3-.53-.62-1-.91-1.47C13.17 9 12.6 9 12 9c-.6 0-1.17 0-1.71.03-.29.47-.61.94-.91 1.47L8.57 12l.81 1.5c.3.53.62 1 .91 1.47.54.03 1.11.03 1.71.03.6 0 1.17 0 1.71-.03.29-.47.61-.94.91-1.47M12 6.78c-.19.22-.39.45-.59.72h1.18c-.2-.27-.4-.5-.59-.72m0 10.44c.19-.22.39-.45.59-.72h-1.18c.2.27.4.5.59.72M16.62 4c-.62-.38-2 .2-3.59 1.7.52.59 1.03 1.23 1.51 1.9.82.08 1.63.2 2.4.36.51-2.14.32-3.61-.32-3.96m-.7 5.74l.29.51c.11-.29.22-.58.29-.86-.27-.06-.57-.11-.88-.16l.3.51m1.45-7.05c1.47.84 1.63 3.05 1.01 5.63 2.54.75 4.37 1.99 4.37 3.68 0 1.69-1.83 2.93-4.37 3.68.62 2.58.46 4.79-1.01 5.63-1.46.84-3.45-.12-5.37-1.95-1.92 1.83-3.91 2.79-5.38 1.95-1.46-.84-1.62-3.05-1-5.63-2.54-.75-4.37-1.99-4.37-3.68 0-1.69 1.83-2.93 4.37-3.68-.62-2.58-.46-4.79 1-5.63 1.47-.84 3.46.12 5.38 1.95 1.92-1.83 3.91-2.79 5.37-1.95M17.08 12c.34.75.64 1.5.89 2.26 2.1-.63 3.28-1.53 3.28-2.26 0-.73-1.18-1.63-3.28-2.26-.25.76-.55 1.51-.89 2.26M6.92 12c-.34-.75-.64-1.5-.89-2.26-2.1.63-3.28 1.53-3.28 2.26 0 .73 1.18 1.63 3.28 2.26.25-.76.55-1.51.89-2.26m9 2.26l-.3.51c.31-.05.61-.1.88-.16-.07-.28-.18-.57-.29-.86l-.29.51m-2.89 4.04c1.59 1.5 2.97 2.08 3.59 1.7.64-.35.83-1.82.32-3.96-.77.16-1.58.28-2.4.36-.48.67-.99 1.31-1.51 1.9M8.08 9.74l.3-.51c-.31.05-.61.1-.88.16.07.28.18.57.29.86l.29-.51m2.89-4.04C9.38 4.2 8 3.62 7.37 4c-.63.35-.82 1.82-.31 3.96a22.7 22.7 0 0 1 2.4-.36c.48-.67.99-1.31 1.51-1.9z" fill="#00bcd4"/></symbol><symbol viewBox="0 0 24 24" id="readme" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="reason" xmlns="http://www.w3.org/2000/svg"><path d="M3 3v18h18V3H3zm5.119 8.993h2.798c.382 0 .71.025.985.075.275.05.534.159.774.326.244.168.435.386.577.654.145.265.218.598.218 1 0 .552-.112 1.001-.335 1.35-.22.348-.536.638-.947.87l2.16 3.203H12.31l-1.763-2.742h-.77v2.742H8.12v-7.478zm6.594 0h4.676v1.447h-3.018v1.29h2.802v1.447h-2.802v1.848h3.018v1.446h-4.676v-7.478zM9.778 13.37v2.014h.513c.266 0 .49-.014.67-.044.18-.03.329-.1.45-.207a.96.96 0 0 0 .253-.34c.055-.128.082-.297.082-.508 0-.187-.034-.35-.1-.483a.698.698 0 0 0-.343-.317 1.086 1.086 0 0 0-.395-.095 6.012 6.012 0 0 0-.526-.02h-.604z" fill="#f44336" stroke-width="1.067"/></symbol><symbol viewBox="0 0 172 193" id="restql" xmlns="http://www.w3.org/2000/svg"><title>Group</title><g transform="translate(14.767 16.713) scale(.82795)" fill="none"><path d="M171.39 55.799c-.975-6.147-4.673-11.642-10.15-14.805L96.381 3.546C93.217 1.72 89.615.756 85.964.756s-7.253.964-10.415 2.788L10.69 40.992A20.896 20.896 0 0 0 .272 59.035v74.89a20.894 20.894 0 0 0 10.416 18.042l64.859 37.446c3.165 1.827 6.767 2.791 10.417 2.791s7.252-.964 10.415-2.79l64.859-37.445c5.479-3.166 9.178-8.66 10.152-14.808zm-16.516 85.147L90.017 178.39a8.104 8.104 0 0 1-8.108 0l-64.857-37.444a8.109 8.109 0 0 1-4.053-7.021v-74.89a8.109 8.109 0 0 1 4.053-7.021l64.857-37.446c1.254-.725 2.654-1.086 4.054-1.086s2.8.361 4.054 1.086l64.857 37.446a8.106 8.106 0 0 1 4.053 7.021v74.89a8.109 8.109 0 0 1-4.053 7.021z" fill="#83e8c2"/><path d="M158.93 59.035a8.109 8.109 0 0 0-4.053-7.021L90.02 14.568c-1.254-.725-2.654-1.086-4.054-1.086s-2.8.361-4.054 1.086L17.055 52.014a8.106 8.106 0 0 0-4.053 7.021v74.89a8.109 8.109 0 0 0 4.053 7.021l64.857 37.444a8.104 8.104 0 0 0 8.108 0l64.857-37.444a8.109 8.109 0 0 0 4.053-7.021zm-46.766 31.681c.119-.069.242-.118.365-.149.044-.012.088-.01.131-.018.076-.012.152-.029.228-.029l.015.001c.02.001.038.005.059.006.093.005.184.019.273.04l.1.03c.077.025.15.057.223.095.028.014.057.027.084.043.094.057.184.122.263.199.007.008.013.017.021.024.07.071.133.15.188.235.018.029.033.059.05.09.04.072.072.148.099.229a1.512 1.512 0 0 1 .081.46v16.209l-3.278 1.893a1.548 1.548 0 0 0-.678.83 1.533 1.533 0 0 0-.098.514v3.785l-14.038 8.104-.01.004a1.55 1.55 0 0 1-.354.146c-.045.012-.09.011-.135.018-.074.012-.15.029-.225.029l-.014-.001c-.02-.001-.039-.005-.059-.006a1.463 1.463 0 0 1-.273-.041c-.034-.008-.066-.019-.1-.03a1.318 1.318 0 0 1-.223-.094c-.029-.015-.057-.027-.084-.044a1.45 1.45 0 0 1-.263-.198c-.009-.008-.015-.019-.023-.027a1.495 1.495 0 0 1-.185-.232c-.019-.029-.034-.06-.051-.09a1.422 1.422 0 0 1-.098-.229 1.702 1.702 0 0 1-.033-.101 1.487 1.487 0 0 1-.048-.358l-.001-.002v-20.053a1.446 1.446 0 0 1 .727-1.255zM85.24 31.369a1.449 1.449 0 0 1 1.452 0l45.741 26.41a1.45 1.45 0 0 1 0 2.512l-17.366 10.027a1.457 1.457 0 0 1-1.452 0l-15.49-8.943 1.727-.996a1.552 1.552 0 0 0 0-2.688l-13.111-7.57c-.239-.139-.508-.207-.775-.207s-.535.068-.775.207l-3.278 1.893-14.038-8.104a1.451 1.451 0 0 1 0-2.513zM57.59 47.558c.251 0 .501.065.726.194l15.489 8.942-1.727.997a1.552 1.552 0 0 0 0 2.688l1.727.996-15.488 8.943a1.457 1.457 0 0 1-1.452 0L39.499 60.291a1.45 1.45 0 0 1 0-2.512l17.366-10.027c.225-.129.475-.194.725-.194zm-9.56 92.328c-.241 0-.489-.062-.724-.196l-17.365-10.026a1.45 1.45 0 0 1-.726-1.256V75.59c0-.847.694-1.453 1.452-1.453.242 0 .49.062.724.197l17.366 10.025c.449.26.726.738.726 1.257v17.886l-1.727-.997a1.552 1.552 0 0 0-2.327 1.344v15.139c0 .555.295 1.067.775 1.344l3.278 1.894v16.209a1.45 1.45 0 0 1-1.452 1.451zm29.828 14.929a1.452 1.452 0 0 1-2.177 1.257l-17.365-10.026a1.452 1.452 0 0 1-.726-1.257v-17.885l1.726.996c.25.145.515.211.773.211.811 0 1.554-.648 1.554-1.555v-1.993l15.489 8.942c.449.26.726.738.726 1.257zm0-32.768c0 .127-.02.246-.049.36-.009.035-.021.067-.032.101-.026.08-.059.157-.099.229-.017.03-.032.061-.05.09a1.48 1.48 0 0 1-.188.235l-.021.025a1.51 1.51 0 0 1-.264.199c-.026.016-.055.028-.082.043a1.597 1.597 0 0 1-.324.124 1.362 1.362 0 0 1-.278.041c-.018.001-.036.006-.055.006l-.015.001c-.077 0-.155-.018-.233-.03-.043-.007-.084-.005-.125-.017a1.484 1.484 0 0 1-.366-.149l-14.035-8.104v-3.784a1.545 1.545 0 0 0-.776-1.343l-3.276-1.892V91.976c0-.127.02-.246.049-.361.009-.034.021-.066.032-.1a1.33 1.33 0 0 1 .099-.229c.017-.03.032-.062.051-.091.054-.084.116-.163.187-.234l.021-.025c.079-.076.168-.142.263-.199.027-.016.056-.029.084-.043a1.476 1.476 0 0 1 .601-.166c.019 0 .036-.005.055-.005l.015-.001c.078 0 .157.018.236.03.04.007.081.005.122.017.124.031.246.08.366.149l17.361 10.023a1.456 1.456 0 0 1 .726 1.259zm-9.984-45.373a1.448 1.448 0 0 1-.544-.55 1.466 1.466 0 0 1 0-1.413c.121-.219.303-.41.544-.55l14.038-8.104 3.277 1.892c.48.276 1.071.276 1.551 0l3.278-1.893 14.038 8.105a1.45 1.45 0 0 1 0 2.513L86.691 86.7a1.447 1.447 0 0 1-1.452 0zm74.842 51.733c0 .518-.276.997-.726 1.256l-45.741 26.409a1.452 1.452 0 0 1-2.177-1.257v-20.053c0-.519.277-.997.727-1.257l15.488-8.941v1.992c0 .906.743 1.555 1.553 1.555.26 0 .523-.066.774-.21l13.11-7.57a1.55 1.55 0 0 0 .776-1.344v-3.784l14.038-8.105a1.452 1.452 0 0 1 2.177 1.257v20.052zm0-32.764c0 .519-.276.997-.726 1.256l-15.489 8.943v-1.993c0-.906-.744-1.554-1.554-1.554a1.519 1.519 0 0 0-.773.21l-1.727.996V85.616c0-.519.277-.997.727-1.257l17.365-10.025c.234-.135.482-.197.724-.197.758 0 1.453.606 1.453 1.453z" fill="#111d5a"/><g fill="#83e8c2"><path d="M59.402 90.568zM94.485 123.06zM94.771 123.29zM77.775 122.51zM77.072 123.33zM77.418 123.09zM77.856 122.05zM76.749 123.45zM94.119 122.41zM77.131 133.51l-15.489-8.942v1.993c0 .906-.743 1.555-1.554 1.555a1.53 1.53 0 0 1-.773-.211l-1.726-.996v17.885c0 .519.276.997.726 1.257l17.365 10.026a1.452 1.452 0 0 0 2.177-1.257v-20.053a1.454 1.454 0 0 0-.726-1.257zM94.25 122.74zM110.28 111.42zM94.494 100.98c.088-.089.189-.168.303-.232l17.365-10.026-17.365 10.026a1.392 1.392 0 0 0-.303.232zM77.627 122.83zM58.027 90.936zM58.374 90.693zM59.044 90.521l-.015.001c.083-.001.167.015.251.029-.079-.012-.158-.03-.236-.03zM57.819 91.195zM58.696 90.568zM57.589 91.977zM76.043 123.46zM57.67 91.516zM75.677 123.31l-14.035-8.11zM76.401 123.5l.015-.001c-.082.001-.166-.016-.248-.029.078.012.156.03.233.03zM112.16 90.716zM77.662 101.27zM113.64 90.734zM96.237 123.31zM113.33 90.597zM112.89 90.52c-.075 0-.151.018-.228.029.081-.014.162-.029.242-.028l-.014-.001zM141.26 74.137c-.241 0-.489.062-.724.197l-17.365 10.025c-.449.26-.727.738-.727 1.257v17.885l1.727-.996c.25-.145.515-.211.773-.21.81 0 1.554.647 1.554 1.554v1.993l15.489-8.943a1.45 1.45 0 0 0 .726-1.256V75.59c0-.847-.695-1.453-1.453-1.453zM112.96 90.526zM95.523 123.5c.074 0 .15-.018.225-.029-.08.013-.159.028-.238.028l.013.001zM95.451 123.5zM85.238 86.7zM95.078 123.43zM141.26 106.9c-.241 0-.489.062-.724.196l-14.038 8.105v3.784c0 .555-.296 1.067-.776 1.344l-13.11 7.57c-.251.144-.515.21-.774.21-.81 0-1.553-.648-1.553-1.555v-1.992l-15.488 8.941c-.449.26-.727.738-.727 1.257v20.053a1.452 1.452 0 0 0 2.177 1.257l45.741-26.409a1.45 1.45 0 0 0 .726-1.256v-20.053a1.454 1.454 0 0 0-1.454-1.452zM67.871 41.396a1.451 1.451 0 0 0 0 2.513l14.038 8.104 3.278-1.893c.24-.139.508-.207.775-.207s.536.068.775.207l13.111 7.57a1.552 1.552 0 0 1 0 2.688l-1.727.996 15.49 8.943a1.457 1.457 0 0 0 1.452 0l17.366-10.027a1.45 1.45 0 0 0 0-2.512l-45.741-26.41a1.449 1.449 0 0 0-1.452 0zM39.497 57.779a1.45 1.45 0 0 0 0 2.512l17.366 10.027a1.457 1.457 0 0 0 1.452 0l15.488-8.943-1.727-.996a1.552 1.552 0 0 1 0-2.688l1.727-.997-15.489-8.942a1.458 1.458 0 0 0-1.451 0zM49.481 138.43v-16.209l-3.278-1.894a1.55 1.55 0 0 1-.775-1.344v-15.139c0-.906.743-1.555 1.554-1.554.259 0 .523.065.773.21l1.727.997V85.611a1.45 1.45 0 0 0-.726-1.257L31.39 74.33a1.436 1.436 0 0 0-.724-.197c-.758 0-1.452.606-1.452 1.453v52.817c0 .518.276.997.726 1.256l17.365 10.026a1.45 1.45 0 0 0 2.176-1.255zM114.34 108.18l-3.278 1.893 3.278-1.893V91.971zM114.11 91.193zM114.16 91.283z"/></g><g fill="#de5941"><path d="M94.494 100.98a1.45 1.45 0 0 0-.424 1.023v20.053l.001.002c0 .126.02.244.048.358.01.034.021.066.033.101.026.08.059.156.098.229.017.03.032.061.051.09.055.084.115.162.185.232.009.009.015.02.023.027.079.077.169.142.263.198.027.017.055.029.084.044a1.46 1.46 0 0 0 .596.165c.02.001.039.005.059.006.079 0 .158-.016.238-.028.045-.007.09-.006.135-.018.119-.031.238-.08.354-.146l.01-.004 14.038-8.104v-3.785c0-.18.04-.35.098-.514.122-.343.353-.643.678-.83l3.278-1.893V91.977c0-.127-.021-.246-.049-.361-.009-.033-.021-.065-.032-.099a1.266 1.266 0 0 0-.099-.229c-.017-.031-.032-.061-.05-.09a1.425 1.425 0 0 0-.188-.235l-.021-.024a1.41 1.41 0 0 0-.263-.199c-.027-.016-.056-.029-.084-.043a1.509 1.509 0 0 0-.323-.125 1.591 1.591 0 0 0-.273-.04c-.021-.001-.039-.005-.059-.006-.08-.001-.161.015-.242.028-.043.008-.087.006-.131.018-.123.031-.246.08-.365.149l-17.365 10.026a1.447 1.447 0 0 0-.302.233zM77.13 100.74L59.769 90.717a1.424 1.424 0 0 0-.366-.149c-.041-.012-.082-.01-.122-.017-.084-.015-.168-.03-.251-.029-.019 0-.036.005-.055.005-.095.005-.188.02-.278.041-.034.009-.065.02-.099.03a1.406 1.406 0 0 0-.224.095c-.028.014-.057.027-.084.043a1.515 1.515 0 0 0-.263.199l-.021.025c-.07.071-.133.15-.187.234-.019.029-.034.061-.051.091-.04.073-.072.149-.099.229a1.463 1.463 0 0 0-.081.461v16.206l3.276 1.892a1.547 1.547 0 0 1 .776 1.343v3.784l14.035 8.104c.119.068.242.117.366.149.041.012.082.01.125.017.082.014.166.03.248.029.019 0 .037-.005.055-.006.095-.004.188-.019.278-.041.034-.008.065-.019.099-.029.077-.025.152-.058.225-.095.027-.015.056-.027.082-.043.095-.058.185-.123.264-.199l.021-.025c.07-.071.133-.15.188-.235.018-.029.033-.06.05-.09.04-.072.072-.149.099-.229a1.448 1.448 0 0 0 .081-.461v-20.047a1.456 1.456 0 0 0-.726-1.259zM86.689 86.7l17.365-10.026a1.45 1.45 0 0 0 0-2.513l-14.038-8.105-3.278 1.893a1.556 1.556 0 0 1-1.551 0l-3.277-1.892-14.038 8.104c-.241.14-.423.331-.544.55a1.466 1.466 0 0 0 0 1.413c.121.218.303.41.544.55L85.238 86.7a1.447 1.447 0 0 0 1.451 0z"/></g></g></symbol><symbol viewBox="0 0 24 24" id="riot" xmlns="http://www.w3.org/2000/svg"><defs><path d="M13.26 3.04l.58.05.54.07.52.09.49.11.46.13.44.14.41.16.39.17.36.19.33.21.32.22.29.23.26.25.22.22.2.22.19.24.17.24.15.25.15.26.12.27.12.28.1.29.08.31.07.31.05.32.04.34.02.35.01.37v.05l-.02.51-.05.49-.09.48-.13.45-.15.43-.19.4-.22.39-.26.37-.28.34-.31.33-.33.3-.37.28-.39.27-.41.24-.44.22L21 21h-7.04l-3.48-5.14H9.17V21H3V3h9.01l.64.01.61.03zm-4.09 8.52h2.66l.99-.11.75-.35.47-.55.16-.74v-.05l-.17-.75-.47-.54-.74-.32-.96-.11H9.17v3.52z" id="ija"/></defs><use xlink:href="#ija" fill="#ff1744"/><use xlink:href="#ija" fill-opacity="0" stroke="#000" stroke-opacity="0"/></symbol><symbol viewBox="0 0 24 24" id="robot" xmlns="http://www.w3.org/2000/svg"><path d="M12.05 2.804a1.787 1.787 0 0 1 1.788 1.788c0 .661-.357 1.242-.893 1.546v1.135h.893a6.256 6.256 0 0 1 6.256 6.256h.894a.894.894 0 0 1 .893.893v2.681a.894.894 0 0 1-.893.894h-.894v.894a1.787 1.787 0 0 1-1.787 1.787H5.795a1.787 1.787 0 0 1-1.787-1.787v-.894h-.894a.894.894 0 0 1-.894-.894v-2.68a.894.894 0 0 1 .894-.894h.894a6.256 6.256 0 0 1 6.255-6.256h.894V6.138a1.773 1.773 0 0 1-.894-1.546 1.787 1.787 0 0 1 1.788-1.788m-4.022 9.83a2.234 2.234 0 0 0-2.234 2.235 2.234 2.234 0 0 0 2.234 2.234 2.234 2.234 0 0 0 2.234-2.234 2.234 2.234 0 0 0-2.234-2.234m8.043 0a2.234 2.234 0 0 0-2.234 2.234 2.234 2.234 0 0 0 2.234 2.234 2.234 2.234 0 0 0 2.235-2.234 2.234 2.234 0 0 0-2.235-2.234z" fill="#ff5722" stroke-width=".894"/></symbol><symbol viewBox="100 100 800 800" id="rollup" xmlns="http://www.w3.org/2000/svg"><style>.ilst0{fill:url(#ilXMLID_4_)}.ilst1{fill:url(#ilXMLID_5_)}.ilst2{fill:url(#ilXMLID_8_)}.ilst3{fill:url(#ilXMLID_9_)}.ilst4{fill:url(#ilXMLID_11_)}.ilst5{opacity:.3;fill:url(#ilXMLID_16_)}</style><g id="ilXMLID_14_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_4_" x1="444.47" x2="598.47" y1="526.05" y2="562.05" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_15_" class="ilst0" d="M721 410c0-33.6-8.8-65.1-24.3-92.4-41.1-42.3-130.5-52.1-152.7-.2-22.8 53.2 38.3 112.4 65 107.7 34-6-6-84-6-84 52 98 40 68-54 158S359 779 345 787c-.6.4-1.2.7-1.9 1h368.7c6.5 0 10.7-6.9 7.8-12.7l-96.4-190.8c-2.1-4.1-.6-9.2 3.4-11.5C683 540.6 721 479.8 721 410z" fill="url(#ilXMLID_4_)"/></g><g id="ilXMLID_2_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_5_" x1="420.38" x2="696.38" y1="475" y2="689" gradientUnits="userSpaceOnUse"><stop stop-color="#BF3338" offset="0"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_10_" class="ilst1" d="M721 410c0-33.6-8.8-65.1-24.3-92.4-41.1-42.3-130.5-52.1-152.7-.2-22.8 53.2 38.3 112.4 65 107.7 34-6-6-84-6-84 52 98 40 68-54 158S359 779 345 787c-.6.4-1.2.7-1.9 1h368.7c6.5 0 10.7-6.9 7.8-12.7l-96.4-190.8c-2.1-4.1-.6-9.2 3.4-11.5C683 540.6 721 479.8 721 410z" fill="url(#ilXMLID_5_)"/></g><linearGradient id="ilXMLID_8_" x1="429.39" x2="469.39" y1="517.16" y2="559.16" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_3_" class="ilst2" d="M329.82 813.46c15.58-8.903 122.41-220.34 227.02-320.5s117.96-66.771 60.094-175.83c0 0-221.46 310.49-301.58 464.06" fill="url(#ilXMLID_8_)" stroke-width="1.113"/><g id="ilXMLID_7_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_9_" x1="502.11" x2="490.11" y1="589.46" y2="417.46" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_12_" class="ilst3" d="M373 537c134.4-247.1 152-272 222-272 36.8 0 73.9 16.6 97.9 46.1-32.7-52.7-90.6-88-156.9-89H307.7c-4.8 0-8.7 3.9-8.7 8.7V691c13.6-35.1 36.7-85.3 74-154z" fill="url(#ilXMLID_9_)"/></g><linearGradient id="ilXMLID_11_" x1="450.12" x2="506.94" y1="514.21" y2="552.85" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FBB040" offset="0"/><stop stop-color="#FB8840" offset="1"/></linearGradient><path id="ilXMLID_6_" class="ilst4" d="M556.84 492.96c-104.61 100.16-211.44 311.6-227.02 320.5s-41.732 10.016-55.643-5.564c-14.801-16.582-37.837-43.401 86.802-272.65 149.57-274.99 169.15-302.7 247.05-302.7 40.953 0 82.24 18.473 108.95 51.302 1.447 2.337 2.893 4.785 4.34 7.233-45.738-47.074-145.23-57.98-169.93-.222-25.373 59.204 42.622 125.08 72.335 119.85 37.837-6.677-6.677-93.48-6.677-93.48 57.757 108.95 44.403 75.563-60.205 175.72z" fill="url(#ilXMLID_11_)" stroke-width="1.113"/><linearGradient id="ilXMLID_16_" x1="508.33" x2="450.33" y1="295.76" y2="933.76" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFF" offset="0"/><stop stop-color="#FFF" stop-opacity="0" offset="1"/></linearGradient><path id="ilXMLID_13_" class="ilst5" d="M373.22 547.49c149.57-274.99 169.15-302.7 247.05-302.7 33.719 0 67.661 12.575 93.48 35.277-26.708-30.492-66.326-47.519-105.72-47.519-77.9 0-97.486 27.71-247.05 302.7-124.64 229.25-101.6 256.07-86.802 272.65 2.114 2.337 4.563 4.34 7.122 6.01-13.02-18.919-18.807-62.877 91.922-266.42z" fill="url(#ilXMLID_16_)" opacity=".3" stroke-width="1.113"/></symbol><symbol viewBox="0 0 24 24" id="ruby" xmlns="http://www.w3.org/2000/svg"><path d="M16 9h3l-5 7m-4-7h4l-2 8M5 9h3l2 7m5-12h2l2 3h-3m-5-3h2l1 3h-4M7 4h2L8 7H5m1-5L2 8l10 14L22 8l-4-6H6z" fill="#f44336"/></symbol><symbol viewBox="0 0 144 144" id="rust" xmlns="http://www.w3.org/2000/svg"><path d="M68.252 26.206a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0M25.766 58.451a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m84.97.166a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m-74.661 4.88a3.252 3.252 0 0 0 1.651-4.29l-1.58-3.574h6.214v28.01H29.823a43.847 43.847 0 0 1-1.42-16.738zm25.994.688v-8.256h14.798c.764 0 5.397.883 5.397 4.347 0 2.877-3.553 3.908-6.475 3.908zm-20.203 44.452a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m52.769.166a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m1.101-8.076a3.246 3.246 0 0 0-3.856 2.498l-1.787 8.342a43.847 43.847 0 0 1-36.566-.175l-1.787-8.342a3.246 3.246 0 0 0-3.854-2.497l-7.365 1.581a43.847 43.847 0 0 1-3.808-4.488h35.834c.406 0 .676-.074.676-.443V84.527c0-.369-.27-.442-.676-.442h-10.48V76.05h11.335c1.035 0 5.532.296 6.97 6.045.45 1.768 1.44 7.519 2.116 9.36.674 2.065 3.417 6.19 6.34 6.19h18.501a43.847 43.847 0 0 1-4.06 4.7zm19.898-33.468a43.847 43.847 0 0 1 .093 7.612h-4.499c-.45 0-.631.296-.631.737v2.066c0 4.863-2.742 5.92-5.145 6.19-2.288.258-4.825-.958-5.138-2.358-1.35-7.593-3.6-9.214-7.152-12.016 4.409-2.8 8.996-6.93 8.996-12.457 0-5.97-4.092-9.729-6.881-11.572-3.914-2.58-8.246-3.096-9.415-3.096H39.336A43.847 43.847 0 0 1 63.867 28.52l5.484 5.753a3.243 3.243 0 0 0 4.59.105l6.137-5.869a43.847 43.847 0 0 1 30.017 21.38l-4.201 9.487a3.256 3.256 0 0 0 1.652 4.29zm10.477.154l-.143-1.467 4.327-4.036c.88-.82.55-2.472-.574-2.891l-5.532-2.068-.433-1.428 3.45-4.792c.704-.974.058-2.53-1.127-2.724l-5.833-.949-.7-1.31 2.45-5.38c.502-1.095-.43-2.496-1.636-2.45l-5.92.206-.935-1.135 1.36-5.766c.275-1.17-.913-2.36-2.084-2.085l-5.765 1.359-1.136-.935.207-5.92c.046-1.198-1.357-2.135-2.45-1.637l-5.379 2.452-1.31-.703-.95-5.833c-.193-1.183-1.75-1.83-2.723-1.128l-4.796 3.45-1.425-.432-2.068-5.532c-.42-1.127-2.072-1.452-2.89-.576l-4.036 4.33-1.467-.143-3.117-5.036c-.63-1.02-2.318-1.02-2.946 0l-3.117 5.036-1.467.143-4.037-4.33c-.819-.876-2.47-.551-2.89.576l-2.069 5.532-1.426.432-4.795-3.45c-.974-.703-2.53-.055-2.723 1.128l-.951 5.833-1.31.703-5.379-2.452c-1.093-.5-2.496.439-2.45 1.637l.206 5.92-1.136.935-5.765-1.36c-1.171-.272-2.36.915-2.086 2.086l1.358 5.766-.933 1.135-5.92-.206c-1.193-.035-2.134 1.355-1.637 2.45l2.453 5.38-.703 1.31-5.832.949c-1.185.192-1.827 1.75-1.128 2.724l3.45 4.792-.433 1.428-5.532 2.068c-1.123.42-1.452 2.07-.574 2.891l4.328 4.036-.143 1.467-5.035 3.116c-1.02.63-1.02 2.318 0 2.946l5.035 3.117.143 1.467-4.328 4.037c-.878.818-.549 2.468.574 2.89l5.532 2.068.433 1.428-3.45 4.793c-.701.976-.056 2.532 1.129 2.723l5.831.948.703 1.312-2.453 5.378c-.5 1.093.444 2.5 1.638 2.451l5.917-.207.935 1.136-1.358 5.768c-.275 1.168.915 2.355 2.086 2.08l5.765-1.357 1.137.932-.207 5.921c-.046 1.199 1.357 2.136 2.45 1.636l5.379-2.45 1.31.702.95 5.83c.193 1.187 1.75 1.829 2.725 1.13l4.792-3.453 1.427.435 2.069 5.53c.42 1.123 2.072 1.454 2.89.574l4.037-4.328 1.467.146 3.117 5.035c.628 1.016 2.316 1.018 2.946 0l3.117-5.035 1.467-.146 4.036 4.328c.818.88 2.47.549 2.89-.574l2.068-5.53 1.428-.435 4.793 3.453c.974.699 2.53.055 2.722-1.13l.952-5.83 1.31-.703 5.378 2.451c1.093.5 2.493-.435 2.45-1.636l-.206-5.92 1.135-.933 5.765 1.357c1.171.275 2.36-.912 2.085-2.08l-1.358-5.768.932-1.136 5.92.207c1.194.048 2.138-1.358 1.636-2.451l-2.45-5.378.7-1.312 5.833-.948c1.187-.19 1.831-1.747 1.127-2.723l-3.45-4.793.433-1.428 5.532-2.068c1.125-.422 1.454-2.072.574-2.89l-4.327-4.037.143-1.467 5.035-3.117c1.02-.628 1.021-2.315.001-2.946z" fill="#ff7043" stroke-width="1.146"/></symbol><symbol viewBox="0 0 500 500" id="sass" xmlns="http://www.w3.org/2000/svg"><path d="M422.676 96.573c-12.192-47.839-91.508-63.557-166.575-36.892-44.68 15.877-93.029 40.786-127.81 73.311-41.349 38.675-47.943 72.328-45.216 86.395 9.583 49.622 77.585 82.069 105.535 106.126v.144c-8.246 4.05-68.565 34.584-82.684 65.799-14.893 32.932 2.372 56.556 13.804 59.742 35.424 9.859 71.764-7.866 91.311-37.01 18.853-28.12 17.28-64.422 9.086-82.487 11.3-2.976 24.476-4.314 41.218-2.36 47.248 5.52 56.517 35.017 54.747 47.366-1.77 12.35-11.681 19.14-14.998 21.186-3.317 2.045-4.326 2.766-4.05 4.287.405 2.215 1.94 2.137 4.758 1.652 3.894-.656 24.804-10.042 25.709-32.828 1.14-28.933-26.587-61.302-75.684-60.45-20.216.354-32.933 2.268-42.123 5.69-.681-.774-1.363-1.547-2.084-2.307-30.35-32.382-86.46-55.285-84.088-98.824.866-15.823 6.372-57.5 107.817-108.052 83.104-41.415 149.637-30.009 161.135-4.76 16.427 36.08-35.554 103.137-121.858 112.812-32.88 3.684-50.198-9.059-54.498-13.804-4.536-4.995-5.204-5.218-6.909-4.287-2.753 1.533-1.01 5.938 0 8.574 2.583 6.712 13.15 18.603 31.176 24.515 15.863 5.205 54.459 8.063 101.156-9.99 52.283-20.255 93.12-76.523 81.125-123.548zM200.213 340.34c3.92 14.5 3.487 28.016-.564 40.248a65.289 65.289 0 0 1-3.225 7.97c-3.12 6.477-7.316 12.534-12.442 18.132-15.653 17.069-37.507 23.532-46.88 18.092-10.122-5.874-5.048-29.944 13.083-49.11 19.52-20.636 47.602-33.903 47.602-33.903l-.039-.079 2.465-1.35z" fill="#ec407a" stroke="#ec407a" stroke-width="16.286552999999998"/></symbol><symbol viewBox="0 0 300 300" id="sbt" xmlns="http://www.w3.org/2000/svg"><path d="M105.46 209.517c-7.875 0-13.452-7.521-13.452-15.37v-.327c0-7.848 5.578-13.735 13.452-13.735h164.05c1.476-4.905 2.625-11.446 3.281-17.986h-137.81c-7.875 0-14.273-6.05-14.273-13.898s6.398-13.898 14.273-13.898h137.31c-.82-6.54-1.969-13.081-3.773-17.986h-104.01c-7.875 0-14.273-6.05-14.273-13.898s6.398-13.898 14.273-13.898h91.87c-21.327-37.607-60.864-61.315-106.14-61.315-67.918 0-123.04 54.448-123.04 122.3 0 67.856 55.122 123.28 123.04 123.28 46.59 0 87.112-25.507 107.95-63.114h-152.73z" fill="#0277bd" stroke-width="1.638"/></symbol><symbol viewBox="0 0 256 256" id="scala" xmlns="http://www.w3.org/2000/svg"><path fill="#f44336" fill-rule="evenodd" stroke-width=".3" d="M59.607 50.647l149.097-21.982v49.488L59.607 100.135zM59.593 114.08L208.69 92.098v49.488L59.593 163.568zM59.587 177.358l149.097-21.982v49.488L59.587 226.846z"/><path fill="#f44336" fill-rule="evenodd" stroke-width=".3" d="M62.425 91.414l95.605 30.923-2.832 8.757-95.605-30.922zM113.084 61.13l95.604 30.922-2.832 8.757-95.605-30.922zM62.425 154.79l95.605 30.922-2.833 8.758-95.604-30.923zM113.097 124.408l95.604 30.923-2.832 8.757-95.605-30.922z"/></symbol><symbol viewBox="0 0 24 24" id="settings" xmlns="http://www.w3.org/2000/svg"><path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97 0-.33-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1 0 .33.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.66z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="shaderlab" xmlns="http://www.w3.org/2000/svg"><path d="M9.11 17H6.5l-4.91-5L6.5 7h2.61l1.31-2.26L17.21 3l1.87 6.74L17.77 12l1.31 2.26L17.21 21l-6.79-1.74L9.11 17m.14-.25l5.13 1.38L11.42 13H5.5l3.75 3.75m6.87.38L17.5 12l-1.38-5.13L13.15 12l2.97 5.13M9.25 7.25L5.5 11h5.92l2.96-5.13-5.13 1.38z" fill="#1976d2"/></symbol><symbol viewBox="0 0 24 24" id="slim" xmlns="http://www.w3.org/2000/svg"><path d="M6.959 2.5a4.605 4.605 0 0 0-4.615 4.615v9.957a4.605 4.605 0 0 0 4.615 4.615h9.957a4.605 4.605 0 0 0 4.615-4.615V7.115A4.605 4.605 0 0 0 16.916 2.5zm4.938 2.691a6.811 6.811 0 0 1 6.81 6.813H13.43L9.938 7.287l.699 4.717H5.086a6.811 6.811 0 0 1 6.81-6.813z" fill="#f57f17"/></symbol><symbol id="smarty" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.iust0{fill:#ffce00}</style><path class="iust0" d="M9.14 20.606c0 .556.398.953.954.953h3.812c.556 0 .953-.397.953-.953v-.953H9.141zM12 2.5c-3.653 0-6.671 3.018-6.671 6.671 0 2.303 1.112 4.289 2.859 5.48v2.144c0 .556.397.953.953.953h5.718c.556 0 .953-.397.953-.953V14.65c1.747-1.191 2.86-3.177 2.86-5.48 0-3.653-3.019-6.671-6.672-6.671zm2.7 10.563l-.794.555v2.224h-3.812v-2.224l-.794-.555A4.712 4.712 0 0 1 7.235 9.17 4.78 4.78 0 0 1 12 4.405a4.78 4.78 0 0 1 4.765 4.765 4.712 4.712 0 0 1-2.065 3.892z"/></symbol><symbol viewBox="0 0 200 200" id="snyk" xmlns="http://www.w3.org/2000/svg"><title>Group 2</title><g transform="translate(15.255 18.22) scale(1.8477)" fill="none" fill-rule="evenodd"><path d="M65.161 24.997c-1.656 5.974-5.255 23.587-5.255 23.587s-6.618-2.464-14.148-2.476h-.055c-.413.002-.822.012-1.23.026v41.649h6.677v.003h5.815v-.003h20.858c.111-8.177-2.036-27.066-2.036-27.066-1.088-2.279.46-7.668.46-7.668-8.869-9.092-11.086-28.051-11.086-28.051zm-3.357 43.958c5.476 0 1.381 4.64.9 5.168H52.35c.944-1.18 4.504-5.168 9.453-5.168z" fill="#607d8b" stroke-width="1.6"/><path d="M26.366 24.995s-2.217 18.961-11.087 28.053c0 0 1.548 5.391.46 7.669 0 0-2.15 18.895-2.038 27.066h19.273v.003h7.079v-.003h5.744V46.107h-.025c-7.532.013-14.151 2.478-14.151 2.478s-3.6-17.615-5.255-23.59zm3.264 43.96c4.95 0 8.51 3.987 9.452 5.168H28.73c-.479-.528-4.573-5.168.9-5.168z" fill="#90a4ae" stroke-width="1.6"/><g transform="translate(23.76 77.45) scale(1.5998)"><g transform="translate(17.526)"><path d="M7.357.06H.177v.075C.177 2.64 2.345 4.67 4.89 4.67 7.431 4.67 9.6 2.64 9.6.135V.059z" fill="#455a64"/><path d="M1.972.06v.075a2.692 2.692 0 1 0 5.386 0V.059z" fill="#fff"/><path d="M5.496.06H4.234c-.012 0-.023.005-.034.007.157.033.243.388.21.624a.721.721 0 0 1-.71.617c.102.471.487.85.997.922a1.188 1.188 0 0 0 1.35-1.007C6.112.743 5.881.06 5.495.06z" fill="#37474f"/></g><path d="M7.552.06H.372v.075c0 2.505 2.17 4.535 4.712 4.535 2.544 0 4.712-2.03 4.712-4.535V.059z" fill="#455a64"/><path d="M2.168.06v.075a2.692 2.692 0 1 0 5.385 0V.059z" fill="#fff"/><path d="M5.692.06H4.428c-.01 0-.022.005-.032.007.156.033.242.388.21.624a.72.72 0 0 1-.712.617c.104.471.488.85.999.922A1.187 1.187 0 0 0 6.24 1.223C6.308.743 6.078.06 5.69.06z" fill="#37474f"/></g><path d="M25.514-.27l-4.202 7.697C19.838 10.17 6.858 34.465 6.858 43.243v.516L12.8 59.573c-.8 7.258-2.203 21.643-1.78 28.21h5.73c-.354-3.787.648-17.008 1.903-28.25l.076-.677-1.075-2.892c3.694-3.868 6.285-9.193 8.073-14.261l.174 1.235 5.869 9.629 2.291-.983c.058-.024 5.935-2.523 11.643-2.523 5.672 0 11.646 2.5 11.702 2.525l2.29.976 5.86-9.626.23-1.608c1.769 5.117 4.358 10.536 8.07 14.49l-1.127 3.035.076.678c1.259 11.286 2.266 24.564 1.916 28.252h5.677c.406-6.567-1.05-20.952-1.848-28.208l5.838-15.817v-.514c0-8.779-12.876-33.074-14.347-35.816L65.923-.27l-5.897 41.229-2.723 4.478c-2.628-.882-7.1-2.11-11.603-2.11-4.498 0-8.94 1.225-11.557 2.108l-2.722-4.476-2.07-14.452a.832.832 0 0 0 .006-.071l-.016-.004zm-3.166 18.39l1.206 8.407c-.46 3.143-2.561 15.47-8.198 23.24l-2.598-6.99c.325-4.554 5.067-15.462 9.59-24.656zm46.763 0c4.523 9.194 9.267 20.104 9.592 24.657L76.166 49.6c-6.09-8.553-8-22.459-8.166-23.73z" fill="#607d8b" stroke-width="1.6"/></g></symbol><symbol viewBox="0 0 24 24" id="solidity" xmlns="http://www.w3.org/2000/svg"><path d="M5.8 14.05l6.253 8.61 6.252-8.61-6.254 3.807z" fill="#0288d1" stroke-width="4.553" stroke-linejoin="round"/><path d="M12.051 1.347L5.8 11.833l6.252 3.807 6.254-3.807z" fill="#0288d1" stroke-width="5.025" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 120 120" id="sonar" xmlns="http://www.w3.org/2000/svg"><style>.a,.b{fill:#fff}.b{stroke:#fff;stroke-miterlimit:10}</style><path d="M115.45 23.033S97.961 33.27 97.534 33.412c-.427.284-.852.57-1.137.854-1.422 1.421-1.848 3.41-1.422 5.26.285.852.711 1.849 1.422 2.56.711.71 1.564 1.137 2.559 1.422 1.848.426 3.84 0 5.262-1.422.426-.427.709-.853.851-1.28l.143-.427 2.56-4.692zm-39.102 9.242c-27.441 0-31.99 13.08-31.99 29.29 0 3.838.569 7.962-1.99 11.942-3.84 5.972-8.957 5.828-10.236 5.828-1.706 0-7.962-.993-8.246-2.841h.994c6.682 0 11.658-5.404 11.658-12.655v-2.56h-5.686c-4.123 0-7.82 1.849-10.238 5.12-2.417-3.271-6.113-5.12-10.236-5.12h-5.83v2.56c0 7.11 5.688 12.795 12.797 12.795h1.848c0 4.124 5.687 20.332 47.63 20.332 16.352 0 40.665-2.843 40.665-33.697 0-5.829-1.848-11.23-4.691-15.78-.996.284-1.992.568-3.13.568a8.92 8.92 0 0 1-8.956-8.957c0-.995.141-1.991.425-2.986-4.265-2.702-8.53-3.838-14.787-3.838z" fill="#1e88e5" stroke-width="1.422"/></symbol><symbol viewBox="0 0 412 395" id="stylelint" xmlns="http://www.w3.org/2000/svg"><title>stylelint-icon-white</title><g transform="translate(31.478 29.499) scale(.84775)" fill="#cfd8dc" fill-rule="evenodd"><path d="M208.8 393.05c45.057-161.12 43.75-161.85 76.32-276.73l7.832 4.523c4.255 2.458 7.738.448 7.738-4.455V61.602c8.643-30.27 15.416-53.66 17.4-60.693h35.287l58.618 54.304-38.498 33.27 29.11 31.473-191.86 273.09c-.938 1.542-2.244 1.19-1.947 0zm20.96-347.28c1.733 0 3.148.958 3.148 2.147v28.077c0 1.186-1.415 2.15-3.147 2.15h-47.396c-1.742 0-3.153-.96-3.153-2.15V47.917c0-1.185 1.41-2.147 3.153-2.147h47.396z"/><path d="M288.26 14.688l-52.14 30.1c.605.92.973 1.98.973 3.136v28.078c0 1.457-.565 2.77-1.496 3.83l52.663 30.402c3.59 2.073 6.535.377 6.535-3.764V18.456c0-4.145-2.944-5.836-6.535-3.768zM175.02 76V47.923c0-1.15.368-2.21.966-3.13l-52.14-30.105c-3.588-2.068-6.53-.376-6.53 3.768v88.013c0 4.14 2.938 5.84 6.53 3.76l52.66-30.405c-.926-1.06-1.487-2.37-1.487-3.827z"/><path d="M201.25 393.05h1.947c-45.05-161.12-43.753-161.85-76.32-276.73l-7.833 4.523c-4.253 2.458-7.737.448-7.737-4.455V61.602C102.662 31.332 95.892 7.942 93.902.909H58.619L.002 55.213l38.494 33.27-29.11 31.473z"/><circle cx="204.57" cy="122.54" r="14.231"/><circle cx="204.57" cy="207.16" r="14.231"/><circle cx="204.57" cy="291.78" r="14.23"/></g></symbol><symbol viewBox="0 0 412 395" id="stylelint_light" xmlns="http://www.w3.org/2000/svg"><title>stylelint-icon-black</title><g transform="translate(31.478 29.499) scale(.84775)" fill="#546e7a" fill-rule="evenodd"><path d="M208.8 393.05c45.057-161.12 43.75-161.85 76.32-276.73l7.832 4.523c4.255 2.458 7.738.448 7.738-4.455V61.602c8.643-30.27 15.416-53.66 17.4-60.693h35.287l58.618 54.304-38.498 33.27 29.11 31.473-191.86 273.09c-.938 1.542-2.244 1.19-1.947 0zm20.96-347.28c1.733 0 3.148.958 3.148 2.147v28.077c0 1.186-1.415 2.15-3.147 2.15h-47.396c-1.742 0-3.153-.96-3.153-2.15V47.917c0-1.185 1.41-2.147 3.153-2.147h47.396z"/><path d="M288.26 14.688l-52.14 30.1c.605.92.973 1.98.973 3.136v28.078c0 1.457-.565 2.77-1.496 3.83l52.663 30.402c3.59 2.073 6.535.377 6.535-3.764V18.456c0-4.145-2.944-5.836-6.535-3.768zM175.02 76V47.923c0-1.15.368-2.21.966-3.13l-52.14-30.105c-3.588-2.068-6.53-.376-6.53 3.768v88.013c0 4.14 2.938 5.84 6.53 3.76l52.66-30.405c-.926-1.06-1.487-2.37-1.487-3.827z"/><path d="M201.25 393.05h1.947c-45.05-161.12-43.753-161.85-76.32-276.73l-7.833 4.523c-4.253 2.458-7.737.448-7.737-4.455V61.602C102.662 31.332 95.892 7.942 93.902.909H58.619L.002 55.213l38.494 33.27-29.11 31.473z"/><circle cx="204.57" cy="122.54" r="14.231"/><circle cx="204.57" cy="207.16" r="14.231"/><circle cx="204.57" cy="291.78" r="14.23"/></g></symbol><symbol viewBox="0 0 200.00001 200.00001" id="stylus" xmlns="http://www.w3.org/2000/svg"><path d="M126.814 155.9c14.64-17.51 16.362-35.595 5.024-69.18-7.177-21.24-19.09-37.602-10.334-50.807 9.329-14.065 29.135-.43 12.63 18.371l3.301 2.297c19.806 2.296 29.566-24.83 14.783-32.58C113.179 3.621 79.02 42.803 94.09 88.156c6.458 19.232 15.5 39.613 8.18 55.83-6.314 13.923-18.514 22.103-26.695 22.39-17.079.862-5.74-38.32 13.922-48.08 1.722-.861 4.162-2.01 1.866-4.88-24.256-2.727-38.464 8.468-46.645 24.112-23.825 45.497 45.21 62.29 82.095 18.371z" fill="#c0ca33" stroke-width="1.435"/></symbol><symbol viewBox="0 0 24 24" id="swc" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="jba"><stop offset="0" stop-color="#791223"/><stop offset="1" stop-color="#d92f3c"/></linearGradient><linearGradient xlink:href="#jba" id="jbb" x1="12.356" y1="21.559" x2="12.356" y2="2.949" gradientUnits="userSpaceOnUse"/></defs><path d="M6 3c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6 3 6.5V19a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6.5c0-.5-.17-.93-.46-1.27l-1.39-1.68C18.88 3.21 18.47 3 18 3H6zm-.07 1h12l.94 1H5.12l.81-1z" fill="url(#jbb)"/><path style="line-height:125%" d="M11.053 11.918h-.008c-.244.022-.475.054-.676.11a2.9 2.9 0 0 0-.856.412 3.399 3.399 0 0 0-.67.683 9.36 9.36 0 0 0-.586.95c-.07.131-.134.244-.201.365v.001h-.002l-.768 1.372-.003-.001c-.136.253-.264.485-.38.686-.123.212-.26.39-.411.539a1.599 1.599 0 0 1-.52.34c-.04.016-.092.024-.138.036h-.567v1.383H5.834v-.001c.245-.02.477-.053.679-.11a2.9 2.9 0 0 0 .856-.411c.245-.185.469-.413.67-.683.195-.275.39-.591.585-.95.07-.131.135-.244.202-.366l.004.001.002-.002.02-.038H10.948v-1.378h-.19v-.001H9.624c.125-.234.246-.452.355-.64.123-.21.259-.39.41-.538.152-.148.325-.26.52-.34.04-.015.091-.024.136-.035h.57V13.3h-.002v-1.381h-.56v-.001z" font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#fff"/></symbol><symbol viewBox="0 0 24 24" id="swift" xmlns="http://www.w3.org/2000/svg"><path d="M17.09 19.72c-2.36 1.36-5.59 1.5-8.86.1A13.807 13.807 0 0 1 2 14.5c.67.55 1.46 1 2.3 1.4 3.37 1.57 6.73 1.46 9.1 0-3.37-2.59-6.24-5.96-8.37-8.71-.45-.45-.78-1.01-1.12-1.51 8.28 6.05 7.92 7.59 2.41-1.01 4.89 4.94 9.43 7.74 9.43 7.74.16.09.25.16.36.22.1-.25.19-.51.26-.78.79-2.85-.11-6.12-2.08-8.81 4.55 2.75 7.25 7.91 6.12 12.24-.03.11-.06.22-.05.39 2.24 2.83 1.64 5.78 1.35 5.22-1.21-2.39-3.48-1.65-4.62-1.17z" fill="#fe5e2f"/></symbol><symbol viewBox="0 0 24 24" id="table" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5m4 7.5h-4v2h1l-2 1.67L10 13h1v-2H7v2h1l3 2.5L8 18H7v2h4v-2h-1l2-1.67L14 18h-1v2h4v-2h-1l-3-2.5 3-2.5h1v-2z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 200 200" id="terraform" xmlns="http://www.w3.org/2000/svg"><g transform="translate(177.03 -58.705) scale(.92881)" fill="#5c6bc0" stroke="#b0aff5" stroke-linejoin="round"><g stroke-width=".288"><path transform="skewY(26.439) scale(.89541 1)" d="M-203.8 170.95h64.714v51.88H-203.8zM-124.37 171.04h64.714v51.88h-64.714zM-124.37 236.09h64.714v51.88h-64.714z"/></g><path transform="skewY(-22.59) scale(-.92328 1)" stroke-width=".284" d="M-19.172 128.27h62.76v51.88h-62.76z"/></g></symbol><symbol viewBox="0 0 24 24" id="test-js" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="test-jsx" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#00bcd4"/></symbol><symbol viewBox="0 0 24 24" id="test-ts" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#0288d1"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="tex" xmlns="http://www.w3.org/2000/svg"><g font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#42a5f5" stroke-linejoin="miter"><text style="line-height:125%" x="9.914" y="364.919"><tspan x="9.914" y="364.919" font-size="287.5">T</tspan></text><text style="line-height:125%" x="136.374" y="435.558"><tspan x="136.374" y="435.558" font-size="287.5">E</tspan></text><text style="line-height:125%" x="307.819" y="361.201"><tspan x="307.819" y="361.201" font-size="287.5">X</tspan></text></g></symbol><symbol viewBox="0 0 24 24" id="todo" xmlns="http://www.w3.org/2000/svg"><path d="M3 5h6v6H3V5m2 2v2h2V7H5m6 0h10v2H11V7m0 8h10v2H11v-2m-6 5l-3.5-3.5 1.41-1.41L5 17.17l4.59-4.58L11 14l-6 6z" fill="#42a5f5"/></symbol><symbol id="travis" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><style id="jkstyle2">.jkst0{fill:#cb3349}.jkst1{fill:#f4edae}.jkst2{fill:#e6ccad}.jkst3{fill:#656c67}.jkst4{fill:#e5caa3}.jkst5{fill:#c7b39a}.jkst6{fill:#ebd599}.jkst7{fill:#2d3136}.jkst8{fill:#edf6fa}.jkst9{opacity:.8}.jkst10{opacity:.75;fill:#ebd599}</style><g id="jkg99" transform="translate(11.017 12.484) scale(.8858)"><g id="jkg10"><path class="jkst0" d="M47.781 86.572s-31.118 21.903-32.335 30.247l2.335-.48S55.045 91.64 84.584 88.628l.669-3.749z" id="jkpath4" fill="#cb3349"/><path class="jkst0" d="M96.629 83.442l-24.511 17.385 1.325 1.063c.999-.806 43.539-13.798 43.539-13.798l8.969-5.623c-6.018.749-29.322.973-29.322.973z" id="jkpath6" fill="#cb3349"/><path class="jkst0" d="M117.932 104.469c17.405 0 43.495-17.046 43.495-17.046l-8.434-1.605c-.417.417-13.6-.462-13.6-.462l-6.258-1.738-14.951 17.036-1.217 2.956c1.075-.437.965.859.965.859z" id="jkpath8" fill="#cb3349"/></g><path class="jkst0" d="M174.728 158.832l-5.377 1.514-24.843-.537-15.541-12.085-18.784 4.7-21.726-1.88-12.166 13.294-22.828 6.819-11.398-3.534-.574-.494 5.116 12.527s11.588 12.424 18.061 13.885c6.472 1.461 18.165-.105 26.935-1.463 8.769-1.357 15.764-4.489 18.582-9.603 2.818-5.117 3.236-6.578 3.236-6.578s8.353 11.797 15.556 13.155c7.203 1.357 28.605-5.952 28.605-5.952s13.051-3.549 15.346-8.038c2.297-4.489 8.353-19.209 8.353-19.209zM44.456 169.038l-.361-.166-2.013-1.736z" id="jkpath12" fill="#cb3349"/><g id="jkg97"><path class="jkst1" d="M195.832 70.085a48.125 48.125 0 0 0-.21-2.009 26.472 26.472 0 0 0-.215-1.424c-1.793-1.509-3.831-2.851-5.952-4.071-2.299-1.343-4.704-2.546-7.159-3.663-2.438-1.15-4.942-2.191-7.461-3.207a134.313 134.313 0 0 0-3.798-1.477c-1.269-.495-2.55-.956-3.835-1.424 2.697.447 5.366 1.059 8.015 1.741 1.723.446 3.437.945 5.14 1.477-12.112-31.655-41.07-52.27-72.687-52.27-31.622 0-60.577 20.615-72.686 52.27a109.044 109.044 0 0 1 5.137-1.477c2.653-.682 5.323-1.294 8.018-1.741-1.289.468-2.567.929-3.84 1.424-1.267.472-2.536.967-3.798 1.477-2.519 1.016-5.016 2.057-7.46 3.207-2.45 1.117-4.857 2.32-7.156 3.663-2.121 1.219-4.157 2.562-5.957 4.071-.075.457-.151.951-.21 1.424a51.768 51.768 0 0 0-.21 2.009 51.354 51.354 0 0 0-.177 4.061 59.216 59.216 0 0 0 .5 8.11c.37 2.692.864 5.366 1.595 7.951.36 1.295.768 2.572 1.24 3.808.237.617.495 1.225.764 1.816.134.294.274.585.413.864l.172.328c.199.101.408.204.607.3l1.204.575c.671.305 1.6.746 2.368 1.09.043-.037.086-.075.123-.114l-2.235-8.513c.474-.13 4.718-1.225 12.032-2.617a38.816 38.816 0 0 1-1.772-.381c-1.665-.414-3.309-.919-4.899-1.564a22.415 22.415 0 0 1-2.309-1.115c-.742-.426-1.472-.908-2.037-1.548 8.036 2.622 24.64 1.434 39.399-.091 13.499-1.391 27.029-2.293 40.63-2.32 13.602.027 27.137.929 40.63 2.32 14.766 1.525 31.37 2.713 39.405.091-.564.64-1.293 1.123-2.035 1.548a22.5 22.5 0 0 1-2.308 1.115c-1.592.645-3.234 1.15-4.899 1.564-.247.059-.496.113-.743.166 8.02 1.488 12.689 2.697 13.188 2.831l-2.138 8.11c.43-.194.864-.381 1.29-.574l1.202-.575c.2-.097.403-.199.607-.3l.166-.328c.146-.279.286-.57.419-.864.27-.591.528-1.199.764-1.816a42.235 42.235 0 0 0 1.241-3.808c.731-2.585 1.225-5.259 1.595-7.951.345-2.685.526-5.398.501-8.11a50.874 50.874 0 0 0-.179-4.059z" id="jkpath14" fill="#f4edae"/><path class="jkst2" d="M116.787 182.661c-1.064.16-2.128.295-3.186.375-.682.033-1.404.102-2.059.102l-.242.005c.822-1.837 1.446-3.26 1.919-4.339.963 1.08 2.188 2.417 3.568 3.857z" id="jkpath16" fill="#e6ccad"/><path class="jkst2" d="M119.101 185.018c3.304 3.272 7.398 5.146 11.904 5.479-7.569 3.074-14.702 4.26-20.197 4.63-5.478.367-11.032-.279-16.474-1.771.456-.082.79-.14 1.193-.189.447-.054 10.206-1.327 14.605-7.868l.413.009 1.08-.009c.731 0 1.395-.06 2.094-.087a43.69 43.69 0 0 0 4.878-.703c.167.171.333.338.504.509z" id="jkpath18" fill="#e6ccad"/><path class="jkst3" d="M128.464 87.071a98.82 98.82 0 0 1-1.048 1.343c-1.933 2.444-4.614 5.57-7.794 8.627a369.585 369.585 0 0 0-11.404-.177c-6.46 0-12.655.171-18.537.457 8.311-3.449 18.296-6.818 29.109-8.842a113.323 113.323 0 0 1 9.674-1.408z" id="jkpath20" fill="#656c67"/><path class="jkst3" d="M79.821 90.792c-2.966 2.084-6.317 4.744-9.566 7.971a360.155 360.155 0 0 0-21.567 2.81c9.207-4.232 19.713-8.127 31.133-10.781z" id="jkpath22" fill="#656c67"/><path class="jkst3" d="M181.48 107.969l-3.384 23.679-16.212 11.355-42.283-4.807-6.365-20.961a1.383 1.383 0 0 0-1.108-.971c-1.567-.253-2.953-.382-4.108-.382-1.16 0-2.541.129-4.115.382-.522.086-.95.461-1.106.971l-6.209 20.45-42.047 9.357-16.662-11.672-3.283-26.572c.715-.404 1.441-.806 2.176-1.209 1.031-.222 2.191-.457 3.475-.704l3.094 25.073c.048.392.264.741.586.967l11.462 8.032a1.425 1.425 0 0 0 1.101.213l34.57-7.692c.119-.027.237-.069.344-.124a1.39 1.39 0 0 0 .682-.827l6.225-20.498c1.67-.43 5.947-1.429 9.706-1.429 3.749 0 8.03.999 9.701 1.429l6.225 20.498c.161.532.624.912 1.176.977l34.57 3.927c.335.037.677-.05.952-.242l11.469-8.025c.31-.22.52-.566.573-.946l3.062-21.421c2.301.444 4.224.846 5.733 1.172z" id="jkpath24" fill="#656c67"/><path class="jkst3" d="M185.751 93.119l-2.976 11.29c-6.086-1.342-19.456-3.975-37.654-5.747 5.946-2.535 12-5.715 17.531-9.69 10.829 1.53 18.78 3.169 23.099 4.147z" id="jkpath26" fill="#656c67"/><g id="jkg32"><path class="jkst4" d="M63.841 128.441c2.357-1.274 5.021-1.085 9.19-1.079.447.011.908.005 1.39-.005.41-.005.822-.011 1.258-.022 4.296-.042 7.869.366 7.806-6.381-.065-6.746-3.062-12.198-7.354-12.155-4.297.037-8.454 5.564-8.197 12.306.07 1.756.328 3.023.742 3.937-3.745.938-4.777 3.254-4.835 3.399zm51.657-27.749a46.634 46.634 0 0 1-5.249 3.712l-6.097 3.68a52.065 52.065 0 0 0-7.331 1.467 1.216 1.216 0 0 0-.317.14 1.406 1.406 0 0 0-.629.794l-6.209 20.46-33.185 7.38-10.452-7.321-3.041-24.634c5.936-1.09 13.874-2.352 23.41-3.42a56.802 56.802 0 0 0-2.955 3.855l-5.677 8.149 8.266-5.511c.123-.086 5.387-3.549 13.998-7.761a377.407 377.407 0 0 1 35.468-.99z" id="jkpath28" fill="#e5caa3"/><path class="jkst4" d="M151.835 125.675c-.042-.16-.945-2.873-4.942-2.397.461-1.003.666-2.356.521-4.21-.528-6.731-4.443-12.08-8.735-11.931-4.292.152-7.042 5.731-6.805 12.478.236 6.741 3.84 6.694 8.132 6.543 5.77-.107 8.939-1.88 11.829-.483zm21.18-19.385l-2.992 20.944-10.539 7.379-33.141-3.766-6.183-20.363a1.41 1.41 0 0 0-.945-.934c-.205-.06-4.308-1.23-8.659-1.607l.795-.053c.687-.049 12.118-1.451 25.767-6.157 15.115 1.161 27.458 3.02 35.897 4.557z" id="jkpath30" fill="#e5caa3"/></g><g id="jkg38"><path class="jkst5" d="M63.841 128.441c2.357-1.274 5.021-1.085 9.19-1.079.447.011.908.005 1.39-.005.41-.005.822-.011 1.258-.022 4.296-.042 7.869.366 7.806-6.381-.065-6.746-3.062-12.198-7.354-12.155-4.297.037-8.454 5.564-8.197 12.306.07 1.756.328 3.023.742 3.937-3.745.938-4.777 3.254-4.835 3.399zm51.657-27.749a46.634 46.634 0 0 1-5.249 3.712l-6.097 3.68a52.065 52.065 0 0 0-7.331 1.467 1.216 1.216 0 0 0-.317.14 1.406 1.406 0 0 0-.629.794l-6.209 20.46-33.185 7.38-10.452-7.321-3.041-24.634c5.936-1.09 13.874-2.352 23.41-3.42a56.802 56.802 0 0 0-2.955 3.855l-5.677 8.149 8.266-5.511c.123-.086 5.387-3.549 13.998-7.761a377.407 377.407 0 0 1 35.468-.99z" id="jkpath34" fill="#c7b39a"/><path class="jkst5" d="M151.835 125.675c-.042-.16-.945-2.873-4.942-2.397.461-1.003.666-2.356.521-4.21-.528-6.731-4.443-12.08-8.735-11.931-4.292.152-7.042 5.731-6.805 12.478.236 6.741 3.84 6.694 8.132 6.543 5.77-.107 8.939-1.88 11.829-.483zm21.18-19.385l-2.992 20.944-10.539 7.379-33.141-3.766-6.183-20.363a1.41 1.41 0 0 0-.945-.934c-.205-.06-4.308-1.23-8.659-1.607l.795-.053c.687-.049 12.118-1.451 25.767-6.157 15.115 1.161 27.458 3.02 35.897 4.557z" id="jkpath36" fill="#c7b39a"/></g><path class="jkst2" d="M187.481 115.502c.508.419.911 1.504.456 6.558-.559 6.188-3.16 17.049-4.771 18.8-1.778.344-5.505-.064-7.778-.595.393-1.559.505-2.306.822-3.9l3.975-2.781c.317-.22.526-.566.58-.941l2.778-19.466c1.686.912 3.421 1.899 3.938 2.325z" id="jkpath40" fill="#e6ccad"/><path class="jkst2" d="M40.937 140.908c.199.704.408 1.407.624 2.1-2.139.628-6.495 1.23-8.465.886-1.633-1.645-4.679-12.966-5.345-18.978-.543-4.871-.162-5.924.333-6.334.575-.483 2.728-1.708 4.593-2.707l2.519 20.449c.048.393.257.741.586.967z" id="jkpath42" fill="#e6ccad"/><path class="jkst2" d="M121.347 141.194l-.151 1.305s-4.581 4.248-11.956 5.199c-7.375.95-13.171-3.582-13.171-3.582.242.788.586 2.567 2.256 4.086a53.184 53.184 0 0 0-6.313-.393c-.804 0-1.616.023-2.401.061-4.539.237-10.924 7.1-15.414 14.014-2.203.697-9.089 2.883-17.06 5.237-7.44-10.309-11.098-20.842-11.469-21.932l.005-.006c-.15-.419-.301-.839-.441-1.268l1.913 1.338v.005l4.726 3.309 1.58 1.101c.236.167.515.253.794.253.102 0 .204-.011.305-.031l43.435-9.67a1.385 1.385 0 0 0 1.025-.95l6.194-20.39c1.069-.145 2.008-.22 2.814-.22.801 0 1.746.075 2.815.22l6.374 20.997c.162.532.624.919 1.171.977z" id="jkpath44" fill="#e6ccad"/><path class="jkst2" d="M170.926 140.066l1.402-.984c-.232.973-.484 1.94-.747 2.896-1.949 6.248-4.25 11.774-6.805 16.656-.565.039-1.161.061-1.8.061-1.972 0-3.986-.167-6.215-.371-3.868-.355-10.007-1.058-11.946-1.283-1.67-1.332-7.385-5.873-12.14-9.615-.187-.151-.348-.291-.505-.42-.837-.708-1.789-1.513-3.717-1.513-1.751 0-4.308.638-10.489 2.508 3.212-2.401 3.233-5.5 3.233-5.5l.151-1.305 40.748 4.629a1.41 1.41 0 0 0 .955-.241l4.094-2.868z" id="jkpath46" fill="#e6ccad"/><path class="jkst6" d="M140.937 54.337c.124 3.625.033 10.194-1.655 16.345a1.335 1.335 0 0 0 0 .704 259.298 259.298 0 0 0-6.446-.591c2.412-5.054 2.938-10.436 3.052-12.332 1.852-1.317 3.696-2.896 5.049-4.126z" id="jkpath48" fill="#ebd599"/><path class="jkst6" d="M79.456 58.462c.112 1.896.638 7.267 3.046 12.317-2.149.171-4.297.37-6.441.596a1.328 1.328 0 0 0 0-.694c-1.686-6.139-1.772-12.714-1.654-16.345 1.353 1.231 3.19 2.81 5.049 4.126z" id="jkpath50" fill="#ebd599"/><path class="jkst7" d="M151.835 125.675c-2.89-1.396-6.059.377-11.828.484-4.292.151-7.896.198-8.132-6.543-.237-6.747 2.513-12.326 6.805-12.478 4.292-.15 8.207 5.2 8.735 11.931.145 1.854-.06 3.207-.521 4.21 3.996-.477 4.899 2.235 4.941 2.396zm-13.488-9.878a2.203 2.203 0 0 0 2.154-2.235 2.186 2.186 0 0 0-2.235-2.153 2.194 2.194 0 0 0 .081 4.388z" id="jkpath52" fill="#2d3136"/><circle transform="rotate(-1.049 138.093 113.428)" class="jkst8" cx="138.307" cy="113.602" id="jkellipse54" r="2.194" fill="#edf6fa"/><path class="jkst7" d="M83.484 120.953c.063 6.747-3.509 6.339-7.806 6.381-.435.011-.848.016-1.258.022-.482.011-.944.016-1.39.005-4.168-.005-6.833-.194-9.19 1.079.058-.145 1.09-2.461 4.835-3.4-.414-.914-.673-2.181-.742-3.937-.257-6.741 3.9-12.269 8.197-12.306 4.292-.042 7.289 5.411 7.354 12.156zm-6.634-3.529a2.195 2.195 0 1 0-.122-4.388 2.195 2.195 0 0 0 .122 4.388z" id="jkpath56" fill="#2d3136"/><circle transform="rotate(-1.473 76.78 115.216)" class="jkst8" cx="76.79" cy="115.23" id="jkellipse58" r="2.195" fill="#edf6fa"/><g class="jkst9" id="jkg64" opacity=".8"><path class="jkst6" d="M50.691 75.155s.667-8.692 2.03-12.023c.702-1.717 4.996-2.81 8.276-3.591 3.278-.78 8.508-2.342 9.524 2.264 1.015 4.606 2.653 7.963 3.746 9.446l-1.404-18.97-22.562 5.464-1.484 16.786.703 1.327 1.171-.703" id="jkpath60" fill="#ebd599"/><path class="jkst6" d="M164.855 75.155s-.666-8.692-2.029-12.023c-.703-1.717-4.997-2.81-8.275-3.591-3.28-.78-8.51-2.342-9.526 2.264-1.013 4.606-2.654 7.963-3.748 9.446l1.407-18.97 22.562 5.464 1.483 16.786-.703 1.327-1.171-.703" id="jkpath62" fill="#ebd599"/></g><path class="jkst10" d="M132.965 18.378s-.598 45.49-11.224 45.49h-14.875-12.752c-10.626 0-11.484-45.47-11.484-45.47l-5.22 15.438.085 21.183 3.707 2.947 1.685 9.096 2.357 5.307 45.482.084 2.105-3.791 1.769-6.4.254-4.043 5.023-14.341z" id="jkpath66" opacity=".75" fill="#ebd599"/><path class="jkst10" d="M166.429 60.794s2.187 15.692 7.974 18.522c5.788 2.829 0 0 0 0l-8.103-2.444z" id="jkpath68" opacity=".75" fill="#ebd599"/><path class="jkst10" d="M48.908 60.794s-2.187 15.692-7.975 18.522c-5.788 2.829 0 0 0 0l8.104-2.444z" id="jkpath70" opacity=".75" fill="#ebd599"/><path class="jkst7" d="M167.987 76.8c2.755.902 5.526 1.858 8.036 3.325-1.343-.532-2.729-.913-4.126-1.257a70.385 70.385 0 0 0-4.201-.924c-2.82-.531-5.65-.982-8.498-1.327-2.841-.37-5.687-.682-8.546-.924-2.858-.241-5.709-.483-8.573-.65-11.446-.704-22.924-.88-34.41-.892-11.483.006-22.962.221-34.409.897-2.862.166-5.715.409-8.572.651-2.857.241-5.71.548-8.546.923-2.847.345-5.678.796-8.498 1.327-1.407.264-2.81.57-4.206.919-1.391.344-2.783.725-4.126 1.257 2.509-1.466 5.28-2.427 8.041-3.331.232-.075.467-.139.703-.214-.015-.059-.032-.113-.043-.177-.048-.317-1.069-7.859.709-18.645.086-.516.456-.935.962-1.075l2.917-.831c.634-22.625 9.952-33.266 10.243-33.594-8.326 13.397-8.25 29.286-8.106 32.986l18.128-5.152c.016-.005.026-.005.042-.01.076-.016.151-.027.226-.032.021 0 .049-.006.075-.006a1.19 1.19 0 0 1 .297.027c.015 0 .031.011.053.016.075.016.145.042.224.075.033.016.054.033.086.049.058.033.119.07.177.112.016.011.034.016.049.033l.032.032c.016.016.037.027.054.044.012.016.494.493 1.262 1.209-.182-5.973.102-23.108 8.262-37.31-.172.498-6.646 19.428-4.415 40.645.724.58 1.486 1.149 2.229 1.649.359.247.58.655.585 1.09.006.07.161 6.833 3.148 12.586.042.086.074.177.102.268 7.429-.505 14.878-.709 22.312-.714 7.436.005 14.88.22 22.307.731.027-.097.06-.193.109-.285 2.986-5.753 3.142-12.516 3.142-12.586.01-.436.231-.843.591-1.09.741-.5 1.493-1.069 2.224-1.649 2.234-21.217-4.24-40.147-4.411-40.645 8.153 14.201 8.444 31.336 8.262 37.31a62.536 62.536 0 0 0 1.261-1.209c.016-.016.039-.027.053-.044.012-.01.018-.021.033-.032.016-.016.033-.022.049-.033.06-.042.119-.079.177-.118.028-.01.054-.027.081-.043.081-.033.155-.059.236-.08.016 0 .033-.011.049-.011.096-.021.2-.032.296-.027.027 0 .049.006.07.006.075.005.156.016.231.032.012.006.028.006.042.01l18.129 5.152c.146-3.7.221-19.59-8.104-32.986.289.328 9.609 10.969 10.237 33.594l2.922.831c.499.14.875.559.962 1.075 1.777 10.786.752 18.328.708 18.645-.01.065-.026.124-.042.182.239.07.47.139.707.215zm-3.297-.968c.14-1.207.789-7.809-.591-16.801l-20.52-5.833c.184 3.475.265 11.012-1.707 18.199a1.619 1.619 0 0 1-.101.258c.203.021.408.037.606.064 5.769.661 11.511 1.584 17.189 2.83 1.712.398 3.426.823 5.124 1.283zm-25.409-5.151c1.688-6.15 1.779-12.72 1.655-16.345-1.353 1.23-3.197 2.809-5.049 4.125-.114 1.896-.64 7.278-3.052 12.332 2.149.173 4.298.366 6.446.591a1.33 1.33 0 0 1 0-.703zm-56.78.098c-2.408-5.05-2.934-10.422-3.046-12.317-1.858-1.316-3.696-2.895-5.049-4.125-.119 3.631-.032 10.206 1.654 16.345.065.237.058.473 0 .694 2.145-.227 4.292-.425 6.441-.597zm-8.933.864a1.65 1.65 0 0 1-.098-.247c-1.975-7.187-1.889-14.723-1.712-18.199L51.244 59.03c-1.38 8.982-.736 15.583-.597 16.797 1.703-.462 3.411-.887 5.131-1.284 2.835-.628 5.693-1.154 8.556-1.638 2.869-.478 5.747-.843 8.626-1.192.205-.027.404-.042.608-.07z" id="jkpath72" fill="#2d3136"/><g id="jkXMLID_1_"><g id="jkg78"><path class="jkst7" d="M129.293 18.973v17.025h-12.068v-4.974h-2.72v22.981h4.109v12.85H97.505v-12.85h4.092v-22.98h-2.711v4.974h-12.06V18.973zm-3.626 13.408v-9.789H90.443v9.789h4.816v-4.974h9.964v30.225h-4.1v5.606h13.865v-5.606h-4.1V27.407h9.964v4.974z" id="jkpath74" fill="#2d3136"/><path class="jkst0" id="jkpolygon76" fill="#cb3349" d="M101.123 57.632h4.1V27.407h-9.964v4.974h-4.816v-9.79h35.224v9.79h-4.816v-4.974h-9.964v30.225h4.1v5.606h-13.864z"/></g></g><path class="jkst3" d="M30.694 93.119c1.759-.399 4.136-.907 7.051-1.47a104.37 104.37 0 0 0-6.222 4.597z" id="jkpath83" fill="#656c67"/><path class="jkst5" d="M95.111 139.78s.492 3.165-3.938 4.519c-4.428 1.355-32.482 9.716-35.682 9.263-3.199-.451-11.319-5.874-11.319-5.874l-1.969-7.004 12.016 7.492z" id="jkpath85" fill="#c7b39a"/><path class="jkst5" d="M120.242 139.167s-.354 3.182 4.131 4.345c4.484 1.161 32.875 8.295 36.05 7.704 3.176-.591 11.053-6.361 11.053-6.361l1.663-7.084-11.045 6.588z" id="jkpath87" fill="#c7b39a"/><path class="jkst5" d="M28.412 133.956s3.887 7.775 10.166 5.083l4.485 1.645-.448 3.29-9.419 1.195-2.541-1.494z" id="jkpath89" fill="#c7b39a"/><path class="jkst5" d="M187.551 131.822s-6.353 8.115-12.632 5.424l-2.019 1.302.448 3.289 9.419 1.196 2.54-1.495z" id="jkpath91" fill="#c7b39a"/><path class="jkst5" d="M89.279 192.904s23.03 11.611 49.106-4.188l-8.374-.571s-18.272 7.232-32.738 3.235z" id="jkpath93" fill="#c7b39a"/><path class="jkst7" d="M112.626 171.509l1.594 1.899c.036.046 3.577 4.26 7.906 8.552 2.879 2.853 6.357 4.297 10.343 4.297 1.361 0 2.791-.175 4.235-.523 1.34-.326 2.796-.673 4.287-1.03 5.384-1.287 11.482-2.749 14.438-3.577.585-.166 1.238-.315 1.925-.472 3.935-.909 9.329-2.163 12.187-7.889 2.149-4.297 5.047-9.874 7.197-13.961-1.863.859-3.816 1.79-5.203 2.52-2.138 1.123-4.938 1.667-8.558 1.667-2.152 0-4.266-.181-6.605-.389-4.675-.43-12.586-1.361-12.667-1.372l-.606-.067-.478-.383c-.071-.052-7.003-5.575-12.606-9.981-.227-.186-.434-.358-.621-.513-.59-.503-.59-.503-.942-.503-1.797 0-7.02 1.62-18.462 5.167l-.703.223-.689-.26c-.078-.026-7.585-2.81-16.581-2.81-.736 0-1.47.019-2.185.056-.901.046-5.958 2.448-12.425 12.68l-.419.657-.741.238c-.107.037-11.238 3.63-23.042 7.005l-.766.218-.725-.337c-.077-.031-4.696-2.174-9.091-4.194 2.397 3.541 5.462 7.958 8.159 11.422 4.711 6.067 10.649 11.674 22.034 11.674 1.428 0 2.945-.088 4.503-.265 11.581-1.309 14.563-1.837 16.168-2.117.543-.092.973-.171 1.522-.238.088-.011 9.571-1.237 12.232-7.206 2.744-6.134 3.298-7.595 3.319-7.651l.968-2.583s.12-.669.317-.877c0 .005 0 .005.005.005l.019.016c.305.219.757.902.757.902zM40.499 55.71c-2.516 1.014-5.016 2.06-7.46 3.209-2.449 1.119-4.856 2.32-7.155 3.66-2.121 1.222-4.157 2.563-5.954 4.076-.077.455-.149.952-.211 1.423a51.357 51.357 0 0 0-.388 6.068c-.026 2.713.16 5.426.502 8.112.372 2.692.864 5.369 1.594 7.952a41.963 41.963 0 0 0 1.243 3.804c.233.623.492 1.228.762 1.818.134.294.274.585.413.864l.172.326c.201.104.409.207.605.3l1.206.574c.673.311 1.6.751 2.366 1.093.046-.037.088-.078.124-.114l-2.231-8.511c.471-.129 4.717-1.227 12.032-2.619a33.744 33.744 0 0 1-1.775-.379 36.704 36.704 0 0 1-4.898-1.563 22.857 22.857 0 0 1-2.309-1.119c-.741-.425-1.471-.905-2.035-1.547 8.035 2.624 24.637 1.433 39.398-.088 13.501-1.393 27.028-2.293 40.628-2.325 13.6.031 27.138.931 40.63 2.325 14.77 1.522 31.374 2.713 39.406.088-.564.642-1.293 1.122-2.034 1.547-.739.42-1.522.782-2.309 1.119a36.965 36.965 0 0 1-4.903 1.563c-.244.056-.492.114-.741.166 8.02 1.486 12.689 2.697 13.186 2.832l-2.138 8.107c.43-.192.864-.377 1.288-.574l1.207-.574c.196-.094.404-.196.606-.3l.166-.326c.144-.279.284-.57.419-.864.27-.591.528-1.196.767-1.818.471-1.231.879-2.51 1.236-3.804.731-2.583 1.228-5.26 1.595-7.952.346-2.686.528-5.4.502-8.112a52.755 52.755 0 0 0-.176-4.059 51.573 51.573 0 0 0-.213-2.009 29.83 29.83 0 0 0-.213-1.423c-1.797-1.513-3.831-2.853-5.954-4.076-2.299-1.34-4.704-2.541-7.159-3.66-2.438-1.149-4.943-2.195-7.46-3.209a140.105 140.105 0 0 0-3.801-1.476c-1.267-.491-2.552-.956-3.835-1.423 2.696.445 5.369 1.06 8.013 1.739 1.724.446 3.444.948 5.141 1.481-12.11-31.658-41.07-52.272-72.685-52.272-31.622 0-60.576 20.614-72.684 52.272a107.832 107.832 0 0 1 5.135-1.481c2.651-.678 5.322-1.294 8.02-1.739-1.29.466-2.568.931-3.842 1.423-1.268.47-2.535.967-3.799 1.475zm159.43 18.316a53.972 53.972 0 0 1-.258 8.733 55.462 55.462 0 0 1-1.619 8.605c-.4 1.414-.86 2.811-1.404 4.198a38.295 38.295 0 0 1-.89 2.071c-.161.341-.331.678-.523 1.025l-.284.512a8.975 8.975 0 0 1-.348.574l-.294.457-.461.237c-.492.254-.895.445-1.342.653l-1.298.585a88.22 88.22 0 0 1-2.62 1.065c-.611.239-1.15.457-1.662.674l-1.444 5.487c-.036-.009-.471-.12-1.283-.315l-.078.574c1.594.833 4.726 2.522 5.793 3.403 2.148 1.775 2.299 4.587 1.823 9.841-.244 2.697-1.139 7.946-2.381 12.767-2.144 8.298-3.283 9.273-4.753 9.649-.746.192-1.894.383-3.008.383-2.266 0-5.353.063-7.429-.439-.533 1.888-2.055 6.812-5.068 12.962.151-.073.3-.135.435-.207 3.717-1.952 10.861-5.064 11.162-5.199l5.643-2.452-2.89 5.435c-.067.118-6.264 11.773-10.059 19.383-3.769 7.538-10.835 9.179-15.065 10.151-.637.151-1.241.291-1.733.425-3.035.854-9.18 2.319-14.599 3.623-.064.016-.13.033-.197.042a64.057 64.057 0 0 1-10.955 5.411c-14.568 5.518-29.923 5.208-43.844.092a647.05 647.05 0 0 1-9.193 1.097 45.12 45.12 0 0 1-4.985.291c-13.264 0-20.294-6.736-25.425-13.331-5.493-7.062-12.212-17.546-12.497-17.985L31 158.426l6.585 2.961c3.152 1.419 12.524 5.757 15.205 7 .217-.061.43-.124.642-.186-4.457-6.357-8.112-13.605-10.695-21.634-2.195.662-5.576 1.175-8.206 1.175-.961 0-1.822-.072-2.484-.228-1.471-.336-3.148-1.754-5.431-9.795-1.325-4.668-2.314-9.764-2.603-12.387-.57-5.121-.466-7.864 1.662-9.636 1.283-1.071 5.611-3.344 6.507-3.809l-.192-1.58c-13.75 8.08-21.991 15.22-22.157 15.366L0 134.302l7.005-11.047c5.544-8.755 11.948-15.832 17.84-21.284-.244-.098-.471-.196-.71-.294l-1.299-.585a34.907 34.907 0 0 1-1.34-.653l-.461-.237-.295-.457c-.166-.249-.238-.388-.347-.574l-.29-.512c-.181-.347-.358-.684-.518-1.025a30.878 30.878 0 0 1-.89-2.071 44.74 44.74 0 0 1-1.404-4.198 54.745 54.745 0 0 1-1.62-8.605 54.664 54.664 0 0 1-.259-8.733c.078-1.455.218-2.909.419-4.354.104-.725.213-1.45.358-2.17.15-.734.296-1.418.518-2.221l.155-.564.404-.317c2.294-1.802 4.768-3.163 7.284-4.369a78.87 78.87 0 0 1 6.311-2.616c5.943-16.493 16.162-31.118 29.591-41.311C74.337 5.57 90.664 0 107.671 0s33.334 5.57 47.218 16.106c13.43 10.193 23.649 24.819 29.588 41.307a78.282 78.282 0 0 1 6.316 2.62c2.515 1.206 4.99 2.567 7.283 4.369l.404.317.156.564c.227.803.372 1.487.517 2.221.146.72.26 1.445.357 2.17.203 1.443.348 2.897.419 4.352zm-11.995 48.031c.456-5.052.058-6.139-.455-6.554-.513-.43-2.247-1.412-3.935-2.329l-2.779 19.464a1.39 1.39 0 0 1-.58.942l-3.977 2.781c-.315 1.593-.429 2.345-.817 3.903 2.273.528 5.999.938 7.775.595 1.612-1.748 4.214-12.61 4.768-18.802zm-5.161-17.648l2.977-11.29c-4.318-.978-12.27-2.615-23.1-4.148-5.53 3.976-11.582 7.155-17.53 9.691 18.199 1.771 31.57 4.406 37.653 5.747zm-4.68 27.237l3.385-23.676a240.127 240.127 0 0 0-5.731-1.169l-3.059 21.422a1.415 1.415 0 0 1-.575.943l-11.472 8.023c-.27.192-.616.28-.947.243l-34.572-3.929a1.391 1.391 0 0 1-1.176-.973l-6.227-20.5c-1.668-.431-5.949-1.43-9.696-1.43-3.764 0-8.041.999-9.708 1.43l-6.228 20.5a1.388 1.388 0 0 1-1.025.947l-34.572 7.692a1.483 1.483 0 0 1-.306.033 1.36 1.36 0 0 1-.792-.25l-11.467-8.029a1.396 1.396 0 0 1-.585-.968l-3.091-25.072c-1.284.249-2.443.487-3.479.703-.734.405-1.46.809-2.174 1.213l3.281 26.568 16.666 11.675 42.047-9.354 6.207-20.449a1.389 1.389 0 0 1 1.108-.975c1.574-.253 2.95-.382 4.116-.382 1.153 0 2.536.129 4.105.382.528.083.957.461 1.108.975l6.366 20.956 42.282 4.808zm-8.07-4.411l2.992-20.948c-8.439-1.536-20.78-3.394-35.897-4.554-13.647 4.707-25.077 6.108-25.766 6.155l-.797.057c4.353.374 8.454 1.544 8.66 1.605.452.135.804.481.944.933l6.186 20.366 33.138 3.764zm2.303 11.845l-1.404.983-3.779 2.651-4.095 2.868c-.279.192-.621.28-.954.243l-40.746-4.633-2.966-.337a1.39 1.39 0 0 1-1.171-.977l-6.377-20.998c-1.066-.145-2.014-.219-2.81-.219-.809 0-1.751.073-2.817.219l-6.192 20.392a1.383 1.383 0 0 1-1.025.946l-43.435 9.672c-.103.02-.206.03-.305.03-.279 0-.559-.083-.798-.253l-1.578-1.098-4.726-3.307v-.011l-1.91-1.335c.135.43.289.85.441 1.268l-.006.006c.368 1.092 4.028 11.622 11.467 21.929a873.96 873.96 0 0 0 17.057-5.234c4.488-6.917 10.877-13.777 15.418-14.014a51.12 51.12 0 0 1 2.402-.061c2.221 0 4.344.16 6.31.393-1.671-1.517-2.013-3.298-2.256-4.085 0 0 5.793 4.53 13.17 3.584 7.378-.953 11.959-5.204 11.959-5.204s-.021 3.102-3.236 5.503c6.182-1.869 8.739-2.511 10.489-2.511 1.931 0 2.883.808 3.717 1.519.161.129.322.268.507.419a3519.302 3519.302 0 0 1 12.141 9.614c1.936.227 8.075.926 11.943 1.283 2.23.201 4.245.372 6.217.372.637 0 1.233-.026 1.797-.063 2.558-4.88 4.857-10.411 6.808-16.653.261-.96.516-1.928.743-2.901zm-15.034-51.593c-.01-.006-.02-.012-.031-.012a551.624 551.624 0 0 0-9.826-.651 905.6 905.6 0 0 0-13.667-.668 72.95 72.95 0 0 1-1.574 2.225c-2.479 3.355-7.398 9.51-13.704 14.729 8.926-1.6 24.409-5.56 37.803-14.905.336-.238.668-.486.999-.718zm-29.876.926c.377-.471.729-.926 1.044-1.34-3.281.331-6.512.808-9.67 1.408-10.814 2.024-20.801 5.389-29.11 8.837a383.259 383.259 0 0 1 18.54-.455c3.908 0 7.708.067 11.404.176 3.179-3.056 5.861-6.182 7.792-8.626zm3.587 102.085c-4.503-.332-8.598-2.205-11.903-5.477a271.86 271.86 0 0 0-.502-.512 44.25 44.25 0 0 1-4.881.704c-.698.026-1.361.087-2.091.087l-1.083.011-.413-.011c-4.396 6.539-14.159 7.813-14.605 7.87-.403.046-.734.103-1.191.186 5.442 1.491 10.996 2.138 16.474 1.77 5.492-.367 12.627-1.558 20.195-4.628zm-17.4-7.461a45.604 45.604 0 0 0 3.184-.378 138.958 138.958 0 0 1-3.568-3.857 398.441 398.441 0 0 1-1.92 4.339h.243c.658.001 1.378-.071 2.061-.104zm-3.354-78.632c1.827-1.103 3.582-2.366 5.249-3.712a422.33 422.33 0 0 0-7.278-.072c-10.137 0-19.606.415-28.189 1.061-8.61 4.209-13.875 7.672-13.998 7.76l-8.268 5.514 5.679-8.149a52.452 52.452 0 0 1 2.956-3.857c-9.536 1.066-17.477 2.329-23.41 3.422l3.038 24.632 10.453 7.321 33.184-7.378 6.212-20.464c.104-.337.331-.621.627-.793.098-.063.202-.109.315-.14.192-.052 3.51-.999 7.336-1.465zm3.816-18.788c-2.31-.036-4.623-.057-6.933-.062h-.005c-3.39.005-6.787.041-10.189.109l-6.269 2.971c-.005.005-.041.021-.088.048-.942.46-9.174 4.613-16.919 12.021 6.943-3.65 17.146-8.418 29.153-12.115a144.186 144.186 0 0 1 11.25-2.972zM70.251 98.761c3.251-3.225 6.605-5.886 9.567-7.967-11.415 2.651-21.923 6.543-31.128 10.778a360.846 360.846 0 0 1 21.561-2.811zm2.159-9.949a150.122 150.122 0 0 1 11.813-2.796c-5.798.212-11.6.481-17.393.808-3.366.186-6.715.414-10.065.667-1.678.129-3.345.263-5.007.445-.476.046-.942.098-1.418.16-4.369 2.614-21.127 13.134-32.631 26.889 11.179-7.769 30.654-19.443 54.701-26.173zm-30.85 54.197a68.861 68.861 0 0 1-.621-2.102l-5.162-3.612a1.391 1.391 0 0 1-.586-.969l-2.516-20.449c-1.864.999-4.017 2.225-4.592 2.707-.497.409-.875 1.46-.336 6.332.668 6.01 3.712 17.333 5.348 18.979 1.968.347 6.327-.258 8.465-.886zm-3.815-51.36a229.005 229.005 0 0 0-7.051 1.47l.829 3.127a103.93 103.93 0 0 1 6.222-4.597z" id="jkpath95" fill="#2d3136"/></g></g></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="tune" xmlns="http://www.w3.org/2000/svg"><path d="M6.85 2.852h-2v6h2v-6m12 0h-2v10h2v-10m-16 10h2v8h2v-8h2v-2h-6v2m12-6h-2v-4h-2v4h-2v2h6v-2m-4 14h2v-10h-2v10m4-6v2h2v4h2v-4h2v-2h-6z" fill="#fbc02d" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 50 50" id="twig" xmlns="http://www.w3.org/2000/svg"><path d="M9.727 47.556c-.125-.223-.297-2.168-.183-2.087.034.025.171.267.304.537.132.27.282.487.332.482.123-.011.075-1.196-.1-2.454-.331-2.398-1.176-4.435-2.358-5.69-.2-.212-.344-.4-.319-.419.093-.067 1.327.843 1.842 1.359.293.293.735.825.981 1.181.328.474.465.618.51.534.078-.147-.21-9.903-.376-12.701-.074-1.255.063-1.023.61 1.035 1.064 4.006 1.858 7.922 2.342 11.55.086.637.173 1.172.195 1.19.022.016.092.001.157-.034.888-.483 1.524-.667 2.55-.736.727-.048.945.062.35.178-1.15.222-1.99 1.013-2.344 2.201-.315 1.061-.327 2.707-.024 3.434.152.366.037.426-1.067.56-.716.088-.977.096-1.202.037-.356-.092-1.118-.098-1.195-.008-.031.036-.243.066-.47.066-.38 0-.423-.017-.535-.215zm1.974-3.233c.152-.205.072-.41-.204-.522-.225-.09-.263-.088-.437.025-.21.137-.252.43-.08.554.18.13.607.096.72-.057zm1.248.086a.763.763 0 0 0 .214-.203c.241-.33-.352-.622-.745-.366-.406.265.08.785.531.569zm2.288 3.094c-.033-.039.117-.387.334-.775.216-.387.411-.665.433-.618.07.152-.201 1.28-.33 1.372-.15.108-.354.117-.437.02zM8.2 47.092c-.29-.343-.221-.434.14-.182.176.123.321.263.321.31 0 .165-.279.087-.46-.128zm8.649-.145c0-.053.102-.18.227-.282.25-.204.312-.113.143.207-.095.18-.37.236-.37.075zm8.065-.827c-.243-.025-.48-.088-.527-.141-.11-.125-.114-3.043-.004-3.043.045 0 .132.149.193.331.127.38.228.42.31.124.094-.337.065-3.472-.039-4.297-.449-3.55-1.865-6.124-4.342-7.89-1.086-.774-2.653-1.436-4.047-1.711-.764-.15-.522-.224.598-.182 2.364.089 4.167.706 5.847 2.001a11.046 11.046 0 0 1 2.32 2.502c.453.682.64.854.64.584 0-.07.063-.882.139-1.805.679-8.26 2.396-15.1 4.984-19.86 1.86-3.422 5.108-6.817 7.885-8.244 1.397-.718 2.539-.988 4.02-.952.933.023 1.01.036 1.77.307a6.822 6.822 0 0 1 1.363.662c.612.407 1.309 1.004 1.235 1.058-.026.018-.343-.165-.705-.407-2.657-1.771-5.062-1.52-7.12.742-1.108 1.22-2.651 3.53-3.634 5.443-2.828 5.503-4.541 11.464-5.291 18.413-.163 1.509-.282 3.76-.195 3.703.032-.022.266-.52.518-1.108 1.597-3.723 3.578-6.428 5.79-7.908.672-.449 1.612-.904 1.715-.83.022.016-.172.22-.432.454-1.957 1.754-3.248 3.76-4.232 6.572-.938 2.68-1.366 5.588-1.368 9.3-.002 1.741.188 4.385.366 5.101.125.505.08.546-.585.546-.55 0-2.306.138-3.416.27-.414.05-.817.04-1.609-.036-.58-.056-1.129-.119-1.218-.14-.165-.037-.18-.014-.2.302-.01.186-.098.203-.728.139zm2.507-6.725c.294-.11.375-.22.375-.517 0-.63-1.309-.706-1.524-.088-.074.211.13.51.42.616.297.108.413.106.73-.011zm2.369-.052c.277-.222.318-.364.174-.611-.4-.691-1.755-.307-1.428.404.121.266.299.35.738.354.227 0 .387-.045.516-.147zm3.011 6.681c-.027-.05.088-.268.256-.484.879-1.135 1.22-1.544 1.284-1.544.04 0 .056.037.036.082l-.423.964c-.212.485-.445.924-.519.977-.169.122-.57.125-.634.005zm2.446-.596c0-.121.853-.683.896-.59.018.04-.056.209-.166.376-.168.259-.238.305-.464.305-.164 0-.266-.035-.266-.091zm-13.04-.124c-.177-.159-.493-.656-.462-.725.018-.038.248.1.512.309.264.207.457.405.428.438-.075.088-.371.074-.478-.022z" fill="#9bb92f" stroke-width=".078"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="typescript" xmlns="http://www.w3.org/2000/svg"><path d="M49 51h408v408H49V51zm246.669 314.879l19.463-1.702c.922 7.8 3.067 14.199 6.435 19.198 3.368 4.998 8.597 9.04 15.688 12.124 7.09 3.085 15.067 4.627 23.93 4.627 7.87 0 14.819-1.17 20.845-3.51 6.027-2.34 10.512-5.548 13.455-9.625 2.942-4.077 4.413-8.526 4.413-13.348 0-4.892-1.418-9.164-4.254-12.816-2.836-3.651-7.516-6.718-14.039-9.2-4.183-1.63-13.436-4.165-27.759-7.604s-24.355-6.683-30.099-9.732c-7.445-3.899-12.993-8.739-16.644-14.517-3.652-5.779-5.478-12.249-5.478-19.41 0-7.871 2.234-15.227 6.701-22.069 4.467-6.842 10.99-12.036 19.569-15.581 8.58-3.546 18.116-5.318 28.61-5.318 11.557 0 21.75 1.861 30.577 5.584 8.828 3.722 15.617 9.199 20.368 16.432 4.75 7.232 7.303 15.421 7.657 24.568l-19.782 1.489c-1.064-9.856-4.662-17.301-10.795-22.335-6.133-5.034-15.191-7.551-27.174-7.551-12.479 0-21.573 2.286-27.281 6.86-5.707 4.573-8.561 10.086-8.561 16.538 0 5.602 2.021 10.21 6.062 13.826 3.971 3.617 14.34 7.321 31.109 11.115 16.769 3.793 28.273 7.108 34.513 9.944 9.076 4.183 15.776 9.483 20.101 15.9 4.325 6.417 6.488 13.809 6.488 22.175 0 8.296-2.375 16.113-7.126 23.452-4.751 7.338-11.575 13.046-20.474 17.123-8.898 4.077-18.913 6.116-30.045 6.116-14.11 0-25.933-2.056-35.47-6.169-9.537-4.112-17.017-10.299-22.441-18.559-5.424-8.26-8.278-17.602-8.562-28.025zm-65.728 50.094V278.454h51.583v-18.399H157.938v18.399h51.37v137.519h20.633z" fill="#0288d1"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="typescript-def" xmlns="http://www.w3.org/2000/svg"><path d="M457 459H49V51h408v408zM69 71v368h368V71H69z" fill="#0288d1"/><text x="342.219" y="344.544" font-family="ArialMT" font-size="12" fill="#0288d1" transform="translate(-6058.94 -5838) scale(18.1514)"><tspan style="-inkscape-font-specification:sans-serif" font-family="sans-serif" font-weight="400">TS</tspan></text></symbol><symbol viewBox="0 0 24 24" id="url" xmlns="http://www.w3.org/2000/svg"><path d="M16 6h-3v1.9h3a4.1 4.1 0 0 1 4.1 4.1 4.1 4.1 0 0 1-4.1 4.1h-3V18h3a6 6 0 0 0 6-6c0-3.32-2.69-6-6-6M3.9 12A4.1 4.1 0 0 1 8 7.9h3V6H8a6 6 0 0 0-6 6 6 6 0 0 0 6 6h3v-1.9H8c-2.26 0-4.1-1.84-4.1-4.1M8 13h8v-2H8v2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="verilog" xmlns="http://www.w3.org/2000/svg"><path d="M17.282 17.08H6.718V6.513h10.564m4.226 4.226V8.627h-2.113V6.514c0-1.173-.95-2.113-2.113-2.113H15.17V2.288h-2.113v2.113h-2.112V2.288H8.83v2.113H6.718c-1.173 0-2.113.94-2.113 2.113v2.113H2.492v2.113h2.113v2.113H2.492v2.113h2.113v2.113a2.113 2.113 0 0 0 2.113 2.113H8.83v2.113h2.113v-2.113h2.112v2.113h2.113v-2.113h2.113a2.113 2.113 0 0 0 2.113-2.113v-2.113h2.113v-2.113h-2.113V10.74m-6.339 2.113h-2.112V10.74h2.112m2.113-2.113H8.831v6.34h6.338z" fill="#ff7043" stroke-width="1.056"/></symbol><symbol viewBox="0 0 24 23.999999" id="vfl" xmlns="http://www.w3.org/2000/svg"><defs><style>.jra{fill:#f05223}.jrb{fill:url(#jra)}</style><radialGradient id="jra" cx="205.45" cy="208.29" r="225.35" gradientTransform="matrix(.04556 0 0 .0456 2.888 2.88)" gradientUnits="userSpaceOnUse"><stop stop-color="#ffd104" offset="0"/><stop stop-color="#faa60e" offset=".35"/><stop stop-color="#f05023" offset="1"/></radialGradient></defs><title>houdinibadge</title><g stroke-width=".046"><path class="jra" d="M19.97 3H4.03A1.03 1.031 0 0 0 3 4.031v4.135C4.548 6.977 6.563 6.21 8.948 6.21c5.107.003 8.35 3.574 8.348 8.081 0 3.13-1.46 5.485-3.746 6.71h6.42A1.03 1.031 0 0 0 21 19.968V4.031a1.03 1.031 0 0 0-1.03-1.03z" fill="#f4511e"/><path class="jrb" d="M3 17.722v2.247A1.03 1.031 0 0 0 4.03 21h1.837C4.474 20.21 3.49 19 3 17.722z" fill="url(#jra)"/><path class="jra" d="M8.948 8.231c-2.586-.09-4.598.86-5.948 2.264v3.163c.918-2.654 3.447-3.87 5.565-3.85 2.647.027 4.689 2.025 4.7 4.284.012 2.159-.892 3.748-3.33 4.14-1.33.213-3.411-.567-3.318-2.578.046-1.037.854-1.622 1.777-1.58-.905 1.213.293 2.102 1.139 1.921 1.048-.224 1.475-1.156 1.475-1.878 0-.762-.718-1.994-2.498-1.951-2.204.052-3.591 1.639-3.638 3.602-.056 2.468 2.253 4.091 4.622 4.121 3.48.046 5.543-2.24 5.539-5.586-.005-3.029-2.434-5.946-6.085-6.072z" fill="#f05223"/></g></symbol><symbol viewBox="0 0 24 24" id="virtual" xmlns="http://www.w3.org/2000/svg"><path d="M21 14H3V4h18m0-2H3c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h7l-2 3v1h8v-1l-2-3h7a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 281.25 281.25" id="visualstudio" xmlns="http://www.w3.org/2000/svg"><path d="M196.18 101.74l-52.778 42.444 52.778 40.889V101.74m-136.67 110l-30-18.889v-100L62.843 81.74l47.778 37 96.666-89.222 44.444 27.778v172.22l-55.555 22.222-85.111-81.555-51.555 41.555m3.333-48.889l20.667-19.111-20.667-19.778z" fill="#ab47bc" stroke-width="11.111"/></symbol><symbol viewBox="0 0 300 300" id="vscode" xmlns="http://www.w3.org/2000/svg"><defs><style>.icon-canvas-transparent{fill:#f6f6f6;opacity:0}.icon-white{fill:#fff}</style></defs><title>BrandVisualStudioCode</title><path d="M218.62 29.953l-105.41 96.92L54.301 82.47 29.955 96.64l58.068 53.359-58.068 53.359 24.346 14.212 58.909-44.402 105.41 96.878 51.424-24.976V54.93zm0 63.744v112.6l-74.719-56.302z" fill="#2196f3" stroke-width="17.15"/></symbol><symbol viewBox="0 0 24 24" id="vue" xmlns="http://www.w3.org/2000/svg"><path d="M1.821 4.15l10.21 17.618L22.24 4.235V4.15h-7.692L12.113 8.33 9.691 4.15H1.82z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179H5.937z" fill="#35495e"/></symbol><symbol viewBox="0 0 420 419" id="watchman" xmlns="http://www.w3.org/2000/svg"><g stroke="#fff" stroke-linecap="round" stroke-linejoin="bevel"><path d="M166.95 145.32a93.935 123.23 0 0 1 92.934 3.263" fill="none" stroke-width="18.467"/><path d="M162.92 137.96L44.63 256.25a174.07 173.93 0 0 0 5.705 16.486l123.68-123.68-11.096-11.096zM266.54 144.04l-11.096 11.096 117.16 117.16a174.07 173.93 0 0 0 5.691-16.5l-111.76-111.76zm170.65 170.65v22.193l17.1 17.1 11.096-11.098-28.195-28.195z" fill="#fff" stroke-width="1.963"/><path d="M167.52 273.36a93.935 123.23 0 0 1 92.934-3.263" fill="none" stroke-width="18.467"/><path d="M49.516 144.56a174.07 173.93 0 0 0-.809 2.213 174.07 173.93 0 0 0-4.757 14.344 174.07 173.93 0 0 0-.016.055l119.56 119.56 11.098-11.096-125.07-125.07zM454.87 64.703l-17.668 17.668v22.191l28.764-28.764-11.096-11.096zm-80.984 80.984l-117.86 117.86 11.098 11.096 112.18-112.18a174.07 173.93 0 0 0-5.416-16.777z" fill="#fff" stroke-width="1.963"/></g><image x="21.229" y="20.262" width="378" height="377.1" preserveAspectRatio="none" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaQAAAGjCAYAAABjSWGNAAAgAElEQVR4AeydB3hUVdrH/+fOpNMF JAFUmivFXtZuIBRRQUUTil1RV3et6Lr6rSu6rg3B1dXVtXeBCCioCASIimLDhkFsgAIJSAkhPZmZ 8z3vjReGMJlMueeWmfc88MzMLaf8zp3855zznvcV4MQEkoxA7sVr01PrUrv4pdZNBNA1AHQRkJ0g ZAcJrYOA7Aipv8/Q6JgUKdBkG0iZISDSDVwSaAfAY3z+/bVSAD56L+mfwA5INAqgSkrUCKA2IOQO IcQOIUU5pNwhNZQLia2Q2OyH3OJBxqaiwk4VzfLlj0wg4QmIhG8hNzDJCEgxJH9DjgfevgGBAyCx vxSyh5DoAWA/CHQH0MEFUOoBbJbAeiHEBiED6wMSv0DgF02In1J2Vq+ZP78fXcOJCSQMARakhOnK 5GrICaO3tE3P8B0kERgkgP6Qop8E+gqgL4Bdo5gEphKAwHpIrIHAjxL4DpCrAo1yVfHsHhsSuN3c tAQmwIKUwJ2bGE2TYsjY33qLQOBwCHk4JA6HhgE08kmM9ilpRQUgvwPE1xD4MiC0r2R67dfFz/eq U1IaZ8oETCLAgmQSSM7GHAIj8td38sN7HAROkpDHATgMTWs15hSQvLn4JLBaQHwqEPgQQiwvmpHz XfLi4JY7kQALkhN7JYnqlDd2w4GQnuMk5AkCOBHAQQD4ubTgGZDAFg1ieQCBj6Dhg7SKmhW8LmUB eC6iRQL8xW8RDZ8wn4AUQ8eVHib9YjCELj4nAOhqfjmcY4wE6iGxAkIsCwDvSel5v7iwa1WMefFt TCBqAixIUSPjG6IhkDtmQw/N4xkKTQ4TEkNZgKKhZ/u1DZD4CAKLIMSik/p3WzF5sgjYXiuuQMIS YEFK2K61p2GTJ0tt2Xebj5EyMArAab+vAdlTGS7VVAI0xSeAd6QQb2uBtIW8V8pUvJwZz9XzM2AG AdpoKqpTh3uEdlYA8gwBdDEjX87D0QQaALlMCLzha5Rz2NTc0X3lmsrxCMk1XeWsig6/YFOWr0Ge JoBzICWNhNo6q4ZcGwsJkFOKT4TUZiOgzSqate8aC8vmohKIAAtSAnWm6qaMHPljWl2bzBGapk2A lKMBZKguk/N3JYHPpMCrHuGbuWj6fqWubAFX2hYCLEi2YHdToVIMKSg7SQAXABgDoJObas91tZWA H5DvCeCV2rrUwg/ndqm0tTZcuOMJsCA5vovsqeDwszd19af4LwbEZQAOtKcWXGoCEaiCwAwB7emi Gd0+TqB2cVNMJMCCZCJMt2dFFnIflJQNh8BEgKbkRIrb28T1dySBlULIpzyBwCsLCntud2QNuVK2 EGBBsgW7swqlvUIer+cSQE7UPWI7q3pcm8QlUAtgtibFU4sKu70PCJm4TeWWRUKABSkSSgl5DTkt 3XSqJuVfJDAiRFyfhGw1N8qxBH4AxJP+hrpnit/otcOxteSKKSXAgqQUr/My18216wMXCOB6AH9w Xg25RklOoEoI8bzw+R5ZNKvnj0nOIumaz4KUJF0+YsyWbL/Xdx0gr5BAxyRpNjfTvQQCkPIdDdqD iwqz33NvM7jm0RBgQYqGlguvbfKmrd2MJrPtNBc2gauc5ASElJ8EIO4/eWD2m+xLL7EfBhakBO3f wfmlR2oCtwI4G4CWoM3kZiURASmwGpAPVLTPeXnFk6IxiZqeNE1lQUqwrs4bW3Y0AoF/QIjT2Vdh gnUuN8cgsFYK3LNPoPzFwsKBDcZBfnU/ARYk9/eh3oIh+WXHaELeIZs8bCdIq7gZTCAsgXUQuK9T oPw5FqawnFxzkgXJNV0VuqJD8zcdDCHvlvpG1tDX8FEmkOAE1gohJp/Yv9vLvMbk7p5mQXJp/w0Z u7mPkP7JACbwGpFLO5GrbS4BiRII/H3xzOw3eZOtuWityo0FySrSJpWTO760s8cv7wQEeVVINSlb zoYJJBAB+bGQ4uaiwpxlCdSopGgKC5JLuvm4/PUZmcJ7NSD/DqCDS6rN1WQCdhKY45f+vxYX9vzJ zkpw2ZETYEGKnJVNV1L4h9JxAuI+9jNnUxdwsW4mQFZ4//E31N/NLomc340sSA7uo2H5Gw7zC+3f AjjFwdXkqjEBNxDYKgVuPbl/9rNs+ODc7mJBcmDf5J61toMnNfVfgLgCgNeBVeQqMQF3EpD4XGja NRyTyZndx4LkqH6RYkj+pouEkFMAdHZU1bgyTCBxCEgIPFvvabx52av7lydOs9zfEhYkh/Th0HM2 95Ye//8ADHVIlbgaTCDRCWwWENcWzcyemegNdUv7WJBs7qncXOnVum66XoBMuZFpc3W4eCaQdAQE xFyfz//n4tk9NiRd4x3WYBYkGzuEjBak0J6WwJE2VoOLZgJMANgJIW49qX+3J9jowb7HgQXJBva0 pyhLeO+QkJPYaMGGDuAimUDLBJYJgSuKZuR81/IlfEYVARYkVWRbyHfouLJcBPCUhOzbwiV8mAkw AXsJ1EOKe3Z07HYvh7mwtiNYkCzinXvx2nRvTdq9EriOw0JYBJ2LYQLxEfgCUlywuDB7VXzZ8N2R EmBBipRUHNcNGVt6hJB4CcCAOLLhW5kAE7CeQB1tqF0yI/thdtiqHj4LkkLG+fnSUy7KbpWQ/wBE isKiOGsmwASUEpBLPBouXji9+3qlxSR55ixIih6A3DEbemhe7WV2+6MIMGfLBKwnsA3AxMUzc96w vujkKJEFSUE/D87fOFoT4lkA+yjInrNkAkzAVgLyv/7MhknFz/eqs7UaCVg4C5KJnTpy5I9p9W3b TBGQf2HDBRPBclZMwHkEVnr8KFg4K2e186rm3hqxIJnUd0PGlO4vPHgdAkeZlCVnwwSYgLMJVEHI yxfP6D7d2dV0T+1YkEzoq6H5ZadLIV/gKToTYHIWTMBlBIQQj6bsrLpp/vx+9S6ruuOqy4IUR5fo VnRa2Z1S4jaeoosDJN/KBNxP4DMhcW5RYc6v7m+KfS1gQYqRvR6zKCXtVQiMjDELvo0JMIEEIiCB LR4p8hcVZr+XQM2ytCmapaUlSGF5+WUDvKnpn7EYJUiHcjOYgAkEBNAlIAKLho7deI0J2SVlFjxC irLbh+SXni0EXgTQJspb+XImwASShICAeC6lsuoqXleKrsNZkKLglVdQeiuAf/F6URTQ+FImkLwE lnkatXMWzun2W/IiiK7lLEgR8KL9RQ1tsyia60URXM6XMAEmwAQMAmsDHjFq6WvZJcYBfm2ZAAtS y2z0M7njSzt7/JgD4MRWLuXTTIAJMIFQBCqkkAVLZnRfGOokH9tNgI0adrPY611u/vq+Xr9YzmK0 Fxo+wASYQOQE2gsp3h4ytnRi5Lck55UsSC30+5D8smM8wvMhB9JrARAfZgJMIBoCXiHxVN7YjXcC kmemWiDHYEKAyRu76QzIALkDyQpxmg8xASbABGImQBZ4vt+6XVFcLHwxZ5KgN7IgNevYvPyNl0EI MmDwNDvFH5kAE2AC5hCQmJ9Zh3PnzcupMSfDxMiFp+yC+nFIQdlNEOIpFqMgKPyWCTAB8wkIjKxJ xyLy+GJ+5u7NkUdIet9JkVdQRvuLaJ8RJybABJiAVQS+9kvvqcWFXTdZVaCTy0l6QZo8WWrLVpU9 JoE/ObmjuG5MgAkkJgEB8ZNPw7Di6dnrErOFkbcqqQWJvHVvF2UU2fXCyJHxlUyACTAB0wn86pf+ vOLCnj+ZnrOLMkzaNaTcXOndLja9zGLkoqeVq8oEEpfAfh7hfW/4OaUHJW4TW29ZUo6Q8vNLUreh 42tCYEzriPgKJsAEmIBlBDZDiiGLC7NXWVaigwpKOkEiMSoXnQol5GgH9QNXhQkwASagE6C4SprU 8ooKu61MNiRJNWVH03Q0MmIxSrbHnNvLBNxDgOIqSREoGjq2tL97am1OTZNmhEQGDNtE6asCosAc dJwLE2ACTEAlAVkGIXMXz+jxg8pSnJR3UoyQfreme57FyEmPHteFCTCB8ARENqS2mJw8h78ucc4m gSBJUS7KHgVwfuJ0G7eECTCBJCHQwyM8i3LHbOiRDO1NeEEaMrbsHt70mgyPMreRCSQsgQM8Hu3d kfllXRK2hb83LKEFaUjBxluExN8SvRO5fUyACSQ4AYGBjQjMG5q/vX0itzRhBSlvbOmVAuLeRO48 bhsTYALJQ0AK8UcpamfT1pVEbXVCCtLg/I2jIfEYgKSxIkzUB5TbxQSYQDABMWS76Phsogb5SzhB Gjxu0x81IV7jEBLBDzG/ZwJMIIEInJc3tuyhBGrPrqYklCCReaQIBOYByNzVQn7DBJgAE0g0AhLX Dc0vTbj18YSZ0iILlEaBjyRk0tjsJ9p3jNvDBJhAVAQkpBy/uLD7jKjucvDFCTFCokW+Bsg3WIwc /KRx1ZgAEzCbgIAQz9IyhdkZ25VfAgiSFPoin8DxdkHkcpkAE2ACNhHI1AKBuYmycdb1gjS0oOz/ AJxn08PAxTIBJsAE7CbQ1ePV3kmEPUquFqS8/LJzJXCX3U8Dl88EmAATsJnAwVLUv+R2c3CPzRBj Ln5o/qaDIeRbABJ2k1jMcPhGJsAEkpHAH3oPrNTWlkxb6tbGu9LKLu/sDfsgRfsEQB+3gud6MwEm wAQUEJBC4NyiGTmzFeStPEvXTdlRKAmkaLTxlcVI+ePBBTABJuAyAkJKvKDPILms4lRd1wlSudj0 LwDDXMiaq8wEmAATsIJAG6kFZrnRyMFVgkQ+6iTkX63oUS6DCTABJuBaAhL9Aqhznc871wjS0HM2 99aEeIEdprr2K8IVZwJMwEICQmBMXsGmGy0sMu6iXGHUkHvx2nRPddpHEDg87hZzBkyACTCBpCEg G4UUQ4oKc5a5ocmuGCF5a9IeYjFyw+PEdWQCTMBZBESKFGKGbpnsrIqFrI3jBWno2NIxHII8ZN/x QSbABJhABARkDlI8z7lh06yjBWnImNL9IfF0BMT5EibABJgAE2iRgByVl192bYunHXLCsWtItN9o O8reBztNdcijwtVgAkzA5QTqhSaPK5re/UuntsOxI6RyUXo7i5FTHxuuFxNgAi4kkBaQ4tVRo0od G8DUkYI0JL/sGAlBXrw5MQEmwASYgEkEhMRBtZnifpOyMz0bx03ZDb9gU5a/PvAFgANNby1nyASY ABNgAlIKeeqSGd0XOg2F40ZI/nr/AyxGTntMuD5MgAkkEAEhpHh2RP76Tk5rk6MEacjYjcMBcZXT IHF9mAATYAIJRqC7X3gedVqbHDNld8LoLW3T0xu/BbCf0yBxfZgAE2ACiUhASoxZUpgzxyltc8wI KT29cQqLkVMeC64HE2ACyUBACPnYiRN+6eiUtjpCkIbll50C4AqnQOF6MAEmwASSg4DITvN7pzml rbZP2ZFNfE0GvuGAe055JLgeTIAJJB0BiZGLC3Petbvdto+QajPEP1iM7H4MuHwmwASSmoDA407Y MGurIOWNX3+IRMBV8TqS+qHlxjMBJpCoBA6oyRB32N042wRp8mSpwe99AhApdkPg8pkAE2ACTEDe OHjshkPt5GCbIC0r2XQlII+zs/FcNhNgAkyACewi4NWkeEIfLOw6ZO0bWwRpZH5ZFynkPdY2lUtj AkyACTCB8ATEse9/V3Zp+GvUnbVFkBqaxKiDumZxzkyACTABJhATAYl7cs9aa8vfZ8sFadi40qMA 2KbAMXUQ38QEmAATSBICAuiipabfZUdzLRYkKQIB+R8AFpdrB1oukwkwASbgTgIC8uph+RsOs7r2 lgrDkPxNFwHiWKsbyeUxASbABJhAVAQ8AWgPRXWHCRdb5qnhd48MPwDobkK9OQsmwASYABNQTEAI eVbRjO5vKi5mV/aWjZBq0sVNLEa7uPMbJsAEmIDjCUgpHjjyCmnZXlFLBGnEmC3ZEPJmx9PnCjIB JsAEmEAwgQM7lJddHXxA5XtLBMmX0vhPAG1UNoTzZgJMgAkwAQUEBG63KkSFckEafk7pQZC4SAEm zpIJMAEmwATUE9gn1Z/yV/XFWGB+7feARkdeKxrDZTABJsAEmID5BITENXnjN+9rfs575qh0hPT7 Jthz9iySPzEBJsAEmIDLCGSJQODvquusVJACAZC/OstMy1XD4vyZABNgAslKQEp5xfD8Tb1Utl+Z IA0pKD0ZwDCVlee8mQATYAJMwDICqX7NTwFVlSVlgiQgbQ/2pIwaZ8wEmAATSEYCUpw/ZOzmPqqa rkSQ8saVngSIIaoqzfkyASbABJiALQS8Aj5la0lKBAkBOdkWVFwoE2ACTIAJqCUgxflDz9ncW0Uh pgvS0PzSE3l0pKKrOE8mwASYgCMIeOFRY3FnuiBB4FZHIONKMAEmwASYgBICEvK84eM29jQ7c1MF KW/8+kMkMNLsSnJ+TIAJMAEm4CgCqb6AuMHsGpkqSPB7yL0E7zsyu5c4PybABJiAwwgI4PIR+es7 mVkt0wQpd1zZAQDGmlk5zosJMAEmwAQcS6CNT3j/bGbtTBMkLYAb2WedmV3DeTEBJsAEnE5AXntc /voMs2ppitPT3LPWdhCQl5hVKc6HCdhNwOsVaN9WQ1amQFaGQEaGhox0gdQUAaEBaali19x0Q6PU j1OdG30S/gDQ0CDh9wPVtQFUVUlUVgdQXRNATa20u2lcPhMwk0DnLOE5H8BTZmRqiiBpqekTAcnx jszoEc7DUgIkLL3396JvrxT03T8FPXK8yNnXgy77eJTUo7pG4tdSH9Zv9OGXDT78vK4Rq39uRFV1 QEl5nCkTUE1AAtcD8mlAxP1rK24DhNxc6fV0LfsZwH6qG875M4F4CXTqoOHQAWk4dGAqBv0hFQf0 NOU3WbzV0gXqu58asXJ1A75YWY/NW/xx58kZMAGrCAjg1KKZOQviLS/ub6O3a9mZksUo3n7g+xUR SEkROKR/Kv54eJr+v3t23I+8kpr27O4F/R9+StN0/MZNPnzxTQM++7oeK76pR31D3D8+ldSbM2UC REBKXAUgbkGKe4SUl1+6FAK53C1MwCkEaK3n2CPSMfiEdBx/VLpTqhVXPZZ9WocPP6vDxyvq9fWo uDLjm5mA+QQCwu/pVzRr3zXxZB2XIOXllw2AkCXxVIDvZQJmEKCR0PFHpWHw8Rk48ZjEEKGWuJAw LXq/Fp98WY/GRh45tcSJj1tLQEDcXzQz+2/xlBqfIBVsfBQQptqhx9MYvjf5CPQ5IAUjB2fgrFOz kq/xAN4qqsE7i2vww5rGpGw/N9pRBLZ2kuXdCwsHNsRaq5gFadSo0syaDGwE0CHWwvk+JhALAbKM yzsxA6OGZ6Jfr5RYski4e0iQ5i2qwdIPa1FXz6OmhOtglzRISHleUWH3V2OtbsyCNLRg46US4plY C+b7mEC0BPbt4sGoYZkYdybvMAjHbtbb1Zi7qAYby3zhLuNzTMB8AhIfLC7MoWjhMaWYBElKeeCM udULXp1TeQDtq+DEBFQSGHRQKkYPy8SQE03bEK6yuo7J+73ldZi7sBpfr4p5BsUxbeGKuIOApkE+ PLnTjQMOSv93LDWOWpBIjAB8T4XNW1iDp1/bCRalWNDzPa0ROO7IdH1a7pjD0lq7lM+HIfDNqgZ9 xFT8UW2Yq/gUE4iPAE2fP35fZyOTvwsh/mV8iPQ1FkG6F8AuSwoWpUhR83WREsg9PkMfER0yIDXS W/i6CAjQxtt5C6t1Cz3JExsREONLIiXQe78UPDlllxgZt3mFEFHt8I5KkKSU5E9lr4lpFiWDP7/G Q+CUY2lElIXDBrIQxcMxknsfeqoCbxfVRHIpX8MEwhJoQYzontOEEPPD3tzsZLSCdCqAkAWQKP3v 5Z1s4dMMMH9snQBNyZHFHE3RcbKOwKofGnTLPNrTxIkJxEIgjBhRdjOFEFGFJIpWkF4EcEFLFSdR eviZipZO83EmsAeBA3unYPTwTJw6OHOP4/zBWgLkmohMxskbBCcmECkB8gP59INdWru8vRBiZ2sX GecjFiQpJf3VqDZubOmVRaklMnzcINCxvaabbp9zuns3s1IYCWMdJjNDoLau6XNKCnaFojDa65ZX GimRVd53P/ImW7f0mV31pC0Y40a30Wc2WqnDJUKI51u5ZtfpaASJhl7Td90Z5g2LUhg4SXxKCGD8 WW1w6bi2jqZAISHWl/qwcZMf5OR0yzY/tu8IoKIygIqdgV1CFK4RXg/QsYMHnTtq6NTRg25dPdi/ hxf75XhBZuxOTrSPqfCtamzdHtV6tJObxHUzkUAUYkSlLhJCDI+0+GgE6U0AoyPNmEUpUlLJcR15 Vrj1Guc59aA/urRP59vVDbr7nTW/+pT7hyNh7pHtxYADU3BQ31TdiKNnjvO8kD8/sxKvzK6KSICT 4ynmVkYpRgawbCHEJuNDuNeIBElK2RHA9nAZhTrHohSKSnIdoz+8Z52a6Shfcx9/UY9PvqjDFysb 9BGQE3qka2cPjjg4DUcflgayNnRSuu/RHSj6gA0fnNQndtQlRjGiql4jhHg0kjpHKkgUnvzZSDJs fg2LUnMiyfP5vDFtcMlYZ0zP0R/UpR/V4qtvGxwfW4g8lx91SBpOOCYNp+Y6w+CDnLjSd/nnX3h9 KXm+wbtb2r6thosL2kayZrT7pt3vioUQg3d/bPldpIL0NtmUt5xN+DMsSuH5JNpZp3hYIKuxhe/V 6kHu3BqmgcSJggtS4D4nxHZ66fUqvPh6JU/jJdqXNkx7sjIFJo5vF6sYGTnvK4T4zfjQ0murgiSl bA9gR0sZRHqcRSlSUu69zuMBLhvXDgWj7bOeW7fBh/lLavQpJjJASKTUrq2GYSdl4KqL2tnerH8+ VI73PmYzcds7QnEFTBIjquWVQognW6tuJII0HkDM7sSDK8CiFEwjsd4POzkDt/zZPqMFGg3NXViD L1bWJxbYEK0ho4hDB6TqXi3sXG+a/U41XplTpVsehqgmH3I5ARPFiEgsFEKMaA1JJII0E0B+axlF ep5FKVJS7riOwoVfPLYtzjnNnlHRzHnVmLeoGmWbk9NEOWdfjy5M+WfYw5+e0keeqdB/DLjjieVa RkKA9tZdPiHuabrmRXUQQoT1nNCqIAUCcqYQ5gkS1ZBFqXk/ufOznaOiFwsrMXt+DaqqE2taLtYn gf6AnD0yyzYjkjcX1GD2/GqOwRRrBzrsvusuax/vmlGoFh0phPgi1AnjWKuCNOutquvHnJ71kHGD Wa8sSmaRtD4fWiuiX0/n2vCr/IXCSsycW+14Sznre6WpRK9XYMxpmbjiPHvWmZ54aSdef6tVhy52 4eFyIyCgSIyoZE0IEdbPPHnvDpu2+f5y7uqfGo/PO8nc4Gh/6JOCju09+OTLxJ/zDwvYZSfJ0uvZ aV0w4EBrvQ2QEP3ffeX63iF/cs7ORfSkBAJAyfeN+tpOba3EkYdYG0vqqEPT0KG9B6Wb/dhZyaPX iDrNQRepEqM/3bK14sWnMqesWnVnfILUa9Ckh0s3+Tuv/qkRLEoOenJsqArtKbpuIhldWpdmzK3C P6aU47Ov6sFCFDl38rNX8kMjXn2jGo0+icMHWSdM9GPzrFOzUF0r2S9e5F1m+5WqxOiav28lLyjp dahatGbV1F/DNTTsCGnI2M19hJR3UQbk14tFKRzKxD3X94AUfVOclc5Q58yvxgOPV2Dph3U8PRfH o0UjppXfNWDGvGpoAjjYQj96Rx+ahvbtPPh5nU93PhtHM/hWxQRUidH1d2zDqh9+30wtsGltydQl 4ZoSVpB6DbxxrADOMDIgUfplgw+nHMfTdwaTRH+lX7p33dwR/XqnWNLU5Svq8PgLO/HGuzU85WMi cRpdfvltAxYva9o71L+fNVOuB/VNAVkA/rYtgJ/WsZcHE7vUtKyuubQ9Ro8w3yMIiRH5iAxKaWtL pj4d9Hmvt2GNGobkl84SAmOa30V7H26/gdzbmZvY0MFcnvHmRhswrTTnfvQ5EiJnLoiTFdthA9NA 01H7dfcip5sHbTI1tMnSdMwNjVIfBZBn8A2lPtAG3a9K6rH2170CLMfbLabcT/uYRg/PwinHWec3 j4wdyOiBk3MIkBidaY0YUaP9XunvuqCwZ4t+UVsUpPx86dkuyrYCCLnbUZUokfnof54Na6runN5M 0JrQegMFzjvpj9b8saJ1ohdmVoH+qDsppaUKDD4hA8NPzsAhA2IbUWwvD+CDT+vwzmJn+oEbNSzT 0nXBjz6v04MB0pogJ3sJqBKjG+7YhpV7jox2N1SK/MWF2a/vPrDnuxYFKS9/w3EQ2kd7Xr7nJxal PXkkwieaXrnyAmtMht9bTt4VqvXwD05iRzvUzz29DS44t42p1fr863p9Ayn9UXZS0jToa4QTzja3 veHa+NyMptAW4a7hc+oIqBKj2+7djk/D/dgQeHLxjJwrW2pZi2tIvQbedJEQGNLSjXSc1pNUrCnR vDMthn7KJuHh8Jt+jqboLjjXGu/cT76yU18r2rzFOTbcNCKiP8r33baP7prHbMA53bz6iOsPfVJR XRvAxjJntJ0s8mh9icJy0B6zvr3UrxfSKJy+4zSlSdF3OVlH4M8Xt1MSDqZVMWpqYoe1JVMfaam1 LY6QhuaXLpACEUX645FSS3jdcZz2FNHUDXleUJ30Hf3vVDsmDhG1lzaTjh1tvZcDGimR/z0aOTkp 0ZoC/YK2Kt37nx1YvIzjLVnBm8SIPHqYnSIUI71YKf09lhT23BiqDiFHSLm50ivaVP0XQEQT5zxS CoXWHcdGDc/EnZM6os/+6n8VP/x0BV6aVYXKKmdsmKTRwLgz22DaHftYuk/HeDIoSuzQkzJ0I4kd lQE4ZbT4/c+NmPVONVJThCUboGmt0uMR+KpkD4ssAxO/mkTACWJETdEgVqxZNW1lqGaFFKRex151 tJDy6lA3tHSMRaklMs49ftn4tpg4Qf16EVnO0eZWChXuhETesmmt7N93ddajtNpdpwN6pmBEbia6 dfVie7kfW7fbL9iNjdBHbr+W+nGyBRFsDx63vWsAACAASURBVOmfqntuIevE6hqewjP7mVQlRpOn lmP5iihH+EJsXVsy9a1QbQwpSH37T5oAgWGhbgh3jEUpHB3nnOvYXsOV57eDFRtdyWKSgrrV1tn/ R4aEiPZVPfqvziAXN05LfQ5IwWl5mejU0YMt2wIor7BfmNat9+mjWlpfG/SHiCZMYsZKJvW0zWB9 qR9ULidzCKgSI4qJ9cEnMRnoZK0tmfpYqNaFXEPKKyibC8hRoW6I5BivKUVCyZ5rjjksDffc2kl5 4bQ2MnNuFTY5wGiBhIj+0N9wuXXrImYApvW2eQur9T1NZuQXbx4nHJ2OO28yf/9hqHqRN/cXX68K dYqPRUHgTxeocYIcb4DGVCm6zi/M3tK8KSFHSL0GTnpYADGvfPFIqTlmZ3ymxcxbrwm5rczUCj79 WiWeea0SVQ6YeqHQ3/+7vwsorLrbElmbjh6RpW++JWG321np+tKm0VJ6qoaBikdLhw5MA0XI/ea7 BvicYYzotscHThUjAtkoRPG6kqk/Noe6lyANz9/USwp5W/MLo/3MohQtMbXXTxzfFpeOU2vSvWRZ LR5+Zifo1e405MQMkH8uFRZFVreN3PzQVGN6uoYNZfavsaxYWY9fNvpwyrFqrTIP6puqm+GvXN2I Tb+xKkXz3CkTo3+bE7peCPnz2pJpxc3btJcgHXDwDacC4tzmF8bymUUpFmrm3pOWJnD1Re2Vxy4i bwsPP70Tv2219w8HucK59rL2utFC1857Pd7mwrU4NxqV0BoLWb+tXe9DXb1963L03SaHrRlpAqr9 4tEot6IyALL+49Q6AVViNOVxMs+Pac0oRKVFw9qSqS81P7HXN7b3oJsmAji2+YWxfmZRipVc/PfR lM+F57bF6UPNd5wYXLsHn6jQg+YFH7P6PcVposXb8We1Qbcuez3WVldHaXmDDkpFwag2ujB9v6YR ZBFnRyKHrZ99XY/ynQEce4TaKdE/Hp6u7xejDbycWiYwcUJbFIw23+MGidGCYlNnPjpflP/gA8XF e8ZH2suoIa9g43JAmCZIBjo2dDBIWPOad2KG8vUicoY7Y16VrdMpZC1HfvdIkJI1vTK7Cq+9UWXr iClnXw/yz2ijIuz1Ht1Khh5Pv7rTEVabe1TMAR9IjGhfndlJgRjpVfT40X/hrJzVwfXd46ckOVSt FVX/BmD6LkkeKQVjV/ueHkrVgfTojyB5bq6qtmfaiJydXj6haR8VbTBN5kR7eMjlEfXEdz82gmIg WZ0qq6Ue/ZnqQF7RVSUa9Xfr6sGOioDt08Oq2hhLvm4TI72NQi5vvkF2D0HKOfjKgwBcHwuQSO5h UYqEUnzXkPHChflqjRfuf2yHvpM/vprGdnf/vim4dFw7XH1RO9CGUjsTTR9R8Luff/FhQ5kfASn1 zZ121YmE4PwxbdDogx5M0w5h+mZVA35c68OQE9QZPPTaLwWnDs7E9h0BikRqF27HlKtKjMizyjtL TJ2m24OZEFi7pmTaouCDe0zZ5Y0tPQ8SLwdfoOL9iNwM3HyV+ebHyR66QtUGOOMZoCm619+2xw9d v14p+nTQaUPUrocZbW3p1fDYTYEEySlp89Q2S9MNSM4bY/7USfOyWvtMDmxnvV1tS+j3lBShj2DH nBbz7pHWmqeff3lWFZ6fWRnRtYl4kSoxeuSZCt3PokpmUorFSwqzhwaXsccIqffASRcBOD74AhXv KaTx5q1+0EY7M1Oyegnvso8Hl09ohzNPVfflnzm3Gv99YaflfugO6OnVR3yTrmwPEiW7Erk9euqV nfr+KtqP01KimE7kk418wdEIhabT7EpHHpKGC85pq6+30FSelYnaTgYPZAlI9VCVaOqW9iuFDXmg qnCb8724oK0+VWt2NawQI6qzEGi/tmTqA8H133OEVFBKw6c9FCv4YrPf80gpfqL0B2/a5H3izyhM DnZEcqV1IfJArvoXdphm66dKvm/QfynG6o2a3DSRqfa4s+wfMVE/vrmgOuTIrjUO8Zy3IuAjjd4L 36pC6WZ7tx3Ewymae0mMzj/H/GfKKjEy2iok9i8qzPl112fjDb3mFWwsBUR28DHV71WJ0pz51Xjs +cQOl2yFJd3N/9ymx8pR/RwY+dOUF5luF4xWN9ozymrt9YH/7sDC98yZQ9+3iwdnjshCwSj72/XQ UxV4u6imteabep6CAF51oZrQB8EVjSYMQvB9bnqfKGKkM5cYubgw512D/64puxMn/NLRG/DcbZyw 6lXV9B1t1mvbRkOihkomx6g3XKHONxv5orvlX9vx68aWp6fMfkbox8kjd3dW7pamtXrTH2zyTk7P plmJPFiv+KYeRR/U6lN5FIPKrkRulAb8IVWfygs3/Whm/Wi9jb6LZHBxxMHqpvDyTsrAth0B/Jig xg6qxOh/L+3EnPnW/kjRny8hvlpbMnW58aztEqQDD/rrURC41Dhh5SuLUnS06aGk0BGqEnldePyF naD1EKsSmS3/5RJ1AhtJO2hK6//u367UcovMo8kwYulHTUYRqr0ctNTunH29GHx8Bsj4wMrNpt+u brJKpLJVJRJcenLJ4i+REhnKXKTAgpbEqPCtaptQiQ1rS6bONQrfJUi9Dp40EsAZxgmrX1mUIiNO bkFUrkfQw0lB9KxMNNq74jz1cZlaahO1+dZ7t+um0i1dY/ZxcpRKI4YPP6unxV0c2Nseg42DD0rV nbeSAYJViUZlbxXVIC1NA4WcUJHIBD4zQ8Pn31jXLhXtMPIkMbpkrPk/Qu0VI2qdrF9bMu0Zo527 BKn3oEkXmOkyyCggmlcWpfC0/nJJO6WL/HdOK8f8peasmYRvye6zfQ9IsSykwe5Sm96RR3ISom+/ t9YCLbgeFPPo4y/qdXGiUOoUE8nqRKO01FSBL1ZaN6Kg+FiffFkP8rWoKs4STYt2bO9ByQ+Nlo72 ze6/xBUjIiXarS2Zep/BbJcg9Rk46RoA/YwTdr2yKO1Nnhb6aUGYFsVVJLJQuu/RHVi52ro/SEY7 aPqxn8Wjg+dmkBCV61M6ofYSGXWz8pWixH74WR2+WtUACoZn9aZf8o9nbFy3st0kghSm5OjD1Kwr 0QisXRtND3hIG2ndllSJ0bPTKzFjrl3TdHv0QlrvQ/76xNpvH9Qrs0uQeg2c9A8Anfe41KYPLEq7 we/X3YsJZ6nzETb7nWrQ2okdsXbItFvFBund9PZ890JhJf7vvnJ9zcQOLwZ71ib0p81b/Hj/kzpQ yIWMdIH9e1jnFql9O49pVoWhWxf6KO2RIiexZDWqItEPnjOGZeplbCxzj1m4KjGiH2SvzrF2Wj5c v2oy8Maakmnr6RpdkJp82FVOBcQugQqXgRXnWJSAwwel4rF7OiubZ6fQ4k+/at8ud/rCDein3tqM /O7d/sB2fP51gy1eC2L5vlD8n/eW12HVj43IzBCwwl8f+Yhb9UOjLXt5SCjeXlyL9FSh7Hknwftt WwA/rbNvijbSZyH/jCxMnGD+uiqJEX0fHJWE9t6akqlfU510Aeox8KoDpMCNjqokoJvdqvDo4AaT 8NzjM3D3X9WFGievC9PftPfBJFdH7dtqyh676W9UYfK0cny8ot62EA3xNo42epJF3k/rfPo2hpxu akdM6ekCxcvNinkTXeuNdSUKRKgqIi15hSfvEbSu5NREYnTlBUkiRmTWAJSsLZm6lPpDf7r98PcF 9nDa4Ji+MmJwmD21Y0QSdeLmWfJQoNJb9z8p6qNNf3SMB8vrgbJf/TPnVYOmIrdud8/0jMGlpdeP Pq8D/acfKqOHZYJc5qhIJx6Trlv92bm29uTLO1G+w6/kjzIxu+L8droFnhN94CWbGFF/aEAf41nW BUlqYn9h3ZYTo+yIX5NJlFQ9kAbsa2/fhlU/WG+8YJRvvHbsYP7sMDkSJQ8dm7YkjhAZvIzX4o9q Qf8piuqo4Vkg7+dmp306emwXc9oXQ/14x40dzW6enh+53UlPE3oIFSUFxJCpqu8+OaB13DRdEB8p sb/xUf+r0HvQjWcC4hTjoBNfk2FNidZUVMwbU3+SJR25VdlQZp73gXieE/pjkD/KHF9cNBp64L8V IH9zZLGVDIlCXsxfUqOviZgdnHDRB7Uod4BFGnkJ+fDzemiK9mmRWTgZcnz6pf17lc4ckYmrLzZ/ Y7grvKELYG3J1Ifoe6uPkITUekp9b7Ozv8qJPFJS5RKEetSJfv0aTJjCp82VJLQ//2JCZs5+9Fus 3btLa7CguAan5WXihsvN/4PWYsEWnfh5XaPuZd7nB+iPttmJ8iTBe+H1Sj3on9n5R5IfRTy+5lLz +84VYtQEKCc3V3qLi4VPHyEdMOCma4RA70jg2X1NIo6UKKYJuc5RkWio/uQr9lnStdQm2ogZT7hl cm1EFkO0sZQTdN9t5GFjZ6XEMYfHt6eHhN4JIySjX/1+6KMYVcYOtFcpI03Tpwgrdlr7PJEYXXtZ UosRdbOW0rby6Z+/nbZTN3ESQvY0Ot8NrzRSomiGZicydCDLLyuT7groTDVi9NQrlfofbSvbw2XZ R4AMEd54txpTHt9hXyUUlkzGDuRdQ0UaNTwTz0ztAnKlZFVSJUZkPetEg41wXGUA+9F5w+a2R7iL nXhu3qIaUOwOs5OVokTid+4Zarwv/PupCpCTVE5MIJEIvPZGFR58wvzvvcHooTv3gdlrckbewa8q xcjOvYXBbYzmfUBqetgjbdSoUpqYNX9yNpraxHgthUhwqyjRnLFheh5j81u87Z8PlevOK1u8gE8w ARcToHUzitOlKt11c0cMO1mN1wiqM4VZUTFNRyMjN4oRMZEIdKNXrTrTY2lAPrMfIpWiRNNpZidy B3PdZe2VLNBSXW+6axve+9iejY1ms+L8mEBLBChkxgXX/qYbtbR0TTzHb/lzB9AoxuykKiCpm8WI GAsNXejVK4Xs7OQ9SJE8ECRKlMz+1WFMpz3xkjmRZ7MyBSaObwearzY7kbXZ7Hersd7CgHpmt4Hz YwLRECjb7Mdjz1cgINVY4NHfE/JcMdMkJ6QsRmF6V4ocOuvVAgFXj5CMJjpdlLp29mD8mWqcpJIY Pf3aTlBUUk5MIF4CNbXWWprFU18yB//PsxXw+SQorpbZieJ0kfd18vsYT1IlRq+/Ve3aabpmPHXH 3l4BdEmUP2NOFaVuXTwYO1qNGJFVFXnr5sQEzCIg3aNHu5r8+Is7dR91tLnc7ERRWj2aiNlyjdw9 me36jNpIYmTW7I3ZzKLPT+xL93ghRQe4fc4uqPVOE6Xe+6XgySlqonok1gMZ1In8lgnEQID2pdXU Slx+nvmRVcnVkNeLqEcjpxybjr9f1yGG1oS/JfG++1KHpEnR9CZ88911lkRJhfUdrSlFY+jQr5c6 MaJFzMT5deSu54tr61wCtNVh2pNqzMJpI3c0338So9tvMN8XX+KJkf486aA0IPEEiZpntyhRBM7H 71MzMiKXIG4173TunzKuWaIQeGdxDf4xpVxJcyL9UapKjMgNWIL+EG0aIQkI8yVcyaMQfaZ2iRIF 1vv3nftEX+EI7qBpCbftwo6gWXwJEzCVAIXquO4favYqkSiF8+hysqKRkRN9UprYaem5F69NJ08N 5jtSMrGW8WZltSgdc1gaptyuRoz+99JOR7uRj7ev+H4mYCaBku8bcOmNW5TsVaJN7aEcotL3/x8K pukSXIyaur06q4NXAubv/jTzqTIhLxIlSqr3KdHDeM+taqK8PvxMhZIvlgl4OQsm4FgCFMLi+cIm /3dm7/8zPIXTd5OSqu9/UogRgFQEMryQSHdosFhTH3LVovTFynplYnT/Yzuw6P1aU3lwZkwgWQiQ B+9HnlWzgdYQueUr6pR8/5NFjOhZDABtvBCIz1e9i55qlaJkeHUwG8fkqeVY9im7AjKbK+eXXATI EzptoA0EpOk+JEmUDGEyk+qbC2rw2PPJs8fQ70EmBehL6DWk5g+IKlFqXo4ZnynC66df2R/N0oy2 cB5MwAkE6A88xVdS9QPSrDaSGJGAJlMSgUAaCVLSjJCMznWDKN04eRu++a7BqDK/MgEmYBIBMpuu b5BQ4dXBjComoxgRN02KtmRll24GRLflQaLk1OHw9XewGLnteeL6uosAbZ+g/XxOS8kqRkY/0Agp aRMtGHo9wJUKwkzECvWKm7diza+Nsd7O9zEBJhAhAdrP1+iTuGSs+a6GIqzCHpeRk+Rkm6YLBiAR aJPUgkQwCt+q1pnYLUr0ML65sBrr1vuC+4jfMwEmoJDAK7Or0NAgbf9RSt9/w3xcYXMdnbUAPCRI zvh5YCMqu0WJHsbpc6uweYvfRgpcNBNITgL0/acwFuG8L6gkw2K0my4JEq0jJX2yS5ToYXx5dhW2 lbMYJf1DyABsI0DT936/NH3zfGsNYjHak1DST9kF47BalOhh5MB6wT3A75mAfQTI0Ims71TELgrV Khajvanw6KgZExIl8hmnOrEYqSbM+TOB6AksKK7FPx9S4yk8uDb0/f/fy+r/zgSX6Yb3JEgujA+p Fq1qUWIxUtt/nDsTiIfAex/XKRUl4/tfV58osbrjob3nvSRITZ4H9zye1J+yMgVy9lU3m0luRkYO yUxqxtx4JuBkAocMUOcvgL7/+WeYH2rdyTwjrZu6v7qR1sBh15EYTRzfTolvquCmUuRJEWR2Hnwu Gd5npFPrOTEB5xG47rL2yr//FBKdEsc2293/EvCzIO3mAavEyCiS9j4JAcyc17QXyjieDK8eXr1M hm52VRszMwQun6D+x6gBhUXJINH0KqBV0Z8FdiUNWC5GRldccX47x/rUMurIr0wg0Ql0bK9ZKkYG TxKliwuSfiuogQM0Qkp6d9JWj4x20f/9DbkuSfEKHr43BxPmcy0vCIehw6eiIdCtiwdjR7dRPk3X Up14pNREJiBkJQlScvk4b/ZU0DDdijWjZsXu9ZEeSq8XePpVtjHZC06IA7SJkRMTiJdA7/1S8OSU zvFmE/f99P0nv3rkyihZk9S0eg0yuUdIVs4Zt/agjTuzDSZO4OF7a5z4PBMwg0D/fs4QI6MtNFPi 1JAYRh1Vvnr8qNEgkncNyQprmmg7kESJLPA4MQG7CGRkJL4F5CEDUvGfu+0fGTXv42QWJQ2o0gSQ lNuFnShGxsNJ0SztcvRo1IFf3UugPs64jhr9VUjgdPxR6Zh2xz6ObWGyilIDtNqkXENyshgZ35Kz R2aB/jAkc3wUgwW/RkegsZHX11oiNuzkDNzy5w4tnXbMcRIlSkm1ppRVvUOTkOodNzmmmwFVYvT6 73GVzGzqmSMy9fqmpyX2L1YzmXFeTKAlAqOHZyoTI4pAa3YiUco/I8vsbJ2aX13x873qvIDY4dQa ml0vVWJ0273b8elXTdbzNN1mZiI3I5QK36pC6WYOUWEmW84reQgUjMoC7flTkcgZK/m/o2SMbMwq xwgcakQiMCtfB+aj65AmZHIIkhVi9MRLO6FipESi9OIjXXFQ3xQHPkdcJSbgbAIXntvGEjGi6bWX Z5lvtk2ilAQjJX2mToOQCT9CskKMjK8kiZIqV0CP/qszjj5MndNHow38ygQShQBto7gwX81Witsf 2D0yMniRbzoWJYNGNK9NAyNNAluiuc1t11opRgabJ1/eielvmv9LifK/99ZOGHJChlEUvzIBJtAC Ado+QdsoVKS/3bMdy1eE9rqmUpTMXhJQwSa2POVmuk8LaFpZbBk4/y47xMigQh4XVPxSovxvu7YD Rg3j8BUGa35lAs0JXHNpe6j64339Hdvw+dfhPa6pEiUSWTLOSMC0ldrkFVLobxKtgXaKkcGSHkpy B2L2Qiflf93E9qANjDPnJp+ncIMvvzKB5gQ6ddBwwTltlfmlu/KWrfh5XWPzYkN+pu8/JcNXXciL Yjh47WXt9bso5HrCJCFLqS3erBp/WU2CzQA5QYyMB4UWOn0+4PLzzJ/HvuK8dsjK0KDC5NSoP78y AbcQ+EOfFDx2jxrvCxTldfa71Vi/0RcVDhIl8lFp9tRhoomSDDQtHWnz5uWQzCaM1DpJjIwnd8bc KvzvJTUOMcj31VUXqjFnNerPr0zA6QTI+4JKMZo+typqMTKY0fS9ijVlEqVEmb4T0DYRLyNM2gYD nptfad7Y2LdjZjuC9xnFmi/tI3jkGTWO1c85PQvU9pQU3kAba//wfe4lcFpeJu66uaOSBtDI6OnX dmLzlvj2AKoUpTOGun9NSRMB3ZZBjxgrpVgvhDxQSY9alCn9QSbPBmanux4q37XpNd68ac63vkHi 5qvMd11CbScXZLPnV2N9aXTTCvG2i+9nAnYRoDhGKqbDqT1vLqjBo89VQJrkickILWP29N31l7fX 16oXFNfa1Q1xlys0/EqZ6CMkoQXWx52jjRmoEiPagf3+7zuwzWoePTQkcioSjQ6fe6gLDh/Ee5VU 8OU8nUWAjIVUidGc+dW6H0mzxMggp2qkRD9yR+S61hjA37ApRx8hNU3ZSbhWkFSKkeEOxHiYzHol kbv5n9vNym6vfKbc3glDT3Ltw7lXe/gAE2hO4OqL2imLHUTeVh57Xs2aL7WDREmFRxcXi1JpcbHQ p3V0QZJC6MOl5p3u9M9uFCOD6Zff1uNPt2zFO4vV2JP87S8dcM5p5vrVM+ruhFdaxD7qEB4JOqEv rKxD504e3eHwGEXP9qtzqkDeVlQnVW7GXClKQQMifQ0JkL8C7loQd7MYGQ/7T+sa8eLrVfD7ocQY 46qL2qFDew3PvGa+J2KjDXa9nnB0Ouh/8Ue1mLuoBt+sijMIkF0NMbFcjwf6jxBVTkRNrGpMWR06 IBVTFcYxou0TVoZ7MITP7A28JEqNPmDJMpesKQUNiHRB8gY8P/pFIKaHxI6bEkGMDG5bt/vx3xeb fpGpsBAcf1YbZKQLPPqc+l99RpusfM09PgP0f+F7tZi3sBrf/RTZpkUr66i6LCEAGjEksvk/rY/Q H1pVibZl2OFRW5Uo3XZNB/h9cpcXclXczMlXrjHy8dCbIwZOqawVlbcCQv9snHTiayKJkcE3EAA+ +bIebdto6N8v1Ths2utBfVPRsb0Hm7b4UbHTGT88AhIgsTQr9TkgBWT+26mjB1u2BVBe4Yx2mtW+ UPmQEJ11ahZ0p7uHmjd9+eaCauxwyHNC7R53Vhtcc0mTd4JQHOI99u+nKvDGu2qmziOp2+ff1CMz Q8OAA8397p9yXAZ+2eDT/0dSD7uuEZDPrymZ9iWVrwvQqlV3yt4DbzofgHPj+gL6XhsVpt3B8Uzs 6hQq97Ov1DyYlDftYj9zRBbW/OrDr1HuNlfBhKYUzj2jjel7pw7snaL7+WvXVmsS4MrEFKbTh2bi v/d2xjGHmydERj/TNHJdvUm2zkamMb6SN5ILzjXvh0vzakyeWo6iD+yf2iJRUvGD1BWipImpa7+d qtsx7BoR9Rk4aSSAfs07zCmfE3FkFIotPZgej8Ah/c39tWSURdNbldUSqx0wtdW/Xwp65vy+jGlU 0KRXGhWSAGdladj0mx87qxJDmIafkoHrr1DnXLeqOoDnZ6rxVB9N12ZmCPzpwvagTd+q0rW3b8OK b8I7SVVVdqh86QdpMoqSP+C9bd2qKfpDt0uQeg2adAyAY0OBsvtYsoiRwfmrkgb9F+qRiqzIjjks TRc9KsfOJCFw8rHpSqswoF+qPq2Vnq5hQ5kP1TXO+OUfbaPph8S1l7bX14q6dNr1tY02m1avX/5F vel771ottNkFBx+UiosK2mLkYPM3ulNR5H2BRkY0neW0pFKUflzr078DDmvzzqWF3f5u1GnXk917 0KT9AZxhnHDKa7KJkcG95IdG/LY1gOOPVvMHm0ZgNK21YmW9aTvRjbpH+kpThxeca77T2VDlD/xD qm6BlpoisHa9zzFTUqHqGnzsxGPS8eeLm+L67Ntl19c1+BJT39Pifumm+NzkxFMhcoMzeVJH9NpP TXRk8r7wyLMVqKl17g8TVaJEcdRoZmSjjf0b4tn4Zm3J1KeM47ue8D4DJmVA4FLjhBNek1WMDPZk Fk4PUJ6iTa40rUW/trduD2BbufVTWrQLnkYsVkbBHXRQKgpGNa1d/bCmEY0ONcr74xFpuPrC9nro gpx91UxrGs+Z8frdj436pk3js9Wv5HlBpck6bUb97wvusDZVJUr0t8RhorR4bcnUN4xnbZcg9Rt4 fVVAaLcYJ+x+/csl7fSpFrPr4RQDhkjbRb9mPvi0DgJCN0yI9L5Ir+vbKwW0QL55qx8/r7N+CoP+ CLZv58FBfdX8Im6JA00LkZUfrddRHWgvmBPSUYem4U8XtsPFBW3RI9saITLa/ez0Sqz5xfpngJwC k+cF+qGgKlGwzKddth+PREnFd8NJoiQhX1tbMu1Do993CdLPqx6q7T3wxisBYc0cilGDEK80RXH2 SPMXM+9/bAeWfBg67HCIajjm0I6KAFaubkCbTE2JKFFDaZMppa9t2GBK0Tc7tPMoa1u4jqSpSwrh QRM4q35oBJng25EOGZCKyye0xcQJ7ZQZeoRr18x51SicZ32wx4EHpuLisW11k/1w9Yvn3JMv78Qr c+w31IilDZ9+mdiipEnt32tWTf3RYLNLkOhArwE3jxQCvY2TdryqEqMpj+/QN0/a0SYzyiQzaZV7 laiOhw5M09eVSJSsHDHQ1N2n9GuwrfUjJaNvDhuYpk+PEWea0rBKmPr3TcGl49rpI4QDelo7SjTa To5En/h9c7ZxzIrXkUOawkb03l9du+/9zw68VWTfHiMzOKoUpa9WNcQdWiOeNgrg1jWrpu6Ky7OH IPUeNOkQAMfHU0A896oUIze7Zg9mSsN4r1fgYEVm4bSuRCOGb75r0PfxBJet+j198eoaJFRZF0ZS /yMObhKm2jqJ739uVGbw0Wf/FN2SjEIH0KZeuxK5ynnyFetdS106ri2uPF9tYMnrbt+m/4izi62Z 5aoSpRG5mfji2wb8ttWWOettiwtzCjVkfQAAIABJREFU/i+Y056CNHBSVwBjgi+w6j2LUeSkv/y2 AVWKjQGGn5Jpy36lku8b8frb1boQqBLdSEjTWs4F57RFTZ3U15giuSeSa2jf1QXntMHNV3cAbeK1 Ky1fUYfHX9iJtxU5922pXZ06aLj8vHYw239bcHlk1v23e7Y70cQ5uJpRv1clSqcOtkeUpBTL166a +mIwiD0Eqc+gSWRz9OfgC6x4z2IUPWVaiCevC7Q/RVWi/UoZGZrlmwdp2oxEd96iGgT8AFnG2ZVI mC48ty12VjWNmGKtR86+Hpx/Tlvcek0HJe6hIq0X+fp7bnqlbk1ntfkvmbBTmHHyGqIqGaEjGhqc a9YdT9tJlMgNmNkM7RAlTcjZa0qmLQrmsYcgHTngwfJaUXUTAHVPTHDppH6KDBhozShRpumaIdv1 kfbxqLTAo4Jo0blrZ3tMw8l9DU0nLHivVl/Tor1EdiVy0UPCtG1HAD+uidxWvMs+TUL09+s7mu6r LFoW0/5XgUeeqcDPNljSnX9OG9D0pMr0YmElnplu/fSjyjaFypvWkhNBlITEY2tWTVsZ3Ma9Yk7k FWxcDghLPDawGAV3RezvyckmsSRHmyoT/TGjMOx2pe7ZXowelqnUnUwkbaNRBnkWX/R+bYtrTB3b a/pGXHIManci56E0NWd29NNI2kWbry/KbwsVPiiDy3/46Qp9RB18LNHfX3dZeyVha668ZSt+Xhf5 j65YOQuBAUUzcr4Lvn9vQRpb+m9IXBd8kYr3LEbmU504vq3uGdn8nHfnSFMihsv83Uetfdeze5Mw qdgaEG1LSJRopEpGEB4N6NhBQ98DUkBTfXYnCjlCnrvtECJqO7mF+scNHZVj+L/7tieM8UK0sFSJ 0hU3b8WaX5WKUsVJA7I7TZ68Z9yjEIK0cRykeC1aMNFcz2IUDa3orqXF4j9doNZ6iUK70wjBbl94 ZKlGMaTI3Qyn3QTI/Y9hGLL7qLXvaGMvTdOpTGS88PLsKmwrt8VCTGXTosrbpaK0aPHMnOHNG7rH GhKd7HPQLTXQpLIREotR8y4w9zNt7ly3wQdyO68qHdDDC7LCa2gEvv3ePgetFPPo4y/q9bAdZApv p/m0KtbR5EuRgW+9dzu+/V7pL9uwVdqvu1f3MpE/Su30MW3kJTdANDJN9kTfQRWb5unH3rJP69XE FhPylbUl04qb991eIyS6YEhB6W8C6NL84ng/sxjFSzDy+7t38+Lc07OUzDEH14KcVU5/swpbttn/ K5W8HdAak0rLw+C2O+X9C4WVeO2Navh89v5xpj9g9GtddbIruqvqdsWTf1amwMTx7ZR835VM3wlt 1OIZ3d5q3ua9Rkh0Qe+BN51EMd2aXxzPZxajeOhFf29lVUCfV8/KND8SZXBtyAcdCd9v2wIgZ7B2 ps1b/Hj/kzqQp3QK206/1hM5kX82GhF9sbLBMs8SoXimpQndKSo5R1Wd/jGlPOGtZ2NhSE6CVY6U Fr5Xq+99jKVuoe5Jlbj+p1VT97KQakGQbuwJiGGhMorlGDlOHHOa+UP4ZDDtjoV38D3kJ67BB5AH ApXp+KPS9TDMX5XUg8KT25nKNvtRvLwOZA1Hgd5UBQG0q43kXeH2B7brU5VWungK1V4ajf7v/s6g uFMqE60X3fVQue7WSWU5bs5bpSileIW+FGBSPLHvFhbmTAnFOqQg9Rn0Vz8gJ4a6IdpjtMCuIuoj mbLOX2J/6OFoedhx/berG/S9J4MVbqKldg04MFXf/PnLRp8jgp9RXJ+lH9Xhp3U+PRJnTjd3j5im v1GFO6eVY/mKekeEzZg4oa3ug0/1M134VjUee36na4MrquYTnD+JEq0jZ2WY64iZNuKmmidKs9eW TN1ruo7aEVKQ9usyZZOWVTUJQFw/e0iMVLgIof0wtIufU+QE1pf68M6SWqSlqgljEVyTU47NAE3j kLcFJyRqe9EHtfi11I8O7TRYEejOzHbTAv49/9mBDz6pc0RgwWOPSMNVF7XHqbnqrRtpi8FLr7vT U7eZz0A0eTU0Sn00QwJipkcHs0RJSPlQ8w2xRvtCCtK6dXcG+gyYdAoE+hgXRvuqUozs3JwZLQcn XU9RMmmXN8WfoXhAKtOgP6Tqng0oTLRTQkWvW+/TvT5s2uJHp44ePTihSgbx5j3r7Wrc/98KFH9U 65jRAe11u3Zie0tiNd3yr+1Y/AHPgsTyHNHUGlnbOlGU/H55/brvpoWMlBhSkAjAAQMn7S8EhsQC g8UoFmrW3UMjF4oSe9xRasKjB7eEzM8zMzR8/Z29C+/BdSLXOfOX1FjGILjsSN6/8W41pjxeoY/q yDjFCYnWCGktmLxDq07U/kl3bQeNbDnFTkC1KJERRdQRlwXWLH29+90ttapFQeo98AYfhLispRtb Os5i1BIZZx3/cW2jbpGmKhJtcGv1taUxbWyLShtcl+D3xOClWbSxMoDjjlQvzsFlh3pPcXumPlGh W5FV7HSGENH+LgoaSBGcaSuB6vTcjErQfiq7jTVUt9Oq/FWKEu19ilqUBApbWj8iJi0K0lEDp5bV iqprAUT8TWUxsuoxM6ecHTubTMPT0zRY4biUotKSb7MNZT49tIU5rYg/F3KWSsK0s1KCnKhancik lox0SJBos69T0ojcDDx+b2dLng1qM5l0v2NxOAynsFZZDxKlX0t9GGNyFG5aU4pWlATEA2tKppa0 1N6QG2ONi/MKSmcDONv4HO6VxSgcHeefOy0vEzdeoX5To0Hi6dcqQVZjTkvkqJb2VV2p2P0StZvW huYuqsE3NoSND8edfAWeOTxTubNeow40RUcjI/rDyUkdgd77peDJKZ1NL4BM8p9+LSIryIBX+rss KOy5vaVKtDhCohv6DLipEwROb+lm4ziLkUHCva80fbXkwzp4NHMtc1oiQvuiaCqPDC2ctlZAZrOv zKlCQ4Oa/VsffV6nmzFPf7Pa1vDRofrmgnPb4I4bO4IiB1uR6IfJs9Mro1+LsKJyCVYGjb7JFRB5 1DAzGSOllasbQLHMwqTPFxX2eCTMeYQdIeXmr+/rEZ4fw2XAYhSOjjvP0Y57CmNuVZozv1p3Bkqe FpyWPB6A9m+dPjQzbstEmpKjX5M//2KvR4tQjIeckKH/oVJtfWmU7RQHvUZ9kulV5Ujp4WcqwqG8 Z/HMPUOWN784rCDRxXkFpd8DOLD5jfSZxSgUlcQ4RtE9J09SHzogmBYFWCPvzQHnLKMEVw8d2ms4 8uA0HH5wKg7okYIe2R60ydL2uMb4QKbltFb2/U+NesTdVT80wOc8vdVDZYwengmasrUq0b6qJ18O afVrVRWSvhw7RElInFRUmLMsHPxIBOlBALRJdo/EYrQHjoT8QPuVaDf+OQrcPoUDZncgwHB1a36O XBNlpGtITxfw+yRq6qTugbqx0dnrISSk487MwrgzrRsJE7up/yMPK7ypvflzZMfnfr1S8Ph9ataU QoyUtneS2V0LC0XYn2Vh15AIUu+BN9H8wkXBwFiMgmkk7nsaqZAvvPKdARx7RMTGlnED+eMR6Ti4 f6rulYCC3zk50Zw5hUCg/UJVNRL1DdKxIzziqGnAhLPb4P7/64RBijdHB/cbGS7c8eAOfPOdM7x3 BNctWd9v3xHAF9824NTB5o6OaU2JQqzTJvygNOutwnazgj6HfNuqIHU64cGN6XW7zb9ZjEJyTOiD P/zciIXv18LjESDv3lak7K5e5B6Xgb69UnQT8dLNYX9YWVElV5dB1oMU4v4/d3fG4YOsNW0n9z/P z6zi2EUOfIJ+2+q3RJSkEHevLZm6qjUErU7ZUQZDC0pnSiCfpm9UDPEp0Nbsd6pbqyufdwABKyLS hmqmU02kQ9XVScdIiGh96IbLrTPpN9pPBhxvLqwGuWzi5GwCNFr+9537mF5Jipf2n2d3NAqZ0aWo sFNYiwcqPCJByhu7cdzE8e1eUyFGHGzL9GdAeYb79/DirBHqg/+FasiyT+t0x7orvtljOiDUpUl9 jIRoxCmZuOkq64WIwD/1SiVmzHXePrOkfihaabwqUVr6Ue36ISdk7tdK8frpiASp9LfGU7O7eOdH kmE017AYRUPLeddSWJGrLmxnS8U+/6ZeN6H+8LM6W8p3aqFkpj5qWJbu6seOOvKoyA7q5pV5+KBU TLnd/JESgH5CiJ9aq2lEgiSlfA3AuNYyi+Y8i1E0tJx7bbcuHj3ECK1P2JHW/NqoC9O7xbVwumWb Sj5tszR9H9Gl49RHbW2pHbRW9PpbPPXeEh+3HD/msDTcc2sns6vbTQixubVMIxWkCwG80FpmkZ5n MYqUlHuus9r1UCgy09+swluLakB7gJIl0X4S2nk/api5llLR8CMLOgqi58SNzdG0g6/dTcBkUSoW QgzenXvL7yIVpA4AylvOJvIzLEaRs3LblakpAhcXtEXBaHtGSwavT76ox4L3an6PrOrs/UBGnaN5 peCHucel47QhmZY5Pm2pfryvqCUy7j9uoihdI4R4NBIiEQkSZSSlfBPA6EgybekaFqOWyCTW8aMO SdN/tZN3b7sTWfkUfVCD1T81QrpYm8hI4ZD+qcg7McNSrwot9R+NRskHnVO9arRUbz4eHQGTRClb CLEpkpKjEaSxAKZHkmmoa1iMQlFJ7GMFo7Jwxfn2GD2EIkvrGxQGfNWPDa4QJxKhAf1SccIx6SCW TkhLljV5KP92NW9wdUJ/WFEH+mF5500xuxFbJIQYHmk9oxEkmqSOacWSxSjS7ki869q20XDe2W10 wwcnta7og1p8+mU9Vqysh1OC4RGfrEyBwwam4Y9HpOlTck5i9uhzO0HrRZySj8Apx6bj9htiEqVL hBDPR0osYkGiDKWULwK4INLM6ToWo2hoJe61hw5IxejhWTjlOPun8ZpTXrfBh5XfNej/v1/TiNJN PstGUB3bayBXKxQgkTwoWOUJozmDcJ9fnVOFFwo5ims4RslwLkZRai+EiNiTbrSCdCqAiPcjsRgl w2MaXRuHnZyBW/5MNjLOTuRzjTwMbNzkQ+kmP0o3+0ARdmMZTZGT2k4dNHTu6EGPHA96ZnuxXw+v 7mm7a+dWvXfZBmreoqZwGWRaz4kJEIEoRalQCFEQDbloBYm+PRH5AWExiqYbkutaWhspGNUGl59n 356ZWIn7/UBFZUDf80SOVBt9TZYSXo8Atcv4nJmu6Y5MMzMFaI+QmxJZKc5dVA165cQEmhOIQpRO F0K80/z+cJ+jEiTKSEp5L4C/hcuUxSgcHT5nEKCRw4Sz2oCilHJyBoEHn6jAu0s5PIQzesO5tYhw psMrRPhwE81bGIsg/QHA6uYZGZ/Zh5VBgl8jJdC+rYb8M7Iw7iwWpkiZmX0dGSy8uaDasrUzs+vP +VlPYERuBm6+qsXp9/uEELdGW6uoBYkKkFJ+AODE5oU9N6MSr8xmh4rNufDnyAjQesroYZksTJHh MuUqms14/W0WIlNgJmEmYUTpICEERRuPKsUqSJcCeCa4JBajYBr8Ph4CZHlGUWp5xBQPxfD3khDN nl8NWhPjxATiIRBClJYJIU6KJc9YBYl26e0aCrEYxYKe72mNAIXZPuvUTN0dUWvX8vnICDz8dAXe XlzDHhYiw8VXRUigmSidJ4R4NcJb97gsJkGiHKSU1wJ4+LHnd2LOfN4stwdV/mAqAa9X4PS8DFxz qT2xfUxtjA2Zfb2qAXMXVuP9j+t4jcgG/slSZJssbdurj+57aVaWmBtrm2MWJCrw7oe3n7j0wzpa T+LEBJQTILPqIw9Jw+jhmTj+KOdtsFUOIMoCFhTXYt6iat2PX5S38uVMIGoCAuL+opnZYS2wW8s0 LkGizPPyS5dCILe1gvg8EzCTABlAnDE0ExPOZsu85lzJ6ek7S2qwoyLQ/BR/ZgKqCASE39OvaNa+ a+IpwARBKjsXQhbGUwm+lwnESsDrAY45PB3DT8nAicck76jpvY/r9FhQX5XU87RcrA8T3xczAQEx t2hm9pkxZ/D7jXELUm6u9Hq6lv0MIKKY6fFWmO9nAi0RoP1Mg0/I0GMFDTootaXLEub4N6sasPjD Wn1tqLKKR0MJ07EubIgATi2ambMg3qrHLUhUgSEFZTcJyCnxVobvZwJmEdinowcn/TEdJx+brscR Mitfu/MhH3sffV6H95bXYcs2ttm2uz+4fJ3AqsUzswcBIu6IY6YIUu5Zazt4UtPWA+AJfX5CHUeA zMePPCQVfzw8HUcflgba5+SmRNNxH6+ow8df1INHQm7queSoqwCuKJqZ85QZrTVFkKgiQwrKHhGQ 15hRKc6DCagk0LO7F4f2T8XB/VP10VOXfZzjcZsct67+sRG0FvTFtw1Y84u7I92q7EfO2xEEttZI /37LC3vWmlEb0wRpeP6mXn4R+AGA14yKcR5MwCoC7dtpOLB3CvockIJ+vVLQM8eD7vt6kZZm2tcj ZFPKNvvx60Yfftnow/c/NYBiMW36jafhQsLig84kIMUdiwuz7zKrcqZ+4/IKSl8GcJ5ZleN8mICd BMhIonMnD9q1FaBpP4p+2yZT6EKV4hWgDbspv//8CgRoszjg+X2wVVsndW8IdQ0S9fUSOysD+nTb zqqAbo69dbsfPtYeO7uXy46fQJVX+vdfUNhze/xZNeVg7mjG438Afs8EAKYKnVmN5XyYQDQEaPqM /nNiAkxgbwISeMpMMaISTF3dXfxaz29EFBFl924iH2ECTIAJMAEXEGjwavIhs+tpqiBR5aSG+8yu JOfHBJgAE2ACziEgIF5ZOL07WVabmkwXpMXTcz4A5BJTa8mZMQEmwASYgFMI+ODX7lZRGdMFiSop Ie5UUVnOkwkwASbABGwmIOTL8fqsa6kFSgRpycyc9wEsbalQPs4EmAATYAKuJODzBwL/UlVzJYJE lZXAZFWV5nyZABNgAkzAFgKvFBf2/ElVycoE6fdR0iJVFed8mQATYAJMwFICDR6pKV2OUSZIhEnT cFvTYMlSaFwYE2ACTIAJmE5APrWwsNta07MNylCpIC2anvM5gFlB5fFbJsAEmAATcB+BGq8vVdna kYFDqSBRIR4/bgfgMwrkVybABJgAE3AXASnwyILZXcpU11q5IC2clbMaAi+obgjnzwSYABNgAkoI bGvwND6gJOdmmSoXJCrP25hCo6TqZmXzRybABJgAE3A4ASlw97JX9y+3opqWCBIN9SQER5S1oke5 DCbABJiAWQQEfqxon/2YWdm1lo8lgkSVyKqlEOeitLUK8XkmwASYABNwBgEZwC0rnhSNVtXGMkGa Ny+nRorA361qGJfDBJgAE2ACsROQwHtLCnPmxJ5D9HdaJkhUtZP757wACTIF58QEmAATYALOJeD3 yMD1VlfP8kB6eWPLjoaUH5sdi8lqcFweE2ACTCBRCUghH18yo/vVVrfP0hESNW7xjOzPADxrdUO5 PCbABJgAE2idgAS2BOobyMuO5clyQaIWpkpBjd1heWu5QCbABJgAEwhPQOC24jd62fL32RZBml+Y vQVC/F94KnyWCTABJsAErCQgpPzk5P7Zts1g2SJIBPik/t2eAPS1JCt5c1lMgAkwASYQmoDPr8kr J08WgdCn1R+13KghuEmDx244VJMaWd15g4/zeybABJgAE7CYgJBTFs/o/leLS92jONtGSFSLpTN6 fA2IaXvUiD8wASbABJiA1QTWeVI9SmMdRdIgWwWJKphZKwmC0hgbkYDga5gAE2ACyUpACnH1wpe6 2e5v1HZBIg8Omha4nAP5JetXgdvNBJiAzQReXjIje77NddCLt12QqBaLpvdYDOBJJwDhOjABJsAE kojAJq/0X+eU9jpCkAhGXV3KzQB+dQoYrgcTYAJMINEJSImrFxT23O6UdjpGkD6c26VSCslTd055 MrgeTIAJJDgBMd1q56mtAXWMIFFFl8zovhCQj7dWaT7PBJgAE2AC8RAQpV7p+3M8Oai411GCRA30 pHnIDv4HFY3lPJkAE2ACTABSQF7qpKk6o08cJ0hkehjQtAsB+IxK8isTYAJMgAmYRUD+t2hmzgKz cjMzH8cJEjVu6fRun0DIe8xsKOfFBJgAE0h2AlJgdWatsNUbQ7g+cKQgUYX9m3P+CYmPwlWezzEB JsAEmEDEBBoAnEd7PyO+w+ILHStIxcXCJ/2YIIByi5lwcUyACTCBxCMg8dclM3K+cHLDHCtIBG3J 7JxfpBRXOBkg140JMAEm4AICby8uzH7E6fV0tCARvMWF2a8L4Amng+T6MQEmwAScSUCU+j24GBDS mfXbXSvHCxJVtVr6b4TEl7urze+YABNgAkwgAgI+aHJc8Ws5WyO41vZLXCFIywt71krNkw+gwnZi XAEmwASYgEsISIhbF0/P+cAl1YUrBIlgLpmx789CyIvYK7hbHi2uJxNgAjYTmLNkZrepNtchquJd I0jUqqIZ3d+EkA9G1UK+mAkwASaQbAQEfhQy/RI3rBsFd42rBIkq3imQc6uUgsJVcGICTIAJMIG9 CVSLgHZOUWEn1y1xuE6QCguFPw0YD4Ff9u4HPsIEmAATSGoCUgAXFRV2W+lGCq4TJII8vzB7SwCB MwE4dsexGx8GrjMTYALuJiCBfxXNzJnl1la4UpAI9tIZPb6WQlzMRg5uffS43kyACZhKQMq3Th6Q fYepeVqcmbC4PNOLyysoux2Qd5meMWfIBJgAE3APgZVCpp/kxnWjYMSuFyRAiryCspfIaWBww/g9 E2ACTCAZCEhgS8AXOKJ4do8Nbm+va6fsdoMXspMsvxSQH+8+xu+YABNgAklBoEZq2qhEECPqrQQY ITU9dCPzy7o0CnwkIfsmxWPIjWQCTCDZCUgJed6Smd1fSxQQCTBCauoKsrzzSd9IGr4mSudwO5gA E2ACLREQErclkhhROxNmhGR02uBxm/6oBQJLAGQax/iVCTABJpBIBKSQjy+Z0f3qRGoTtSVhRkhG x1D484CU4ynorHGMX5kAE2ACCUTglX0COdckUHt2NSXhBIlatrSw+1wJ/IX3KO3qZ37DBJhAQhCQ S8iIizzWJERzmjUi4absgts3NL/0b1Lg3uBj/J4JMAEm4FICnwqZPtzte43CsU/IEZLR4KLCnPsE xP3GZ35lAkyACbiUwKpUKc5IZDGifknoEVLTg6dvnH0cwJUufRC52kyACSQzAYFf/I2BExNlr1G4 rkzoEVJTw2njbPafAbwSDgSfYwJMgAk4kMAGf8A/NBnEiNgnwQip6RHLz5eebaL0VQFR4MCHjqvE BJgAE2hGQJZByNzFM3r80OxEwn5MGkGiHjzyCpnSvrxsuhAYk7A9yg1jAkzA9QRog78mcErRjJzv XN+YKBqQBFN2u2mseFI07oPy8QJi7u6j/I4JMAEm4BwCuhhJLS/ZxIh6IKkEiRpcWDiwoaPcni8l ZjvnEeSaMAEmwAR0ApulRwx2a8TXePswqabsgmHl5kqvZ99NL0PKscHH+T0TYAJMwB4CotTjl3kL Z+Wstqd8+0tNuhGSgby4WPg6BbpRDKWXjWP8ygSYABOwicB6v/SdksxiRNyTVpCo8eR+o5PMpjDo /7PpIeRimQATSHICAuInvyZOLi7s+VOSo0ges+/wHS3FkLFl9wiJv4W/js8yASbABEwkIMU3fnhG FBd23WRirq7NKqlHSLt7TcglM3JuFVL8lR2y7qbC75gAE1BIQOKj+pSGXBaj3YyT1qhhN4I93w0Z WzpRSDwBwLPnGf7EBJgAEzCNwLuZtThn3rycGtNyTICMWJBCdOKQgo2jBASFBc4KcZoPMQEmwARi JiAgnivv0O1K2hcZcyYJeiMLUgsdS5FnRSAwTwBdWriEDzMBJsAEoiQg/7l4Zs4dgJBR3pgUl7Mg henm3Pz1fb3CO19C9g1zGZ9iAkyACbRGwC+Aq4pm5jzV2oXJfJ4FqZXezx1f2tnjxxwAJ7ZyKZ9m AkyACYQiUCGAsUUzcxaEOsnHdhNgK7vdLEK+K34tZ2tqZfVQ3kAbEg8fZAJMIDyBtQGPOIHFKDwk 4yyPkAwSEbzmFZTeCuBfyRS2IwIsfAkTYAIhCYjlnkZx1sI53X4LeZoP7kWABWkvJOEPDMkvPVsI vAigTfgr+SwTYAJJS0DI51N31vxp/vx+9UnLIIaGsyDFAG342E2DAlLOYWOHGODxLUwgsQn4hJA3 Fs3o/p/Ebqaa1rEgxcj1xAm/dEzzpbwK4NQYs+DbmAATSCACehwjTRQUTc8uTqBmWdoUNmqIEfey V/cv7ySzzwBwL7sbihEi38YEEoWAxOfw4WgWo/g6lEdI8fHT7x6aX3a6FPIFAPuYkB1nwQSYgIsI CCEeTdlZdROvF8XfaSxI8TPUcxiev6mXH4GZEDjKpCw5GybABJxNoAoCf1o8I+cVZ1fTPbXjKTuT +mphYbe1qVXVJ9KvJZ7CMwkqZ8MEnEtgpcePo1mMzO0gHiGZy1PPbejYjWdKKZ7hKTwFcDlLJmAz ASnk44GMhhuLn+9VZ3NVEq54FiRFXZo7ZkMPzau9LIBTFBXB2TIBJmAtge1SYuKSwhxyJcZJAQGe slMAlbIsnt1jwz4yOw8Q/wAku5lXxJmzZQIWEVgqJA5nMVJLm0dIavnquQ8bV3pUIACywhtgQXFc BBNgAuYRqBPAbScOyH548mQRMC9bzikUARakUFQUHDsuf31Gpua5FxLXsi88BYA5SyZgNgGJLwFx /uLC7FVmZ835hSbAghSai7KjeQVlgwXw5P+3d26xVVRRGP7/PbW2oqYgSIGC8a6QoPLgJYKppYL1 Fi+p0AcUrw/6oDHxHhU1XuOLiYmJqVGjRvAIGjERQ9GCGNEgmGgUlYhSORRRqQhKy5lZZho1EUs9 9zMz5386ObPXWnutb03yZ86Zvbe2HSoZYgUWgUIJDBj48K8NjQ/rVNdCUebmL0HKjVdRrAeflliz ALBbANQUJaiCiIAIFEzAgA/DPS8CAAAGbElEQVRovF5PRQWjzCuABCkvbMVxap275RQL2AlgWnEi KooIiECeBHYCvGvG5Man9V9RngSL4CZBKgLEQkI0N1uNO7z3ZsIeAFBfSCz5ioAI5EOAS/2Mf0P4 Zmw+3vIpHgEJUvFYFhSp9bJtRwUueIa0mQUFkrMIiEC2BLbB7KYVqQmLsnWQXWkJSJBKyzfH6MaW Oen5ND4OYHSOzjIXARHIjoABfK7GMre+k5r4S3YusioHAQlSOSjnOMfs9p5RGboHAV6vlx5yhCdz ERiewDrS3di1qHHN8GYarQQBCVIlqGc55+BLDz6fBDEjSxeZiYAIDE3gJxrunj5lXKdeWhgaUBSu SpCi0IVhczC2XJ6eS/BRAJOGNdWgCIjAvgQGQDzl9/c/2P3GkX37Dup7tAhIkKLVj/1mE65dGgHv JiNuB9CwX0MNiIAI/E3gdd/827pTEzf+fUGf0SYgQYp2f/6TXXNHerTn2/0ArgN4wH8MdEEEqpwA zT6C5+7QceLxuxEkSPHr2WDGze09x3j0FgDoAKBd22PaR6VdVAJfkLina9G41wFaUSMrWFkISJDK grl0k8zs6JmKjHsI5AWlm0WRRSDCBIjvYbxvlDW+lErRj3CmSu1/CEiQ/gdQXIZb5/SebkFwL4i2 uOSsPEWgQAKbaXx0JH55NpWaMlBgLLlHgIAEKQJNKGYKLe1bTyXtXgDnFzOuYolAhAh8R8MjI7Hj eQlRhLpShFQkSEWAGMUQ4aGAvo87SVys/5ii2CHllAeBrwh7fEfD+Bd1LEQe9GLgIkGKQZMKSfHs 9i3HO8dbYZgHoLaQWPIVgQoR+JjEY9NPHPeGFrVWqANlmlaCVCbQlZ7mnLmbxwfm3QzjdVrHVOlu aP4sCAQElhn4xIpXx72Xhb1MEkBAgpSAJuZSwqx5vSP8Absy3OUYwHG5+MpWBMpAYDdgL4D25IpF TV+XYT5NESECEqQINaOcqSxYYG7lF73nerAbDZgNwCvn/JpLBP5FgPjGjM8EA3s6tcXPv8hU1RcJ UlW1e+hiW9vTkwLyKsKu1n55QzPS1ZIQ6AfwGh07uxY2rtRi1pIwjlVQCVKs2lXaZMOnplVf9s5G YNeSdqG2Jiot7yqO/hkMz9bAf1HnEVXxXTBE6RKkIaDoEjCzY9tY8zPzSV4Dw7FiIgIFEtgF8FWY 37ki1fRhgbHknlACEqSENrZ4ZYXHX2ydQeIKGi41YGTxYitSwgkEAFbC7OXa2rrU2y8ftjPh9aq8 AglIkAoEWE3ubW3fHDhwyIg2g3UQvBBAfTXVr1qzJGBYC+AV52UWLl84KZ2ll8xEABIk3QR5ETjz ou2H1B04cB7Jyww4D8CIvALJKQkEDLCPaG4JAre4a/HYb5NQlGooPwEJUvmZJ27G8PDAg+iFr45f YkAbgTGJK1IF7UsgA9gqI5cGe4PXupc0/bCvgb6LQK4EJEi5EpP9sATa28372UufxoAXwHg+aFOH ddBgnAj8BGAZzN4i6pd1pUb9GqfklWv0CUiQot+jWGfYfOkPTTU1nGVw5xhspp6eYtXO8EiHNQSW +84tH+2PXavzhmLVv9glK0GKXcvim3C4zmn1hvRJ8F2rITgL5HTtqxepfvowrIfDKgZ8NwNvZXfq 8F2RylDJJJqABCnR7Y12ceHPezu89FTzeRaIGQBOBzAh2lknKrvdMKwDsdrI9/v/qFn9wZtjfktU hSomVgQkSLFqV/KTbWnvmeCcdxrMzjDwVAAnAzg0+ZWXvMKMARsIfGLAGs+CNXu3T/i8u5uZks+s CUQgSwISpCxByaxSBIwtc348CgimATaNNihQk7Xn3rD92EmzL438FMT6gO7Tg3cHny1dOv73Yb00 KAIVJiBBqnADNH1+BMJ1ULUH+ZO9wJ9ixAk0d7yZHQ3iaAB1+UWNlZcBCF+13mjARho2mLPPXcAN Xanxm2NViZIVgb8ISJB0KySMgHHW3HSTH7hjBp+iDEcYrYmGpsHvxMSY/AQY7oS9zYAewLYQDD83 G7DJkRsz9f2bup8/ck/CmqdyqpyABKnKb4BqLL95/qa62j21Y3xzjQDHGvwxNDfKYA2ObDCzBpg1 GFy9ozWY8QA4Oxhm9QT/efraz75+vxMIxQTh9gUg+mDYS2CXGcKxPwJaH8k+GnfArC8g+hyw3Rx+ 9PZie+DqtmqNTzXemar5T7boKrYfCqI6AAAAAElFTkSuQmCC"/></symbol><symbol viewBox="0 0 24 24" id="webpack" xmlns="http://www.w3.org/2000/svg"><path d="M19.376 15.988l-7.709 4.45-7.708-4.45V7.087l7.708-4.45 7.709 4.45z" fill="#fff" fill-opacity=".785" stroke-width="0"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18.21 0 .41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.939v2.104h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07l7-3.94zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83-4.9-2.83zm-5 5.08v3.58l4 2.308v-3.58l-4-2.308zm10 0l-4 2.308v3.58l4-2.308v-3.58z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83-4.9-2.83zm-5 5.08v3.58l4 2.308v-3.58l-4-2.308zm10 0l-4 2.308v3.58l4-2.308v-3.58z" fill="#1c78c0"/></symbol><symbol viewBox="0 0 24 24" id="wolframlanguage" xmlns="http://www.w3.org/2000/svg"><title>wolframLanguage</title><g transform="scale(.12121)" fill="none" fill-rule="evenodd"><circle cx="99.197" cy="98.946" r="83.28" fill="#212121" stroke-width=".841"/><path d="M182.529 98.828a83.406 83.406 0 0 1-39.14 70.721.064.064 0 0 1-.038.019l-28.62-35.665 23.71 2.612s11.385 1.177 13.978 0c2.373-.938 15.175-18.963 15.175-18.963s-36.75-23.23-49.312-36.032c1.434-21.575-1.656-50.269-1.656-50.03-9.251 9.234-10.429 10.669-19.68 19.203-4.028-13.04-5.923-17.547-9.95-30.588-12.104 9.95-21.337 26.799-27.977 46.48a78.68 78.68 0 0 0-4.23 5.094 109.774 109.774 0 0 0-2.667 3.66 114.558 114.558 0 0 0-5.132 8.002 172.555 172.555 0 0 0-3.403 6.051c-7.706 14.475-14.034 31.066-19.515 46.001a.858.858 0 0 1-.092-.184c-14.988-30.912-9.502-67.85 13.822-93.072 23.325-25.223 59.723-33.575 91.71-21.045 31.988 12.53 53.029 43.382 53.017 77.736z" fill="#e53935"/><path d="M101.452 69.178s-1.416-8.295-2.373-11.367c6.401-6.18 7.357-7.118 13.52-13.04.477 11.845.238 18.006-.479 32.481-3.55-3.568-10.668-8.074-10.668-8.074zm-27.737 40.778s-6.64-4.029-11.624-4.728c1.435-3.329 5.223-7.596 6.18-8.773-1.913.699-15.653 6.86-17.087 12.084a74.804 74.804 0 0 1 11.385 3.79 35.993 35.993 0 0 0-8.774 20.158s21.815-3.33 38.185-1.196c.283.168.609.251.938.24l8.534.239 27.111 45.136.221.35c-.037.018-.055.037-.073.037-51.133 18.485-88.085-15.543-95.976-27.443.034-.102.058-.206.074-.313 7.1-30.017 15.855-65.939 30-76.552 7.356-12.82 9.49-31.783 22.751-41.734 3.33 9.951 8.553 30.588 12.103 40.539 15.653 15.652 39.361 35.094 55.234 43.15 1.656.956 3.79 7.596 3.79 7.596l-6.401 8.056-68.276-6.879a54.462 54.462 0 0 0-4.58-.183 86.848 86.848 0 0 0-14.144 1.36c3.311-8.295 10.43-14.935 10.43-14.935zm22.054-8.774c3.789-.46 7.817.956 12.323 3.568 4.267-1.195 4.745-1.434 9.013-2.612-5.463-4.028-11.386-8.295-19.442-7.118a47.249 47.249 0 0 0-1.894 6.162z" fill="#fff" stroke-width=".936"/></g></symbol><symbol viewBox="0 0 24 24" id="word" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5M7 13l1.5 7h2l1.5-3 1.5 3h2l1.5-7h1v-2h-4v2h1l-.9 4.2L13 15h-2l-1.1 2.2L9 13h1v-2H6v2h1z" fill="#01579b"/></symbol><symbol viewBox="0 0 24 24" id="xaml" xmlns="http://www.w3.org/2000/svg"><path d="M18.93 12l-3.47 6H8.54l-3.47-6 3.47-6h6.92l3.47 6m4.84 0l-4.04 7L18 18l3.46-6L18 6l1.73-1 4.04 7M.23 12l4.04-7L6 6l-3.46 6L6 18l-1.73 1-4.04-7z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="xml" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m.12 13.5l3.74 3.74 1.42-1.41-2.33-2.33 2.33-2.33-1.42-1.41-3.74 3.74m11.16 0l-3.74-3.74-1.42 1.41 2.33 2.33-2.33 2.33 1.42 1.41 3.74-3.74z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 24 24" id="yaml" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="yang" xmlns="http://www.w3.org/2000/svg"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2m0 2a8 8 0 0 0-8 8 8 8 0 0 0 8 8 4 4 0 0 1-4-4 4 4 0 0 1 4-4 4 4 0 0 0 4-4 4 4 0 0 0-4-4m0 2.5A1.5 1.5 0 0 1 13.5 8 1.5 1.5 0 0 1 12 9.5 1.5 1.5 0 0 1 10.5 8 1.5 1.5 0 0 1 12 6.5m0 8a1.5 1.5 0 0 0-1.5 1.5 1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5 1.5 1.5 0 0 0-1.5-1.5z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 289.99999 290.00001" id="yarn" xmlns="http://www.w3.org/2000/svg"><path d="M250.733 218.418c-12.39 2.943-18.661 5.653-33.993 15.641-24.004 15.487-50.176 22.688-50.176 22.688s-2.168 3.252-8.44 4.723c-10.84 2.633-51.647 4.878-55.364 4.956-9.988.077-16.105-2.555-17.809-6.66-5.188-12.388 7.434-17.809 7.434-17.809s-2.788-1.703-4.414-3.252c-1.471-1.47-3.02-4.413-3.484-3.33-1.936 4.724-2.943 16.261-8.13 21.45-7.125 7.2-20.598 4.8-28.573.619-8.75-4.646.62-15.564.62-15.564s-4.724 2.788-8.518-2.942c-3.407-5.266-6.582-14.248-5.73-25.32 1.084-12.777 15.176-25.011 15.176-25.011s-2.477-18.661 5.653-37.787c7.356-17.422 27.179-31.437 27.179-31.437s-16.648-18.352-10.454-35c4.027-10.84 5.653-10.763 6.97-11.227 4.645-1.781 9.136-3.717 12.466-7.356 16.648-17.964 37.864-14.557 37.864-14.557s9.911-30.431 19.203-24.469c2.865 1.859 13.163 24.778 13.163 24.778s10.996-6.426 12.235-4.026c6.659 12.931 7.433 37.632 4.49 52.654-4.955 24.778-17.344 38.096-22.3 46.459-1.161 1.936 13.319 8.053 22.456 33.373 8.44 23.152.929 42.587 2.245 44.756.232.387.31.542.31.542s9.679.774 29.114-11.228c10.376-6.427 22.688-13.628 36.703-13.783 13.55-.232 14.247 15.719 4.104 18.12z" fill="#2c8ebb" stroke-width=".774"/></symbol><symbol viewBox="0 0 24 24" id="zip" xmlns="http://www.w3.org/2000/svg"><path d="M14 17h-2v-2h-2v-2h2v2h2m0-6h-2v2h2v2h-2v-2h-2V9h2V7h-2V5h2v2h2m5-4H5c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#afb42b"/></symbol></svg>
+<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 24 24" id="actionscript" xmlns="http://www.w3.org/2000/svg"><text style="line-height:113.99999857%" x="5.605" y="15.892" transform="scale(.91325 1.095)" font-weight="400" font-size="42.822" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#f44336"/><path style="line-height:125%" d="M4.744 2.031c-1.157 0-1.994.31-2.51.93-.515.612-.771 1.678-.771 3.197v2.467c0 1.408-.402 2.111-1.201 2.111v2.035c.8 0 1.2.679 1.2 2.036v2.654c0 1.512.26 2.562.78 3.152.52.59 1.355.885 2.502.885V19.43c-.447 0-.77-.151-.97-.453-.195-.303-.292-.815-.292-1.538v-2.267c0-1.807-.404-2.937-1.214-3.395v-.045c.81-.464 1.214-1.581 1.214-3.351V6.025c0-1.283.42-1.925 1.262-1.925V2.03zm14.66 0V4.1c.842 0 1.262.642 1.262 1.925v2.268c0 1.843.402 2.996 1.207 3.46v.046c-.805.442-1.207 1.544-1.207 3.306v2.356c0 .715-.099 1.22-.299 1.516-.2.302-.52.453-.963.453v2.068c1.152 0 1.984-.295 2.494-.885.516-.59.772-1.663.772-3.218V14.84c0-1.379.404-2.069 1.209-2.069v-2.035c-.805 0-1.21-.696-1.21-2.09V6.113c0-1.49-.255-2.54-.77-3.152-.516-.62-1.348-.93-2.495-.93zm-3.054 4.46c-.455 0-.886.057-1.293.173a3.056 3.056 0 0 0-1.078.527c-.308.241-.551.549-.731.924-.18.37-.27.817-.27 1.336 0 .663.165 1.227.493 1.695.33.468.831.864 1.502 1.188.263.125.509.249.736.37.227.12.422.244.586.374.168.13.299.271.394.424a.963.963 0 0 1 .145.521c0 .144-.03.28-.09.405a.9.9 0 0 1-.275.318c-.12.088-.272.158-.455.21a2.34 2.34 0 0 1-.635.075c-.415 0-.825-.083-1.233-.25a3.644 3.644 0 0 1-1.13-.763v2.222a3.68 3.68 0 0 0 1.101.418c.427.093.875.139 1.346.139.459 0 .894-.05 1.305-.152a3.002 3.002 0 0 0 1.09-.5c.31-.237.556-.543.736-.918.183-.38.275-.849.275-1.405 0-.403-.052-.755-.156-1.056a2.542 2.542 0 0 0-.45-.813 3.295 3.295 0 0 0-.704-.633 6.754 6.754 0 0 0-.922-.535 12.4 12.4 0 0 1-.676-.348c-.2-.115-.37-.231-.51-.347a1.502 1.502 0 0 1-.322-.375.91.91 0 0 1-.115-.453c0-.153.033-.288.101-.408a.948.948 0 0 1 .29-.32c.123-.089.275-.156.454-.202a2.18 2.18 0 0 1 .598-.078c.16 0 .326.015.502.043.18.028.36.07.539.13.18.056.354.13.522.218.171.088.329.188.472.304V6.871a4.039 4.039 0 0 0-.957-.285 6.448 6.448 0 0 0-1.185-.096zm-8.774.165l-3.123 9.967h2.094l.605-2.217h3.053l.61 2.217h2.107L9.869 6.656H7.576zm1.072 1.78h.047c.028.347.077.646.145.896l.922 3.35H7.564l.934-3.377c.08-.288.13-.578.15-.87z" font-weight="400" font-size="51.019" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="android" xmlns="http://www.w3.org/2000/svg"><path d="M15 5h-1V4h1m-5 1H9V4h1m5.53-1.84L16.84.85c.19-.19.19-.51 0-.71a.513.513 0 0 0-.71 0l-1.48 1.48C13.85 1.23 12.95 1 12 1c-.96 0-1.86.23-2.66.63L7.85.14a.501.501 0 0 0-.7 0c-.2.2-.2.52 0 .71l1.31 1.31C6.97 3.26 6 5 6 7h12c0-2-1-3.75-2.47-4.84M20.5 8A1.5 1.5 0 0 0 19 9.5v7a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 20.5 8m-17 0A1.5 1.5 0 0 0 2 9.5v7A1.5 1.5 0 0 0 3.5 18 1.5 1.5 0 0 0 5 16.5v-7A1.5 1.5 0 0 0 3.5 8M6 18a1 1 0 0 0 1 1h1v3.5A1.5 1.5 0 0 0 9.5 24a1.5 1.5 0 0 0 1.5-1.5V19h2v3.5a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5V19h1a1 1 0 0 0 1-1V8H6v10z" fill="#c0ca33"/></symbol><symbol viewBox="0 0 24 24" id="angular" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#e53935"/></symbol><symbol viewBox="0 0 24 24" id="angular-component" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#0288d1"/></symbol><symbol viewBox="0 0 24 24" id="angular-directive" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#ab47bc"/></symbol><symbol viewBox="0 0 24 24" id="angular-guard" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-pipe" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#00897b"/></symbol><symbol viewBox="0 0 24 24" id="angular-resolver" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-routing" xmlns="http://www.w3.org/2000/svg"><path d="M11 10H5L3 8l2-2h6V3l1-1 1 1v1h6l2 2-2 2h-6v2h6l2 2-2 2h-6v6a2 2 0 0 1 2 2H9a2 2 0 0 1 2-2V10z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-service" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#ffca28"/></symbol><symbol viewBox="0 0 100 100" id="apiblueprint" xmlns="http://www.w3.org/2000/svg"><title>api-blueprint</title><path d="M50.133 7.521A16.998 16.998 0 0 0 33.135 24.52a16.998 16.998 0 0 0 4.945 11.974L24.861 57.398a16.998 16.998 0 0 0-3.175-.308A16.998 16.998 0 0 0 4.688 74.088a16.998 16.998 0 0 0 16.998 16.998 16.998 16.998 0 0 0 16.998-16.998 16.998 16.998 0 0 0-7.063-13.773l12.576-19.89a16.998 16.998 0 0 0 5.936 1.093 16.998 16.998 0 0 0 6.154-1.155l12.537 19.83a16.998 16.998 0 0 0-7.244 13.895 16.998 16.998 0 0 0 16.998 17 16.998 16.998 0 0 0 16.998-17A16.998 16.998 0 0 0 78.578 57.09a16.998 16.998 0 0 0-2.95.262L62.337 36.327A16.998 16.998 0 0 0 67.13 24.52 16.998 16.998 0 0 0 50.132 7.522z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="applescript" xmlns="http://www.w3.org/2000/svg"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" fill="#78909c"/></symbol><symbol viewBox="0 0 24 24" id="appveyor" xmlns="http://www.w3.org/2000/svg"><path d="M12 2c-.084 0-.165.008-.248.01a10 10 0 0 0-.266.01 9.952 9.952 0 0 0-.754.066 10 10 0 0 0-.148.018 9.855 9.855 0 0 0-.93.177 10 10 0 0 0-.07.02c-.196.049-.392.1-.584.16v.012a10 10 0 0 0-2 .875V3.34c-.02.012-.038.027-.059.039a10 10 0 0 0-.953.635c-.09.067-.172.142-.26.213a10 10 0 0 0-.628.546c-.109.104-.211.211-.315.319a10 10 0 0 0-.476.539c-.1.12-.201.237-.295.361a10 10 0 0 0-.52.766c-.088.143-.17.288-.252.435a10 10 0 0 0-.363.723c-.072.161-.136.327-.2.492a10 10 0 0 0-.269.778c-.02.067-.044.131-.062.199a10 10 0 0 0-.008.027c-.098.364-.166.728-.22 1.09-.012.077-.024.153-.034.23a9.85 9.85 0 0 0-.08 1.182c0 .03-.006.057-.006.086a10 10 0 0 0 .008.148c.001.094-.002.188.002.282l.011.004a10 10 0 0 0 .333 2.158l-.012-.004c.012.047.033.091.047.139a10 10 0 0 0 .322.955c.02.052.037.106.059.158a10 10 0 0 0 .503 1.035c.065.116.14.226.21.34a10 10 0 0 0 .423.64c.092.128.187.252.285.375a10 10 0 0 0 .448.52c.112.123.222.248.341.365a10 10 0 0 0 .803.719 10 10 0 0 0 .01.006c.099.078.207.146.309.22a10 10 0 0 0 .648.442c.138.085.28.163.424.242a10 10 0 0 0 .715.358c.114.051.226.106.343.154a10 10 0 0 0 1.133.389c.016.004.031.01.047.015a10 10 0 0 0 .461.098 10 10 0 0 0 .482.103 10 10 0 0 0 .418.051 10 10 0 0 0 .575.065 10 10 0 0 0 .144.005A10 10 0 0 0 12 22a10 10 0 0 0 .197-.01 10 10 0 0 0 .496-.025 10 10 0 0 0 .49-.043 10 10 0 0 0 .489-.074 10 10 0 0 0 .51-.098 10 10 0 0 0 .47-.12 10 10 0 0 0 .477-.14 10 10 0 0 0 .47-.172 10 10 0 0 0 .481-.197 10 10 0 0 0 .414-.201 10 10 0 0 0 .475-.252 10 10 0 0 0 .39-.238 10 10 0 0 0 .452-.301 10 10 0 0 0 .38-.291 10 10 0 0 0 .385-.315 10 10 0 0 0 .375-.347 10 10 0 0 0 .36-.363 10 10 0 0 0 .293-.334 10 10 0 0 0 .353-.434 10 10 0 0 0 .28-.393 10 10 0 0 0 .263-.4 10 10 0 0 0 .264-.461 10 10 0 0 0 .228-.436 10 10 0 0 0 .195-.437 10 10 0 0 0 .196-.48 10 10 0 0 0 .228-.69 10 10 0 0 0 .028-.094 10 10 0 0 0 .021-.066 10 10 0 0 0 .098-.461 10 10 0 0 0 .103-.482 10 10 0 0 0 .051-.418 10 10 0 0 0 .065-.575 10 10 0 0 0 .005-.144A10 10 0 0 0 22 12a10 10 0 0 0-.01-.197 10 10 0 0 0-.025-.496 10 10 0 0 0-.043-.49 10 10 0 0 0-.074-.489 10 10 0 0 0-.098-.51 10 10 0 0 0-.12-.47 10 10 0 0 0-.14-.477 10 10 0 0 0-.172-.47 10 10 0 0 0-.197-.481 10 10 0 0 0-.201-.414 10 10 0 0 0-.252-.475 10 10 0 0 0-.238-.39 10 10 0 0 0-.301-.452 10 10 0 0 0-.291-.38 10 10 0 0 0-.315-.385 10 10 0 0 0-.347-.375 10 10 0 0 0-.363-.36 10 10 0 0 0-.334-.293 10 10 0 0 0-.434-.353 10 10 0 0 0-.393-.28 10 10 0 0 0-.4-.263 10 10 0 0 0-.461-.264 10 10 0 0 0-.436-.228 10 10 0 0 0-.437-.196 10 10 0 0 0-.48-.195 10 10 0 0 0-.69-.228 10 10 0 0 0-.094-.028 10 10 0 0 0-.066-.021 10 10 0 0 0-.461-.098 10 10 0 0 0-.482-.103 10 10 0 0 0-.418-.051 10 10 0 0 0-.575-.065 10 10 0 0 0-.144-.005A10 10 0 0 0 12 2zm-.016 5.002a5 5 0 0 1 .262.01 5 5 0 0 1 .227.011 5 5 0 0 1 .341.05 5 5 0 0 1 .135.019 5 5 0 0 1 .014.004 5 5 0 0 1 .115.025 5 5 0 0 1 .303.076 5 5 0 0 1 .265.086 5 5 0 0 1 .2.074 5 5 0 0 1 .242.106 5 5 0 0 1 .228.11 5 5 0 0 1 .196.109 5 5 0 0 1 .244.15 5 5 0 0 1 .17.12 5 5 0 0 1 .224.171 5 5 0 0 1 .186.16 5 5 0 0 1 .176.164 5 5 0 0 1 .172.18 5 5 0 0 1 .177.203 5 5 0 0 1 .133.172 5 5 0 0 1 .16.223 5 5 0 0 1 .133.214 5 5 0 0 1 .12.21 5 5 0 0 1 .107.216 5 5 0 0 1 .109.24 5 5 0 0 1 .084.223 5 5 0 0 1 .08.242 5 5 0 0 1 .07.264 5 5 0 0 1 .047.207 5 5 0 0 1 .045.277 5 5 0 0 1 .028.227 5 5 0 0 1 .02.351 5 5 0 0 1 .003.079 5 5 0 0 1-.012.271 5 5 0 0 1-.011.227 5 5 0 0 1-.05.341 5 5 0 0 1-.019.135 5 5 0 0 1-.004.014 5 5 0 0 1-.025.115 5 5 0 0 1-.076.303 5 5 0 0 1-.086.265 5 5 0 0 1-.074.2 5 5 0 0 1-.106.242 5 5 0 0 1-.11.228 5 5 0 0 1-.109.196 5 5 0 0 1-.15.244 5 5 0 0 1-.12.17 5 5 0 0 1-.171.224 5 5 0 0 1-.16.186 5 5 0 0 1-.164.176 5 5 0 0 1-.18.172 5 5 0 0 1-.203.177l-.002.002c-.018.019-.028.035-.047.053l-3.959 5.09-3.05-.979a141.684 141.684 0 0 0 3.177-3.084 5 5 0 0 1-.103-.015 5 5 0 0 1-.149-.024 5 5 0 0 1-.115-.025 5 5 0 0 1-3.57-3.04 5.072 5.072 0 0 1-.206-.661 5 5 0 0 1-.033-.147c-.025-.118-.036-.24-.054-.36-.987.993-1.964 1.993-2.954 3.05l-.98-3.053 5.092-3.957c.043-.044.082-.07.125-.11a5 5 0 0 1 .71-.634c.18-.13.367-.25.561-.356a5 5 0 0 1 .16-.08 4.94 4.94 0 0 1 .516-.222 5 5 0 0 1 .147-.057c.211-.07.43-.123.654-.164a5 5 0 0 1 .172-.027c.236-.035.476-.058.722-.059zM12 9a3 3 0 0 0-.053.002 3 3 0 0 0-.166.01 3 3 0 0 0-.133.011 3 3 0 0 0-.17.026 3 3 0 0 0-.113.021 3 3 0 0 0-.19.05 3 3 0 0 0-.103.03 3 3 0 0 0-.16.057 3 3 0 0 0-.129.053 3 3 0 0 0-.146.072 3 3 0 0 0-.12.063 3 3 0 0 0-.132.082 3 3 0 0 0-.123.08 3 3 0 0 0-.116.088 3 3 0 0 0-.126.105 3 3 0 0 0-.1.094 3 3 0 0 0-.111.111 3 3 0 0 0-.096.107 3 3 0 0 0-.094.116 3 3 0 0 0-.098.136 3 3 0 0 0-.072.11 3 3 0 0 0-.076.133 3 3 0 0 0-.07.132 3 3 0 0 0-.063.14 3 3 0 0 0-.054.14 3 3 0 0 0-.077.228 3 3 0 0 0-.007.026 3 3 0 0 0-.03.138 3 3 0 0 0-.031.149 3 3 0 0 0-.014.11 3 3 0 0 0-.02.183 3 3 0 0 0-.001.052A3 3 0 0 0 9 12a3 3 0 0 0 .002.053 3 3 0 0 0 .01.166 3 3 0 0 0 .011.133 3 3 0 0 0 .026.17 3 3 0 0 0 .021.113 3 3 0 0 0 .05.19 3 3 0 0 0 .03.103 3 3 0 0 0 .057.16 3 3 0 0 0 .053.129 3 3 0 0 0 .072.146 3 3 0 0 0 .063.12 3 3 0 0 0 .082.132 3 3 0 0 0 .08.123 3 3 0 0 0 .088.116 3 3 0 0 0 .105.126 3 3 0 0 0 .094.1 3 3 0 0 0 .111.111 3 3 0 0 0 .107.096 3 3 0 0 0 .116.094 3 3 0 0 0 .136.098 3 3 0 0 0 .11.072 3 3 0 0 0 .133.076 3 3 0 0 0 .132.07 3 3 0 0 0 .135.06 3 3 0 0 0 .153.061 3 3 0 0 0 .216.07 3 3 0 0 0 .004.003 3 3 0 0 0 .026.007 3 3 0 0 0 .138.03 3 3 0 0 0 .149.031 3 3 0 0 0 .11.014 3 3 0 0 0 .183.02 3 3 0 0 0 .011.001 3 3 0 0 0 .041 0A3 3 0 0 0 12 15a3 3 0 0 0 .053-.002 3 3 0 0 0 .166-.01 3 3 0 0 0 .133-.011 3 3 0 0 0 .17-.026 3 3 0 0 0 .113-.021 3 3 0 0 0 .19-.05 3 3 0 0 0 .103-.03 3 3 0 0 0 .16-.057 3 3 0 0 0 .129-.053 3 3 0 0 0 .146-.072 3 3 0 0 0 .12-.063 3 3 0 0 0 .132-.082 3 3 0 0 0 .123-.08 3 3 0 0 0 .116-.088 3 3 0 0 0 .126-.105 3 3 0 0 0 .1-.094 3 3 0 0 0 .111-.111 3 3 0 0 0 .096-.107 3 3 0 0 0 .094-.116 3 3 0 0 0 .098-.136 3 3 0 0 0 .072-.11 3 3 0 0 0 .076-.133 3 3 0 0 0 .07-.132 3 3 0 0 0 .06-.135 3 3 0 0 0 .061-.153 3 3 0 0 0 .07-.216 3 3 0 0 0 .003-.004 3 3 0 0 0 .007-.026 3 3 0 0 0 .03-.138 3 3 0 0 0 .031-.149 3 3 0 0 0 .002-.008 3 3 0 0 0 .012-.101 3 3 0 0 0 .02-.184 3 3 0 0 0 .001-.011 3 3 0 0 0 0-.041A3 3 0 0 0 15 12a3 3 0 0 0-.002-.053 3 3 0 0 0-.01-.166 3 3 0 0 0-.011-.133 3 3 0 0 0-.026-.17 3 3 0 0 0-.021-.113 3 3 0 0 0-.05-.19 3 3 0 0 0-.03-.103 3 3 0 0 0-.057-.16 3 3 0 0 0-.053-.129 3 3 0 0 0-.072-.146 3 3 0 0 0-.063-.12 3 3 0 0 0-.082-.132 3 3 0 0 0-.08-.123 3 3 0 0 0-.088-.116 3 3 0 0 0-.105-.126 3 3 0 0 0-.094-.1 3 3 0 0 0-.111-.111 3 3 0 0 0-.107-.096 3 3 0 0 0-.116-.094 3 3 0 0 0-.136-.098 3 3 0 0 0-.11-.072 3 3 0 0 0-.133-.076 3 3 0 0 0-.132-.07 3 3 0 0 0-.14-.063 3 3 0 0 0-.14-.054 3 3 0 0 0-.228-.077 3 3 0 0 0-.026-.007 3 3 0 0 0-.138-.03 3 3 0 0 0-.149-.031 3 3 0 0 0-.008-.002 3 3 0 0 0-.101-.012 3 3 0 0 0-.184-.02 3 3 0 0 0-.011-.001 3 3 0 0 0-.041 0A3 3 0 0 0 12 9z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 720 720" id="arduino" xmlns="http://www.w3.org/2000/svg"><defs><symbol id="ana" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke-opacity="100%" stroke-width="60" stroke="#00979c" d="M174 30a10.5 10.1 0 0 0 0 280C364 320 344 30 544 30a10.5 10.1 0 0 1 0 280C354 320 374 30 174 30"/><path d="M528 205v-32.8h-32.5v-13.7H528V126h13.9v32.5h32.5v13.7h-32.5V205H528z" text-anchor="middle" fill="#00979c" stroke-width="20" stroke="#00979c" font-family="sans-serif" font-size="167"/><path fill="#00979c" stroke="#00979c" stroke-width="23.6" transform="matrix(1.56 0 0 .64 -366 .528)" d="M321 266v-17.4h53.3V266H321z"/></symbol></defs><title>Layer 1</title><use x="20.063" y="360.85" transform="matrix(.997 0 0 .997 -18.596 -159.19)" xlink:href="#ana"/></symbol><symbol viewBox="0 0 24 24" id="assembly" xmlns="http://www.w3.org/2000/svg"><path d="M1.746 1.566v20.905H5.13v-2.088H3.438V3.656h1.69v-2.09H1.747zm17.219 0v2.09h1.693v16.727h-1.693v2.09h3.383V1.566h-3.383zM15.196 3.988c-.5 0-.93.076-1.29.225-.359.15-.652.372-.877.671-.226.302-.39.673-.494 1.108a6.715 6.715 0 0 0-.155 1.54c0 .573.049 1.083.15 1.528.1.442.264.811.49 1.11.222.298.512.524.872.676.36.153.795.23 1.304.23.518 0 .954-.075 1.308-.224.353-.153.643-.376.869-.671.219-.29.38-.661.484-1.112.104-.454.156-.967.156-1.54 0-.573-.052-1.079-.152-1.515a2.92 2.92 0 0 0-.485-1.106 2.09 2.09 0 0 0-.868-.686c-.354-.155-.79-.234-1.312-.234zm-6.814.12a.941.941 0 0 1-.138.458.849.849 0 0 1-.356.296A1.71 1.71 0 0 1 7.385 5a5.244 5.244 0 0 1-.631.037v1.11H8.19v3.6H6.754v1.188h4.545V9.745H9.894V4.11H8.382zm6.814 1.138c.375 0 .643.176.805.527.161.348.241.933.241 1.756 0 .814-.082 1.399-.247 1.756-.164.356-.43.534-.799.534-.369 0-.636-.178-.8-.534-.165-.357-.248-.941-.248-1.749 0-.829.082-1.415.243-1.763.162-.35.43-.527.805-.527zm-6.33 7.64c-.5 0-.93.073-1.29.223-.359.15-.651.374-.877.673-.225.302-.39.67-.494 1.106a6.715 6.715 0 0 0-.155 1.54c0 .573.05 1.082.15 1.527.1.442.264.814.49 1.112.222.3.514.525.874.677.36.152.793.229 1.302.229.519 0 .954-.076 1.308-.225.354-.153.643-.376.869-.672.22-.29.38-.66.484-1.111.104-.455.156-.967.156-1.54 0-.573-.05-1.079-.15-1.515a2.923 2.923 0 0 0-.487-1.106 2.084 2.084 0 0 0-.867-.686c-.353-.156-.791-.232-1.313-.232zm5.846.119a.941.941 0 0 1-.138.457.85.85 0 0 1-.356.296 1.71 1.71 0 0 1-.503.137 5.245 5.245 0 0 1-.631.037v1.112h1.435v3.597h-1.435v1.189h4.545v-1.189h-1.405v-5.636h-1.512zm-5.846 1.137c.375 0 .643.176.805.527.162.347.241.933.241 1.756 0 .813-.08 1.399-.245 1.755-.164.357-.432.534-.8.534-.37 0-.637-.177-.802-.534-.164-.356-.245-.939-.245-1.746 0-.83.08-1.418.242-1.765.161-.35.43-.527.804-.527z" fill="#ff6e40"/></symbol><symbol viewBox="0 0 24 24" id="aurelia" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="api" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#apa"/><linearGradient id="apa" x1="-3.881" x2="2.377" y1="-1.442" y2="4.304"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#apb"/><linearGradient id="apb" x1=".729" x2="-.971" y1=".844" y2="-1.477"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="apk" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#apc"/><linearGradient id="apc" x1="-2.839" x2="2.875" y1="-6.936" y2="1.017"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apl" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#apd"/><linearGradient id="apd" x1="-8.212" x2="1.02" y1="-4.691" y2="2.882"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apm" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#ape"/><linearGradient id="ape" x1="-1.404" x2="4.19" y1="-2.309" y2="2.62"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apn" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#apf"/><linearGradient id="apf" x1="1.911" x2=".204" y1="2.539" y2=".204"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="apo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#apg"/><linearGradient id="apg" x1="-3.881" x2="2.377" y1="-1.738" y2="5.19"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="app" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#aph"/><linearGradient id="aph" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><g transform="rotate(11.282 -1.694 21.569) scale(.47102)" clip-rule="evenodd" fill="none" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#api)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#apj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#apk)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#apl)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#apm)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#apn)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#apo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#app)"/></g></symbol><symbol viewBox="0 0 24 24" id="autohotkey" xmlns="http://www.w3.org/2000/svg"><path d="M5 3c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5zm3.668 3.447a.9.9 0 0 1 .652.256.84.84 0 0 1 .262.625c0 .34-.014.852-.041 1.537-.022.68-.033 1.19-.033 1.53 0 .111-.016.326-.047.644a6.149 6.149 0 0 0-.033.68l2.578-.485c1.007-.179 1.874-.281 2.603-.308.018-.3.048-1.105.088-2.416.01-.345.115-.742.317-1.19.25-.55.533-.826.851-.826.237 0 .448.08.631.236.197.17.295.382.295.637a.775.775 0 0 1-.025.201c-.09.327-.135.612-.135.854 0 .125-.014.32-.041.584-.023.26-.033.453-.033.578 0 .425-.022 1.056-.067 1.893a38.963 38.963 0 0 0-.068 1.892c0 .327.025.816.074 1.465.05.649.074 1.136.074 1.463a.84.84 0 0 1-.261.625.893.893 0 0 1-.65.254 1 1 0 0 1-.686-.254.777.777 0 0 1-.29-.611c0-.327-.015-.818-.046-1.471a39.552 39.552 0 0 1-.041-1.47c0-.256.004-.482.013-.679-.702.032-1.57.142-2.603.33-.86.157-1.719.316-2.578.477-.01.304-.042.812-.096 1.523a22.354 22.354 0 0 0-.066 1.538.84.84 0 0 1-.262.625.893.893 0 0 1-.65.253.898.898 0 0 1-.653-.253.84.84 0 0 1-.262-.625c0-.452.038-1.128.114-2.028.08-.9.12-1.575.12-2.027 0-.573.015-1.436.042-2.586.027-1.155.04-2.017.04-2.59a.84.84 0 0 1 .263-.625.895.895 0 0 1 .65-.256z" fill="#4caf50"/></symbol><symbol viewBox="0 0 24 24" id="autoit" xmlns="http://www.w3.org/2000/svg"><defs id="ardefs8"><style id="arstyle4482">.cls-1{fill:#5d83ac}.cls-2{fill:#f0f0f0;fill-rule:evenodd}</style><style id="arstyle4510">.cls-1{fill:#5d83ac}.cls-2{fill:#f0f0f0;fill-rule:evenodd}</style></defs><g id="arg4522" transform="translate(-59.538 -26.404) scale(.0555)"><path d="M12.8 2.133A10.666 10.666 0 0 0 2.136 12.799 10.666 10.666 0 0 0 12.8 23.465a10.666 10.666 0 0 0 10.668-10.666A10.666 10.666 0 0 0 12.8 2.133zm.15 4.713c.456 0 .836.105 1.142.314.306.21.565.469.78.78l6.089 8.812H9.627l1.82-2.506h3.36c.315 0 .589.01.822.03a11.93 11.93 0 0 1-.473-.663 39.13 39.13 0 0 0-.517-.75l-1.748-2.578-4.577 6.467H4.746l6.25-8.813c.204-.281.46-.534.772-.757.31-.224.705-.336 1.181-.336z" transform="matrix(16.89188 0 0 16.89188 1072.761 475.745)" id="arcircle4514" fill="#1976d2" stroke-width=".026"/></g></symbol><symbol viewBox="0 0 213.33333 213.33333" id="babel" xmlns="http://www.w3.org/2000/svg"><path d="M50.22 199.659c-.875-.406-1.261-1.6-.857-2.652.404-1.053.12-1.914-.63-1.914s-1.615.748-1.92 1.663c-.328.983-1.27.302-2.304-1.667-.962-1.831-3.718-5.533-6.126-8.226-9.418-10.535-7.71-27.444 5.432-53.77 12.459-24.96 23.117-39.033 45.966-60.696 30.229-28.66 52.679-46.223 70.587-55.22 10.98-5.518 13.025-5.059 2.778.624-11.004 6.102-11.378 6.359-10.512 7.226.33.33 7.306-2.67 15.504-6.667 15.87-7.737 16.34-7.912 16.34-6.082 0 .652-4.95 3.738-11 6.858-13.062 6.736-12.722 6.48-10.472 7.872 1.117.69 5.428-.582 11.54-3.406 5.367-2.48 10.397-4.508 11.179-4.508 2.755 0-3.928 5.302-11.541 9.157-20.437 10.35-68.937 46.043-68.07 50.097.166.777-5.792 7.639-13.241 15.248-15.257 15.587-26.14 30.002-33.748 44.706-6.379 12.326-7.457 17.734-5.385 26.996 3.482 15.56 11.592 18.366 31.482 10.895 28.228-10.603 45.758-28.704 47.022-48.556.602-9.442-1.317-13.479-8.52-17.93-4.01-2.48-5.268-2.621-12.065-1.365-4.173.771-10.153 2.906-13.289 4.744s-6.455 3.34-7.377 3.34c-.922 0-3.216 1.336-5.096 2.968-1.88 1.633.48-1.13 5.247-6.14 6.82-7.167 7.956-8.9 5.333-8.132-5.208 1.525-10.194 4.33-15.649 8.803-2.76 2.264-.923.175 4.08-4.641 11.565-11.131 21.183-15.97 33.088-16.641 17.097-.966 27.254 5.805 31.964 21.31 2.435 8.017 2.609 10.24 1.353 17.37-1.65 9.361-7.034 21.553-15.593 35.307-4.398 7.067-8.434 11.427-15.588 16.844-9.166 6.94-15.654 11.02-15.654 9.845 0-.295 2.455-2.161 5.455-4.147 8.818-5.835 5.075-5.377-8.326 1.02-6.854 3.27-15.199 6.593-18.542 7.38-7.106 1.675-30.527 3.164-32.846 2.089zm-8.408-19.899c0-1.1-.6-2-1.333-2-.734 0-1.334.9-1.334 2s.6 2 1.333 2c.734 0 1.334-.9 1.334-2zm89.255-8.204c1.53-1.945 2.473-3.845 2.097-4.222-.377-.377-.836-.435-1.02-.13-.182.306-1.787 2.206-3.565 4.223-1.778 2.016-2.571 3.666-1.763 3.666s2.72-1.591 4.25-3.536zm-77.644-1.745c-.82-2.172-1.74-3.7-2.045-3.396-.951.952 1.088 7.345 2.343 7.345.656 0 .522-1.777-.298-3.95zm82.303-27.915c-.837-.837-3.217 2.55-3.184 4.53.012.734.896.178 1.965-1.235 1.07-1.413 1.618-2.896 1.219-3.295zm-66.238-36.904c-1.312-1.312-3.676.702-3.676 3.133 0 2.035.175 2.031 2.254-.047 1.24-1.24 1.88-2.628 1.422-3.086zm39.657.768c4.403-2.196 6.8-3.986 5.333-3.982-2.838.01-16.667 6.028-16.667 7.254 0 1.6 3.717.527 11.333-3.272zm16.667-5.333c0-.733-.9-1.333-2-1.333s-2 .6-2 1.333.9 1.333 2 1.333 2-.6 2-1.333zm-3.334-3.923l5.334-1.104-7.334-.133c-4.033-.073-8.233.45-9.333 1.16-2.539 1.64 3.572 1.682 11.333.077zm35.738-63.976c2.788-1.69 4.765-3.376 4.393-3.748-.947-.947-11.942 5.654-14.237 8.548-1.792 2.258-1.714 2.276 1.44.329a1452.76 1452.76 0 0 1 8.403-5.13z" fill="#ffca28" stroke-width="1.333"/></symbol><symbol viewBox="0 0 400 400" fill-opacity=".05" id="bithound" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.88 0 0 .88 24.121 2.895)" fill="#e53935" fill-opacity="1"><path d="M370.5 207c-1.5-14.8-4.8-29.9-9.5-44-13.5-40.3-38.6-81.6-70.3-110.1-1.4-1.2-6.7-4.4-8.7-3.3-5.2 2.9 4.6 22.8 5.8 26.4 7.4 22 12.1 45.3 6.8 68.3-7.1 30.4-30.4 51.7-61.5 54.3-17.1 1.4-34.3-.5-51.4 1.5-25.6 3-51.7 11.8-68 32.8-1.9 2.4-3.6 5.1-5.2 7.9h-.4c-6.3.7-12.6-2-15.7-3.7-.8-.5-1.6-.9-2.2-1.2-19-10.5-33-34-41.6-53.4-3.9-9-7.2-18.4-9.3-27.9-1-4.3-1.1-8.8-1.3-13.2-.1-2.7.3-6.5-1.2-8.9-3.3-5.2-7.5-.2-8.2 4-1.1 6.9-2.1 13.7-1.8 20.7.5 11.8 3.8 23.5 8 34.5 6.2 16.2 14.9 31.1 26.2 44.4 4.7 5.5 9.7 10.6 15.1 15.3 4.8 4.3 10.9 7.7 14.5 13.2 4.2 6.3 4.9 14.1 4.5 21.4-1 19.3-1.6 37.4 3.9 56.2 4.8 16.7 10.8 33.8 20.8 48.1 5 7.1 11.2 14.6 18 19.9 4.6 3.6 13.3 4 8.3-9.2-11.1-29.3-12.1-59.7 5.2-87.1 14.5-22.8 40.1-43.1 69-39.5 42.5 5.3 72.1 44.3 70 86-.6 11.7-1 21.7-4.7 32.7-1.5 4.4-2.6 10-1.5 14.6 1.8 7.8 10.5 4.9 14.3-.2 10.3-14 21.1-27.6 30.8-42 31.6-47.2 47-101.8 41.3-158.5z"/><path d="M132.4 92.1c.7 2.3 1.4 4.8 1.9 7.5.1 1.1.4 2.3 1 3.4 2.6 6.8 8.9 10.5 14.8 14 3.6 2.2 10.1 4.3 14.1 5.9 5.2 2.1 16.4-.6 21.7-1 12.2-1 23.5-5.3 34.7 1.2-57.4 67.3-3.2 82.3 38.8 49.9 48-37 2.8-124.3 2.8-124.3s-1-6.8-19.2-10.8c-1.7-.9-3.4-1.7-5.1-2.4-18-8.3-34.2 5.3-47.2 16.4-3.8 3.2-7.5 6.4-11.5 9.4-5.4 4-11.2 7.3-17.3 10.2-6.4 3-14 6.4-21.1 6.7-1 0-2.9.2-4.9.6-3.1.3-4.7 1.1-5.4 2.5-1.2 1-2 2.4-1.8 4.2.2 2.5 1.4 4.6 2.7 6.2.4.1.7.3 1 .4z"/></g></symbol><symbol viewBox="0 0 400.00001 399.99999" id="bower" xmlns="http://www.w3.org/2000/svg"><g transform="translate(12.061 33.203) scale(.81733)"><path d="M447.61 200.08c-23.139-22.234-138.85-36.114-175.36-40.154a107.137 107.137 0 0 0 4.517-12.944 146.107 146.107 0 0 1 15.905-5.901c.677 1.997 3.865 9.648 5.682 13.279 73.415 2.025 77.184-54.557 80.17-70.058 2.92-15.157 2.771-29.802 27.953-56.575-37.516-10.933-91.467 16.945-109.54 58.437-6.79-2.545-13.597-4.424-20.328-5.586-4.824-19.46-29.944-73.672-95.863-73.672-83.46 0-174.43 68.853-174.43 185.41 0 97.976 66.891 183.84 104.68 183.84 16.505 0 30.703-12.36 34.036-23.44 2.795 7.597 11.368 31.213 14.184 37.225 4.162 8.89 23.41 16.583 31.833 7.357 10.83 6.017 30.703 9.641 41.534-6.405 20.86 4.412 39.3-8.026 39.702-22.868 10.235-.546 15.256-14.918 13.021-26.363-1.647-8.426-19.248-38.66-26.113-49.098 13.59 11.054 48.013 14.183 52.194.007 21.911 17.198 56.057 8.171 58.765-5.815 26.624 6.917 57.16-8.276 52.146-26.676 42.771-2.958 37.296-48.464 25.296-59.996z" fill="#543729" stroke-width=".973"/><path d="M328.514 103.025c9.212-18.277 20.788-38.234 35.409-50.58-16.093 6.485-31.981 25.873-41.375 46.595a144.914 144.914 0 0 0-14.552-8.132c13.105-27.972 43.555-51.332 77.112-53.157-22.477 20.385-14.498 62.754-32.979 85.183-5.288-5.311-17.43-15.562-23.615-19.909zm-14.53 29.762c.01-.7.272-6.094.763-8.557-1.288-.304-9.3-1.87-13.476-1.772-.304 5.245 2.204 14.17 4.684 19.541 17.075-.358 29.408-5.471 36.667-10.172-6.18-2.88-16.726-5.442-24.745-6.974-.894 1.851-3.097 6.568-3.892 7.934z" fill="#00acee"/><g stroke-width=".973"><path d="M250.54 277.39c.004.024.015.057.018.082-2.165-4.657-4.463-10.314-7.208-17.708 10.688 15.557 44.184 7.533 42.427-6.407 16.395 12.336 50.143-2.055 42.471-19.353 16.423 7.653 35.168-7.745 30.964-14.455 28 5.4 54.832 10.783 63.256 12.938-5.595 9.123-18.339 15.566-37.549 11.089 10.38 14.14-9.773 31.105-37.844 21.76 6.18 13.883-18.814 26.38-47.22 11.91.361 13.889-35.24 15.488-49.315.143zm55.543-70.194c32.497 2.495 86.238 7.34 119.51 11.997-2.102-10.828-7.844-13.921-25.905-18.772-19.425 2.072-68.706 6.913-93.604 6.776z" fill="#2baf2b"/><path d="M285.78 253.36c16.395 12.336 50.143-2.055 42.471-19.353 16.423 7.653 35.168-7.745 30.964-14.455-33.103-6.383-67.84-12.788-75.719-13.908 4.78.254 12.702.797 22.59 1.556 24.899.137 74.18-4.704 93.604-6.775-31.452-7.975-95.666-19.613-140.01-22.48-2.055 3.003-5.833 8.097-12.413 13.51-19.403 41.053-54.557 68.34-93.454 68.34-11.335 0-24.018-1.912-38.233-6.456-8.865 9.497-46.661 16.694-77.329 1.641 24.326 56.961 80.74 94.984 143.19 94.984 52.591 0 75.912-53.704 70.808-67.914-1.238-3.45-6.145-14.889-8.891-22.283 10.689 15.556 44.185 7.532 42.429-6.408z" fill="#ffcc2f"/><path d="M253.91 145.27c4.644-2.526 20.69-12.253 35.981-15.908a67.843 67.843 0 0 1-.536-5.12c-10.032 2.403-28.945 10.51-39.784-.661 22.866 6.9 34.283-6.149 51.09-6.149 10.014 0 24.305 2.798 35.57 7.22-9.061-8.37-38.772-33.63-75.558-33.717-8.213 9.957-17.09 31.526-6.764 54.334z" fill="#cecece"/><path d="M115.58 253.33c14.215 4.544 26.898 6.457 38.233 6.457 38.896 0 74.05-27.29 93.454-68.341-14.351 11.978-39.291 22.228-78.241 22.228 34.694-7.866 64.56-25.156 79.753-50.427-10.68-16.998-22.263-54.603 7.07-84.33-4.512-14.497-26.475-52.766-75.095-52.766-84.85 0-155.17 71.001-155.17 166.15 0 22.525 4.547 43.65 12.67 62.664 30.666 15.054 68.462 7.858 77.327-1.64z" fill="#ef5734"/><path d="M141.03 108.45c0 21.644 17.546 39.191 39.19 39.191s39.192-17.548 39.192-39.191c0-21.644-17.548-39.191-39.192-39.191-21.644 0-39.19 17.547-39.19 39.191z" fill="#ffcc2f"/><path d="M156.76 108.45c0 12.958 10.507 23.463 23.463 23.463 12.96 0 23.464-10.506 23.464-23.463 0-12.959-10.504-23.464-23.464-23.464-12.957 0-23.463 10.506-23.463 23.464z" fill="#543729"/><ellipse cx="180.22" cy="98.044" rx="13.673" ry="8.501" fill="#fff"/></g></g></symbol><symbol viewBox="0 0 140 140" id="browserlist" xmlns="http://www.w3.org/2000/svg"><title>Browserslist logo</title><path d="M70.314 10.066a59.828 59.828 0 0 0-59.828 59.828 59.828 59.828 0 0 0 59.828 59.828 59.828 59.828 0 0 0 59.828-59.828 59.828 59.828 0 0 0-59.828-59.828zm-4.836 8.785c.496 4.043 1.352 7.322 2.572 10.223 4.779-4.287 10.265-7.546 16.041-9.02-.981 3.938-1.357 7.295-1.261 10.43 6.026-2.314 12.349-3.404 18.3-2.706-3.182 2.413-5.482 4.717-7.128 7.015-2.201 12.074 6.858 20.43 14.779 24.551a5.128 5.128 0 0 1 5.183-3.888 5.128 5.128 0 0 1 3.7 8.435v.002c-.487 1.055-2.002 2.343-3.497 3.219-4.075 2.39-11.172 5.736-20.914 7.39.045 1.214.077 2.453.077 3.747 0 4.817-.485 8.291-1.385 10.699-3.3 13.313-12.648 26.76-24.695 31.95.357-4.083.197-7.485-.402-10.591-5.582 3.218-11.646 5.278-17.623 5.52h-.002c1.785-3.662 2.855-6.878 3.412-9.976-6.347.996-12.727.742-18.377-1.17 2.93-2.732 5.054-5.314 6.673-7.96-6.292-1.344-12.169-3.87-16.766-7.686 3.822-1.544 6.795-3.239 9.3-5.197-5.426-3.517-10.034-7.998-12.972-13.23 4.012-.07 7.321-.568 10.3-1.453-3.786-5.215-6.468-11.032-7.333-16.951 3.861 1.405 7.196 2.133 10.36 2.355-1.662-6.22-2.081-12.605-.768-18.436 3.03 2.634 5.824 4.48 8.63 5.815.678-6.406 2.576-12.52 5.893-17.496 1.926 3.622 3.914 6.391 6.111 8.672 2.93-5.754 6.9-10.798 11.791-14.262zm26.465 19.557c-2.395 5.514-1.665 11.297-.555 18.732a2.138 2.138 0 0 0 .28-4.178 3.419 3.419 0 1 1 .092 6.704c.574 3.882 1.157 8.18 1.421 13.125a67.143 67.143 0 0 0 3.25-.649c6.616-1.487 12.258-3.801 16.871-6.506.45-.264.884-.563 1.276-.867.366-.557.333-.957.035-1.285-4.831-1.245-10.891-4.53-15.258-8.795-4.764-4.653-7.428-10.164-7.412-16.281z" fill="#ffca28" stroke-width=".855"/></symbol><symbol viewBox="0 0 140 140" id="browserlist_light" xmlns="http://www.w3.org/2000/svg"><title>Browserslist logo</title><g transform="translate(10.823 10.1)" stroke-width=".855"><circle cx="59.492" cy="59.795" r="59.828" fill="#ffca28"/><path d="M54.656 8.752c-4.89 3.464-8.862 8.508-11.791 14.262-2.198-2.28-4.185-5.05-6.111-8.672-3.318 4.976-5.216 11.09-5.893 17.496-2.807-1.335-5.6-3.18-8.63-5.814-1.314 5.83-.895 12.216.767 18.436-3.164-.223-6.498-.95-10.36-2.356.865 5.92 3.548 11.737 7.333 16.951-2.978.885-6.287 1.383-10.3 1.453 2.939 5.233 7.547 9.714 12.972 13.23-2.505 1.959-5.478 3.654-9.299 5.198 4.596 3.815 10.474 6.341 16.766 7.685-1.62 2.647-3.743 5.228-6.674 7.96 5.65 1.912 12.03 2.166 18.377 1.17-.556 3.098-1.626 6.314-3.412 9.975h.002c5.977-.24 12.042-2.3 17.623-5.52.6 3.108.76 6.51.402 10.593 12.047-5.19 21.395-18.638 24.695-31.951.9-2.408 1.385-5.881 1.385-10.7 0-1.293-.031-2.531-.076-3.745 9.742-1.655 16.839-5.001 20.914-7.39 1.494-.877 3.01-2.165 3.496-3.22v-.002a5.128 5.128 0 0 0-3.7-8.435 5.128 5.128 0 0 0-5.183 3.889c-7.92-4.122-16.98-12.477-14.779-24.551 1.646-2.299 3.947-4.603 7.13-7.016-5.952-.698-12.276.392-18.302 2.707-.095-3.135.28-6.492 1.262-10.43-5.776 1.473-11.262 4.733-16.041 9.02-1.22-2.902-2.076-6.18-2.572-10.223zm26.465 19.557c-.015 6.117 2.648 11.628 7.412 16.281 4.366 4.265 10.426 7.55 15.258 8.795.298.328.331.728-.035 1.285-.392.304-.825.603-1.275.867-4.613 2.704-10.256 5.019-16.871 6.506-1.071.24-2.154.458-3.25.649-.265-4.945-.848-9.243-1.422-13.125a3.419 3.419 0 1 0-.092-6.703 2.138 2.138 0 0 1-.28 4.177c-1.11-7.435-1.84-13.218.555-18.732z" fill="#37474f"/></g></symbol><symbol viewBox="0 0 24 24" id="bucklescript" xmlns="http://www.w3.org/2000/svg"><path d="M3 3v18h18V3H3zm14.1 8.858a5.5 5.5 0 0 1 1.26.145c.417.093.778.213 1.082.357v1.723h-.18a3.281 3.281 0 0 0-.959-.603 2.867 2.867 0 0 0-1.155-.247c-.14 0-.277.011-.416.035a1.4 1.4 0 0 0-.395.12.756.756 0 0 0-.291.231.54.54 0 0 0-.123.348c0 .198.065.35.196.456.13.104.376.2.738.288.237.057.466.11.683.164.22.054.455.128.706.222.496.188.86.444 1.095.77.238.32.357.738.357 1.253 0 .737-.271 1.336-.813 1.798-.538.46-1.27.689-2.197.689a5.447 5.447 0 0 1-1.402-.161 6.725 6.725 0 0 1-1.117-.416v-1.794h.183c.344.318.73.563 1.155.734.429.17.839.256 1.233.256.1 0 .235-.01.4-.03.166-.02.3-.055.403-.102a.97.97 0 0 0 .313-.225c.084-.09.127-.223.127-.4a.568.568 0 0 0-.183-.424c-.119-.12-.294-.213-.526-.276-.243-.067-.5-.128-.773-.185a5.523 5.523 0 0 1-.76-.227c-.544-.204-.936-.48-1.177-.828-.237-.351-.357-.786-.357-1.305 0-.697.27-1.265.81-1.703.54-.442 1.235-.663 2.083-.663zm-8.981.135h2.51c.521 0 .903.02 1.143.06.243.041.484.13.721.266.246.144.43.338.548.583.121.24.181.518.181.83 0 .36-.082.68-.247.959a1.697 1.697 0 0 1-.7.642v.04c.423.098.758.298 1.004.603.249.305.373.706.373 1.205 0 .361-.063.686-.19.97-.125.285-.296.52-.516.707a2.31 2.31 0 0 1-.845.472c-.304.094-.69.141-1.159.141H8.12v-7.478zm1.659 1.372v1.582h.262c.263 0 .486-.007.672-.017.185-.01.332-.043.44-.1.15-.077.248-.175.294-.295.046-.124.07-.266.07-.427a.91.91 0 0 0-.083-.371.518.518 0 0 0-.282-.277 1.187 1.187 0 0 0-.456-.086c-.18-.007-.433-.01-.76-.01h-.157zm0 2.873V18.1H9.9c.469 0 .804-.002 1.007-.006.202-.003.39-.046.56-.13a.712.712 0 0 0 .357-.33c.067-.142.099-.302.099-.483 0-.237-.04-.42-.121-.547-.078-.13-.214-.228-.405-.291a1.842 1.842 0 0 0-.538-.072 49.47 49.47 0 0 0-.716-.003h-.366z" fill="#26a69a" stroke-width="1.067"/></symbol><symbol viewBox="0 0 24 24" id="c" xmlns="http://www.w3.org/2000/svg"><path d="M15.45 15.97l.42 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96-1.14-1.27-1.68-2.88-1.68-4.83C6 9.9 6.68 8.13 8 6.89 9.28 5.64 10.92 5 12.9 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.6 2.49-1.04-.34c-.4-.1-.87-.15-1.4-.15-1.15-.01-2.11.36-2.86 1.1-.76.73-1.14 1.85-1.18 3.34.01 1.36.37 2.42 1.08 3.2.71.77 1.7 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.09-.32z" fill="#0277bd"/></symbol><symbol viewBox="0 0 300 300" id="cabal" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -822.52)" fill-rule="evenodd" color="#000"><rect transform="matrix(-.98339 .18149 .60192 .79856 0 0)" x="405.55" y="967.22" width="107.25" height="156.59" rx="12.306" ry="12.31" fill="#2d9bbd"/><rect transform="matrix(-.98528 .17093 -.59175 .80612 0 0)" x="-1156.5" y="1461.9" width="108.34" height="123.15" rx="10.69" ry="12.31" fill="#4a4bcd"/><path d="M52.112 965.158c-1.343 3.515-26.292 23.248-25.744 27.277.548 4.03 29.812 16.023 32.04 19.027s10.545 41.668 13.603 42.5 18.828-31.274 21.548-32.932c2.72-1.658 32.808 2.503 34.15-1.01 1.343-3.515-18.174-35.352-18.721-39.381-.548-4.03 9.732-40.12 7.502-43.125-2.229-3.005-30.06 9.427-33.118 8.594-3.059-.833-26.793-27.3-29.514-25.643-2.72 1.657-.405 41.177-1.747 44.693z" fill="#2e5bc1"/></g></symbol><symbol viewBox="0 0 24 24" id="cake" xmlns="http://www.w3.org/2000/svg"><path d="M12.254 6.621a1.807 1.807 0 0 0 1.808-1.807c0-.344-.09-.66-.262-.932l-1.546-2.684-1.546 2.684a1.72 1.72 0 0 0-.262.932 1.808 1.808 0 0 0 1.808 1.807m4.158 9.04l-.967-.976-.976.976c-1.175 1.166-3.236 1.175-4.42 0l-.959-.976-.994.976a3.134 3.134 0 0 1-3.977.353v4.167a.904.904 0 0 0 .904.904h14.463a.904.904 0 0 0 .904-.904v-4.167a3.134 3.134 0 0 1-3.977-.353m1.265-6.328h-4.52V7.525H11.35v1.808H6.83a2.712 2.712 0 0 0-2.711 2.712v1.392c0 .977.795 1.772 1.771 1.772.489 0 .94-.18 1.248-.515l1.952-1.926 1.908 1.926c.669.669 1.835.669 2.504 0l1.916-1.926 1.944 1.926c.316.334.768.515 1.247.515.976 0 1.78-.795 1.78-1.772v-1.392a2.712 2.712 0 0 0-2.711-2.712z" fill="#ff7043" stroke-width=".904"/></symbol><symbol viewBox="0 0 24 24" id="certificate" xmlns="http://www.w3.org/2000/svg"><path d="M4 3c-1.11 0-2 .89-2 2v10a2 2 0 0 0 2 2h8v5l3-3 3 3v-5h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H4m8 2l3 2 3-2v3.5l3 1.5-3 1.5V15l-3-2-3 2v-3.5L9 10l3-1.5V5M4 5h5v2H4V5m0 4h3v2H4V9m0 4h5v2H4v-2z" fill="#ff5722"/></symbol><symbol viewBox="0 0 24 24" id="changelog" xmlns="http://www.w3.org/2000/svg"><path d="M11 7v5.11l4.71 2.79.79-1.28-4-2.37V7m0-5C8.97 2 5.91 3.92 4.27 6.77L2 4.5V11h6.5L5.75 8.25C6.96 5.73 9.5 4 12.5 4a7.5 7.5 0 0 1 7.5 7.5 7.5 7.5 0 0 1-7.5 7.5c-3.27 0-6.03-2.09-7.06-5h-2.1c1.1 4.03 4.77 7 9.16 7 5.24 0 9.5-4.25 9.5-9.5A9.5 9.5 0 0 0 12.5 2z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 24 24" id="clojure" xmlns="http://www.w3.org/2000/svg"><path d="M3.355 1.78c-.845 0-1.525.68-1.525 1.525v17.441c0 .845.68 1.525 1.525 1.525h17.442c.845 0 1.525-.68 1.525-1.525V3.305c0-.845-.68-1.526-1.525-1.526H3.355zm6.168 2.572h1.963l6.368 14.931H15.93l-3.38-8.086-3.349 8.086H7.21l4.346-10.38-2.032-4.551z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="cmake" xmlns="http://www.w3.org/2000/svg"><path d="M11.99 2.965L2.977 20.999l9.874-8.47-.863-9.564z" fill="#1e88e5"/><path d="M12.007 2.963l.002.29 1.312 14.498-.001.006.023.26 7.362 2.979h.416l-.158-.311-.114-.228h-.002l-8.84-17.494z" fill="#e53935"/><path d="M8.607 16.11L2.98 20.995h17.743v-.016L8.607 16.11z" fill="#7cb342"/></symbol><symbol class="bfmain_logo__svg" viewBox="0 0 300 300.00001" id="code-climate" xmlns="http://www.w3.org/2000/svg"><path class="bfsymbol" d="M196.19 75.562l-51.846 51.561 30.766 30.766 21.08-21.08 59.252 59.537 30.481-30.766zm-61.246 60.961l-30.481-30.481-78.053 78.053-11.964 11.964 30.766 30.766 11.964-12.249 39.596-39.312 7.691-7.691 30.481 30.48 28.772 28.773 30.766-30.766-28.772-28.772z" fill="#eee" stroke-width="2.849"/></symbol><symbol class="bgmain_logo__svg" viewBox="0 0 300 300.00001" id="code-climate_light" xmlns="http://www.w3.org/2000/svg"><path class="bgsymbol" d="M196.19 75.562l-51.846 51.561 30.766 30.766 21.08-21.08 59.252 59.537 30.481-30.766zm-61.246 60.961l-30.481-30.481-78.053 78.053-11.964 11.964 30.766 30.766 11.964-12.249 39.596-39.312 7.691-7.691 30.481 30.48 28.772 28.773 30.766-30.766-28.772-28.772z" fill="#455a64" stroke-width="2.849"/></symbol><symbol viewBox="0 0 24 24" id="coffee" xmlns="http://www.w3.org/2000/svg"><path d="M2 21h18v-2H2M20 8h-2V5h2m0-2H4v10a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4v-3h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="coldfusion" xmlns="http://www.w3.org/2000/svg"><rect transform="rotate(90)" x="2.283" y="-21.86" width="19.487" height="19.487" ry="0" fill="#0d3858" stroke="#4dd0e1" stroke-width=".7"/><text x="6.653" y="16.426" fill="#4dd0e1" font-family="Calibri" font-size="29.001" font-weight="bold" letter-spacing="0" stroke-width=".725" word-spacing="0" style="line-height:1.25"><tspan x="6.653" y="16.426" font-family="'Segoe UI'" font-size="10.634" font-weight="normal">C<tspan font-size="11.844">f</tspan></tspan></text></symbol><symbol viewBox="0 0 24 24" id="conduct" xmlns="http://www.w3.org/2000/svg"><path d="M10 17l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9m-6-6a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#cddc39"/></symbol><symbol viewBox="0 0 24 24" id="console" xmlns="http://www.w3.org/2000/svg"><path d="M20 19V7H4v12h16m0-16a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16m-7 14v-2h5v2h-5m-3.42-4L5.57 9H8.4l3.3 3.3c.39.39.39 1.03 0 1.42L8.42 17H5.59z" fill="#ff7043"/></symbol><symbol viewBox="0 0 24 24" id="contributing" xmlns="http://www.w3.org/2000/svg"><path d="M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="cpp" xmlns="http://www.w3.org/2000/svg"><path d="M10.5 15.97l.41 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96C1.56 15.77 1 14.16 1 12.21c.05-2.31.72-4.08 2-5.32C4.32 5.64 5.96 5 7.94 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.58 2.49-1.06-.34c-.4-.1-.86-.15-1.39-.15-1.16-.01-2.12.36-2.87 1.1-.76.73-1.15 1.85-1.18 3.34 0 1.36.37 2.42 1.08 3.2.71.77 1.71 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.1-.32M11 11h2V9h2v2h2v2h-2v2h-2v-2h-2v-2m7 0h2V9h2v2h2v2h-2v2h-2v-2h-2v-2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="credits" xmlns="http://www.w3.org/2000/svg"><path d="M3 3h18v2H3V3m4 4h10v2H7V7m-4 4h18v2H3v-2m4 4h10v2H7v-2m-4 4h18v2H3v-2z" fill="#9ccc65"/></symbol><symbol viewBox="0 0 200 200" id="crystal" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:none}</style><path d="M179.363 121.67l-57.623 57.507c-.23.23-.576.346-.806.23l-78.713-21.09c-.346-.115-.577-.345-.577-.576L20.44 79.144c-.115-.345 0-.576.23-.806L78.294 20.83c.23-.23.576-.346.807-.23l78.713 21.09c.345.114.576.345.576.575l21.09 78.597c.23.346.115.577-.115.807zM102.148 59.09l-77.33 20.63c-.115 0-.23.23-.115.345l56.586 56.47c.115.115.346.115.346-.115l20.744-77.215c.115 0-.115-.23-.23-.116z" stroke-width="1.153" fill="#cfd8dc"/></symbol><symbol viewBox="0 0 200 200" id="crystal_light" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:none}</style><path d="M179.363 121.67l-57.623 57.507c-.23.23-.576.346-.806.23l-78.713-21.09c-.346-.115-.577-.345-.577-.576L20.44 79.144c-.115-.345 0-.576.23-.806L78.294 20.83c.23-.23.576-.346.807-.23l78.713 21.09c.345.114.576.345.576.575l21.09 78.597c.23.346.115.577-.115.807zM102.148 59.09l-77.33 20.63c-.115 0-.23.23-.115.345l56.586 56.47c.115.115.346.115.346-.115l20.744-77.215c.115 0-.115-.23-.23-.116z" fill="#37474f" stroke-width="1.153"/></symbol><symbol viewBox="0 0 24 24" id="csharp" xmlns="http://www.w3.org/2000/svg"><path d="M11.5 15.97l.41 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96C2.56 15.77 2 14.16 2 12.21c.05-2.31.72-4.08 2-5.32C5.32 5.64 6.96 5 8.94 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.58 2.49-1.06-.34c-.4-.1-.86-.15-1.39-.15-1.16-.01-2.12.36-2.87 1.1-.76.73-1.15 1.85-1.18 3.34 0 1.36.37 2.42 1.08 3.2.71.77 1.71 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.1-.32M13.89 19l.61-4H13l.34-2h1.5l.32-2h-1.5L14 9h1.5l.61-4h2l-.61 4h1l.61-4h2l-.61 4H22l-.34 2h-1.5l-.32 2h1.5L21 15h-1.5l-.61 4h-2l.61-4h-1l-.61 4h-2m2.95-6h1l.32-2h-1l-.32 2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="css" xmlns="http://www.w3.org/2000/svg"><path d="M5 3l-.65 3.34h13.59L17.5 8.5H3.92l-.66 3.33h13.59l-.76 3.81-5.48 1.81-4.75-1.81.33-1.64H2.85l-.79 4 7.85 3 9.05-3 1.2-6.03.24-1.21L21.94 3H5z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="css-map" xmlns="http://www.w3.org/2000/svg"><path d="M18 8v2h2v10H10v-2H8v4h14V8h-4z" fill="#42a5f5"/><path d="M4.676 3l-.488 2.51h10.211l-.33 1.623H3.864l-.496 2.502H13.58l-.57 2.863-4.119 1.36-3.569-1.36.248-1.232H3.06l-.593 3.005 5.898 2.254 6.8-2.254.902-4.53.18-.91L17.406 3H4.675z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 33 33" id="cucumber" xmlns="http://www.w3.org/2000/svg"><title>cucumber-mark-transparent-pips</title><g transform="translate(0 -5)" fill="none" fill-rule="evenodd"><path d="M-4-1h40v40H-4z"/><path d="M16.641 7.092c-7.028 0-12.714 5.686-12.714 12.714 0 6.187 4.435 11.327 10.288 12.471v3.64C21.824 34.77 28.561 28.73 29.063 20.8c.303-4.772-2.076-9.644-6.09-12.01a10.575 10.575 0 0 0-1.455-.728l-.243-.097c-.223-.082-.448-.175-.68-.242a12.614 12.614 0 0 0-3.954-.632zm2.62 4.707a1.387 1.387 0 0 0-1.213.485c-.233.31-.379.611-.534.923-.466 1.087-.31 2.251.388 3.105 1.087-.233 2.01-.927 2.475-2.014a2.45 2.45 0 0 0 .243-1.02c.048-.824-.634-1.404-1.359-1.479zm-5.654.073c-.708.068-1.382.63-1.382 1.407 0 .31.087.709.243 1.02.466 1.086 1.46 1.78 2.546 2.013.621-.854.782-2.018.316-3.105-.155-.311-.3-.617-.534-.85a1.364 1.364 0 0 0-1.188-.485zm-3.809 3.735c-1.224.063-1.77 1.602-.752 2.402.31.233.612.403.922.559 1.087.466 2.344.306 3.275-.316-.233-1.009-1.023-1.936-2.11-2.402-.388-.155-.703-.243-1.092-.243-.087-.009-.161-.004-.243 0zm11.961 4.708a3.551 3.551 0 0 0-2.013.582c.233 1.01 1.023 1.936 2.11 2.401.389.156.705.244 1.093.244 1.397.077 2.08-1.65.994-2.427-.31-.233-.611-.379-.922-.534a3.354 3.354 0 0 0-1.262-.266zm-10.603.072a3.376 3.376 0 0 0-1.261.267c-.389.155-.69.325-.923.558-1.009.854-.33 2.48 1.068 2.402.388 0 .782-.087 1.092-.243 1.087-.465 1.859-1.392 2.014-2.401a3.474 3.474 0 0 0-1.99-.582zm3.931 2.378c-1.087.233-2.009.927-2.475 2.014-.155.31-.243.684-.243.995-.077 1.32 1.724 2.028 2.5 1.02.233-.312.378-.613.534-.923.466-1.01.306-2.174-.316-3.106zm2.887.073c-.621.854-.781 2.019-.315 3.106.155.31.3.615.534.848.854.932 2.65.243 2.572-.921 0-.31-.088-.71-.243-1.02-.466-1.087-1.46-1.78-2.547-2.013z" fill="#4caf50" stroke-width=".776"/></g></symbol><symbol id="cuda" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><style>.bust0{fill:#76b900}</style><title>NVIDIA-Logo</title><path id="buEye_Mark" class="bust0" d="M76.362 75.199V64.116c1.095-.068 2.19-.137 3.284-.137 30.377-.958 50.286 26.135 50.286 26.135s-21.483 29.83-44.539 29.83c-3.079 0-6.089-.48-8.962-1.438v-33.66c11.836 1.436 14.23 6.636 21.277 18.471l15.804-13.273s-11.562-15.12-30.992-15.12c-2.053-.068-4.105.069-6.158.274m0-36.67v16.556l3.284-.205c42.213-1.437 69.784 34.618 69.784 34.618s-31.608 38.45-64.516 38.45c-2.873 0-5.678-.274-8.483-.753v10.262c2.326.274 4.72.48 7.046.48 30.65 0 52.817-15.668 74.3-34.14 3.558 2.874 18.13 9.784 21.14 12.794-20.388 17.104-67.937 30.856-94.893 30.856-2.6 0-5.062-.137-7.525-.41v14.436h116.44V38.532zm0 79.977v8.757C48.038 122.2 40.17 92.712 40.17 92.712s13.615-15.05 36.192-17.514v9.579h-.068c-11.836-1.437-21.14 9.646-21.14 9.646s5.268 18.678 21.209 24.082M26.077 91.481S42.839 66.714 76.43 64.115v-9.03C39.213 58.094 7.057 89.565 7.057 89.565s18.199 52.68 69.305 57.47v-9.579c-37.492-4.652-50.286-45.975-50.286-45.975z" fill="#8bc34a" stroke-width=".684"/></symbol><symbol viewBox="0 0 24 24" id="dart" xmlns="http://www.w3.org/2000/svg"><title>Dart</title><path d="M12.486 1.385a.978.978 0 0 0-.682.281l-.01.007-6.387 3.692 6.371 6.372v.004l7.659 7.659 1.46-2.63-5.265-12.64-2.456-2.457a.972.972 0 0 0-.69-.288z" fill="#00ca94"/><path d="M5.422 5.35L1.73 11.733l-.007.01a.967.967 0 0 0 .006 1.371l3.059 3.061 11.963 4.706 2.704-1.502-.073-.073-.018.002-7.5-7.512h-.01L5.423 5.35z" fill="#1565c0"/><path d="M5.405 5.353l6.518 6.525h.01l7.502 7.51 2.855-.544.005-8.449-3.016-2.955c-.66-.647-1.675-1.064-2.695-1.202l.002-.032-11.181-.853z" fill="#1565c0"/><path d="M5.414 5.361l6.521 6.522v.009l7.506 7.506-.546 2.855h-8.448l-2.954-3.017c-.647-.66-1.064-1.676-1.2-2.696l-.033.003L5.414 5.36z" fill="#00ee94"/></symbol><symbol viewBox="0 0 24 24" id="database" xmlns="http://www.w3.org/2000/svg"><path d="M12 3C7.58 3 4 4.79 4 7s3.58 4 8 4 8-1.79 8-4-3.58-4-8-4M4 9v3c0 2.21 3.58 4 8 4s8-1.79 8-4V9c0 2.21-3.58 4-8 4s-8-1.79-8-4m0 5v3c0 2.21 3.58 4 8 4s8-1.79 8-4v-3c0 2.21-3.58 4-8 4s-8-1.79-8-4z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="diff" xmlns="http://www.w3.org/2000/svg"><path d="M3 1c-1.11 0-2 .89-2 2v11c0 1.11.89 2 2 2h2v-2H3V3h11v2h2V3c0-1.11-.89-2-2-2H3m6 6c-1.11 0-2 .89-2 2v2h2V9h2V7H9m4 0v2h1v1h2V7h-3m5 0v2h2v11H9v-2H7v2c0 1.11.89 2 2 2h11c1.11 0 2-.89 2-2V9c0-1.11-.89-2-2-2h-2m-4 5v2h-2v2h2c1.11 0 2-.89 2-2v-2h-2m-7 1v3h3v-2H9v-1H7z" fill="#42a5f5"/></symbol><symbol id="docker" viewBox="0 0 41 34.5" xmlns="http://www.w3.org/2000/svg"><style id="bystyle2">.byst0{fill:#fff}.byst1{clip-path:url(#bySVGID_4_)}</style><g id="byg34" transform="translate(.292 1.9)" fill="#0087c9"><g id="byg32"><g id="byg30"><g id="byg28"><g id="byg26"><g id="byg9"><path id="bySVGID_1_" class="byst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2c1.2 0 2.1.9 2.1 2s-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></g></g></g></g></g></g></symbol><symbol viewBox="0 0 24 24" id="document" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m9 16v-2H6v2h9m3-4v-2H6v2h12z" fill="#42a5f5"/></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 200 200" id="drone" xmlns="http://www.w3.org/2000/svg"><g fill="#e0e0e0" transform="translate(9.063 22.346) scale(.71044)"><path d="M128.22.723C32.095.723.39 84.566.39 115.222h77.928S89.36 75.275 128.22 75.275s49.906 39.947 49.906 39.947h77.476c0-30.66-31.257-114.5-127.38-114.5m98.82 134.45h-48.914s-8.55 39.946-49.906 39.946c-41.355 0-49.902-39.948-49.902-39.948H30.255c0 10.25 37.727 82.708 98.443 82.708 60.714 0 98.344-59.604 98.344-82.708"/><circle cx="128" cy="126.08" r="32.768"/></g></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 200 200" id="drone_light" xmlns="http://www.w3.org/2000/svg"><g fill="#424242" transform="translate(9.063 22.346) scale(.71044)"><path d="M128.22.723C32.095.723.39 84.566.39 115.222h77.928S89.36 75.275 128.22 75.275s49.906 39.947 49.906 39.947h77.476c0-30.66-31.257-114.5-127.38-114.5m98.82 134.45h-48.914s-8.55 39.946-49.906 39.946c-41.355 0-49.902-39.948-49.902-39.948H30.255c0 10.25 37.727 82.708 98.443 82.708 60.714 0 98.344-59.604 98.344-82.708"/><circle cx="128" cy="126.08" r="32.768"/></g></symbol><symbol viewBox="0 0 3473 3473" id="editorconfig" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" xmlns="http://www.w3.org/2000/svg"><defs id="ccdefs4"><style id="ccstyle2">.ccfil2{fill:#020202}.ccfil0{fill:#e3e3f8}.ccfil5{fill:#efefef}.ccfil6{fill:#faf1f1}.ccfil3{fill:#fdf2f2}.ccfil1{fill:#fdfdfd}.ccfil4{fill:#fef3f3}</style></defs><g id="ccLayer_x0020_1" transform="matrix(.8945 0 0 .8945 138.649 275.985)"><g id="cc_631799120"><g id="ccg11"><path class="ccfil0" d="M967 1895c46-30 84-105 61-158-63 27-60 89-61 158z" id="ccpath7" fill="#e3e3f8"/><path class="ccfil0" d="M1679 2067c50-16 98-72 71-130-39 27-64 64-71 130z" id="ccpath9" fill="#e3e3f8"/></g><g id="ccg21"><path class="ccfil1" d="M280 2895c0 63 16 131 60 155 162 91 730 20 923-23 101-23 183-98 278-139 214-93 369-168 540-293 124-91 321-347 342-500l-169-38c-4 172-43 211-196 251-103 28-304 34-409 16-139-23-202-96-265-179-122-162 27-275-166-286-203 249-561 70-718 45-67 97-224 727-222 871 97-33 158 3 245 37 308 119 39 224-84 193-84-20-110-75-159-110z" id="ccpath13" fill="#fdfdfd"/><path class="ccfil1" d="M683 1458c125 24 236 76 342 129 173 86 204 74 220 194 2 22-2 34 61 54 106 33-61-26 223-25 169 1 556 69 681 148 52 33 42 75 218 70-2-207-57-516-138-706-99-230-230-265-497-351-156-50-614-105-756-17-133 83-158 182-282 356-36 51-49 90-72 148z" id="ccpath15" fill="#fdfdfd"/><path class="ccfil1" d="M1784 1883c100 41-5 306-144 242-45-127 62-199 91-256-60-9-231-36-282-17-66 25-81 166-47 232 160 314 867 247 792 3-30-99-58-115-159-149-81-27-162-55-251-55z" id="ccpath17" fill="#fdfdfd"/><path class="ccfil1" d="M527 1848c80 77 261 89 378 95 15-155 28-271 152-262 61 83 29 181-35 244 109-1 172-83 156-202-92-66-371-198-511-217-39 42-135 272-140 342z" id="ccpath19" fill="#fdfdfd"/></g><path class="ccfil2" d="M339 2838c66-6 238 44 252 100-107 13-243 3-252-100zm-59 57c49 35 75 90 159 110 123 31 392-74 84-193-87-34-148-70-245-37-2-144 155-774 222-871 157 25 515 204 718-45 193 11 44 124 166 286 63 83 126 156 265 179 105 18 306 12 409-16 153-40 192-79 196-251l169 38c-21 153-218 409-342 500-171 125-326 200-540 293-95 41-177 116-278 139-193 43-761 114-923 23-44-24-60-92-60-155zm1399-828c7-66 32-103 71-130 27 58-21 114-71 130zm105-184c89 0 170 28 251 55 101 34 129 50 159 149 75 244-632 311-792-3-34-66-19-207 47-232 51-19 222 8 282 17-29 57-136 129-91 256 139 64 244-201 144-242zm-817 12c1-69-2-131 61-158 23 53-15 128-61 158zm-440-47c5-70 101-300 140-342 140 19 419 151 511 217 16 119-47 201-156 202 64-63 96-161 35-244-124-9-137 107-152 262-117-6-298-18-378-95zm-100-80c-37-102-37-261 120-274l-80 223c-21 48-21 37-40 51zm256-310c23-58 36-97 72-148 124-174 149-273 282-356 142-88 600-33 756 17 267 86 398 121 497 351 81 190 136 499 138 706-176 5-166-37-218-70-125-79-512-147-681-148-284-1-117 58-223 25-63-20-59-32-61-54-16-120-47-108-220-194-106-53-217-105-342-129zm1770-49c-19-63 16-59 77-102 35-25 63-51 106-75 161-90 461-105 589 2 52 43 137 127 124 237-27 219-177 339-300 439-125 102-333 207-548 137-18-44-4-323-25-426-19-92-9-102 44-157 156-162 494-280 686-141 81 60 58 83 100 129 52-56-45-244-403-232-243 8-348 198-450 189zM997 840c5-139 133-427 261-527 155-120 317-233 555-98 59 33 56 50 62 132 5 79-2 108-22 172-158 510-290 217-796 338 19-166 163-314 243-391 137-133 236-219 442-191 57 95 63 155-6 266-92 148-115 139-101 240 72-18 94-88 127-158 201-420-91-471-270-394-120 51-334 287-404 429-14 28-29 64-42 95zm792 21c21-125 145-156 145-541 0-166-204-315-471-204-229 94-264 166-386 350-115 174-111 365-210 526-29 46-55 62-87 108-23 34-40 77-63 117-47 77-95 133-133 225-120 3-221 5-233 129-16 170 64 212 64 276-1 69-281 765-203 1180 22 114 97 115 217 129 289 35 664 23 923-81l470-225c119-67 319-194 408-287 63-65 96-120 150-197 74-108 76-106 92-253 98 18 281 61 342 114-7 69-41 36-41 98 39 1 104-48 120-102-41-60-84-50-143-98 47-37 132-54 197-81 140-58 379-234 438-394 47-129 12-344-64-428-80-88-266-133-418-133-181 0-368 130-514 186-56-49-60-105-101-159-47-64-353-224-499-255z" id="ccpath23" fill="#020202"/><path class="ccfil3" d="M2453 1409c102 9 207-181 450-189 358-12 455 176 403 232-42-46-19-69-100-129-192-139-530-21-686 141-53 55-63 65-44 157 21 103 7 382 25 426 215 70 423-35 548-137 123-100 273-220 300-439 13-110-72-194-124-237-128-107-428-92-589-2-43 24-71 50-106 75-61 43-96 39-77 102z" id="ccpath25" fill="#fdf2f2"/><path class="ccfil4" d="M997 840l49-87c13-31 28-67 42-95 70-142 284-378 404-429 179-77 471-26 270 394-33 70-55 140-127 158-14-101 9-92 101-240 69-111 63-171 6-266-206-28-305 58-442 191-80 77-224 225-243 391 506-121 638 172 796-338 20-64 27-93 22-172-6-82-3-99-62-132-238-135-400-22-555 98-128 100-256 388-261 527z" id="ccpath27" fill="#fef3f3"/><path class="ccfil5" d="M427 1768c19-14 19-3 40-51l80-223c-157 13-157 172-120 274z" id="ccpath29" fill="#efefef"/><path class="ccfil6" d="M591 2938c-14-56-186-106-252-100 9 103 145 113 252 100z" id="ccpath31" fill="#faf1f1"/></g></g></symbol><symbol viewBox="0 0 24 24" id="elixir" xmlns="http://www.w3.org/2000/svg"><path d="M12.431 22.383c-3.86 0-6.99-3.64-6.99-8.13 0-3.678 2.774-8.172 4.916-10.91 1.014-1.295 2.931-2.321 2.931-2.321s-.982 5.238 1.683 7.318c2.365 1.847 4.105 4.25 4.105 6.363 0 4.232-2.784 7.68-6.645 7.68z" fill="#9575cd" stroke-width="1.256"/></symbol><symbol viewBox="0 0 323.00001 322.99999" id="elm" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.8053 0 0 .8053 30.106 31.524)"><path fill="#f0ad00" d="M160.8 153.865l68.028-68.03H92.77z"/><path fill="#7fd13b" d="M160.983 5.098H12.033l68.524 68.525H229.51z"/><path fill="#7fd13b" stroke-width=".974" d="M243.906 88.021l74.136 74.137-74.474 74.475-74.137-74.137z"/><path fill="#60b5cc" d="M318.2 145.045V5.098H178.252z"/><path fill="#5a6378" d="M152.164 162.499L3.4 13.733v297.533z"/><path fill="#f0ad00" d="M252.205 245.27l65.995 65.996v-131.99z"/><path fill="#60b5cc" d="M160.8 171.134L12.034 319.899h297.53z"/></g></symbol><symbol viewBox="0 0 24 24" id="email" xmlns="http://www.w3.org/2000/svg"><path d="M20 8l-8 5-8-5V6l8 5 8-5m0-2H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 30 30" id="erlang" xmlns="http://www.w3.org/2000/svg"><path style="line-height:1.25;-inkscape-font-specification:'Wide Latin'" d="M5.217 4.367c-.048.052-.097.1-.144.153C2.697 7.182 1.51 10.798 1.51 15.366c0 4.418 1.156 7.862 3.46 10.34h19.414c2.553-1.152 4.127-3.43 4.127-3.43l-3.147-2.52-1.454 1.381c-.866.773-.845.931-2.314 1.78-1.496.674-3.04.966-4.634.966-2.516 0-4.423-.909-5.723-2.059-1.286-1.15-1.985-4.511-2.097-6.68l17.458.067-.182-1.472s-.847-7.129-2.542-9.372zm8.76.846c1.565 0 3.22.535 3.96 1.471.742.937.932 1.667.974 3.524H9.12c.111-1.955.436-2.81 1.372-3.697.937-.888 2.03-1.298 3.484-1.298z" font-weight="400" font-size="48" font-family="Wide Latin" letter-spacing="0" word-spacing="0" fill="#f44336" stroke-width=".97"/></symbol><symbol viewBox="0 0 299.99999 300.00001" id="eslint" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-2.88 18.438) scale(1.0344)"><path d="M97.021 99.016l48.432-27.962c1.212-.7 2.706-.7 3.918 0l48.433 27.962a3.92 3.92 0 0 1 1.959 3.393v55.924a3.924 3.924 0 0 1-1.959 3.394l-48.433 27.962c-1.212.7-2.706.7-3.918 0l-48.432-27.962a3.92 3.92 0 0 1-1.959-3.394v-55.924a3.922 3.922 0 0 1 1.959-3.393" fill="#7986cb"/><path d="M273.34 124.49L215.473 23.82c-2.102-3.64-5.985-6.325-10.188-6.325H89.545c-4.204 0-8.088 2.685-10.19 6.325L21.488 124.27c-2.102 3.641-2.102 8.236 0 11.877l57.867 99.847c2.102 3.64 5.986 5.501 10.19 5.501h115.74c4.203 0 8.087-1.805 10.188-5.446l57.867-100.01c2.104-3.639 2.104-7.907.001-11.547m-47.917 48.41c0 1.48-.891 2.849-2.174 3.59l-73.71 42.527a4.194 4.194 0 0 1-4.17 0l-73.767-42.527c-1.282-.741-2.179-2.109-2.179-3.59V87.847c0-1.481.884-2.849 2.167-3.59l73.707-42.527a4.185 4.185 0 0 1 4.168 0l73.772 42.527c1.283.741 2.186 2.109 2.186 3.59z" fill="#3f51b5"/></g></symbol><symbol viewBox="0 0 24 24" id="exe" xmlns="http://www.w3.org/2000/svg"><path d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h14m0 14V8H5v10h14z" fill="#e64a19"/></symbol><symbol viewBox="0 0 24 24" id="favicon" xmlns="http://www.w3.org/2000/svg"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.62L12 2 9.19 8.62 2 9.24l5.45 4.73L5.82 21 12 17.27z" fill="#ffd54f"/></symbol><symbol viewBox="0 0 24 24" id="file" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m5 2H6v16h12v-9h-7V4z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 400 400" id="firebase" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 103)"><path d="M72.55 208.77l44.456-292.29 56.209 90.445L195.49-37.57 330.6 209.28z" fill="#ffa712"/><path d="M195.7 276.73l134.9-67.45-46.5-224.83L72.55 208.77z" fill="#fcca3f"/><path d="M173.22 6.932L72.56 208.772l136.35-144.58z" fill="#f6820c"/></g></symbol><symbol viewBox="0 0 24 24" id="flash" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="cma"><stop offset="0" stop-color="#d92f3c"/><stop offset="1" stop-color="#791223"/></linearGradient><linearGradient xlink:href="#cma" id="cmb" x1="2.373" y1="12.027" x2="21.86" y2="12.027" gradientUnits="userSpaceOnUse" gradientTransform="translate(-.09 -24.144)"/></defs><rect width="19.487" height="19.487" x="2.283" y="-21.86" transform="rotate(90)" ry="0" fill="url(#cmb)"/><path style="line-height:125%" d="M16.802 5.768l-.013.002a6.43 6.43 0 0 0-1.182.192 5.062 5.062 0 0 0-1.494.718c-.428.323-.817.72-1.17 1.191-.34.48-.682 1.032-1.022 1.66-.12.228-.233.424-.35.636v.002h-.004l-1.34 2.394-.005-.002c-.238.443-.461.847-.665 1.198a4.358 4.358 0 0 1-.716.94 2.79 2.79 0 0 1-.907.594c-.072.027-.161.042-.242.063h-.989v2.414h.989v-.002a6.427 6.427 0 0 0 1.185-.192 5.062 5.062 0 0 0 1.494-.718 5.94 5.94 0 0 0 1.171-1.191c.34-.48.681-1.033 1.021-1.66.12-.228.235-.425.353-.637l.006.002.003-.005.037-.066h2.53v.002h1.124v-2.408h-.33v-.001h-1.98c.22-.407.432-.789.621-1.115.214-.37.452-.682.717-.94a2.79 2.79 0 0 1 .906-.594c.07-.027.16-.041.239-.061h.992V8.18h-.002V5.77h-.977v-.002z" font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#fff"/></symbol><symbol class="cnflow-logo" viewBox="0 0 299.99999 300" id="flow" xmlns="http://www.w3.org/2000/svg"><title>Flow logo</title><path d="M38.75 33.427l77.461 77.47H54.436l61.145 61.16H38.437l93.462 93.478v-77.158l.01-.01v-77.47h-.01V66.982h46.691l20.394 20.393H153.57v76.531h22.05l24.474 24.473h-15.806l-.01-.01v.01h-31.665l-.01-.01v.01h-.313l.313.313v77.148h109.149l-39.2-39.2v-15.806l8.465 8.466v-77.37h-15.682l.017-38.191 30.09 30.086V56.362h-64.874l-22.94-22.934H113.71z" fill="#fbc02d" fill-opacity=".976" stroke-width=".955" class="cnflow-logo-mark"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-aurelia" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="coa" x1="-388.15%" x2="237.68%" y1="-144.18%" y2="430.41%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cob" x1="72.945%" x2="-97.052%" y1="84.424%" y2="-147.7%"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="coc" x1="-283.88%" x2="287.54%" y1="-693.6%" y2="101.71%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cod" x1="-821.19%" x2="101.99%" y1="-469.05%" y2="288.24%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="coe" x1="-140.36%" x2="419.01%" y1="-230.93%" y2="261.98%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cof" x1="191.08%" x2="20.358%" y1="253.95%" y2="20.403%"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cog" x1="-388.09%" x2="237.67%" y1="-173.85%" y2="518.99%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="coi" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#coa"/><linearGradient id="coj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#cob"/><linearGradient id="cok" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#coc"/><linearGradient id="col" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#cod"/><linearGradient id="com" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#coe"/><linearGradient id="con" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#cof"/><linearGradient id="coo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cog"/><linearGradient id="cop" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#coh"/><linearGradient id="coh" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#f06292" fill-rule="nonzero"/><g transform="matrix(.31022 .0619 -.0619 .31022 11.807 7.546)" fill="none"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#coi)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#coj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#cok)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#col)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#com)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#con)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#coo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#cop)"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-aurelia-open" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="cpi" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cpa"/><linearGradient id="cpa" x1="-3.881" x2="2.377" y1="-1.442" y2="4.304"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#cpb"/><linearGradient id="cpb" x1=".729" x2="-.971" y1=".844" y2="-1.477"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cpk" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#cpc"/><linearGradient id="cpc" x1="-2.839" x2="2.875" y1="-6.936" y2="1.017"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpl" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#cpd"/><linearGradient id="cpd" x1="-8.212" x2="1.02" y1="-4.691" y2="2.882"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpm" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#cpe"/><linearGradient id="cpe" x1="-1.404" x2="4.19" y1="-2.309" y2="2.62"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpn" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#cpf"/><linearGradient id="cpf" x1="1.911" x2=".204" y1="2.539" y2=".204"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cpo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cpg"/><linearGradient id="cpg" x1="-3.881" x2="2.377" y1="-1.738" y2="5.19"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpp" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#cph"/><linearGradient id="cph" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#f06292" fill-rule="nonzero"/><g transform="matrix(.31022 .0619 -.0619 .31022 11.807 7.546)" fill="none"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#cpi)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#cpj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#cpk)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#cpl)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#cpm)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#cpn)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#cpo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#cpp)"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-components" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#cddc39" fill-rule="nonzero"/><path d="M11.185 9.613h5.346v2.9l3.782-3.775 3.775 3.775-3.775 3.782h2.9v5.346h-5.346v-5.346h2.446l-3.782-3.782v2.446h-5.346V9.613m0 6.682h5.346v5.346h-5.346z" fill="#f0f4c3" stroke-width=".668"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-components-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#cddc39"/><path d="M11.185 9.613h5.346v2.9l3.782-3.775 3.775 3.775-3.775 3.782h2.9v5.346h-5.346v-5.346h2.446l-3.782-3.782v2.446h-5.346V9.613m0 6.682h5.346v5.346h-5.346z" fill="#f0f4c3" stroke-width=".668"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-config" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00acc1" fill-rule="nonzero"/><path d="M17.293 17.786a2.308 2.308 0 0 1-2.308-2.308 2.308 2.308 0 0 1 2.308-2.307 2.308 2.308 0 0 1 2.308 2.307 2.308 2.308 0 0 1-2.308 2.308m4.899-1.668c.026-.211.046-.422.046-.64 0-.217-.02-.435-.046-.659l1.391-1.075a.333.333 0 0 0 .08-.422l-1.32-2.28c-.079-.146-.257-.205-.402-.146l-1.641.66a4.779 4.779 0 0 0-1.115-.647l-.244-1.747a.333.333 0 0 0-.33-.277h-2.637a.333.333 0 0 0-.33.277l-.243 1.747a4.78 4.78 0 0 0-1.114.646l-1.642-.659a.324.324 0 0 0-.402.145l-1.319 2.281a.325.325 0 0 0 .08.422l1.39 1.075c-.026.224-.046.442-.046.66s.02.428.046.639l-1.39 1.094a.325.325 0 0 0-.08.422l1.319 2.282c.079.145.257.197.402.145l1.642-.666c.342.264.698.488 1.114.653l.244 1.747a.333.333 0 0 0 .33.277h2.637a.333.333 0 0 0 .33-.277l.243-1.747a4.802 4.802 0 0 0 1.115-.653l1.641.666c.145.052.323 0 .403-.145l1.318-2.282a.333.333 0 0 0-.079-.422z" fill="#80deea" stroke-width=".659"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-config-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00acc1"/><path d="M17.293 17.786a2.308 2.308 0 0 1-2.308-2.308 2.308 2.308 0 0 1 2.308-2.307 2.308 2.308 0 0 1 2.308 2.307 2.308 2.308 0 0 1-2.308 2.308m4.899-1.668c.026-.211.046-.422.046-.64 0-.217-.02-.435-.046-.659l1.391-1.075a.333.333 0 0 0 .08-.422l-1.32-2.28c-.079-.146-.257-.205-.402-.146l-1.641.66a4.779 4.779 0 0 0-1.115-.647l-.244-1.747a.333.333 0 0 0-.33-.277h-2.637a.333.333 0 0 0-.33.277l-.243 1.747a4.78 4.78 0 0 0-1.114.646l-1.642-.659a.324.324 0 0 0-.402.145l-1.319 2.281a.325.325 0 0 0 .08.422l1.39 1.075c-.026.224-.046.442-.046.66s.02.428.046.639l-1.39 1.094a.325.325 0 0 0-.08.422l1.319 2.282c.079.145.257.197.402.145l1.642-.666c.342.264.698.488 1.114.653l.244 1.747a.333.333 0 0 0 .33.277h2.637a.333.333 0 0 0 .33-.277l.243-1.747a4.802 4.802 0 0 0 1.115-.653l1.641.666c.145.052.323 0 .403-.145l1.318-2.282a.333.333 0 0 0-.079-.422z" fill="#80deea" stroke-width=".659"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-css" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#42a5f5" fill-rule="nonzero"/><path d="M12.488 9.415l-.44 2.259h9.188l-.298 1.46h-9.18l-.447 2.251H20.5l-.514 2.576-3.705 1.224-3.211-1.224.223-1.109h-2.258l-.534 2.704 5.307 2.029 6.118-2.029.812-4.076.162-.818 1.041-5.247H12.488z" fill-rule="nonzero" fill="#bbdefb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-css-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#42a5f5" fill-rule="nonzero"/><path d="M12.488 9.415l-.44 2.259h9.188l-.298 1.46h-9.18l-.447 2.251H20.5l-.514 2.576-3.705 1.224-3.211-1.224.223-1.109h-2.258l-.534 2.704 5.307 2.029 6.118-2.029.812-4.076.162-.818 1.041-5.247H12.488z" fill-rule="nonzero" fill="#bbdefb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-dist" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#e57373" fill-rule="nonzero"/><path d="M18.575 11.113h-2.576V9.825h2.576m3.864 1.288h-2.576V9.825l-1.288-1.288h-2.576L14.71 9.825v1.288h-2.577c-.715 0-1.288.573-1.288 1.288v7.085a1.288 1.288 0 0 0 1.288 1.288H22.44a1.288 1.288 0 0 0 1.288-1.288V12.4c0-.715-.58-1.288-1.288-1.288z" fill="#ffcdd2" stroke-width=".644"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-dist-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#e57373"/><path d="M18.575 11.113h-2.576V9.825h2.576m3.864 1.288h-2.576V9.825l-1.288-1.288h-2.576L14.71 9.825v1.288h-2.577c-.715 0-1.288.573-1.288 1.288v7.085a1.288 1.288 0 0 0 1.288 1.288H22.44a1.288 1.288 0 0 0 1.288-1.288V12.4c0-.715-.58-1.288-1.288-1.288z" fill="#ffcdd2" fill-rule="evenodd" stroke-width=".644"/></symbol><symbol id="folder-docker" clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><defs id="cydefs10"><path id="cySVGID_2_" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></defs><path id="cypath2" d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><style id="cystyle2">.cyst0{fill:#fff}.cyst1{clip-path:url(#cySVGID_4_)}</style><g id="cyg34" transform="translate(8.319 9.626) scale(.39491)" fill="#b3e5fc"><g id="cyg32"><g id="cyg30"><title id="cytitle4">Group 3</title><g id="cyg28"><g id="cyg26"><g id="cyg9"><path id="cySVGID_1_" class="cyst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></g><g id="cyg24"><clipPath id="cySVGID_4_"><use id="cyuse14" width="100%" height="100%" xlink:href="#cySVGID_2_"/></clipPath><g id="cyg22" class="cyst1" clip-path="url(#cySVGID_4_)"><g id="cyg20"><g id="cyg18"><path id="cySVGID_3_" class="cyst0" d="M-48.8-21H1226v151.4H-48.8z"/></g></g></g></g></g></g></g></g></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docker-open" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="cza"><use width="100%" height="100%" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#SVGID_2_"/></clipPath></defs><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><g transform="matrix(.3949 0 0 .39489 8.319 9.626)" fill="#b3e5fc"><title>Group 3</title><path class="czst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/><g class="czst1" clip-path="url(#cza)"><path class="czst0" d="M-48.8-21H1226v151.4H-48.8z"/></g></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docs" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#0277bd" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m6.075 10.8v-1.35H13.85v1.35h6.075m2.025-2.7v-1.35h-8.1v1.35h8.1z" fill-rule="nonzero" fill="#b3e5fc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docs-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#0277bd" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m6.075 10.8v-1.35H13.85v1.35h6.075m2.025-2.7v-1.35h-8.1v1.35h8.1z" fill-rule="nonzero" fill="#b3e5fc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-expo" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#01579b" fill-rule="nonzero"/><style>.dcst0{fill:#1173b6}.st1{fill:#585d67}</style><path class="dcst0" d="M18.575 9.82c-.489-.745-.605-.844-1.6-.844h-.024c-.996 0-1.106.099-1.601.844-.46.699-5.024 9.058-5.024 9.291 0 .338.087.658.402 1.112.32.46.873.716 1.275.309.273-.274 3.201-5.321 4.616-7.23a.425.425 0 0 1 .693 0c1.414 1.909 4.343 6.956 4.616 7.23.402.407.955.15 1.275-.309.314-.454.402-.774.402-1.112-.006-.233-4.57-8.598-5.03-9.291z" fill="#1173b6" stroke-width=".058"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-expo-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#01579b"/><path class="ddst0" d="M18.575 9.82c-.489-.745-.605-.844-1.6-.844h-.024c-.996 0-1.106.099-1.601.844-.46.699-5.024 9.058-5.024 9.291 0 .338.087.658.402 1.112.32.46.873.716 1.275.309.273-.274 3.201-5.321 4.616-7.23a.425.425 0 0 1 .693 0c1.414 1.909 4.343 6.956 4.616 7.23.402.407.955.15 1.275-.309.314-.454.402-.774.402-1.112-.006-.233-4.57-8.598-5.03-9.291z" fill="#1173b6" stroke-width=".058" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-font" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef9a9a" fill-rule="nonzero"/><path d="M14.62 17.403l2.38-6.33 2.37 6.33m-3.37-9l-5.5 14h2.25l1.12-3h6.25l1.13 3h2.25l-5.5-14h-2z" fill="#f44336" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-font-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef9a9a" fill-rule="nonzero"/><path d="M14.62 17.403l2.38-6.33 2.37 6.33m-3.37-9l-5.5 14h2.25l1.12-3h6.25l1.13 3h2.25l-5.5-14h-2z" fill="#f44336" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-git" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ff8a65" fill-rule="nonzero"/><path d="M10.43 14.14l4.044-4.052 1.183 1.19a1.387 1.387 0 0 0 .65 1.56v3.877c-.42.238-.699.693-.699 1.21 0 .768.632 1.4 1.4 1.4.767 0 1.4-.632 1.4-1.4 0-.517-.28-.972-.7-1.21v-3.4l1.448 1.462c-.05.105-.05.224-.05.35 0 .767.633 1.399 1.4 1.399.768 0 1.4-.632 1.4-1.4 0-.767-.632-1.4-1.4-1.4-.126 0-.245 0-.35.05l-1.798-1.799a1.385 1.385 0 0 0-.805-1.637c-.3-.112-.615-.14-.895-.063l-1.19-1.183.553-.545a1.381 1.381 0 0 1 1.973 0l5.591 5.59a1.381 1.381 0 0 1 0 1.974l-5.59 5.591a1.381 1.381 0 0 1-1.974 0l-5.591-5.59a1.381 1.381 0 0 1 0-1.974z" fill="#e64a19" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-git-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ff8a65" fill-rule="nonzero"/><path d="M10.43 14.14l4.044-4.052 1.183 1.19a1.387 1.387 0 0 0 .65 1.56v3.877c-.42.238-.699.693-.699 1.21 0 .768.632 1.4 1.4 1.4.767 0 1.4-.632 1.4-1.4 0-.517-.28-.972-.7-1.21v-3.4l1.448 1.462c-.05.105-.05.224-.05.35 0 .767.633 1.399 1.4 1.399.768 0 1.4-.632 1.4-1.4 0-.767-.632-1.4-1.4-1.4-.126 0-.245 0-.35.05l-1.798-1.799a1.385 1.385 0 0 0-.805-1.637c-.3-.112-.615-.14-.895-.063l-1.19-1.183.553-.545a1.381 1.381 0 0 1 1.973 0l5.591 5.59a1.381 1.381 0 0 1 0 1.974l-5.59 5.591a1.381 1.381 0 0 1-1.974 0l-5.591-5.59a1.381 1.381 0 0 1 0-1.974z" fill="#e64a19" fill-rule="nonzero"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-global" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#5c6bc0" fill-rule="nonzero"/><path d="M21.132 18.585a1.22 1.22 0 0 0-1.156-.846h-.609v-1.825a.608.608 0 0 0-.608-.609h-3.65v-1.217h1.216a.608.608 0 0 0 .609-.608v-1.217h1.217a1.217 1.217 0 0 0 1.216-1.217v-.25a4.858 4.858 0 0 1 1.765 7.79m-4.198 1.545a4.86 4.86 0 0 1-4.26-4.826c0-.377.049-.742.128-1.089l2.915 2.915v.608a1.217 1.217 0 0 0 1.217 1.217m.608-9.735a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.085-6.085 6.085 6.085 0 0 0-6.085-6.084z" fill="#c5cae9" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-global-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#5c6bc0"/><path d="M21.133 18.585a1.22 1.22 0 0 0-1.156-.846h-.609v-1.825a.608.608 0 0 0-.608-.609h-3.65v-1.217h1.216a.608.608 0 0 0 .609-.608v-1.217h1.217a1.217 1.217 0 0 0 1.216-1.217v-.25a4.858 4.858 0 0 1 1.765 7.79m-4.198 1.545a4.86 4.86 0 0 1-4.26-4.826c0-.377.049-.742.128-1.089l2.915 2.915v.608a1.217 1.217 0 0 0 1.216 1.217m.609-9.735a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.085-6.085 6.085 6.085 0 0 0-6.085-6.084z" fill="#c5cae9" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-i18n" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#5c6bc0" fill-rule="nonzero"/><path d="M17.293 17.786l-1.53-1.512.018-.018a10.555 10.555 0 0 0 2.235-3.934h1.765v-1.205h-4.217V9.912h-1.205v1.205h-4.217v1.205h6.73a9.5 9.5 0 0 1-1.91 3.223 9.424 9.424 0 0 1-1.392-2.018h-1.205c.44.982 1.042 1.91 1.795 2.747l-3.067 3.024.856.856 3.012-3.013 1.874 1.874.458-1.229m3.392-3.054H19.48l-2.711 7.23h1.205l.674-1.808h2.862l.68 1.807h1.206l-2.711-7.23m-1.579 4.218l.976-2.609.976 2.609z" fill="#c5cae9" stroke-width=".602"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-i18n-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#5c6bc0"/><path d="M17.293 17.786l-1.53-1.512.018-.018a10.555 10.555 0 0 0 2.235-3.934h1.765v-1.205h-4.217V9.912h-1.205v1.205h-4.217v1.205h6.73a9.5 9.5 0 0 1-1.91 3.223 9.424 9.424 0 0 1-1.392-2.018h-1.205c.44.982 1.042 1.91 1.795 2.747l-3.067 3.024.856.856 3.012-3.013 1.874 1.874.458-1.229m3.392-3.054H19.48l-2.711 7.23h1.205l.674-1.808h2.862l.68 1.807h1.206l-2.711-7.23m-1.579 4.218l.976-2.609.976 2.609z" fill="#c5cae9" stroke-width=".602"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-images" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#009688" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m0 12.15h8.1v-5.4l-2.7 2.7-1.35-1.35-4.05 4.05m1.35-7.425c-.74 0-1.35.61-1.35 1.35s.61 1.35 1.35 1.35 1.35-.61 1.35-1.35-.61-1.35-1.35-1.35z" fill-rule="nonzero" fill="#b2dfdb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-images-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#009688" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m0 12.15h8.1v-5.4l-2.7 2.7-1.35-1.35-4.05 4.05m1.35-7.425c-.74 0-1.35.61-1.35 1.35s.61 1.35 1.35 1.35 1.35-.61 1.35-1.35-.61-1.35-1.35-1.35z" fill-rule="nonzero" fill="#b2dfdb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-include" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><path d="M20.788 15.981h-2.434v2.434h-1.217V15.98h-2.434v-1.217h2.434V12.33h1.217v2.434h2.434m-3.042-5.476a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.084-6.085 6.085 6.085 0 0 0-6.084-6.084z" fill="#b3e5fc" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-include-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><path d="M20.788 15.981h-2.434v2.434h-1.217V15.98h-2.434v-1.217h2.434V12.33h1.217v2.434h2.434m-3.042-5.476a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.084-6.085 6.085 6.085 0 0 0-6.084-6.084z" fill="#b3e5fc" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-javascript" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ffca28" fill-rule="nonzero"/><path d="M17.935 18.374a2.18 2.18 0 0 0 1.972 1.213c.829 0 1.354-.415 1.354-.987 0-.682-.542-.927-1.452-1.324l-.502-.216c-1.435-.613-2.404-1.378-2.404-3.005 0-1.5 1.167-2.638 2.917-2.638a2.957 2.957 0 0 1 2.842 1.599l-1.552.999a1.362 1.362 0 0 0-1.29-.858.873.873 0 0 0-.957.858c0 .583.374.84 1.226 1.213l.502.216c1.697.733 2.654 1.47 2.654 3.139 0 1.798-1.411 2.783-3.308 2.783a3.839 3.839 0 0 1-3.618-2.046zm-7.048.175c.315.583.583 1.027 1.283 1.027s1.066-.256 1.066-1.255v-6.774h1.998v6.804c0 2.064-1.214 3.01-2.982 3.01a3.104 3.104 0 0 1-2.993-1.826z" fill-rule="nonzero" fill="#ffecb3"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-javascript-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ffca28"/><path d="M17.935 18.374a2.18 2.18 0 0 0 1.972 1.213c.829 0 1.354-.415 1.354-.987 0-.682-.542-.927-1.452-1.324l-.502-.216c-1.435-.613-2.404-1.378-2.404-3.005 0-1.5 1.167-2.638 2.917-2.638a2.957 2.957 0 0 1 2.842 1.599l-1.552.999a1.362 1.362 0 0 0-1.29-.858.873.873 0 0 0-.957.858c0 .583.374.84 1.226 1.213l.502.216c1.697.733 2.654 1.47 2.654 3.139 0 1.798-1.412 2.783-3.308 2.783a3.839 3.839 0 0 1-3.618-2.046zm-7.048.175c.315.583.583 1.027 1.283 1.027s1.066-.256 1.066-1.255v-6.774h1.998v6.804c0 2.064-1.214 3.01-2.982 3.01a3.104 3.104 0 0 1-2.993-1.826z" fill="#ffecb3"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-lib" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#c0ca33" fill-rule="nonzero"/><path d="M17.39 12.544a2.05 2.05 0 0 0 2.05-2.05 2.05 2.05 0 0 0-2.05-2.052 2.05 2.05 0 0 0-2.05 2.051 2.05 2.05 0 0 0 2.05 2.051m0 2.42a8.992 8.992 0 0 0-6.152-2.42v7.52c2.392 0 4.539.923 6.152 2.42a8.992 8.992 0 0 1 6.152-2.42v-7.52c-2.392 0-4.539.923-6.152 2.42z" fill="#f0f4c3" stroke-width=".684"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-lib-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#c0ca33"/><path d="M17.391 12.543a2.05 2.05 0 0 0 2.05-2.05 2.05 2.05 0 0 0-2.05-2.052 2.05 2.05 0 0 0-2.05 2.051 2.05 2.05 0 0 0 2.05 2.051m0 2.42a8.992 8.992 0 0 0-6.152-2.42v7.52c2.392 0 4.539.923 6.152 2.42a8.992 8.992 0 0 1 6.152-2.42v-7.52c-2.392 0-4.539.923-6.152 2.42z" fill="#f0f4c3" stroke-width=".684"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-actions" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ab47bc" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#e1bee7" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-actions-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ab47bc"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#e1bee7" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-effects" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00bcd4" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#b2ebf2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-effects-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00bcd4"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#b2ebf2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef5350" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#ffcdd2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-reducer-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef5350"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#ffcdd2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-state" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#dcedc8" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-state-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#dcedc8" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-node" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.25 8.403c-.188 0-.382.048-.542.139l-5.166 2.986a1.096 1.096 0 0 0-.542.944v5.959c0 .388.208.75.542.944l1.354.778c.66.32.882.326 1.187.326.973 0 1.535-.59 1.535-1.618V12.98a.154.154 0 0 0-.153-.153h-.646c-.09 0-.16.07-.16.153v5.882c0 .458-.472.91-1.228.528l-1.424-.813a.181.181 0 0 1-.076-.145v-5.959c0-.062.027-.118.076-.146l5.167-2.979a.15.15 0 0 1 .152 0l5.167 2.98a.164.164 0 0 1 .076.145v5.959a.181.181 0 0 1-.076.145l-5.167 2.98c-.041.027-.11.027-.16 0l-1.305-.792c-.055-.021-.111-.028-.146-.007-.368.208-.437.25-.778.354-.083.028-.215.076.05.222l1.721 1.021c.167.097.348.146.542.146s.375-.049.542-.146l5.166-2.979c.334-.194.542-.556.542-.944v-5.959c0-.389-.208-.75-.542-.944l-5.166-2.986a1.103 1.103 0 0 0-.542-.14m1.389 4.272c-1.472 0-2.354.618-2.354 1.66 0 1.117.875 1.444 2.291 1.583 1.688.166 1.82.416 1.82.75 0 .576-.465.82-1.549.82-1.375 0-1.666-.341-1.77-1.022a.157.157 0 0 0-.153-.125h-.667c-.083 0-.146.063-.146.153 0 .861.472 1.903 2.736 1.903 1.632 0 2.57-.646 2.57-1.771 0-1.118-.75-1.41-2.34-1.625-1.605-.208-1.765-.32-1.765-.694 0-.313.14-.73 1.327-.73 1.042 0 1.451.23 1.611.945.014.07.076.118.146.118h.673a.134.134 0 0 0 .105-.049c.027-.028.048-.07.034-.11-.097-1.237-.916-1.806-2.57-1.806z" fill-rule="nonzero" fill="#f1f8e9"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-node-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.25 8.403c-.188 0-.382.048-.542.139l-5.166 2.986a1.096 1.096 0 0 0-.542.944v5.959c0 .388.208.75.542.944l1.354.778c.66.32.882.326 1.187.326.973 0 1.535-.59 1.535-1.618V12.98a.154.154 0 0 0-.153-.153h-.646c-.09 0-.16.07-.16.153v5.882c0 .458-.472.91-1.228.528l-1.424-.813a.181.181 0 0 1-.076-.145v-5.959c0-.062.027-.118.076-.146l5.167-2.979a.15.15 0 0 1 .152 0l5.167 2.98a.164.164 0 0 1 .076.145v5.959a.181.181 0 0 1-.076.145l-5.167 2.98c-.041.027-.11.027-.16 0l-1.305-.792c-.055-.021-.111-.028-.146-.007-.368.208-.437.25-.778.354-.083.028-.215.076.05.222l1.721 1.021c.167.097.348.146.542.146s.375-.049.542-.146l5.166-2.979c.334-.194.542-.556.542-.944v-5.959c0-.389-.208-.75-.542-.944l-5.166-2.986a1.103 1.103 0 0 0-.542-.14m1.389 4.272c-1.472 0-2.354.618-2.354 1.66 0 1.117.875 1.444 2.291 1.583 1.688.166 1.82.416 1.82.75 0 .576-.465.82-1.549.82-1.375 0-1.666-.341-1.77-1.022a.157.157 0 0 0-.153-.125h-.667c-.083 0-.146.063-.146.153 0 .861.472 1.903 2.736 1.903 1.632 0 2.57-.646 2.57-1.771 0-1.118-.75-1.41-2.34-1.625-1.605-.208-1.765-.32-1.765-.694 0-.313.14-.73 1.327-.73 1.042 0 1.451.23 1.611.945.014.07.076.118.146.118h.673a.134.134 0 0 0 .105-.049c.027-.028.048-.07.034-.11-.097-1.237-.916-1.806-2.57-1.806z" fill-rule="nonzero" fill="#f1f8e9"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-public" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><path d="M20.036 16.746c.05-.408.087-.817.087-1.237s-.037-.83-.087-1.238h2.091c.099.396.16.81.16 1.238a5.1 5.1 0 0 1-.16 1.237m-3.186 3.44c.371-.687.656-1.43.854-2.203h1.825a4.968 4.968 0 0 1-2.679 2.203m-.155-3.44h-2.895c-.062-.408-.099-.817-.099-1.237s.037-.835.1-1.238h2.894c.056.403.1.817.1 1.238s-.044.829-.1 1.237m-1.447 3.687a8.39 8.39 0 0 1-1.182-2.45h2.363a8.39 8.39 0 0 1-1.181 2.45m-2.475-7.399h-1.806a4.902 4.902 0 0 1 2.672-2.202c-.37.686-.65 1.429-.866 2.202m-1.806 4.95h1.806c.217.773.495 1.515.866 2.202a4.954 4.954 0 0 1-2.672-2.203m-.508-1.237a5.099 5.099 0 0 1-.16-1.237 5.1 5.1 0 0 1 .16-1.238h2.091c-.049.409-.086.817-.086 1.238s.037.829.086 1.237m2.698-6.168a8.425 8.425 0 0 1 1.181 2.456h-2.363a8.426 8.426 0 0 1 1.182-2.456m4.28 2.456h-1.824a9.682 9.682 0 0 0-.854-2.202 4.94 4.94 0 0 1 2.679 2.202m-4.281-3.712a6.193 6.193 0 0 0-6.187 6.187 6.186 6.186 0 0 0 6.187 6.186 6.186 6.186 0 0 0 6.186-6.186 6.186 6.186 0 0 0-6.186-6.187z" fill="#b3e5fc" stroke-width=".619"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-public-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><path d="M20.037 16.746c.05-.408.087-.817.087-1.237s-.037-.83-.087-1.238h2.091c.099.396.16.81.16 1.238a5.1 5.1 0 0 1-.16 1.237m-3.186 3.44c.371-.687.656-1.43.854-2.203h1.825a4.967 4.967 0 0 1-2.68 2.203m-.154-3.44h-2.895c-.062-.408-.099-.817-.099-1.237s.037-.835.099-1.238h2.895c.056.403.1.817.1 1.238s-.044.829-.1 1.237m-1.447 3.687a8.39 8.39 0 0 1-1.182-2.45h2.363a8.39 8.39 0 0 1-1.181 2.45m-2.475-7.399H13.06a4.902 4.902 0 0 1 2.672-2.202c-.371.686-.65 1.429-.866 2.202m-1.806 4.95h1.806c.217.773.495 1.515.866 2.202a4.954 4.954 0 0 1-2.672-2.203m-.508-1.237a5.099 5.099 0 0 1-.16-1.237 5.1 5.1 0 0 1 .16-1.238h2.091c-.05.409-.086.817-.086 1.238s.037.829.086 1.237m2.698-6.168a8.425 8.425 0 0 1 1.181 2.456h-2.363a8.426 8.426 0 0 1 1.182-2.456m4.28 2.456h-1.824a9.682 9.682 0 0 0-.854-2.202 4.941 4.941 0 0 1 2.679 2.202M17.34 9.322a6.193 6.193 0 0 0-6.187 6.187 6.186 6.186 0 0 0 6.187 6.186 6.186 6.186 0 0 0 6.186-6.186 6.186 6.186 0 0 0-6.186-6.187z" fill="#b3e5fc" stroke-width=".619"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-react-components" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00bcd4" fill-rule="nonzero"/><path d="M16.473 13.927c.723 0 1.313.59 1.313 1.327 0 .703-.59 1.3-1.313 1.3a1.318 1.318 0 0 1-1.313-1.3c0-.737.59-1.327 1.313-1.327m-3.252 6.946c.443.267 1.412-.14 2.529-1.194a17.015 17.015 0 0 1-1.06-1.335 15.945 15.945 0 0 1-1.686-.252c-.358 1.502-.225 2.535.217 2.78m.499-4.03l-.204-.359a5.558 5.558 0 0 0-.203.604c.19.042.4.078.618.113l-.211-.359m4.593-.533l.569-1.054-.569-1.053c-.21-.372-.435-.702-.639-1.032-.38-.022-.78-.022-1.2-.022-.422 0-.823 0-1.202.022-.204.33-.428.66-.639 1.032l-.569 1.053.57 1.054c.21.372.434.702.638 1.032.38.021.78.021 1.201.021.421 0 .822 0 1.201-.02.204-.331.428-.661.639-1.033m-1.84-4.72c-.133.155-.274.316-.414.506h.828c-.14-.19-.28-.351-.414-.506m0 7.332c.133-.154.274-.316.414-.505h-.828c.14.19.28.35.414.505m3.245-9.284c-.436-.267-1.405.14-2.522 1.194.366.414.724.864 1.06 1.334.577.057 1.146.14 1.686.253.359-1.503.225-2.535-.224-2.78m-.492 4.03l.204.358c.077-.203.154-.407.203-.604-.19-.042-.4-.077-.618-.112l.211.358m1.018-4.95c1.033.589 1.145 2.141.71 3.953 1.784.527 3.069 1.398 3.069 2.584 0 1.187-1.285 2.058-3.07 2.585.436 1.812.324 3.364-.709 3.954-1.025.59-2.423-.085-3.77-1.37-1.35 1.285-2.747 1.96-3.78 1.37-1.025-.59-1.137-2.142-.702-3.954-1.783-.527-3.069-1.398-3.069-2.585s1.286-2.057 3.07-2.584c-.436-1.812-.324-3.364.702-3.954 1.032-.59 2.43.084 3.778 1.37 1.348-1.286 2.746-1.96 3.771-1.37m-.203 6.538c.239.527.45 1.054.625 1.588 1.475-.443 2.303-1.075 2.303-1.588 0-.512-.828-1.144-2.303-1.587a15.81 15.81 0 0 1-.625 1.587m-7.136 0a15.806 15.806 0 0 1-.625-1.587c-1.474.443-2.303 1.075-2.303 1.587 0 .513.829 1.145 2.303 1.588.176-.534.387-1.06.625-1.588m6.321 1.588l-.21.358c.217-.035.428-.07.617-.113-.049-.196-.126-.4-.203-.604l-.204.359m-2.03 2.837c1.117 1.053 2.086 1.46 2.522 1.194.45-.246.583-1.278.224-2.781-.54.112-1.11.196-1.685.253-.337.47-.695.92-1.06 1.334m-3.477-6.012l.21-.358c-.217.035-.428.07-.617.113.049.196.126.4.203.604l.204-.359m2.03-2.837c-1.117-1.053-2.086-1.46-2.529-1.194-.442.246-.576 1.278-.217 2.781.54-.112 1.11-.196 1.685-.253.337-.47.695-.92 1.06-1.334z" fill="#b2ebf2" stroke-width=".702"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-react-components-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00bcd4"/><path d="M16.473 13.928c.723 0 1.313.59 1.313 1.327 0 .703-.59 1.3-1.313 1.3a1.318 1.318 0 0 1-1.313-1.3c0-.737.59-1.327 1.313-1.327m-3.252 6.946c.443.267 1.412-.14 2.529-1.194a16.997 16.997 0 0 1-1.06-1.335 15.945 15.945 0 0 1-1.686-.252c-.358 1.502-.225 2.535.217 2.78m.499-4.03l-.204-.359c-.077.204-.154.408-.203.604.19.042.4.078.618.113l-.211-.359m4.593-.533l.569-1.054-.57-1.053c-.21-.372-.434-.702-.638-1.032-.38-.022-.78-.022-1.201-.022-.421 0-.822 0-1.2.022-.205.33-.43.66-.64 1.032l-.569 1.053.569 1.054c.21.372.435.702.64 1.032.378.021.779.021 1.2.021.421 0 .822 0 1.2-.02.205-.33.43-.661.64-1.033m-1.84-4.72c-.133.155-.274.316-.414.506h.828c-.14-.19-.28-.351-.414-.506m0 7.332c.133-.154.274-.316.414-.505h-.828c.14.19.28.35.414.505m3.244-9.284c-.435-.267-1.404.14-2.52 1.194.364.414.723.864 1.06 1.334.575.057 1.144.14 1.685.253.358-1.503.225-2.535-.225-2.78m-.491 4.03l.203.358c.078-.203.155-.407.204-.604-.19-.042-.4-.077-.618-.112l.21.358m1.02-4.95c1.032.589 1.144 2.141.708 3.953 1.784.527 3.07 1.398 3.07 2.584 0 1.187-1.286 2.058-3.07 2.585.436 1.812.323 3.364-.709 3.954-1.025.59-2.423-.085-3.771-1.37-1.348 1.285-2.746 1.96-3.778 1.37-1.026-.59-1.138-2.142-.703-3.954-1.783-.527-3.069-1.398-3.069-2.585s1.286-2.057 3.07-2.584c-.436-1.812-.324-3.364.702-3.954 1.032-.59 2.43.084 3.778 1.37 1.348-1.286 2.746-1.96 3.771-1.37m-.204 6.538c.24.527.45 1.054.625 1.588 1.475-.443 2.304-1.075 2.304-1.588 0-.512-.829-1.144-2.304-1.587a15.81 15.81 0 0 1-.625 1.587m-7.135 0a15.808 15.808 0 0 1-.625-1.587c-1.475.443-2.303 1.075-2.303 1.587 0 .513.828 1.145 2.303 1.588.176-.534.386-1.06.625-1.588m6.32 1.588l-.21.358c.218-.035.428-.07.618-.113a5.56 5.56 0 0 0-.204-.604l-.203.359m-2.03 2.837c1.117 1.053 2.086 1.46 2.521 1.194.45-.246.583-1.278.225-2.781-.54.112-1.11.196-1.685.253-.338.47-.696.92-1.06 1.334m-3.477-6.012l.21-.358c-.217.035-.428.07-.617.112.049.197.126.4.203.604l.204-.358m2.03-2.837c-1.117-1.053-2.086-1.46-2.529-1.194-.442.246-.576 1.278-.217 2.781.54-.112 1.11-.196 1.685-.253.337-.47.695-.92 1.06-1.334z" fill="#b2ebf2" stroke-width=".702"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-actions" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ab47bc" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#e1bee7" stroke="#e1bee7" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-actions-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ab47bc"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#e1bee7" stroke="#e1bee7" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef5350" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#ffcdd2" stroke="#ffcdd2" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-reducer-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef5350"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#ffcdd2" stroke="#ffcdd2" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-store" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#dcedc8" stroke="#dcedc8" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-store-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#dcedc8" stroke="#dcedc8" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-resource" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#fbc02d" fill-rule="nonzero"/><path d="M21.598 12.059h-6.085v-1.217h6.085m-2.434 6.085h-3.65V15.71h3.65m2.434-1.217h-6.085v-1.217h6.085m.608-4.26h-7.301a1.217 1.217 0 0 0-1.217 1.218v7.301a1.217 1.217 0 0 0 1.217 1.217h7.301a1.217 1.217 0 0 0 1.217-1.217v-7.301a1.217 1.217 0 0 0-1.217-1.217m-9.735 2.434h-1.217v8.518a1.217 1.217 0 0 0 1.217 1.217h8.519v-1.217H12.47z" fill="#fff9c4" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-resource-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#fbc02d"/><path d="M21.598 12.059h-6.085v-1.217h6.085m-2.434 6.085h-3.65V15.71h3.65m2.434-1.217h-6.085v-1.217h6.085m.608-4.26h-7.301a1.217 1.217 0 0 0-1.217 1.218v7.301a1.217 1.217 0 0 0 1.217 1.217h7.301a1.217 1.217 0 0 0 1.217-1.217v-7.301a1.217 1.217 0 0 0-1.217-1.217m-9.735 2.433h-1.217v8.519a1.217 1.217 0 0 0 1.217 1.217h8.519v-1.217H12.47z" fill="#fff9c4" stroke-width=".608"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" id="folder-sass" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#f8bbd0" fill-rule="nonzero"/><path d="M23.36 10.506c-.39-1.527-2.922-2.03-5.319-1.178-1.426.507-2.97 1.302-4.08 2.34-1.32 1.235-1.53 2.31-1.444 2.759.306 1.584 2.477 2.62 3.37 3.388v.005c-.264.13-2.19 1.104-2.64 2.1-.476 1.052.075 1.806.44 1.908 1.131.315 2.292-.251 2.916-1.182.602-.897.551-2.056.29-2.633.36-.095.781-.138 1.316-.076 1.508.177 1.804 1.118 1.748 1.513-.057.394-.373.61-.48.676-.105.065-.137.088-.129.137.013.07.062.068.152.053.125-.021.792-.321.821-1.048.037-.924-.849-1.958-2.416-1.93-.646.01-1.052.072-1.345.181-.022-.024-.044-.05-.067-.073-.969-1.034-2.76-1.765-2.684-3.156.027-.505.203-1.835 3.442-3.45 2.653-1.322 4.777-.957 5.145-.151.524 1.152-1.136 3.293-3.891 3.601-1.05.118-1.603-.289-1.74-.44-.145-.16-.166-.167-.22-.137-.088.049-.033.19 0 .274.082.214.42.594.995.782.506.166 1.739.258 3.23-.319 1.669-.646 2.972-2.443 2.59-3.944zm-7.103 7.783a2.2 2.2 0 0 1-.065 1.413 2.405 2.405 0 0 1-.453.704c-.5.546-1.198.752-1.497.579-.323-.188-.161-.956.418-1.568.623-.66 1.52-1.083 1.52-1.083l-.002-.002.079-.043z" fill="#ec407a" fill-rule="nonzero" stroke="#ec407a" stroke-width=".5199012000000001"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" id="folder-sass-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#f8bbd0" fill-rule="nonzero"/><path d="M23.36 10.506c-.39-1.527-2.922-2.03-5.319-1.178-1.426.507-2.97 1.302-4.08 2.34-1.32 1.235-1.53 2.31-1.444 2.759.306 1.584 2.477 2.62 3.37 3.388v.005c-.264.13-2.19 1.104-2.64 2.1-.476 1.052.075 1.806.44 1.908 1.131.315 2.292-.251 2.916-1.182.602-.897.551-2.056.29-2.633.36-.095.781-.138 1.316-.076 1.508.177 1.804 1.118 1.748 1.513-.057.394-.373.61-.48.676-.105.065-.137.088-.129.137.013.07.062.068.152.053.125-.021.792-.321.821-1.048.037-.924-.849-1.958-2.416-1.93-.646.01-1.052.072-1.345.181-.022-.024-.044-.05-.067-.073-.969-1.034-2.76-1.765-2.684-3.156.027-.505.203-1.835 3.442-3.45 2.653-1.322 4.777-.957 5.145-.151.524 1.152-1.136 3.293-3.891 3.601-1.05.118-1.603-.289-1.74-.44-.145-.16-.166-.167-.22-.137-.088.049-.033.19 0 .274.082.214.42.594.995.782.506.166 1.739.258 3.23-.319 1.669-.646 2.972-2.443 2.59-3.944zm-7.103 7.783a2.2 2.2 0 0 1-.065 1.413 2.405 2.405 0 0 1-.453.704c-.5.546-1.198.752-1.497.579-.323-.188-.161-.956.418-1.568.623-.66 1.52-1.083 1.52-1.083l-.002-.002.079-.043z" fill="#ec407a" fill-rule="nonzero" stroke="#ec407a" stroke-width=".5199012000000001"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-scripts" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#546e7a" fill-rule="nonzero"/><path d="M18.466 20.241c.69 0 1.259-.568 1.259-1.258v-8.18H15.32a.632.632 0 0 0-.63.63v6.292h-1.887v-6.922c0-1.036.852-1.888 1.888-1.888h6.921c1.036 0 1.888.852 1.888 1.888v.63h-2.517v8.18a1.896 1.896 0 0 1-1.888 1.887h-6.292a1.896 1.896 0 0 1-1.888-1.888v-.629h6.293c0 .69.568 1.258 1.258 1.258z" fill-rule="nonzero" fill="#cfd8dc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-scripts-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#546e7a" fill-rule="nonzero"/><path d="M18.466 20.241c.69 0 1.259-.568 1.259-1.258v-8.18H15.32a.632.632 0 0 0-.63.63v6.292h-1.887v-6.922c0-1.036.852-1.888 1.888-1.888h6.921c1.036 0 1.888.852 1.888 1.888v.63h-2.517v8.18a1.896 1.896 0 0 1-1.888 1.887h-6.292a1.896 1.896 0 0 1-1.888-1.888v-.629h6.293c0 .69.568 1.258 1.258 1.258z" fill-rule="nonzero" fill="#cfd8dc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-src" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#4caf50" fill-rule="nonzero"/><g fill="#c8e6c9" transform="translate(2.065 -.225) scale(.70678)"><path d="M19.146 30.989a.902.902 0 0 1-.207-.025 1.045 1.045 0 0 1-.726-1.213l2.709-14.431c.049-.279.209-.525.444-.683a.891.891 0 0 1 .7-.122c.519.152.837.684.727 1.213L20.077 30.16a1.032 1.032 0 0 1-.442.681.895.895 0 0 1-.489.148zM24.578 28.944h-.068a.932.932 0 0 1-.668-.377 1.104 1.104 0 0 1 .1-1.419l4.658-4.553-4.638-4.239a1.105 1.105 0 0 1-.141-1.416.938.938 0 0 1 .661-.4.9.9 0 0 1 .709.237l5.47 5c.386.372.448.974.144 1.416a1.05 1.05 0 0 1-.142.163l-5.447 5.324a.913.913 0 0 1-.638.264zM16.423 28.947a.917.917 0 0 1-.639-.267l-5.452-5.327a.874.874 0 0 1-.132-.153 1.097 1.097 0 0 1 .141-1.414l5.471-5a.882.882 0 0 1 .7-.238.939.939 0 0 1 .665.4 1.104 1.104 0 0 1-.14 1.417L12.4 22.6l4.659 4.551c.377.382.42.988.1 1.419a.928.928 0 0 1-.669.377z" fill-rule="nonzero"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-src-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#4caf50"/><g fill="#c8e6c9" fill-rule="evenodd" transform="translate(2.064 -.224) scale(.70678)"><path d="M19.146 30.989a.902.902 0 0 1-.207-.025 1.045 1.045 0 0 1-.726-1.213l2.709-14.431c.049-.279.209-.525.444-.683a.891.891 0 0 1 .7-.122c.519.152.837.684.727 1.213L20.077 30.16a1.032 1.032 0 0 1-.442.681.895.895 0 0 1-.489.148zM24.578 28.944h-.068a.932.932 0 0 1-.668-.377 1.104 1.104 0 0 1 .1-1.419l4.658-4.553-4.638-4.239a1.105 1.105 0 0 1-.141-1.416.938.938 0 0 1 .661-.4.9.9 0 0 1 .709.237l5.47 5c.386.372.448.974.144 1.416a1.05 1.05 0 0 1-.142.163l-5.447 5.324a.913.913 0 0 1-.638.264zM16.423 28.947a.917.917 0 0 1-.639-.267l-5.452-5.327a.874.874 0 0 1-.132-.153 1.097 1.097 0 0 1 .141-1.414l5.471-5a.882.882 0 0 1 .7-.238.939.939 0 0 1 .665.4 1.104 1.104 0 0 1-.14 1.417L12.4 22.6l4.659 4.551c.377.382.42.988.1 1.419a.928.928 0 0 1-.669.377z" fill-rule="nonzero"/></g></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-test" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#1de9b6" fill-rule="nonzero"/><path d="M14 8.097v1.39h.695v9.732A2.794 2.794 0 0 0 17.475 22a2.794 2.794 0 0 0 2.781-2.78V9.486h.695v-1.39H14m2.78 9.732c-.417 0-.695-.278-.695-.695 0-.417.278-.695.696-.695.417 0 .695.278.695.695 0 .417-.278.695-.695.695m1.39-2.78c-.417 0-.695-.278-.695-.696 0-.417.278-.695.695-.695.417 0 .695.278.695.695 0 .418-.278.696-.695.696m.695-3.476h-2.78V9.487h2.78v2.086z" fill="#00897b" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-test-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#1de9b6" fill-rule="nonzero"/><path d="M14 8.097v1.39h.695v9.732A2.794 2.794 0 0 0 17.475 22a2.794 2.794 0 0 0 2.781-2.78V9.486h.695v-1.39H14m2.78 9.732c-.417 0-.695-.278-.695-.695 0-.417.278-.695.696-.695.417 0 .695.278.695.695 0 .417-.278.695-.695.695m1.39-2.78c-.417 0-.695-.278-.695-.696 0-.417.278-.695.695-.695.417 0 .695.278.695.695 0 .418-.278.696-.695.696m.695-3.476h-2.78V9.487h2.78v2.086z" fill="#00897b" fill-rule="nonzero"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-tools" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#1e88e5" fill-rule="nonzero"/><path d="M21.043 15.266a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141-2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-3.569 0a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141 2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m3.925-6.424a6.424 6.424 0 0 0-6.423 6.424 6.424 6.424 0 0 0 6.423 6.424 1.07 1.07 0 0 0 1.071-1.07c0-.28-.107-.53-.278-.715a1.105 1.105 0 0 1-.271-.713 1.07 1.07 0 0 1 1.07-1.071h1.263a3.569 3.569 0 0 0 3.57-3.569c0-3.154-2.877-5.71-6.425-5.71z" fill="#bbdefb" stroke-width=".714"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-tools-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#1e88e5"/><path d="M21.043 15.266a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141-2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-3.569 0a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141 2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m3.925-6.424a6.424 6.424 0 0 0-6.423 6.424 6.424 6.424 0 0 0 6.423 6.424 1.07 1.07 0 0 0 1.071-1.07c0-.28-.107-.53-.278-.715a1.105 1.105 0 0 1-.271-.713 1.07 1.07 0 0 1 1.07-1.071h1.263a3.569 3.569 0 0 0 3.57-3.569c0-3.154-2.877-5.71-6.425-5.71z" fill="#bbdefb" stroke-width=".714"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-views" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ff8a65" fill-rule="nonzero"/><path d="M12.487 21.868L11.384 9.5H23.5l-1.104 12.366-4.961 1.375-4.948-1.373zm4.464-3.2l-3.926-2.36v-.855l3.926-2.361v1.323l-2.504 1.465 2.504 1.465v1.323zm.982-.001v-1.323l2.522-1.464-2.522-1.464v-1.323l3.926 2.35v.874l-3.926 2.35z" fill="#e44d26"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-views-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ff8a65" fill-rule="nonzero"/><path d="M12.487 21.868L11.384 9.5H23.5l-1.104 12.366-4.961 1.375-4.948-1.373zm4.464-3.2l-3.926-2.36v-.855l3.926-2.361v1.323l-2.504 1.465 2.504 1.465v1.323zm.982-.001v-1.323l2.522-1.464-2.522-1.464v-1.323l3.926 2.35v.874l-3.926 2.35z" fill="#e44d26"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vscode" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#42a5f5" fill-rule="nonzero"/><path d="M20.811 8.52l-5.988 5.506-3.346-2.522-1.383.805 3.298 3.03-3.298 3.032 1.383.807 3.346-2.522 5.988 5.503 2.921-1.419V9.94zm0 3.622v6.396l-4.245-3.198z" fill="#bbdefb" stroke-width=".974"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vscode-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#42a5f5" fill-rule="nonzero"/><path d="M20.81 8.52l-5.988 5.506-3.346-2.522-1.384.805 3.3 3.03-3.3 3.032 1.384.807 3.346-2.522 5.988 5.503 2.921-1.419V9.94zm0 3.621v6.397l-4.245-3.198z" fill="#bbdefb" stroke-width=".974"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vue" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#009688" fill-rule="nonzero"/><g transform="translate(8.459 6.362) scale(.69572)"><path d="M1.821 4.15l10.21 17.618L22.239 4.235v-.084h-7.692l-2.434 4.178-2.422-4.178z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179z" fill="#35495e"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vue-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#009688"/><g transform="translate(8.458 6.362) scale(.69572)"><path d="M1.821 4.15l10.21 17.618L22.239 4.235v-.084h-7.692l-2.434 4.178-2.422-4.178z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179z" fill="#35495e"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-webpack" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#03a9f4" fill-rule="nonzero"/><g transform="translate(9.192 7.48) scale(.66328)"><path d="M19.376 15.988l-7.708 4.45-7.709-4.45v-8.9l7.709-4.451 7.708 4.45z" fill="#fff" fill-opacity=".785"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18s.41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.94v2.103h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#1c78c0"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-webpack-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#03a9f4"/><g transform="translate(9.193 7.48) scale(.66328)"><path d="M19.376 15.988l-7.708 4.45-7.709-4.45v-8.9l7.709-4.451 7.708 4.45z" fill="#fff" fill-opacity=".785"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18s.41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.94v2.103h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#1c78c0"/></g></symbol><symbol viewBox="0 0 24 24" id="font" xmlns="http://www.w3.org/2000/svg"><path d="M9.62 12L12 5.67 14.37 12M11 3L5.5 17h2.25l1.12-3h6.25l1.13 3h2.25L13 3h-2z" fill="#f44336"/></symbol><symbol viewBox="0 0 500 500" id="fsharp" xmlns="http://www.w3.org/2000/svg"><path d="M235.906 36.66L21.963 250.601l213.943 213.943v-84.36L106.209 250.487l129.697-129.696z" fill="#378bba" stroke-width="14.706"/><path d="M235.906 156.614l-93.622 93.62 93.622 93.622z" fill="#378bba" stroke-width="15.006"/><path d="M263.417 36.64L477.36 250.583 263.417 464.526v-84.36l129.696-129.697-129.696-129.696z" fill="#30b9db" stroke-width="14.706"/></symbol><symbol viewBox="0 0 152.99 160.01" id="fusebox" xmlns="http://www.w3.org/2000/svg"><defs id="fkdefs4"><style id="fkstyle2">.fkcls-1{fill:#fff}.fkcls-2{fill:#515151}.fkcls-3{fill:#1d79bf}.fkcls-4{fill:#383838}</style></defs><title id="fktitle6">Asset 3</title><g id="fkLayer_2" data-name="Layer 2" transform="matrix(.87285 0 0 .87285 10.17 10.175)"><g id="fkFuse_Box" data-name="Fuse Box"><g id="fkLOGO"><path class="fkcls-1" id="fkpolygon8" fill="#fff" d="M76.56 2.19l74.22 24.93-7.7 87.77-65.41 42.66-69.79-43.93-5.7-86.13z"/><path class="fkcls-2" d="M77.69 160L5.87 114.81 0 26 76.55 0 153 25.67l-7.94 90.4zM9.88 112.43l67.77 42.66 63.45-41.39 7.47-85.13-72-24.18L4.36 28.95z" id="fkpath10" fill="#515151"/><path class="fkcls-3" id="fkpolygon12" fill="#1d79bf" d="M76.4 148.8V61.68l66.93-29.82-5.99 78.77z"/><path id="fkF" class="fkcls-4" fill="#383838" d="M76.4 148.8l-60.35-37.39L9.63 31.8 76.4 61.68z"/><path class="fkcls-1" d="M25.58 52.73l.54 15.93 37.35 18.18.12 14.69-37-18.21 1.64 37.1-14.56-9-5.05-80.55 67.79 30.82v15.46z" id="fkpath15" fill="#fff"/><path class="fkcls-1" d="M135.91 90.77c-.08 13.12-6.33 26.59-16.77 33.12l-42.8 27.93V61.71l42.27-18.84c5.16-2.41 9.51-1.43 12.4 3.11 1.9 3 2.89 7.23 2.86 12.21A35.69 35.69 0 0 1 129.34 76c4.29 2 6.66 6.55 6.57 14.77zM123 63.76c0-4.64-2-6.93-4.92-5.45l-29 14.48L89 90l29.44-15.59c2.5-1.32 4.56-5.91 4.56-10.65zM125.15 96c0-5.71-2.42-8.24-6.55-5.93L89 106.64v19.58l29.34-17.46c4.43-2.64 6.79-7.27 6.81-12.76z" id="fkpath17" fill="#fff"/><path id="fkTOP" class="fkcls-4" fill="#383838" d="M76.4 8.82L9.71 31.77l109.77 2.38-84.02 9.21L76.4 61.68l20.76-9.25-27.73-1.37 49.78-8.46 24.12-10.74z"/></g></g></g></symbol><symbol viewBox="0 0 24 24" id="git" xmlns="http://www.w3.org/2000/svg"><path d="M2.6 10.59L8.38 4.8l1.69 1.7c-.24.85.15 1.78.93 2.23v5.54c-.6.34-1 .99-1 1.73a2 2 0 0 0 2 2 2 2 0 0 0 2-2c0-.74-.4-1.39-1-1.73V9.41l2.07 2.09c-.07.15-.07.32-.07.5a2 2 0 0 0 2 2 2 2 0 0 0 2-2 2 2 0 0 0-2-2c-.18 0-.35 0-.5.07L13.93 7.5a1.98 1.98 0 0 0-1.15-2.34c-.43-.16-.88-.2-1.28-.09L9.8 3.38l.79-.78c.78-.79 2.04-.79 2.82 0l7.99 7.99c.79.78.79 2.04 0 2.82l-7.99 7.99c-.78.79-2.04.79-2.82 0L2.6 13.41c-.79-.78-.79-2.04 0-2.82z" fill="#e64a19"/></symbol><symbol viewBox="0 0 164 158" id="gitlab" xmlns="http://www.w3.org/2000/svg"><path d="M161.26 62.6545L161.031 62.0689L138.845 4.16909C138.394 3.03428 137.595 2.07162 136.562 1.41923C135.529 0.777924 134.324 0.46905 133.11 0.534313C131.896 0.599576 130.731 1.03583 129.773 1.78418C128.825 2.55402 128.137 3.59717 127.804 4.77168L112.824 50.6027H52.1654L37.1855 4.77168C36.8606 3.59077 36.1716 2.54239 35.2164 1.77569C34.2581 1.02734 33.0931 0.591087 31.8789 0.525824C30.6647 0.460561 29.4597 0.769435 28.4267 1.41074C27.3965 2.06577 26.598 3.02759 26.1436 4.1606L3.91558 62.035L3.69492 62.6206C0.501198 70.9653 0.106986 80.1221 2.57171 88.7104C5.03644 97.2986 10.2265 104.853 17.3593 110.234L17.4357 110.293L17.6394 110.438L51.4355 135.747L68.1553 148.401L78.34 156.09C79.5313 156.995 80.986 157.485 82.4818 157.485C83.9775 157.485 85.4322 156.995 86.6235 156.09L96.8082 148.401L113.528 135.747L147.528 110.285L147.613 110.217C154.73 104.835 159.907 97.2884 162.368 88.7117C164.829 80.135 164.44 70.9913 161.26 62.6545V62.6545Z" fill="#e24329"/><path d="M161.26 62.6545L161.031 62.0688C150.221 64.2878 140.034 68.8667 131.198 75.4787L82.4733 112.322C99.0658 124.874 113.511 135.78 113.511 135.78L147.511 110.319L147.596 110.251C154.723 104.869 159.909 97.3176 162.373 88.7336C164.837 80.1496 164.447 70.9973 161.26 62.6545Z" fill="#fc6d26"/><path d="M51.4355 135.78L68.1553 148.435L78.34 156.124C79.5313 157.029 80.9859 157.518 82.4817 157.518C83.9775 157.518 85.4322 157.029 86.6235 156.124L96.8081 148.435L113.528 135.78C113.528 135.78 99.0657 124.84 82.4732 112.322C65.8807 124.84 51.4355 135.78 51.4355 135.78Z" fill="#fca326"/><path d="M33.7397 75.4787C24.9112 68.8531 14.7266 64.2622 3.91558 62.0349L3.69492 62.6205C0.501198 70.9652 0.106986 80.122 2.57171 88.7103C5.03644 97.2985 10.2265 104.853 17.3593 110.234L17.4357 110.293L17.6394 110.437L51.4355 135.746C51.4355 135.746 65.8638 124.84 82.4733 112.288L33.7397 75.4787Z" fill="#fc6d26"/></symbol><symbol viewBox="0 0 24 24" id="go" xmlns="http://www.w3.org/2000/svg"><path d="M10.575 1.695c-2.634 0-4.756 2.453-4.756 5.502v4.6l-.027-.003v4.71c0 3.05 2.123 5.502 4.757 5.502h2.286c2.634 0 4.757-2.453 4.757-5.502v-4.6a5.1 5.1 0 0 0 .026.003v-4.71c0-3.049-2.122-5.502-4.756-5.502h-2.287z" fill="#73cddc"/><rect width="2.289" height="3.335" x="-1.178" y="6.092" ry="1.125" transform="matrix(.4849 -.87457 .85979 .51065 0 0)" fill="#73cddc"/><rect width="2.297" height="3.39" x="10.261" y="-15.076" ry="1.143" transform="matrix(.44646 .8948 -.89204 .45195 0 0)" fill="#73cddc"/><circle cx="9.267" cy="5.13" r="2.054" fill="#fff" stroke="#5e5d5b" stroke-width=".1"/><circle cx="14.214" cy="5.116" r="2.054" fill="#fff" stroke="#5e5d5b" stroke-width=".1"/><ellipse cx="8.039" cy="5.051" rx=".792" ry=".901" fill="#030d18"/><path d="M11.792 9.556l.763.138a.403.689 0 0 1 .008.138.403.689 0 0 1-.402.69.403.689 0 0 1-.404-.69.403.689 0 0 1 .035-.276z" fill="#fff" stroke="#fff" stroke-width=".155"/><ellipse cx="8.51" cy="5.365" rx=".138" ry=".166" fill="#fff"/><ellipse cx="12.945" cy="5.189" rx=".792" ry=".901" fill="#030d18"/><ellipse cx="13.414" cy="5.446" rx=".138" ry=".166" fill="#fff"/><ellipse cx="-12.982" cy="-3.409" rx=".708" ry="1.026" transform="rotate(-129.403)" fill="#f6d2a1" stroke-width=".4"/><path d="M11.772 9.553l-.757.135a.4.672 0 0 0-.008.135.4.672 0 0 0 .4.672.4.672 0 0 0 .4-.672.4.672 0 0 0-.035-.27z" fill="#fff" stroke="#fff" stroke-width=".153"/><ellipse cx="1.841" cy="-21.563" rx=".707" ry="1.026" transform="scale(1 -1) rotate(50.597)" fill="#f6d2a1" stroke-width=".4"/><ellipse cx="-17.281" cy="-21.784" rx=".864" ry="1.27" transform="matrix(.3054 -.95222 -.97065 -.2405 0 0)" fill="#f6d2a1" stroke-width=".4"/><ellipse cx="22.885" cy="2.587" rx=".864" ry="1.27" transform="matrix(.22652 .974 .95652 -.29167 0 0)" fill="#f6d2a1" stroke-width=".4"/><path d="M10.708 8.392a.594.594 0 0 0-.594.597v.115c0 .331.264.598.594.598h.386a.973.772 0 0 1 .697-.235.973.772 0 0 1 .698.235h.334c.33 0 .594-.267.594-.598V8.99a.595.595 0 0 0-.594-.597h-2.115z" fill="#f6d2a1" stroke="#657075" stroke-width=".1"/><ellipse cx="11.734" cy="8.203" rx="1.208" ry=".68" fill="#030d18" stroke="#fff" stroke-width=".162"/></symbol><symbol viewBox="0 0 24 24" id="gradle" xmlns="http://www.w3.org/2000/svg"><path d="M21.718 5.503c-.731-1.315-2.04-1.708-2.963-1.727-1.133-.023-2.065.605-1.888 1.017.037.088.25.55.38.741.19.275.527.064.646 0 .353-.187.73-.248 1.16-.198.409.048.954.3 1.319 1.001.859 1.652-1.794 5.05-5.114 2.697-3.32-2.353-6.548-1.574-8.01-1.1-1.462.475-2.135.952-1.556 2.055.785 1.498.524 1.038 1.285 2.28 1.21 1.97 3.856-.908 3.856-.908-1.972 2.906-3.662 2.204-4.31 1.188a15.864 15.864 0 0 1-1.038-1.97c-4.993 1.76-3.642 9.534-3.642 9.534h2.48c.632-2.862 2.892-2.757 3.28 0h1.892c1.673-5.59 5.914 0 5.914 0h2.466c-.69-3.812 1.388-5.01 2.697-7.246 1.31-2.235 2.551-4.969 1.146-7.364zm-6.362 7.362c-1.304-.426-.837-1.723-.837-1.723s1.139.368 2.68.87c-.09.403-.856 1.175-1.843.853z" fill="#0097a7" stroke-width=".47"/></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 300 300" id="graphcool" xmlns="http://www.w3.org/2000/svg"><path d="M246.886 107.727c-12.237-6.892-27.616 2.1-30.081 3.646l-52.834 29.965c-7.8-6.196-18.914-5.933-26.412.625-7.499 6.558-9.24 17.537-4.14 26.094 5.102 8.556 15.588 12.246 24.923 8.768 9.335-3.478 14.852-13.129 13.111-22.937l52.688-29.9.321-.196c3.464-2.188 11.5-5.462 15.256-3.34 2.706 1.524 4.252 6.629 4.376 14.148h-.066v66.092a17.313 17.313 0 0 1-8.635 14.95l-75.739 43.755a17.312 17.312 0 0 1-17.261 0l-75.74-43.756a17.312 17.312 0 0 1-8.634-14.95V113.22c.01-6.165 3.3-11.86 8.634-14.95l68.549-39.562c6.522 7.482 17.451 9.25 26 4.206s12.283-15.468 8.886-24.794c-3.397-9.327-12.962-14.904-22.751-13.27-9.79 1.636-17.022 10.02-17.204 19.944L59.397 85.632a31.932 31.932 0 0 0-15.978 27.588v87.454a31.933 31.933 0 0 0 15.927 27.602l75.74 43.755a31.934 31.934 0 0 0 31.846 0l75.74-43.755a31.933 31.933 0 0 0 15.927-27.58V137.12h.05c.373-14.913-3.616-24.794-11.762-29.389z" fill="#27ae60" stroke="#27ae60" stroke-width="7.883622079999999"/></symbol><symbol viewBox="0 0 400 400" id="graphql" xmlns="http://www.w3.org/2000/svg"><path d="M67.008 293.022l-13.143-7.588L200.282 31.839l13.143 7.588z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M50.855 265.174H343.69v15.177H50.855z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M203.122 358.269L56.649 273.7l7.589-13.143 146.472 84.568zm127.24-220.407L183.889 53.293l7.589-13.143 146.472 84.568z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M64.278 137.803l-7.588-13.142 146.472-84.568 7.588 13.143z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M327.661 293.025L181.244 39.43l13.143-7.589 146.417 253.596zM62.466 114.597h15.176v169.136H62.466zm254.528 0h15.176v169.136h-15.176z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M200.538 351.845l-6.628-11.481L321.3 266.812l6.629 11.48z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M352.284 288.67c-8.777 15.268-28.342 20.48-43.61 11.703-15.268-8.777-20.48-28.342-11.703-43.61 8.777-15.268 28.342-20.48 43.61-11.703 15.36 8.869 20.57 28.342 11.703 43.61M97.574 141.567c-8.778 15.268-28.343 20.48-43.61 11.703-15.269-8.777-20.48-28.342-11.703-43.61 8.777-15.268 28.342-20.48 43.61-11.703 15.268 8.869 20.479 28.342 11.702 43.61M42.353 288.67c-8.777-15.268-3.566-34.741 11.702-43.61 15.268-8.776 34.741-3.565 43.61 11.703 8.776 15.268 3.565 34.741-11.703 43.61-15.36 8.776-34.833 3.565-43.61-11.703m254.71-147.103c-8.776-15.268-3.565-34.741 11.703-43.61 15.268-8.776 34.742-3.565 43.61 11.703 8.777 15.268 3.566 34.741-11.702 43.61-15.268 8.776-34.833 3.565-43.61-11.703m-99.745 236.608c-17.645 0-31.907-14.262-31.907-31.907s14.262-31.907 31.907-31.907 31.907 14.262 31.907 31.907c0 17.554-14.262 31.907-31.907 31.907m0-294.206c-17.645 0-31.907-14.262-31.907-31.907s14.262-31.907 31.907-31.907 31.907 14.262 31.907 31.907-14.262 31.907-31.907 31.907" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/></symbol><symbol viewBox="0 0 24 24" id="groovy" xmlns="http://www.w3.org/2000/svg"><path d="M12 1.982a10.119 10.119 0 0 0-10.12 10.12A10.119 10.119 0 0 0 12 22.22 10.119 10.119 0 0 0 22.12 12.1 10.119 10.119 0 0 0 12 1.983zm1.254 2.422c.91 0 1.647.261 2.213.78.571.518.857 1.188.857 2.013 0 .889-.319 1.673-.959 2.35-.64.677-1.376 1.015-2.207 1.015-.486 0-.89-.119-1.213-.357-.317-.238-.476-.532-.476-.88 0-.212.06-.4.181-.563.127-.164.274-.246.438-.246.159 0 .238.092.238.277 0 .164.06.29.182.38.121.09.261.136.42.136.423 0 .828-.29 1.215-.866.391-.582.587-1.202.587-1.863 0-.465-.151-.844-.453-1.135-.301-.296-.69-.445-1.166-.445-.714 0-1.406.318-2.078.953-.666.635-1.211 1.47-1.635 2.506-.417 1.031-.627 2.014-.627 2.945 0 .857.185 1.54.555 2.047.37.503.863.754 1.477.754 1.037 0 2.027-.734 2.974-2.2l1.493-.212c.185-.026.277.018.277.135 0 .053-.072.28-.215.681-.143.402-.337 1.074-.586 2.016.82-.476 1.455-1.003 1.904-1.58v.914c-.36.418-1.046.888-2.062 1.412-.212 1.407-.682 2.493-1.406 3.26-.725.772-1.54 1.16-2.444 1.16-.433 0-.775-.102-1.023-.303-.243-.2-.365-.477-.365-.832 0-.984.955-1.94 2.865-2.865.2-.714.395-1.356.586-1.928-.333.482-.817.907-1.451 1.278-.635.37-1.225.554-1.77.554-.889 0-1.628-.383-2.22-1.15-.588-.772-.881-1.748-.881-2.928 0-1.243.333-2.42 1-3.531a7.747 7.747 0 0 1 2.625-2.674c1.084-.672 2.134-1.008 3.15-1.008zM12.03 16.592c-1.375.687-2.062 1.365-2.062 2.031 0 .354.169.533.508.533.666 0 1.184-.856 1.554-2.564z" fill="#26c6da"/></symbol><symbol viewBox="0 0 24 24" id="gulp" xmlns="http://www.w3.org/2000/svg"><path d="M8.37 15.94a596.238 596.238 0 0 1-.482-4.982c.002-.042-.225-.077-.505-.077h-.508V8.95h3.966V5.198l1.871-1.124c1.14-.685 1.978-1.125 2.144-1.125.4 0 .866.506.866.939 0 .19-.057.422-.127.517-.07.095-.722.53-1.45.966l-1.321.792-.029 1.393-.028 1.393h3.972v1.932h-.98l-.495 4.983-.495 4.983H8.854l-.485-4.906z" fill="#e53935"/></symbol><symbol viewBox="0 0 24 24" id="h" xmlns="http://www.w3.org/2000/svg"><path d="M16.745 19.818h-3.007v-5.882q0-2.381-1.736-2.381-.869 0-1.438.663-.56.662-.56 1.718v5.882H6.988V4.533h3.016v6.508h.037q1.186-1.802 3.193-1.802 3.511 0 3.511 4.239z" stroke-width=".478" fill="#0277bd"/></symbol><symbol viewBox="0 0 253.6 253.6" id="hack" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-29.243 -29.515) scale(1.2301)"><path fill="#607d8b" d="M69.496 159.551v52.576l51.77-52.576zM123.507 41.523l-54.01 52.755v55.084l54.01-54.009z"/><path fill="#eceff1" d="M130.023 95.663v51.501l52.128-51.5z"/><path fill="#607d8b" d="M185.465 101.867l-55.442 55.174v55.083l55.442-55.262z"/><path fill="#ffa000" d="M73.068 154.283l50.427.09v-50.248z"/></g></symbol><symbol viewBox="0 0 300 300.00001" id="haml" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 165.6)"><path d="M78.42-132.307c-12.047-.302-26.924 5.998-26.924 5.998l49.195 99.791L74.605 85.005c23.81 20.134 50.07 10.504 50.07 10.504L136.76 9.212c1.526 1.446 3.146 2.77 4.777 3.995 5.244 3.714 10.925 6.553 16.606 8.738 5.68 2.185 11.583 3.933 17.482 5.244 3.933.874 7.645 1.53 11.578 1.967-1.748 3.933-2.84 8.083-2.621 12.672 0 .437.22.873.656 1.092h.217c4.152 2.185 8.521 3.934 13.328 5.027 4.589.874 9.615 1.312 14.422.656 5.026-.655 10.051-2.623 13.984-5.9 3.933-3.278 6.774-7.648 8.522-12.237l.219-.218v-.217l.656-5.899v-.22c2.185-1.311 4.37-2.621 6.555-4.37 2.622-2.184 5.025-4.589 6.773-7.648 1.748-3.059 2.84-6.774 2.621-10.488-.218-3.496-1.53-6.99-3.06-10.049-1.53-3.059-3.495-5.901-5.68-8.523-4.37-5.026-9.614-9.176-15.295-12.454-5.462-3.496-11.581-6.338-17.7-8.304l-2.404-.656-1.962-.655c-1.311-.437-2.406-1.092-3.498-1.53-2.185-1.31-3.717-2.622-4.809-4.37-2.185-3.278-2.403-8.301-1.31-13.545.218-1.311.656-2.623 1.093-3.934a96.064 96.064 0 0 0 1.31-4.152c.314-1.412.51-2.829.598-4.402l29.203-25.553c-2.275-8.404-27.488-17.158-27.488-17.158l-74.931 63.726-43.243-81.584c-1.553-.35-3.218-.527-4.94-.57zm107.682 73.14c-.449 2.336-.647 4.795-.647 7.258.219 3.715 1.311 7.87 3.715 11.366 2.403 3.496 5.68 6.117 8.957 7.646a29.663 29.663 0 0 0 5.027 1.967l2.623.654 2.184.438c5.68 1.53 11.142 3.714 16.168 6.554 5.025 2.84 9.833 6.337 13.766 10.27s6.992 8.959 7.43 13.984c.218 3.496-.22 6.118-1.313 8.303-1.093 2.404-2.84 4.588-4.807 6.555-.874.874-1.966 1.747-2.84 2.402a27.11 27.11 0 0 0-.654-5.898c-.219-1.093-.438-1.966-.875-3.059-.437-.874-.872-1.966-1.965-2.621-.218 0-.44-.001-.44.217-1.31 3.277-3.494 6.12-5.898 8.086-2.403 1.966-5.462 2.84-8.521 3.058-3.06.219-6.338-.436-9.616-1.31-3.277-.874-6.552-1.968-9.83-3.06l-.439-.22c-.656-.218-1.526.002-1.963.44-1.748 2.185-3.06 4.149-4.59 6.334a58.435 58.435 0 0 0-2.84 5.027c-3.933-1.53-7.649-2.841-11.582-4.37-5.462-2.186-10.925-4.37-15.95-6.991-5.245-2.404-10.268-5.246-14.638-8.524-3.15-2.363-6.062-4.845-8.185-7.681l2.404-17.172z" fill="#f4511e" stroke-width="0" stroke-linejoin="round"/></g></symbol><symbol viewBox="0 0 24 24" id="handlebars" xmlns="http://www.w3.org/2000/svg"><path d="M8.55 10.32c-2.753 0-4.202 3.48-5.793 3.48-.98 0-1.126-.677-1.126-.915 0-.332.236-.706.564-.706.59 0 .414.77.414.77s.798-.555.272-1.298c-.42-.595-1.31-.623-1.92-.17-.617.458-1.057 1.146-.853 2.287.1.551.468 1.35 1.233 1.805.764.455 1.925.566 2.335.566 2.194 0 4.342-1.633 6.639-2.322a5.513 5.513 0 0 1 1.497-.222 6.19 6.19 0 0 1 1.92.226c2.296.689 4.444 2.323 6.638 2.323.41 0 1.57-.11 2.335-.566.765-.455 1.132-1.256 1.231-1.807.204-1.14-.235-1.829-.853-2.287-.61-.453-1.497-.423-1.918.172-.526.743.27 1.297.27 1.297s-.176-.77.414-.77c.329 0 .565.373.565.705 0 .238-.147.914-1.126.914-1.592 0-3.04-3.478-5.794-3.478-2.565 0-3.076 1.177-3.462 1.718-.004.005-.005.011-.008.016-.005-.006-.007-.013-.012-.02-.386-.54-.896-1.717-3.461-1.717z" fill="#ff7043" fill-rule="evenodd" stroke-width=".3"/></symbol><symbol viewBox="0 0 300.00001 300" id="haskell" xmlns="http://www.w3.org/2000/svg"><g stroke-width="2.422"><path d="M23.928 240.5l59.94-89.852-59.94-89.855h44.955l59.94 89.855-59.94 89.852z" fill="#ef5350"/><path d="M83.869 240.5l59.94-89.852-59.94-89.855h44.955l119.88 179.71h-44.95l-37.46-56.156-37.468 56.156z" fill="#ffa726"/><path d="M228.72 188.08l-19.98-29.953h69.93v29.956h-49.95zm-29.97-44.924l-19.98-29.953h99.901v29.953z" fill="#ffee58"/></g></symbol><symbol viewBox="0 0 210 210" id="haxe" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -87)"><path fill="#f68712" stroke-width=".221" d="M42.78 191.545l63.431-63.43 63.431 63.43-63.431 63.431z"/><path d="M42.8 191.592L31.193 148.28 19.59 104.97 62.9 116.575l43.311 11.605-31.706 31.706z" fill="#fab20b" stroke-width=".266"/><path d="M105.956 128.111l-43.19-11.544-43.177-11.597 22.927.185 23.228.294 20.264 11.36z" fill="#fbc707" stroke-width=".265"/><path d="M19.59 104.97l11.596 43.176 11.545 43.19-11.303-19.948-11.36-20.263-.294-23.228z" fill="#fff200" stroke-width=".265"/><path d="M106.23 128.133l43.312-11.605 43.311-11.605-11.605 43.31-11.605 43.312-31.706-31.706z" fill="#f47216" stroke-width=".266"/><path d="M169.711 191.289l11.545-43.19 11.597-43.176-.185 22.927-.294 23.228-11.36 20.263z" fill="#f1471d" stroke-width=".265"/><path d="M192.853 104.923l-43.176 11.597-43.19 11.544 19.947-11.303 20.264-11.36 23.228-.293z" fill="#fbc707" stroke-width=".265"/><path d="M169.643 191.545l11.605 43.31 11.605 43.312-43.311-11.605-43.311-11.606 31.706-31.705z" fill="#f25c19" stroke-width=".266"/><path d="M106.487 255.025l43.19 11.544 43.176 11.598-22.927-.185-23.228-.294-20.264-11.36z" fill="#f68712" stroke-width=".265"/><path d="M192.853 278.167l-11.597-43.176-11.545-43.19 11.303 19.947 11.36 20.264.294 23.228z" fill="#f1471d" stroke-width=".265"/><path d="M106.211 254.976l-43.31 11.605-43.312 11.605 11.605-43.31L42.8 191.563l31.706 31.706z" fill="#f89c0e" stroke-width=".266"/><path d="M42.731 191.82l-11.545 43.19-11.597 43.176.185-22.927.294-23.228 11.36-20.263z" fill="#fff200" stroke-width=".265"/><path d="M19.59 278.186l43.175-11.597 43.19-11.544-19.947 11.303-20.264 11.36-23.228.293z" fill="#f25c19" stroke-width=".265"/></g></symbol><symbol viewBox="0 0 144 152" id="heroku" xmlns="http://www.w3.org/2000/svg"><path d="M118.68 13.279H26.865c-6.337 0-11.476 5.139-11.476 11.476V129.32c0 6.338 5.139 11.477 11.476 11.477h91.813c6.338 0 11.477-5.14 11.477-11.477V24.755c0-6.337-5.139-11.476-11.477-11.476zM44.08 121.669V96.165l14.346 12.752zm44.632 0v-38.08c-.063-2.976-1.496-6.551-7.97-6.551-12.966 0-27.51 6.52-27.654 6.586l-9.008 4.08V32.407h12.752v36.201c6.366-2.072 15.266-4.321 23.91-4.321 7.882 0 12.6 3.099 15.17 5.698 5.484 5.547 5.56 12.613 5.551 13.43v38.255zm3.188-68.54H79.149c5.011-6.576 8.158-13.496 9.564-20.723h12.751c-.86 7.243-3.796 14.187-9.563 20.722z" fill="#6963b9"/></symbol><symbol viewBox="0 0 24 24" id="hpp" xmlns="http://www.w3.org/2000/svg"><path d="M9.757 19.818H6.751v-5.882q0-2.381-1.737-2.381-.868 0-1.438.663-.56.662-.56 1.718v5.882H0V4.533h3.016v6.508h.037Q4.24 9.239 6.247 9.239q3.51 0 3.51 4.239z" stroke-width=".478" fill="#0277bd"/><path d="M13.073 11.448v2h-2v2h2v2h2v-2h2v-2h-2v-2zm7 0v2h-2v2h2v2h2v-2h2v-2h-2v-2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="html" xmlns="http://www.w3.org/2000/svg"><path d="M12 17.56l4.07-1.13.55-6.1H9.38L9.2 8.3h7.6l.2-1.99H7l.56 6.01h6.89l-.23 2.58-2.22.6-2.22-.6-.14-1.66h-2l.29 3.19L12 17.56M4.07 3h15.86L18.5 19.2 12 21l-6.5-1.8L4.07 3z" fill="#e44d26"/></symbol><symbol viewBox="0 0 24 24" id="http" xmlns="http://www.w3.org/2000/svg"><path d="M16.046 13.784c.074-.613.13-1.225.13-1.856s-.056-1.244-.13-1.856h3.137c.148.594.241 1.215.241 1.856a7.65 7.65 0 0 1-.241 1.856m-4.78 5.16c.557-1.03.984-2.144 1.281-3.304h2.738a7.452 7.452 0 0 1-4.019 3.304m-.232-5.16H9.828a12.314 12.314 0 0 1-.149-1.856c0-.631.056-1.253.149-1.856h4.343c.084.603.149 1.225.149 1.856 0 .63-.065 1.243-.149 1.856M12 19.315c-.77-1.113-1.393-2.348-1.773-3.675h3.545c-.38 1.327-1.002 2.562-1.773 3.675m-3.712-11.1h-2.71a7.353 7.353 0 0 1 4.01-3.304c-.557 1.03-.975 2.144-1.3 3.304m-2.71 7.425h2.71c.325 1.16.743 2.274 1.3 3.304a7.433 7.433 0 0 1-4.01-3.304m-.761-1.856a7.65 7.65 0 0 1-.241-1.856c0-.64.093-1.262.241-1.856h3.137c-.074.612-.13 1.225-.13 1.856 0 .63.056 1.243.13 1.856m4.046-9.253c.77 1.114 1.393 2.357 1.773 3.684h-3.545c.38-1.327 1.002-2.57 1.773-3.684m6.422 3.684h-2.738a14.523 14.523 0 0 0-1.28-3.304 7.412 7.412 0 0 1 4.018 3.304m-6.423-5.568c-5.132 0-9.28 4.176-9.28 9.28a9.28 9.28 0 0 0 9.28 9.282 9.28 9.28 0 0 0 9.281-9.281A9.28 9.28 0 0 0 12 2.647z" fill="#e53935" stroke-width=".928"/></symbol><symbol viewBox="0 0 24 24" id="image" xmlns="http://www.w3.org/2000/svg"><path d="M13.009 9.202h5.368l-5.368-5.368v5.368M6.177 2.37h7.808l5.856 5.856v11.711a1.952 1.952 0 0 1-1.952 1.952H6.178a1.951 1.951 0 0 1-1.952-1.952V4.322c0-1.083.868-1.952 1.952-1.952m0 17.567h11.71V12.13l-3.903 3.903-1.952-1.951-5.856 5.855M8.13 9.202a1.952 1.952 0 0 0-1.952 1.952 1.952 1.952 0 0 0 1.952 1.952 1.952 1.952 0 0 0 1.952-1.952A1.952 1.952 0 0 0 8.13 9.202z" fill="#26a69a" stroke-width=".976"/></symbol><symbol viewBox="0 0 512 512" id="ionic" xmlns="http://www.w3.org/2000/svg"><g fill="#4f8ff7"><path d="M423.592 132.804A31.855 31.855 0 0 0 429 115c0-17.675-14.33-32-32-32a31.853 31.853 0 0 0-17.805 5.409C344.709 63.015 302.11 48 256 48 141.125 48 48 141.125 48 256c0 114.877 93.125 208 208 208 114.873 0 208-93.123 208-208 0-46.111-15.016-88.71-40.408-123.196zM391.83 391.832c-17.646 17.646-38.191 31.499-61.064 41.174-23.672 10.012-48.826 15.089-74.766 15.089-25.94 0-51.095-5.077-74.767-15.089-22.873-9.675-43.417-23.527-61.064-41.174s-31.5-38.191-41.174-61.064C68.982 307.096 63.905 281.94 63.905 256c0-25.94 5.077-51.095 15.089-74.767 9.674-22.873 23.527-43.417 41.174-61.064s38.191-31.5 61.064-41.174c23.673-10.013 48.828-15.09 74.768-15.09 25.939 0 51.094 5.077 74.766 15.089a191.221 191.221 0 0 1 37.802 21.327A31.853 31.853 0 0 0 365 115c0 17.675 14.327 32 32 32 5.293 0 10.28-1.293 14.678-3.568a191.085 191.085 0 0 1 21.327 37.801c10.013 23.672 15.09 48.827 15.09 74.767 0 25.939-5.077 51.096-15.09 74.768-9.675 22.873-23.527 43.418-41.175 61.064z"/><circle cx="256.003" cy="256" r="96"/></g></symbol><symbol viewBox="0 0 24 24" id="java" xmlns="http://www.w3.org/2000/svg"><path d="M2 21h18v-2H2M20 8h-2V5h2m0-2H4v10a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4v-3h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="javascript" xmlns="http://www.w3.org/2000/svg"><path d="M3 3h18v18H3V3m4.73 15.04c.4.85 1.19 1.55 2.54 1.55 1.5 0 2.53-.8 2.53-2.55v-5.78h-1.7V17c0 .86-.35 1.08-.9 1.08-.58 0-.82-.4-1.09-.87l-1.38.83m5.98-.18c.5.98 1.51 1.73 3.09 1.73 1.6 0 2.8-.83 2.8-2.36 0-1.41-.81-2.04-2.25-2.66l-.42-.18c-.73-.31-1.04-.52-1.04-1.02 0-.41.31-.73.81-.73.48 0 .8.21 1.09.73l1.31-.87c-.55-.96-1.33-1.33-2.4-1.33-1.51 0-2.48.96-2.48 2.23 0 1.38.81 2.03 2.03 2.55l.42.18c.78.34 1.24.55 1.24 1.13 0 .48-.45.83-1.15.83-.83 0-1.31-.43-1.67-1.03l-1.38.8z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="javascript-map" xmlns="http://www.w3.org/2000/svg"><path d="M18 8v2h2v10H10v-2H8v4h14V8h-4z" fill="#ffca28"/><path d="M2.444 2.506h14.135v14.136H2.444V2.506m3.714 11.811c.315.668.935 1.218 1.995 1.218 1.178 0 1.987-.629 1.987-2.003V8.993H8.805v4.508c0 .675-.275.848-.707.848-.455 0-.644-.314-.856-.683l-1.084.651m4.697-.14c.392.769 1.185 1.358 2.426 1.358 1.257 0 2.199-.652 2.199-1.854 0-1.107-.636-1.602-1.767-2.089l-.33-.141c-.573-.243-.816-.408-.816-.801 0-.322.243-.573.636-.573.377 0 .628.165.856.573l1.028-.683c-.432-.754-1.044-1.045-1.884-1.045-1.186 0-1.948.754-1.948 1.752 0 1.083.636 1.594 1.594 2.002l.33.141c.613.267.974.432.974.888 0 .377-.354.652-.903.652-.652 0-1.029-.338-1.312-.81l-1.083.63z" fill="#ffca28"/></symbol><symbol viewBox="0 0 180 180" id="jenkins" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="gia"><path transform="scale(1 -1)" fill="#37474f" d="M.899-144.42h144.42V0H.899z"/></clipPath></defs><g transform="matrix(1.0691 0 0 -1.0691 9.4 166.143)" clip-path="url(#gia)"><g fill-rule="evenodd"><path d="M107.96 30.661l-12.506-1.876-16.883-1.876-10.943-.312-10.629.312-8.13 2.502-7.19 7.815-5.628 15.945-1.25 3.44-7.504 2.5-4.377 7.191-3.126 10.317 3.44 9.067 8.128 2.814 6.565-3.127 3.127-6.878 3.752.626 1.25 1.563-1.25 7.19-.313 9.068 1.876 12.505-.074 7.143 5.701 9.114 10.005 7.19 17.508 7.504 19.383-2.814 16.883-12.193 7.817-12.505 5.002-9.067 1.25-22.51-3.752-19.384-6.877-17.195-6.566-9.066" fill="#f0d6b7"/><path d="M97.334-23.425l-44.709-1.876v-7.503l3.752-26.262-1.876-2.19-31.264 10.63-2.19 3.752-3.126 35.328-7.19 21.26-1.563 5.002 25.01 17.195 7.818 3.127 6.877-8.441 5.94-5.315 6.88-2.188 3.125-.938L68.57 1.899l2.814-3.44 7.19 2.502-5.002-9.693 27.2-12.818-3.439-1.876" fill="#335061"/><path d="M23.238 85.687l8.128 2.814 6.566-3.127 3.127-6.878 3.751.626.938 3.751-1.876 7.19 1.876 17.197-1.563 9.379 5.627 6.565 12.193 9.692-3.44 4.69-17.194-8.442-7.191-5.627-4.064-8.754-6.253-8.442-1.876-10.005 1.251-10.63" fill="#6d6b6d"/><path d="M36.055 115.07s4.69 11.567 23.448 17.195c18.759 5.628.938 4.065.938 4.065l-20.321-7.817-7.817-7.816-3.438-6.253 7.19.626M26.676 87.875s-6.566 21.886 18.446 25.012l-.938 3.752-17.195-4.065-5.003-16.257 1.251-10.63 3.439 2.188" fill="#dcd9d8"/></g><g fill="#f7e4cd"><path d="M36.681 58.799l4.094 3.966s1.847-.214 2.16-2.402c.312-2.19 1.25-21.886 14.693-32.516 1.227-.97-10.004 1.564-10.004 1.564L37.62 45.042M94.209 64.739s.729 9.477 3.28 8.748c2.553-.729 2.553-3.28 2.553-3.28s-6.198-4.01-5.833-5.468" fill-rule="evenodd"/><path d="M120.16 99.442s-5.153-1.088-5.628-5.628c-.474-4.54 5.628-.938 6.566-.625M82.327 99.129s-6.879-.938-6.879-5.314c0-4.378 7.817-4.065 10.005-2.19"/><g fill-rule="evenodd"><path d="M39.807 78.808s-11.881 7.191-13.131.312c-1.25-6.877-4.065-11.88 1.876-19.07l-4.064 1.25-3.752 9.691-1.25 9.38 7.19 7.504 8.129-.626 4.69-3.751.312-4.69M45.435 98.504s5.315 27.512 32.203 32.827c22.136 4.375 33.765-.938 38.142-5.94 0 0-19.696 23.447-38.455 16.257-18.759-7.191-32.514-20.322-32.202-28.762.532-14.377.313-14.382.313-14.382M117.97 122.27s-9.066.312-9.38-7.817c0 0 0-1.25.625-2.5 0 0 7.192 8.129 11.568 3.751"/><path d="M78.268 111.1s-1.56 12.477-12.199 5.223c-6.878-4.69-6.252-11.255-5.002-12.505s.91-3.77 1.862-2.04c.952 1.728.638 7.356 4.078 8.918 3.439 1.564 9.077 3.31 11.26.404"/></g></g><g fill="#49728b" fill-rule="evenodd"><path d="M48.874 26.597L19.486 13.466s12.193-48.46 5.94-63.467l-4.377 1.563-.313 18.446-8.128 35.015-3.44 9.692 30.639 20.633 9.067-8.753M51.896-.206l4.17-5.087v-18.76h-5.003s-.625 13.132-.625 14.696c0 1.563.624 7.19.624 7.19M52-26.866l-14.069-.625 4.065-2.813L52-31.868"/></g><g fill-rule="evenodd"><path d="M100.15-23.739l11.567.313 2.814-28.764-11.881-1.563-2.5 30.014" fill="#335061"/><path d="M103.27-23.739l17.508.938s7.19 18.133 7.19 19.07c0 .939 6.253 26.263 6.253 26.263l-14.069 14.694-2.813 2.501-7.504-7.503V3.148l-6.565-26.887" fill="#335061"/><path d="M111.09-21.55l-10.942-2.188 1.563-8.755c4.064-1.876 10.943 3.127 10.943 3.127M111.4 33.162l21.885-16.257.626 7.503-16.57 15.32-5.94-6.566" fill="#49728b"/><path d="M62.85-85.332l-6.473 26.266-3.22 19.38-.531 14.385 29.296 1.56 18.226.003-1.658-32.83 2.814-25.324-.312-4.69-23.76-1.876-14.382 3.126" fill="#fff"/><path d="M96.083-23.426s-1.563-32.515 3.127-55.65c0 0-9.38-5.94-23.136-7.503l26.262.938 3.126 1.875-3.752 51.273-.938 10.944" fill="#dcd9d8"/><path d="M115.06-49.691l12.193 3.44 23.135 1.25 3.44 10.629-6.254 18.446-7.19.938-10.005-3.127-9.599-4.686-5.095.935-3.972-1.56" fill="#fff"/><path d="M114.84-43.435s8.128 3.751 9.38 3.438L120.78-22.8l4.065 1.563s2.814-16.257 2.814-18.133c0 0 17.507-.938 19.07-.938 0 0 3.752 7.191 2.814 14.694l3.44-10.005.312-5.628-5.002-7.503-5.627-1.25-9.38.312-3.126 4.064-10.943-1.563-3.44-1.25" fill="#dcd9d8"/></g><path d="M102.56-21.241L95.682-3.733l-7.19 10.317s1.562 4.377 3.75 4.377h7.192l6.878-2.501-.625-11.568-3.127-18.134" fill="#fff"/><path d="M103.9-15.297S95.145 1.585 95.145 4.086c0 0 1.563 3.752 3.752 2.814 2.19-.938 6.879-3.439 6.879-3.439v5.94l-10.63 2.19-7.19-.939 12.193-28.763 2.5-.313" fill="#dcd9d8" fill-rule="evenodd"/><path d="M65.664 25.968l-8.661.942-8.13 2.501v-2.814l3.972-4.38 12.506-5.627" fill="#fff"/><path d="M51.689 25.031s9.693-4.065 12.819-3.127l.311-3.748-8.752 1.872-5.316 3.752.938 1.251" fill="#dcd9d8" fill-rule="evenodd"/><path d="M115.03 9.897c-5.305.156-10.098.786-14.294 1.97.285 1.72-.249 3.408.18 4.647 1.17.843 3.13.83 4.898 1.027-1.529.752-3.677 1.049-5.44.615-.042 1.194-.578 1.934-.902 2.868 2.982 1.064 10.024 8.044 13.984 5.732 1.887-1.099 2.689-7.377 2.835-10.43.122-2.533-.23-5.088-1.261-6.43" fill="#d33833" fill-rule="evenodd"/><path d="M115.03 9.897c-5.305.156-10.098.786-14.294 1.97.285 1.72-.249 3.408.18 4.647 1.17.843 3.13.83 4.898 1.027-1.529.752-3.677 1.049-5.44.615-.042 1.194-.578 1.934-.902 2.868 2.982 1.064 10.024 8.044 13.984 5.732 1.887-1.099 2.689-7.377 2.835-10.43.122-2.533-.23-5.088-1.261-6.43z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M89.66 18.569c-.014-.401-.03-.806-.047-1.21-1.656-1.089-4.33-1.076-6.148-1.99 2.68-.117 4.79-.763 6.614-1.672l-.118-3.033c-3.036-2.078-5.81-5.173-9.384-7.122-1.69-.922-7.622-3.294-9.42-2.875-1.017.236-1.109 1.499-1.516 2.689-.866 2.548-2.861 6.605-3.035 10.44-.222 4.846-.71 12.967 4.51 11.969 4.213-.804 9.113-2.745 12.375-4.527 1.993-1.09 3.146-2.436 6.17-2.669" fill="#d33833" fill-rule="evenodd"/><path d="M89.66 18.569c-.014-.401-.03-.806-.047-1.21-1.656-1.089-4.33-1.076-6.148-1.99 2.68-.117 4.79-.763 6.614-1.672l-.118-3.033c-3.036-2.078-5.81-5.173-9.384-7.122-1.69-.922-7.622-3.294-9.42-2.875-1.017.236-1.109 1.499-1.516 2.689-.866 2.548-2.861 6.605-3.035 10.44-.222 4.846-.71 12.967 4.51 11.969 4.213-.804 9.113-2.745 12.375-4.527 1.993-1.09 3.146-2.436 6.17-2.669z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M92.675 12.788c-.463 2.64-.999 3.393-.792 5.695 7.04 4.693 8.361-8.061.792-5.695" fill="#d33833" fill-rule="evenodd"/><path d="M92.675 12.788c-.463 2.64-.999 3.393-.792 5.695 7.04 4.693 8.361-8.061.792-5.695z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M102.87 10.649s-2.19 3.127-.626 4.065c1.564.938 3.127 0 4.065 1.563s0 2.501.313 4.377 1.877 2.189 3.44 2.501c1.562.313 5.94.938 6.565-.625l-1.876 5.627-3.752 1.25-11.88-6.877-.626-3.44v-6.877M70.041.331c-.376 4.88-.773 9.752-1.215 14.626-.662 7.279 1.748 6.009 8.057 6.009.964 0 5.933-1.15 6.289-1.876 1.705-3.483-2.851-2.709 1.964-5.335 4.065-2.216 11.246 1.346 9.603 6.273-.919 1.095-4.789.341-6.176 1.06l-7.327 3.8c-3.108 1.612-10.29 3.962-13.603 1.709-8.395-5.71.53-19.974 3.524-25.93" fill="#ef3d3a" fill-rule="evenodd"/><g fill="#231f20" fill-rule="evenodd"><path d="M78.268 111.1c-8.521 1.985-12.755-3.566-15.338-9.323-2.306.559-1.389 3.695-.806 5.294 1.525 4.194 7.672 9.778 12.694 9.02 2.161-.325 5.086-2.301 3.45-4.99M119.79 101.4l.404-.016c1.926-4 3.593-8.238 6.022-11.769-1.628-3.79-12.322-7.144-12.157-.338 2.313 1.01 6.305.206 8.356 1.497-1.186 3.254-2.897 6.024-2.625 10.626M82.63 101.29c1.827-3.35 2.422-6.868 5.019-9.4 1.17-1.14 3.444-2.529 2.316-5.698-.263-.747-2.189-2.414-3.3-2.741-4.06-1.2-13.521-.248-10.317 4.814 3.358-.157 7.871-2.18 10.38.257-1.927 3.081-5.363 9.177-4.098 12.768M118.26 67.253c-6.113-3.927-12.93-8.197-22.947-7.207-2.14 1.86-2.956 6.002-.877 8.737 1.082-1.861.402-5.284 3.419-5.799 5.684-.972 12.299 3.477 16.387 5.032 2.535 4.275-.219 5.847-2.503 8.597-4.675 5.636-10.947 12.622-10.72 21.06 1.89 1.37 2.053-2.092 2.325-2.722 2.44-5.714 8.585-13.021 13.07-17.912 1.1-1.205 2.914-2.36 3.115-3.157.582-2.315-1.513-5.09-1.27-6.63M37.668 71.387c-1.916 1.094-2.372 5.91-4.622 6.048-3.215.195-2.629-6.25-2.616-10.018-2.213 2.009-2.602 8.194-.976 11.37-1.853.91-2.68-1.003-3.708-1.677 1.32 9.595 14.036 4.45 11.922-5.723M122.15 63.257c-2.846-5.417-6.871-11.382-15.222-11.555-.17 1.75-.3 4.411.009 5.464 6.384.614 10.325 3.863 15.212 6.091M82.149 59.745c5.326-2.8 15.114-3.102 22.353-2.89.388-1.586.379-3.545.394-5.48-9.305-.463-20.307 1.84-22.747 8.37M81.136 54.523c3.683-9.247 16.341-8.182 27.016-7.927-.47-1.2-1.489-2.62-2.755-3.132-3.42-1.392-12.855-2.448-17.604.074-3.011 1.601-4.946 5.219-6.596 7.34-.797 1.024-4.765 3.64-.06 3.645"/></g><path d="M117.82 3.516c-4.322-7.402-8.457-15.005-13.585-21.534 2.15 6.32 3.07 16.9 3.394 24.965 4.498 2.105 8.349-.474 10.191-3.43" fill="#81b0c4" fill-rule="evenodd"/><g fill="#231f20" fill-rule="evenodd"><path d="M141.07-23.089c-4.839-.969-8.239-5.671-12.959-5.37 2.594 3.658 7.14 5.2 12.959 5.37M143.21-30.661c-3.944-.417-8.576-1.055-12.577-.726 1.894 2.892 9.19 1.894 12.577.726M144.58-37.19c-4.433-.096-9.942-.008-14.155.346 2.492 2.677 11.28.993 14.155-.346"/></g><g fill-rule="evenodd"><path d="M109.48-55.057c.636-5.567 2.843-11.207 2.566-17.304-2.45-.827-3.858-1.55-7.142-1.545-.232 5.181-.925 13.102-.718 18.041 1.615-.107 3.997 1.154 5.294.808" fill="#dcd9d8"/><path d="M102.33 26.985c-2.226-1.453-4.121-3.267-6.259-4.818-4.74-.235-7.327.328-10.81 3.05.057.219.407.121.42.39 5.075-2.262 11.524.92 16.648 1.378" fill="#f0d6b7"/><path d="M75.694-7.603c1.394 6.04 6.857 9.17 11.817 12.497 5.12-6.498 8.234-14.855 11.663-22.92-8.102 2.443-16.38 6.406-23.481 10.423" fill="#81b0c4"/><path d="M104.18-55.865c-.207-4.94.486-12.86.718-18.041 3.283-.004 4.691.718 7.142 1.545.276 6.096-1.93 11.737-2.566 17.304-1.298.346-3.679-.914-5.294-.808zm-51.13 28.09c2.165-19.906 5.301-36.639 11.054-54.266 12.766-3.876 28.157-4.214 39.441-.716-2.072 9.948-1.167 22.06-2.378 32.677-.912 7.98-.447 16.009-1.698 24.15-13.673 2.844-33 .665-46.418-1.845zm49.651 1.72c-.115-8.549.383-16.982 1.036-25.542 3.282.493 5.51.822 8.56 1.49-.99 8.241-.869 17.514-2.886 24.804-2.332-.023-4.385.027-6.71-.752zm16.653 1.378c-1.558.357-3.372.014-4.86-.015.7-6.969 2.397-14.659 2.995-21.974 2.342-.073 3.593 1.032 5.52 1.403.102 6.421-.562 15.268-3.655 20.586zm25.215-23.038c4.882 1.186 7.952 7.165 6.586 13.305-.916 4.127-2.548 11.898-4.295 14.538-1.29 1.953-4.79 4.51-7.584 2.72-4.545-2.91-12.552-3.755-15.867-7.278 1.662-5.534 2.178-13.135 2.864-20.146 5.678-.354 12.665 1.562 17.387-.471-3.297-1.068-7.575-1.077-10.423-2.633 2.328-1.125 7.778-.897 11.332-.035zM99.17-18.025c-3.43 8.063-6.543 16.42-11.663 22.918-4.96-3.327-10.423-6.456-11.817-12.497 7.1-4.017 15.379-7.98 23.481-10.422zm8.453 24.971c-.325-8.065-1.245-18.644-3.395-24.965 5.128 6.53 9.263 14.132 13.585 21.534-1.842 2.957-5.693 5.536-10.19 3.431zm-9.582 3.405c-1.943.21-3.592-2.233-6.117-1.177-.58-.64-1.105-1.333-1.695-1.958 5.579-6.723 8.114-16.262 12.423-24.163 2.312 7.59 2.045 15.904 2.555 24.188-3.177-.201-4.94 2.873-7.166 3.11zm-6.161 8.132c-.208-2.303.328-3.056.791-5.695 7.57-2.367 6.248 10.388-.791 5.695zm-8.394 2.755c-3.261 1.782-8.161 3.723-12.374 4.527-5.222.999-4.732-7.123-4.51-11.968.173-3.836 2.168-7.893 3.035-10.441.406-1.19.498-2.453 1.515-2.69 1.798-.418 7.73 1.954 9.42 2.875 3.575 1.95 6.348 5.045 9.384 7.123.04 1.011.078 2.021.119 3.032-1.826.91-3.935 1.555-6.615 1.673 1.818.914 4.492.901 6.148 1.989.016.405.033.81.047 1.21-3.024.234-4.176 1.58-6.17 2.67zm-31.152 5.659c-2.707-2.748 7.592-6.494 10.871-6.696-.018 1.739.991 3.378.788 4.626-3.895.684-9.013.232-11.66 2.07zm33.345-1.29c-.013-.27-.363-.172-.42-.39 3.482-2.722 6.07-3.285 10.81-3.05 2.137 1.551 4.033 3.365 6.259 4.818-5.124-.458-11.574-3.64-16.648-1.379zm30.606-9.282c-.146 3.053-.948 9.332-2.835 10.431-3.961 2.312-11.002-4.668-13.984-5.732.324-.934.86-1.674.901-2.868 1.764.434 3.912.137 5.44-.615-1.767-.198-3.727-.185-4.897-1.027-.429-1.239.105-2.927-.18-4.647 4.196-1.184 8.989-1.814 14.294-1.97 1.032 1.341 1.383 3.896 1.261 6.429zM47.777 24.24c-.85.606-6.6 8.087-7.388 7.777-10.405-4.103-20.134-11.199-28.828-17.91 8.29-17.787 11.635-39.579 12.227-60.582 9.496-4.441 17.836-10.844 30.722-11.512-1.491 10.55-2.852 19.962-3.699 29.895-3.237 1.365-7.882-.062-10.913.423-.025 3.651 4.628 1.6 5.015 4.054.292 1.858-2.56 1.998-1.631 4.923 2.368-.861 3.612-2.763 6.138-3.477 2.309 5.05-.032 13.985.3 18.205.064.792.397 4.39 2.172 3.759 1.57-.559-.09-9.569.082-13.563.157-3.68-.444-7.242 1.046-9.552a355.817 355.817 0 0 0 38.576 3.16c-2.964 1.272-6.485 2.475-10.345 4.651-2.093 1.18-8.69 3.635-9.293 5.622-.964 3.167 2.528 4.855 3.125 7.57-6.285-3.428-7.511 3.286-8.998 8.042-1.347 4.308-2.114 7.526-2.445 10.01-5.414 2.581-11.203 5.195-15.863 8.505zm63.009 6.872c8.67 4.204 10.232-15.711 6.834-22.127.525-1.914 2.331-2.646 3.069-4.366-4.838-8.667-10.211-16.756-15.148-25.32 3.672 2.286 8.917.409 13.238 2.12 1.58.624 2.722 4.24 3.918 7.133 3.29 7.958 6.743 17.99 8.28 25.586.346 1.73 1.292 5.5 1.08 7.04-.378 2.758-4.12 4.803-6.022 6.508-3.506 3.15-5.714 5.921-9.371 8.866-1.483-2.189-4.666-3.66-5.878-5.44zM27.95 107.99c-4.13-4.545-3.266-13.062-2.766-19.121 7.467 4.697 17.377-.372 17.284-8.36 3.565.094 1.332 4.452.687 7.259-2.107 9.169 3.55 19.13.256 27.516-6.395-.485-11.649-3.097-15.46-7.294zm29.558 26.38c-9.352-2.65-21.337-9.446-25.18-17.847 2.976.432 5.041 1.933 7.977 2.119 1.11.072 2.563-.466 3.838-.148 2.54.63 4.685 6.327 6.602 8.447 1.868 2.07 4.114 2.954 5.651 4.841.988.477 2.448.444 2.504 1.927-.428.457-.879.806-1.392.66zm48.681-2.493c-9.707 5.477-26.136 9.596-36.462 4.449-8.331-4.155-19.593-11.027-23.433-19.737 3.587-8.405-1.062-16.106-1.36-24.64-.157-4.54 2.139-8.504 2.315-13.446-1.228-2.025-4.978-2.275-7.574-2.136-.873 4.372-2.403 9.287-6.906 9.78-6.371.697-11.03-4.576-11.319-10.085-.342-6.48 4.978-17.22 12.517-16.475 2.913.287 3.629 3.207 6.802 3.177 1.72-3.432-2.653-4.51-3.103-6.964-.117-.634.363-3.112.642-4.274 1.37-5.658 4.422-12.982 7.427-17.29 3.814-5.464 11.307-6.288 19.37-6.823 1.44 3.101 6.743 2.846 10.2 2.035-4.143 1.64-7.993 5.617-11.185 9.137-3.665 4.039-7.378 8.371-7.566 13.65 6.927-9.61 12.65-18.003 25.246-22.23 9.53-3.196 20.662 1.465 27.986 6.608 3.039 2.137 4.853 5.529 7.013 8.634 8.082 11.626 11.854 28.219 11.024 44.303-.342 6.633-.327 13.244-2.552 17.706-2.326 4.666-10.193 8.84-14.8 4.62-.853 4.537 3.83 7.344 9.331 5.71-3.922 5.063-8.039 11.145-13.614 14.29zm18.084-149.66c7.585 3.77 21.757 10.149 26.512-.014 1.755-3.746 3.814-10.079 4.723-13.946 1.284-5.456-1.392-16.923-7-18.754-4.953-1.617-10.733-1.518-16.7-.32-.702.585-1.484 1.603-2.03 2.665-4.261.165-8.25-.229-11.615-1.98.319-3.15-1.812-3.656-3.81-4.305-1.48-5.872 2.963-13.541 1.9-18.896-.76-3.815-5.453-4.405-8.902-5.118-.113-2.12.15-3.89.386-5.683-.789-2.907-4.327-4.561-7.679-4.967-11.029-1.326-27.775-1.922-38.384 1.893-2.96 7.261-5.292 16.093-7.758 24.384-10.346-1.105-18.715 4.464-26.603 8.113-2.731 1.266-6.51 1.964-7.53 4.138-.99 2.105-.584 6.14-.83 9.95-.625 9.733-1.16 19.12-3.73 29.086-1.154 4.472-3.165 8.418-4.568 12.727C9.358 5.184 7.092 10.12 6.5 14.1c-.877 5.903 4.681 6.232 8.235 8.79 5.494 3.954 9.806 6.142 15.756 9.711 1.762 1.057 7.077 3.733 7.681 4.966 1.202 2.443-2.062 5.888-2.935 7.803-1.38 3.03-2.1 5.602-2.298 8.59-4.992.789-8.775 3.76-11.06 7.109-3.781 5.543-6.403 15.798-3.132 23.599.257.614 1.536 1.822 1.725 2.765.372 1.858-.7 4.329-.768 6.305-.343 10.14 1.716 18.875 8.541 21.932 2.771 11.038 12.688 14.71 22.032 20.195 3.493 2.05 7.343 3.36 11.32 4.824 14.263 5.25 36.15 4.261 47.987-4.692 5.02-3.797 13.044-11.813 15.914-17.617 7.58-15.323 7.042-40.931 1.74-59.571-.712-2.503-1.746-6.181-3.19-9.187-1.006-2.1-4.134-6.3-3.754-8.153.391-1.916 7.132-7.034 8.577-8.428 2.603-2.51 7.548-5.843 7.948-9.012.43-3.372-1.485-7.984-2.456-11.238-3.245-10.858-6.412-20.895-10.091-30.576" fill="#231f20"/><path d="M73.674 57.38c.411.548 2.674 1.38 5.84-.144 0 0-3.752-.626-3.44-6.881l-1.564.313s-1.615 5.672-.836 6.712" fill="#f7e4cd"/><path d="M101.09 3.617a1.72 1.72 0 1 0-3.44.001 1.72 1.72 0 0 0 3.44-.001M102.81-4.355a1.72 1.72 0 1 0-3.44 0 1.72 1.72 0 0 0 3.44 0" fill="#1d1919"/></g><g><rect transform="matrix(.8 0 0 -.8 0 144)" x="16.854" y="177.38" width="70.412" height="4.12" rx=".983" ry=".983"/><rect transform="scale(1 -1)" x="78.502" y="-2.097" width="50.037" height="3.296" rx=".786" ry=".786"/><rect transform="scale(1 -1)" x="13.483" y="-3.697" width="54.831" height="3.296" rx=".786" ry=".786"/><rect transform="scale(1 -1)" x="83.296" y="-3.697" width="45.243" height="3.296" rx=".786" ry=".786"/></g></g></symbol><symbol viewBox="0 0 24 24" id="json" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#fbc02d"/></symbol><symbol viewBox="0 0 50 50" id="julia" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -247)" stroke-width="5.673"><circle cx="13.497" cy="281.632" r="9.555" fill="#bc342d"/><circle cx="36.081" cy="281.632" r="9.555" fill="#864e9f"/><circle cx="24.722" cy="262.389" r="9.555" fill="#328a22"/></g></symbol><symbol viewBox="0 0 64 64" id="karma" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -233)"><path d="M38.556 288.413l-20.29-26.687 9.532-7.246 20.29 26.686h-.001.002l5.527 7.247z" fill="#359b8b" stroke-width=".173"/><path d="M35.681 241.172L24.92 255.327v-14.13H12.947v13.817l7.84 33.235h4.132v-13.147l.003.003 20.29-26.686-.008-.006 5.504-7.24H35.84v.12z" fill="#3cbeae" stroke-width=".206"/></g></symbol><symbol viewBox="0 0 24 24" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7 14a2 2 0 0 1-2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2 2 2 0 0 1-2 2m5.65-4A5.99 5.99 0 0 0 7 6a6 6 0 0 0-6 6 6 6 0 0 0 6 6 5.99 5.99 0 0 0 5.65-4H17v4h4v-4h2v-4H12.65z" fill="#26a69a"/></symbol><symbol viewBox="0 0 24 24" id="kivy" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.89 0 0 1.89 -12.157 -11.429)" fill="#90a4ae"><path d="M7.026 8.63v4.474l1.928-1.928a.437.437 0 0 0 0-.619zM9.38 16.072v-4.474l-1.927 1.927a.437.437 0 0 0 0 .62zM18.576 10.412l-5.346.564-.017.018 2.39 2.39zM9.922 8.502s.023 3.304-.003 4.452c-.02.856.371 1.114.746 1.507.538.564 1.599 1.57 1.599 1.57a.53.53 0 0 0 .75 0l1.843-1.844a.53.53 0 0 0 0-.75z"/></g></symbol><symbol viewBox="0 0 24 24" id="kl" xmlns="http://www.w3.org/2000/svg"><defs><style>.a{fill:#3aaae1}.b{fill:#fdfeff}</style></defs><title>kl</title><path d="M12.033 1.737c-.25-.003-.5.11-.729.337C8.225 5.15 5.15 8.227 2.078 11.31c-.144.144-.229.346-.341.521v.41c.16.223.294.474.485.666a3259.51 3259.51 0 0 0 8.936 8.937c.193.192.443.325.666.486h.41c.205-.142.436-.256.609-.428 3.046-3.041 6.09-6.083 9.133-9.127.47-.47.472-1.005.006-1.472l-9.218-9.217c-.23-.23-.48-.347-.731-.35zm-1.062 4.545l1.386.832c.702.422 1.403.846 2.109 1.262a.544.544 0 0 1 .04.026l.016.013.017.013c.061.056.089.123.088.224a510.281 510.281 0 0 0 0 3.794.463.463 0 0 1-.007.094c-.015.069-.054.103-.142.109a.464.464 0 0 1-.044.002c-.045-.002-.09-.002-.136-.003-.323-.006-.648-.001-.998-.001v-.527-1.34-.671-.003l.004-.668c0-.147-.039-.231-.17-.308-.893-.528-1.78-1.066-2.67-1.6-.051-.03-.101-.065-.173-.111l.001-.003h-.001zm.362 3.39c.068-.003.119.043.173.138.085.148.174.293.264.44l.015.025c.096.154.194.31.292.47l-1.915 1.176c-.337.207-.673.417-1.014.617-.113.067-.154.143-.154.277.01.977.01 1.955.014 2.932V16H7.7V16h-.002c-.004-.053-.014-.112-.014-.17-.005-1.25-.006-2.501-.015-3.751 0-.142.045-.222.164-.294a467.13 467.13 0 0 0 3.353-2.054l.016-.01a.606.606 0 0 1 .032-.017l.016-.008a.308.308 0 0 1 .033-.013l.012-.004a.157.157 0 0 1 .028-.005l.01-.001zm5.677 3.126l.314.54.346.594v.001c-.158.094-.298.178-.438.259l-3.097 1.798c-.106.062-.189.071-.3.01l-.893-.496-1.524-.843-.895-.493c-.035-.02-.068-.044-.129-.085h.001l.137-.25.495-.902 1.446.795c.442.243.886.483 1.323.734.121.07.212.072.334 0 .894-.525 1.792-1.043 2.689-1.563.057-.034.118-.061.191-.1z" fill="#29b6f6" stroke-width=".041"/></symbol><symbol viewBox="0 0 500 500" id="kotlin" xmlns="http://www.w3.org/2000/svg"><path d="M500 500H0V0h500L250 250z" fill="#7F52FF"></path></symbol><symbol viewBox="0 0 240 240" id="laravel" xmlns="http://www.w3.org/2000/svg"><path d="M216.05 119.036c-1.433.343-24.945 6.673-24.945 6.673l-19.227-28.622c-.537-.828-.99-1.656.359-1.849 1.345-.196 23.195-4.477 24.182-4.723.99-.245 1.837-.536 3.053 1.267 1.21 1.8 17.836 24.626 18.464 25.506.627.877-.447 1.41-1.883 1.748m-4.101 49.326c.588 1.003 1.176 1.64-.67 2.367-1.843.73-62.243 22.847-63.418 23.39-1.173.546-2.092.73-3.607-1.637-1.51-2.362-21.16-39.264-21.16-39.264l64.03-18.075c1.876-.644 2.317-.405 3.103.822 1.074 1.68 21.143 31.403 21.726 32.4m-103.7-21.087c-.78.202-37.566 9.733-39.525 10.22-1.965.485-1.965.246-2.188-.49-.226-.727-43.728-98.053-44.333-99.271-.605-1.214-.574-2.177 0-2.177.571 0 34.734-3.313 35.944-3.383 1.207-.07 1.08.205 1.526 1.033l49.025 91.818c.84 1.58 1.239 1.81-.452 2.248m94.588-59.77c-3.5-4.58-5.2-3.751-7.357-3.41-2.154.336-27.277 4.915-30.194 5.449-2.918.536-4.758 1.803-2.963 4.53 1.597 2.422 18.113 27.824 21.751 33.42l-65.663 17.066L66.18 49.832c-2.075-3.342-2.507-4.514-7.236-4.28-4.735.23-40.969 3.495-43.55 3.731-2.58.233-5.416 1.479-2.835 8.09 2.583 6.612 43.734 102.82 44.88 105.62 1.149 2.803 4.128 7.345 11.11 5.527 7.157-1.871 31.969-8.894 45.52-12.742 7.163 14.07 21.77 42.619 24.473 46.707 3.607 5.459 6.089 4.56 11.626 2.738 4.325-1.42 67.65-26.129 70.502-27.4 2.855-1.273 4.613-2.184 2.685-5.275-1.419-2.28-18.124-26.558-26.876-39.26 5.993-1.733 27.305-7.888 29.575-8.557 2.646-.779 3.008-2.19 1.572-3.94-1.436-1.755-21.293-28.72-24.79-33.296z" fill="#ff5722" stroke="#ff5722" stroke-width="8.852" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" id="less" xmlns="http://www.w3.org/2000/svg"><path d="M13.696 2.999V5h2.002v5a2 2 0 0 0 1.999 2 2 2 0 0 0-2 2v5h-2v2h2a2 2 0 0 0 2-2v-4a2 2 0 0 1 2-2h1V11h-1a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2.001zm-.03 12.766v.47a1 1 0 0 0 .03-.236 1 1 0 0 0-.03-.234zM10.566 21v-2.001H8.565v-5a2 2 0 0 0-2-2 2 2 0 0 0 2-2V5h2.001v-2H8.565a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-.999V13h1a2 2 0 0 1 2 2v3.999A2 2 0 0 0 8.564 21zm.03-12.766v-.47a1 1 0 0 0-.03.236 1 1 0 0 0 .03.234z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="lib" xmlns="http://www.w3.org/2000/svg"><path d="M19 7H9V5h10m-4 10H9v-2h6m4-2H9V9h10m1-7H8a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2M4 6H2v14a2 2 0 0 0 2 2h14v-2H4V6z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 40 40" id="livescript" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -257)" fill="#317eac"><path stroke-width="3.299" d="M5.419 260.18h3.685v34.207H5.419z"/><path stroke-width="3.299" d="M37.074 288.197v3.685H2.867v-3.685z"/><path stroke-width="2.894" d="M29.612 265.658l2.004 2.005L7.428 291.85l-2.004-2.005z"/><path stroke-width="2.325" d="M10.73 262.471h2.835v22.08H10.73z"/><path stroke-width="2.063" d="M15.36 262.519h2.835v17.382H15.36z"/><path stroke-width="1.77" d="M19.99 262.471h2.835v12.802H19.99z"/><path stroke-width="1.422" d="M24.526 262.491h2.835v8.254h-2.835z"/><path stroke-width="1.128" d="M28.783 262.463h2.835v5.197h-2.835z"/><path stroke-width="2.325" d="M34.801 286.545v-2.835h-22.08v2.835z"/><path stroke-width="2.063" d="M34.753 281.914v-2.835H17.371v2.835z"/><path stroke-width="1.77" d="M34.801 277.284v-2.835H21.999v2.835z"/><path stroke-width="1.422" d="M34.781 272.749v-2.835h-8.254v2.835z"/><path stroke-width="1.128" d="M34.809 268.492v-2.835h-5.197v2.835z"/></g></symbol><symbol viewBox="0 0 24 24" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z" fill="#ffd54f"/></symbol><symbol viewBox="0 0 24 24" id="lua" xmlns="http://www.w3.org/2000/svg"><circle cx="12.203" cy="12.102" r="10.322" fill="none" stroke="#42a5f5"/><path d="M12.33 5.746a6.483 6.381 0 0 0-6.482 6.381 6.483 6.381 0 0 0 6.482 6.38 6.483 6.381 0 0 0 6.484-6.38 6.483 6.381 0 0 0-6.484-6.38zm1.86 1.916a2.329 2.292 0 0 1 2.33 2.293 2.329 2.292 0 0 1-2.33 2.291 2.329 2.292 0 0 1-2.329-2.29 2.329 2.292 0 0 1 2.328-2.294z" fill="#42a5f5" fill-rule="evenodd"/><ellipse cy="4.615" cx="19.631" rx="2.329" ry="2.292" fill="#42a5f5" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" id="markdown" xmlns="http://www.w3.org/2000/svg"><path d="M2 16V8h2l3 3 3-3h2v8h-2v-5.17l-3 3-3-3V16H2m14-8h3v4h2.5l-4 4.5-4-4.5H16V8z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" preserveAspectRatio="xMidYMid" id="markojs" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -120.96)" stroke-width=".984"><path d="M4.002 126.482c-.655 1.07-1.32 2.14-1.976 3.21-.655 1.06-1.308 2.142-1.963 3.212l.002.002-.002.002c.655 1.07 1.308 2.15 1.963 3.211.655 1.07 1.32 2.141 1.976 3.211h3.33c-.664-1.07-1.318-2.14-1.974-3.21-.653-1.069-1.307-2.145-1.961-3.214.654-1.068 1.308-2.146 1.961-3.215a601.93 601.93 0 0 1 1.974-3.209z" fill="#2196f3"/><path d="M3.999 126.482l-.002.002c.655 1.07 1.31 2.15 1.964 3.212.655 1.07 1.32 2.14 1.974 3.21h3.331c-.664-1.07-1.319-2.14-1.974-3.21-.653-1.068-1.306-2.146-1.96-3.214z" fill="#26a69a"/><path d="M15.203 126.482l.002.002c-.655 1.07-1.31 2.15-1.965 3.212-.655 1.07-1.319 2.14-1.974 3.21h-3.33c.664-1.07 1.318-2.14 1.973-3.21.654-1.069 1.307-2.146 1.961-3.214z" fill="#8bc34a"/><path d="M11.874 126.484c.664 1.07 1.318 2.14 1.974 3.21.653 1.068 1.307 2.146 1.961 3.214-.654 1.069-1.308 2.145-1.961 3.213-.656 1.07-1.31 2.14-1.974 3.21h3.33c.655-1.07 1.319-2.14 1.974-3.21.655-1.06 1.31-2.14 1.966-3.21l-.002-.003.002-.002c-.656-1.07-1.311-2.152-1.966-3.213-.655-1.07-1.319-2.138-1.974-3.209z" fill="#ffc107"/><path d="M16.74 126.482c.665 1.07 1.319 2.14 1.974 3.21.654 1.068 1.306 2.146 1.96 3.214-.654 1.069-1.306 2.145-1.96 3.213-.655 1.07-1.31 2.141-1.974 3.211h3.33c.656-1.07 1.32-2.14 1.974-3.21.655-1.062 1.31-2.141 1.966-3.212l-.002-.002.002-.002c-.655-1.07-1.31-2.152-1.966-3.213-.655-1.07-1.318-2.138-1.973-3.209z" fill="#f44336"/></g></symbol><symbol viewBox="0 0 23 24" id="mathematica" xmlns="http://www.w3.org/2000/svg"><path d="M11.512 1.523l-.073.025-.46.794-.454.763-1.217 2.09H9.29L5.435 3.5l-.1-.047h-.018v.092l.025.163v.086l.132 1.226v.082l.032.252v.082l.22 2.137v.075l.018.082v.06l-2.348.507-.04.015-.457.1-.025.01h-.042l-1.096.244-.04.007-.17.036v.082l.018.01 1.859 2.086.053.052.114.132.804.909v.005l-.053.05-.22.257-2.564 2.875-.01.007v.082l.071.006.295.075 1.697.366v.006l2.139.472h.015v.047l-.036.252v.08l-.046.412v.082l-.036.244v.082l-.045.412v.08l-.05.41v.08l-.036.244v.082l-.046.412v.082l-.05.407v.082l-.032.248V20l-.05.407v.104h.037l3.642-1.6.294-.134h.018l.177.312.539.911.015.032.854 1.465.16.262.404.695.007.022h.092l.005-.022.017-.025.56-.947.014-.042.6-1.033.316-.539.644-1.091.05.013 3.906 1.721h.035v-.085l-.138-1.32v-.082l-.032-.244v-.082l-.035-.245v-.085l-.033-.244v-.081l-.032-.245v-.082l-.032-.244v-.085l-.035-.245v-.082l-.032-.245v-.082l-.033-.244v-.085l-.025-.17v-.053l1.632-.354.043-.008.458-.107h.028v-.01l.23-.05.03-.01h.042l.382-.09.025-.01h.043l.194-.05h.033l1.015-.23.07-.007v-.064l-.015-.013-1.19-1.342-.028-.028-.197-.22-1.428-1.604v-.006l.295-.323.4-.457 2.148-2.408.015-.01v-.065l-.035-.008-1.288-.28-.372-.084-.047-.01-2.481-.544v-.045l.432-4.265v-.02h-.042l-.302.135-.01.014h-.025l-3.307 1.45-.297.135h-.015l-2.028-3.483-.099-.145-.014-.045zm-.001 1.114l1.365 2.323.34.592-.008.025-1.18 1.511-.517.66-.012-.01-.258-.335-.04-.05-1.397-1.787.03-.063 1.378-2.365.287-.491zm4.908 2.039l-.007.025-.168.225-.538.066zm-9.817.004l.053.02.677.3h-.499l-.224-.3zM16.947 5l-.123 1.248-.113-.928.226-.307zm-9.26.156l.053.024.705.309-.757-.175zm7.388.116l.02.168-1.318.403.003-.003.16-.071 1.015-.444zM9.669 6.388l.944 1.204v.01L9.483 7.2zm3.55.172l.21.682-.234.084-.089.022-.702.255.008-.022.776-.982zm-5 .836l.986.356.898.312.048.02 1.054.373.011 3.086-.362-.117-.67-.224-.081-.038-.735-.245-.77-.256-.29-.1-.011-.255-.032-1.195-.01-.287-.015-.894-.013-.297zm6.583 0l-.011.227-.028.9-.008.303-.032 1.475-.01.262-.337.117-.734.245-.77.256-.712.245-.355.117.01-3.086 1.632-.578zm.585.437l.09.735.79-.097-.915 1.302-.018.006.01-.183.018-.877zm-9.451.536l.152.22 1.447 2.049-2.607.968-.05.015-1.972-2.214-.28-.312.003-.01.115-.018.424-.1.14-.021.337-.078.042-.01zm11.146.003l3.284.713.029.01-.022.025-1.954 2.192-.277.312-.092-.036-2.564-.95.475-.681.152-.216zM6.787 8.52h.86l.036 1.258-.013-.006-.763-1.078zm1.358 2.625l.152.06.77.252.712.245.746.247.49.167-.065.092-1.723 2.334-1.015-.302-.082-.017-.035-.015-1.902-.56.938-1.22.981-1.277zm6.73 0l.033.006 1.787 2.327.132.17-.128.036-.032.014-2.196.642-.105.032-.564.17-.018-.003-1.053-1.44-.174-.239-.547-.726-.007-.018.469-.16.769-.254.713-.245.77-.252zm-7.766.305l-.007.02-.405.523-.291-.291.657-.245zm8.802 0l.043.007.578.212.714.27-.661.394-.375-.479-.03-.042-.262-.342zm-10.843.75l-.67.668.355-.397.207-.23zm12.911.016l.068.025.045.042.554.627.042.043.204.228-.255.135zm-6.473.265l.022.015 1.38 1.872.032.05.343.465.008.031-.088.117-.422.629-.047.074-.245.343-.97 1.43-.013.007-1.18-1.72-.096-.16-.493-.708-.008-.037 1.618-2.191.007-.01zm7.827 1.194l.565.633.063.082-.272-.093-.037-.013zm-15.785.148l.297.299-.637.218-.152.05.038-.058zm13.224.47l-.855.448.346.66-.185-.058-.27-.088-1.092-.348.012-.01zm-9.687.255l1.222.356-.006.007-.458.145-.443.135-.032.01-.49.157zm-2.765.048l.318.32 2.007.517-.567.18-.055.004-2.103-.469-.744-.156.007-.006zm14.966.205l.548.188v.003l-.457.1-.043.014-1.069.23zm-10.23.507l.007.227.01.347.025 1.363.025.691-.007.255-.24.107-2.863 1.255.032-.372.033-.255.017-.227.031-.256.037-.407.045-.42.018-.23.032-.251.032-.412.05-.414.013-.14 1.455-.457.003-.014.301-.098zm4.908 0l1.245.39v.014l.312.1 1.146.362.022.23.03.255.043.408.04.42.017.23.033.251.032.412.042.325.078.848-.078-.04-3.025-1.322-.004-.305.06-2.368zm-4.295.617l.015.007.067.107.6.875-.64.531-.034-1.438zm3.671 0h.008l-.005.06-.02.678-.005.214-.479-.223zm-2.888 3.605l.763.915.001.37-.017-.006-.025-.05-.464-.791-.012-.018zm1.53.61l.184.083-.343.586-.018.007.002-.532z" fill="#f44336" fill-rule="evenodd" stroke="#f44336" stroke-width=".7747499999999999" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 720 720" id="matlab" xmlns="http://www.w3.org/2000/svg"><title>Layer 1</title><path d="M209.247 329.98L52.368 387.638l121.325 85.822 96.752-95.804-61.198-47.674z" fill="#4db6ac" fill-rule="evenodd" stroke-width=".3"/><path d="M480.193 71.446c-13.123 1.784-9.565 1.013-28.4 16.09-18.008 14.418-69.925 100.347-97.673 129.256-24.688 25.722-34.46 12.199-60.102 33.661-25.68 21.494-65.273 64.464-65.273 64.464l63.978 47.32L394.15 222.754c23.948-32.932 23.694-37.266 36.744-71.82 6.384-16.907 17.76-29.9 27.756-45.809 12.488-19.874 30.186-34.855 21.543-33.68z" fill="#00897b" fill-rule="evenodd" stroke-width=".3"/><path d="M478.206 69.796c-31.268-.189-62.068 137.245-115.56 242.691-54.543 107.519-162.235 176.82-162.235 176.82 18.156 8.243 34.681 4.91 54.236 23.394 13.375 16.164 52.09 95.976 75.174 146.117 0 0 18.964-10.297 42.994-27.695 24.03-17.397 53.124-41.896 73.384-70.3 26.883-37.692 47.897-61.043 65.703-75.271 17.806-14.23 32.404-19.336 46.458-20.54 50.238-4.305 124.582 85.792 124.582 85.792S527.267 70.09 478.206 69.796z" fill="#ffb74d" fill-rule="evenodd" stroke-width=".3"/></symbol><symbol viewBox="0 0 24 24" id="merlin" xmlns="http://www.w3.org/2000/svg"><text style="line-height:1.25;-inkscape-font-specification:'Century Gothic Bold'" x="1.953" y="21.178" transform="scale(.99582 1.0042)" font-weight="700" font-size="30.255" font-family="Century Gothic" letter-spacing="0" word-spacing="0" fill="#42a5f5" stroke-width=".756"><tspan x="1.953" y="21.178" style="-inkscape-font-specification:'Century Gothic Bold'" font-size="22.745">M</tspan></text></symbol><symbol viewBox="0 0 192 191.99999" id="mocha" xmlns="http://www.w3.org/2000/svg"><title>Mocha Logo</title><g transform="translate(-354.75 -262.42) scale(4.835)" fill="#a1887f"><path d="M103.6 69.6c0-.5-.4-1-1-1H83.8c-.5 0-1 .4-1 1 0 3.4.5 15.1 5.5 20.8.2.2.4.3.7.3h8.4c.3 0 .5-.1.7-.3 5-5.6 5.5-17.3 5.5-20.8zm-7.4 18.2h-5.9c-.3 0-.5-.1-.7-.3-3.4-4-3.8-12-3.9-14.8 0-.5.4-1 1-1h13.2c.5 0 1 .4 1 1 0 2.8-.5 10.7-3.9 14.8-.3.2-.5.3-.8.3zM95.1 66.6s3.6-2.1 1.4-5.9c-1.3-2-1.9-3.7-1.4-4.4-1.3 1.6-3.5 3.3-1.1 6.9.8.9 1.2 2.8 1.1 3.4zM91.1 66.9s2.4-1.4.9-4c-.9-1.3-1.3-2.5-.9-2.9-.9 1.1-2.3 2.2-.7 4.7.5.5.7 1.8.7 2.2z"/><path d="M99.3 78.5c-.4 2.7-1.2 5.8-2.9 7.8-.2.2-.4.3-.6.3h-5c-.2 0-.5-.1-.6-.3-1.2-1.5-2-3.5-2.5-5.6 0 0 5.8.8 9.1-.4 2.4-.9 2.5-1.8 2.5-1.8z"/></g></symbol><symbol viewBox="0 0 24 24" id="movie" xmlns="http://www.w3.org/2000/svg"><path d="M18 4l2 4h-3l-2-4h-2l2 4h-3l-2-4H8l2 4H7L5 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V4h-4z" fill="#ff9800"/></symbol><symbol viewBox="0 0 24 24" id="music" xmlns="http://www.w3.org/2000/svg"><path d="M16 9V7h-4v5.5c-.42-.31-.93-.5-1.5-.5A2.5 2.5 0 0 0 8 14.5a2.5 2.5 0 0 0 2.5 2.5 2.5 2.5 0 0 0 2.5-2.5V9h3m-4-7a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z" fill="#ef5350"/></symbol><symbol viewBox="0 0 24 24" id="mxml" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m.12 13.5l3.74 3.74 1.42-1.41-2.33-2.33 2.33-2.33-1.42-1.41-3.74 3.74m11.16 0l-3.74-3.74-1.42 1.41 2.33 2.33-2.33 2.33 1.42 1.41 3.74-3.74z" fill="#ffa726"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-actions" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#ab47bc" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-effects" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#26c6da" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#e53935" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-state" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#9ccc65" stroke-width="12.914"/></symbol><symbol viewBox="0 0 24 24" id="nim" xmlns="http://www.w3.org/2000/svg"><path d="M4.464 15.75L2.288 3.78l5.985 7.617L12.08 3.78l3.809 7.617 5.985-7.617-2.177 11.97H4.464m15.234 3.264a1.088 1.088 0 0 1-1.088 1.088H5.553a1.088 1.088 0 0 1-1.089-1.088v-1.089h15.234z" stroke-width="1.088" fill="#ffca28"/></symbol><symbol viewBox="0 0 500 500" id="nix" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-1.965 36.302)" stroke-width=".395"><path d="M135.59 415.7c0-.295-2.752-5.283-6.116-11.084-3.364-5.801-6.116-10.776-6.116-11.055s9.514-16.889 21.143-36.912c11.629-20.022 21.323-36.798 21.542-37.279.346-.76-1.608-4.363-14.896-27.466-8.412-14.625-15.294-26.785-15.294-27.023 0-.5 24.46-43.501 25.206-44.31.414-.45.592-.384 1.078.395.32.513 16.876 29.256 36.791 63.87 62.62 108.85 74.852 130.01 75.41 130.46.3.242.544.554.544.694 0 .14-11.836.21-26.302.154-23.023-.09-26.313-.175-26.393-.694-.11-.714-27.662-48.825-28.86-50.392-.746-.978-.906-1.035-1.426-.51-.688.696-28.954 49.323-29.49 50.733l-.365.96h-13.229c-10.896 0-13.229-.095-13.229-.538zm167.58-125.61c-.134-.216 1.188-2.863 2.938-5.882 6.924-11.944 84.291-145.75 96.491-166.88 7.143-12.371 13.142-22.465 13.333-22.433.363.062 25.861 43.105 25.861 43.655 0 .174-6.761 11.952-15.026 26.173-8.46 14.557-14.932 26.104-14.81 26.421.185.483 4.564.564 30.213.564h29.996l.958 1.48c.526.814 3.296 5.547 6.155 10.518 2.859 4.971 5.45 9.29 5.756 9.597.706.705.704.724-.16 1.572-.395.388-3.36 5.323-6.587 10.965-3.228 5.643-6.056 10.387-6.285 10.543-.23.156-19.695.171-43.256.034l-42.84-.249-.804 1.15c-.441.632-7.504 12.736-15.696 26.897l-14.892 25.747H339.03c-8.517 0-20.015.116-25.55.259-6.55.168-10.15.121-10.309-.135zM169.42 132.23c-56.373-.055-102.5-.182-102.5-.282 0-.1 5.617-10.132 12.481-22.294l12.481-22.112h30.332c27.113 0 30.332-.065 30.332-.611 0-.336-6.659-12.228-14.797-26.427-8.139-14.199-14.797-25.917-14.797-26.04 0-.123 2.682-4.853 5.96-10.51s6.003-10.578 6.055-10.934c.086-.586 1.376-.648 13.572-.648 7.413 0 13.463.143 13.446.317-.017.174.222.707.531 1.184.31.476 9.763 16.937 21.007 36.578 11.244 19.64 20.71 36.022 21.036 36.4.554.647 2.549.691 31.428.691h30.837l12.896 22.145c7.093 12.18 12.8 22.301 12.682 22.492-.118.19-4.776.303-10.352.249-5.575-.054-56.26-.143-112.63-.198z" fill="#5075c1"/><path d="M25.289 203.14c-6.098 10.563-6.69 11.711-6.225 12.078.283.224 3.18 5.044 6.44 10.712 3.261 5.668 6.017 10.355 6.124 10.417.106.061 13.585.153 29.95.204 16.367.052 29.994.23 30.285.399.472.273-1.08 3.094-14.637 26.574L62.06 289.793l12.907 21.865c7.1 12.026 12.982 21.906 13.068 21.956.086.05 23.257-39.831 51.492-88.624 11.352-19.617 21.214-36.64 30.37-52.442 23.308-40.452 30.68-53.468 30.73-54.132-1.097-.11-6.141-.187-13.006-.216-3.945-.01-7.82-.02-12.75-.002l-25.341.092-15.42 26.706c-14.256 24.693-15.445 26.663-16.278 26.86l-.024.037c-.011.003-1.62-.001-1.825 0-4.29.062-20.453.063-40.226-.01-22.632-.082-41.615-.125-42.183-.096-.568.03-1.147-.03-1.29-.132-.142-.102-3.29 5.066-6.996 11.485zm205.16-190.3c-.123.149 5.62 10.392 12.761 22.763 12.199 21.131 89.393 155.03 96.276 167 1.502 2.613 2.92 4.803 3.443 5.348.9-1.249 3.531-5.63 7.954-13.219a1342.88 1342.88 0 0 1 10.049-17.76l6.606-11.443c.692-1.403.754-1.818.653-2.117-.162-.48-6.904-12.332-14.982-26.337-8.078-14.005-14.824-25.849-14.991-26.32a.73.73 0 0 1-.009-.366l-.426-.913L359.42 72.5c3.69-6.307 6.425-11.042 9.47-16.29 9.159-15.948 12.037-21.189 11.896-21.55-.126-.324-2.7-4.83-5.72-10.017-3.021-5.185-5.845-10.148-6.275-11.026-.483-.987-.734-1.364-1.1-1.456-.054.014-.083.018-.145.035-.42.112-5.454.195-11.189.185-5.734-.01-11.22.024-12.188.073l-1.76.089-14.997 25.978c-12.824 22.212-15.084 25.964-15.595 25.883-.024-.004-.15-.189-.235-.301-.109.066-.2.09-.272.05-.255-.148-7.143-11.902-15.306-26.119l-14.36-25.016c-.115-.186-.444-.744-.457-.752-.477-.275-50.502.287-50.737.57zm-18.646 283.09c-.047.109-.026.262.042.48.329 1.05 25.338 43.735 25.772 43.985.207.119 14.178.239 31.05.266 26.651.044 30.75.152 31.234.832.308.43 9.988 17.214 21.513 37.296s21.152 36.627 21.394 36.767c.242.14 5.927.243 12.633.23 6.706-.013 12.401.099 12.657.246.132.076.382-.141.852-.795l6.008-10.406c5.234-9.065 6.62-11.684 6.294-11.888-.575-.36-15.597-26.643-23.859-41.482-3.09-5.45-5.37-9.516-5.441-9.774-.195-.712-.065-.822 1.156-.98 1.956-.252 57.397-.057 58.07.205.238.092.79-.569 2.594-3.497 1.866-3.067 5.03-8.524 11-18.866 7.22-12.505 13.044-22.784 12.942-22.843-.102-.059-.771-.051-1.489.016l-.046.001c-4.452.204-33.918.203-149.74.025-38.96-.06-69.786-.09-71.912-.072-1.121.01-2.095.076-2.66.172a.25.25 0 0 0-.062.083z" fill="#7db7e1"/></g></symbol><symbol viewBox="0 0 24 24" id="nodejs" xmlns="http://www.w3.org/2000/svg"><path d="M12 1.85c-.27 0-.55.07-.78.2l-7.44 4.3c-.48.28-.78.8-.78 1.36v8.58c0 .56.3 1.08.78 1.36l1.95 1.12c.95.46 1.27.47 1.71.47 1.4 0 2.21-.85 2.21-2.33V8.44c0-.12-.1-.22-.22-.22H8.5c-.13 0-.23.1-.23.22v8.47c0 .66-.68 1.31-1.77.76L4.45 16.5a.26.26 0 0 1-.11-.21V7.71c0-.09.04-.17.11-.21l7.44-4.29c.06-.04.16-.04.22 0l7.44 4.29c.07.04.11.12.11.21v8.58c0 .08-.04.16-.11.21l-7.44 4.29c-.06.04-.16.04-.23 0L10 19.65c-.08-.03-.16-.04-.21-.01-.53.3-.63.36-1.12.51-.12.04-.31.11.07.32l2.48 1.47c.24.14.5.21.78.21s.54-.07.78-.21l7.44-4.29c.48-.28.78-.8.78-1.36V7.71c0-.56-.3-1.08-.78-1.36l-7.44-4.3c-.23-.13-.5-.2-.78-.2M14 8c-2.12 0-3.39.89-3.39 2.39 0 1.61 1.26 2.08 3.3 2.28 2.43.24 2.62.6 2.62 1.08 0 .83-.67 1.18-2.23 1.18-1.98 0-2.4-.49-2.55-1.47a.226.226 0 0 0-.22-.18h-.96c-.12 0-.21.09-.21.22 0 1.24.68 2.74 3.94 2.74 2.35 0 3.7-.93 3.7-2.55 0-1.61-1.08-2.03-3.37-2.34-2.31-.3-2.54-.46-2.54-1 0-.45.2-1.05 1.91-1.05 1.5 0 2.09.33 2.32 1.36.02.1.11.17.21.17h.97c.05 0 .11-.02.15-.07.04-.04.07-.1.05-.16C17.56 8.82 16.38 8 14 8z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 300 300" id="nodemon" xmlns="http://www.w3.org/2000/svg"><title>nodemon</title><path d="M149.868 20.62c-2.124 0-4.25.55-6.154 1.648L41.899 81.083a12.306 12.306 0 0 0-6.15 10.652v117.633a12.29 12.29 0 0 0 6.152 10.646l101.815 58.766h.001a12.282 12.282 0 0 0 12.291 0l101.84-58.766a12.29 12.29 0 0 0 6.153-10.652V91.738a12.31 12.31 0 0 0-6.146-10.652L156.015 22.27a12.302 12.302 0 0 0-6.153-1.648zM83.303 70.93s11.789 33.031 35.477 31.934l27.74-15.961a7.348 7.348 0 0 1 3.414-.99h.641a7.233 7.233 0 0 1 3.404.99l27.738 15.961c23.69 1.094 35.475-31.934 35.475-31.934 5.233 23.154 1.06 38.641-5.924 48.942l4.541 2.614h.002c2.321 1.327 3.734 3.795 3.737 6.49l-.12 95.811a3.724 3.724 0 0 1-1.855 3.227 3.624 3.624 0 0 1-3.735 0L177.1 206.971c-2.311-1.363-3.742-3.818-3.742-6.48v-44.763a7.44 7.44 0 0 0-3.737-6.465l-15.642-9.01a7.28 7.28 0 0 0-3.715-1.01 7.378 7.378 0 0 0-3.742 1.01l-15.648 9.01c-2.316 1.323-3.729 3.798-3.729 6.467v44.762c0 2.663-1.413 5.1-3.738 6.48l-36.748 21.041a3.571 3.571 0 0 1-3.71 0c-1.173-.65-1.864-1.887-1.864-3.224l-.137-95.812a7.483 7.483 0 0 1 3.74-6.49l4.541-2.615c-6.982-10.302-11.16-25.79-5.925-48.942z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 990 990" id="npm" xmlns="http://www.w3.org/2000/svg"><defs><style>.hncls-1{fill:#cb3837}.cls-2{fill:#fff}</style></defs><title>n</title><path class="hncls-1" d="M113.26 876.74V113.27h763.47v763.47zm143.59-620.4v476.18h240.61V355.63h140.21v376.96h95.457V256.34z" fill="#e53935" stroke-width=".771"/></symbol><symbol id="nunjucks" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.host0{fill:#388e3c}</style><path class="host0" d="M11.2 21.1H8.1l-2.3-7.9v7.9H2.7V2.9h3.1l2.3 7.4V2.9h3.1zM21.3 19.2c0 1-.8 1.9-1.9 1.9h-4.8c-1 0-1.9-.8-1.9-1.9v-3.8l3.2-.7V18h2.3V7.2h3.1v12z"/></symbol><symbol viewBox="0 0 150 150.00001" id="ocaml" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.76136 0 0 .76136 11.616 19.98)"><path d="M83.02 101.645l.023-.062c-.035-.159-.047-.195-.024.062z" fill="none" stroke-width="1.028"/><linearGradient id="hpa" gradientUnits="userSpaceOnUse" x1="-696.735" y1="97.7" x2="-696.735" y2="142.997" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M82.313 138.79c-.471-1.004-1.904-3.621-2.624-4.46-1.562-1.828-1.927-1.966-2.386-4.275-.799-4.02-2.913-11.31-5.405-16.341-1.286-2.596-3.426-4.777-5.385-6.66-1.71-1.652-5.565-4.431-6.237-4.294-6.296 1.257-8.249 7.432-11.21 12.323-1.638 2.705-3.374 5.007-4.665 7.885-1.192 2.646-1.087 5.577-3.128 7.849-2.093 2.333-3.454 4.814-4.48 7.829-.194.574-.747 6.596-1.348 8.015l9.357-.659c8.719.594 6.2 3.936 19.81 3.208l21.487-.665c-.666-1.97-1.584-4.25-1.938-4.991-.599-1.248-1.352-3.69-1.848-4.763z" fill="url(#hpa)" stroke-width="1.028"/><linearGradient id="hpb" gradientUnits="userSpaceOnUse" x1="-666.972" y1="142.12" x2="-666.972" y2="142.12" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><linearGradient id="hpc" gradientUnits="userSpaceOnUse" x1="-675.228" y1="-1.28" x2="-675.228" y2="142.967" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M109.553 94.296c-1.652 1.193-4.88 4.06-11.902 5.145-3.152.487-6.1.527-9.335.365-1.584-.076-3.077-.157-4.665-.177-.936-.008-4.074-.107-3.919.193l-.349.871c.054.287.169 1.004.2 1.177.129.704.165 1.265.192 1.912.048 1.331-.11 2.719-.043 4.062.141 2.787 1.175 5.326 1.306 8.137.143 3.13 1.69 6.442 3.188 8.998.569.973 1.434 1.084 1.811 2.283.442 1.373.024 2.83.239 4.293.842 5.675 2.477 11.606 5.032 16.728.018.043.038.09.06.128 3.156-.53 6.318-1.665 10.418-2.271 7.517-1.115 17.972-.54 24.688-1.17 16.993-1.597 26.216 6.97 41.478 3.459V22.459c0-11.84-9.594-21.438-21.435-21.438H19.239C7.4 1.021-2.197 10.62-2.197 22.458v46.774c3.067-1.11 7.479-7.635 8.861-9.222 2.419-2.775 2.858-6.315 4.062-8.544 2.743-5.078 3.215-8.57 9.451-8.57 2.907 0 4.061.67 6.027 3.31 1.368 1.834 3.731 5.224 4.837 7.49 1.277 2.615 3.357 6.153 4.272 6.867.677.53 1.35.928 1.976 1.163 1.012.38 1.848-.316 2.525-.855.863-.687 1.235-2.088 2.035-3.957 1.152-2.696 2.408-5.926 3.122-7.054 1.237-1.949 1.658-4.261 2.993-5.381 1.97-1.652 4.54-1.768 5.246-1.908 3.957-.781 5.755 1.906 7.704 3.645 1.276 1.138 3.019 3.432 4.256 6.507.967 2.4 2.199 4.622 2.714 6.008.497 1.339 1.725 3.484 2.453 6.055.661 2.336 2.43 4.125 3.102 5.235 0 0 1.029 2.882 7.285 5.516 1.357.572 4.1 1.501 5.736 2.096 2.718.988 5.351.86 8.704.458 2.391 0 3.686-3.462 4.772-6.234.643-1.639 1.259-6.334 1.678-7.667.406-1.297-.544-2.3.265-3.437.946-1.327 1.508-1.399 2.054-3.129 1.172-3.704 7.95-3.89 11.761-3.89 3.176 0 2.772 3.083 8.16 2.028 3.086-.605 6.059.398 9.335 1.265 2.758.732 5.352 1.566 6.906 3.385 1.005 1.178 3.5 7.08.958 7.331.244.3.423.84.88 1.135-.566 2.226-3.03.64-4.4.355-1.845-.383-3.147.057-4.952.856-3.085 1.374-7.598 1.214-10.286 3.452-2.281 1.898-2.277 6.133-3.34 8.507-.002-.001-2.955 7.6-9.402 12.248z" fill="url(#hpc)" stroke-width="1.028"/><linearGradient id="hpd" gradientUnits="userSpaceOnUse" x1="-735.137" y1="90.833" x2="-735.137" y2="141.967" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M38.247 105.09c-1.467-.15-2.83-.317-4.256-.605-2.662-.536-5.57-1.06-8.193-1.688-1.592-.385-6.895-2.263-8.048-2.792-2.702-1.246-4.496-4.63-6.609-4.282-1.348.22-2.662.682-3.5 2.042-.685 1.11-.917 3.016-1.391 4.294-.55 1.485-1.5 2.87-2.331 4.284-1.53 2.595-4.282 4.941-5.468 7.469-.239.52-.45 1.101-.649 1.708V144.415a48.57 48.57 0 0 1 4.45.96c11.955 3.19 14.872 3.46 26.598 2.119l1.1-.146c.897-1.867 1.59-8.227 2.171-10.195.454-1.51 1.077-2.712 1.313-4.253.223-1.463-.02-2.858-.146-4.188-.329-3.332 2.427-4.522 3.742-7.384 1.186-2.589 1.871-5.535 2.853-8.181.941-2.54 2.41-6.13 4.918-7.408-.305-.355-5.237-.518-6.554-.65z" fill="url(#hpd)" stroke-width="1.028"/></g></symbol><symbol viewBox="0 0 24 24" id="pdf" xmlns="http://www.w3.org/2000/svg"><path d="M14 9h5.5L14 3.5V9M7 2h8l6 6v12a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m4.93 10.44c.41.9.93 1.64 1.53 2.15l.41.32c-.87.16-2.07.44-3.34.93l-.11.04.5-1.04c.45-.87.78-1.66 1.01-2.4m6.48 3.81c.18-.18.27-.41.28-.66.03-.2-.02-.39-.12-.55-.29-.47-1.04-.69-2.28-.69l-1.29.07-.87-.58c-.63-.52-1.2-1.43-1.6-2.56l.04-.14c.33-1.33.64-2.94-.02-3.6a.853.853 0 0 0-.61-.24h-.24c-.37 0-.7.39-.79.77-.37 1.33-.15 2.06.22 3.27v.01c-.25.88-.57 1.9-1.08 2.93l-.96 1.8-.89.49c-1.2.75-1.77 1.59-1.88 2.12-.04.19-.02.36.05.54l.03.05.48.31.44.11c.81 0 1.73-.95 2.97-3.07l.18-.07c1.03-.33 2.31-.56 4.03-.75 1.03.51 2.24.74 3 .74.44 0 .74-.11.91-.3m-.41-.71l.09.11c-.01.1-.04.11-.09.13h-.04l-.19.02c-.46 0-1.17-.19-1.9-.51.09-.1.13-.1.23-.1 1.4 0 1.8.25 1.9.35M8.83 17c-.65 1.19-1.24 1.85-1.69 2 .05-.38.5-1.04 1.21-1.69l.48-.31m3.02-6.91c-.23-.9-.24-1.63-.07-2.05l.07-.12.15.05c.17.24.19.56.09 1.1l-.03.16-.16.82-.05.04z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="perl" xmlns="http://www.w3.org/2000/svg"><path d="M12 14c-1 0-3 1-3 2 0 2 3 2 3 2v-1a1 1 0 0 1-1-1 1 1 0 0 1 1-1v-1m0 5s-4-.5-4-2.5c0-3 3-3.75 4-3.75V11.5c-1 0-5 1.5-5 4.5 0 4 5 4 5 4v-1M10.07 7.03l1.19.53c.43-2.44 1.58-4.06 1.58-4.06-.43 1.03-.71 1.88-.89 2.55C13.16 3.55 15.61 2 15.61 2a15.916 15.916 0 0 0-2.64 3.53c1.58-1.68 3.77-2.78 3.77-2.78-2.69 1.72-3.9 4.45-4.2 5.21l.55.08c0 .52 0 1 .25 1.38C14.1 11.31 18 11.47 18 16s-4.03 6-6.17 6C9.69 22 5 21.03 5 16s4.95-5.07 5.83-7.08c.12-.38-.76-1.89-.76-1.89z" fill="#9575cd"/></symbol><symbol viewBox="0 0 24 24" id="php" xmlns="http://www.w3.org/2000/svg"><path d="M12 18.08c-6.63 0-12-2.72-12-6.08s5.37-6.08 12-6.08S24 8.64 24 12s-5.37 6.08-12 6.08m-5.19-7.95c.54 0 .91.1 1.09.31.18.2.22.56.13 1.03-.1.53-.29.87-.58 1.09-.28.22-.71.33-1.29.33h-.87l.53-2.76h.99m-3.5 5.55h1.44l.34-1.75h1.23c.54 0 .98-.06 1.33-.17.35-.12.67-.31.96-.58.24-.22.43-.46.58-.73.15-.26.26-.56.31-.88.16-.78.05-1.39-.33-1.82-.39-.44-.99-.65-1.82-.65H4.59l-1.28 6.58m7.25-8.33l-1.28 6.58h1.42l.74-3.77h1.14c.36 0 .6.06.71.18.11.12.13.34.07.66l-.57 2.93h1.45l.59-3.07c.13-.62.03-1.07-.27-1.36-.3-.27-.85-.4-1.65-.4h-1.27L12 7.35h-1.44M18 10.13c.55 0 .91.1 1.09.31.18.2.22.56.13 1.03-.1.53-.29.87-.57 1.09-.29.22-.72.33-1.3.33h-.85l.5-2.76h1m-3.5 5.55h1.44l.34-1.75h1.22c.55 0 1-.06 1.35-.17.35-.12.65-.31.95-.58.24-.22.44-.46.58-.73.15-.26.26-.56.32-.88.15-.78.04-1.39-.34-1.82-.36-.44-.99-.65-1.82-.65h-2.75l-1.29 6.58z" fill="#1E88E5"/></symbol><symbol viewBox="0 0 79 78" id="postcss" xmlns="http://www.w3.org/2000/svg"><title>postcss-logo-symbol</title><g transform="translate(5.48 5.52) scale(.85425)" fill="#e53935" fill-rule="evenodd" stroke="#e53935" stroke-width="1.519"><path d="M15.447 32.623c.106.08.29.132.106.29-.132.184-.29.342-.395.553-.105.185-.184.237-.342.106.21-.343.42-.66.63-.95zM68.342 60.24c0 .078.026.13.026.21.053-.105.053-.158.08-.21zm0 .236v-.026zm-5.368 10.277l-4.58-25.402c-.078-.025-.183-.077-.368-.13.053.105.08.184.106.263.13-.026.184-.026.236-.052 0-.026 0-.052.027-.08l4.58 25.404zm-4.737-31.12c-.026.078-.026.158-.026.237 0-.08 0-.16.028-.238zm.026.526c-.026 0-.026 0-.052-.028v.026c.028.026.028.026.054 0zm-.052.21v-.185c-.077.026-.156.026-.262.053.132.05.264.078.264.13z"/><path d="M78.71 33.967c-.052-1.028-.078-2.056-.184-3.083-.184-1.397-.368-2.82-.684-4.19-.237-1.133-.63-2.214-1.026-3.294-.5-1.265-1-2.556-1.632-3.768-1.026-1.95-2.368-3.69-3.605-5.508-.818-1.16-1.87-2.108-2.66-3.294-.447-.685-1.105-1.264-1.763-1.79-1.053-.845-2.158-1.61-3.263-2.347a32.525 32.525 0 0 0-2.58-1.634c-.71-.397-1.473-.713-2.21-1.056-.842-.395-1.658-.87-2.605-1.054-.238-.05-.448-.13-.685-.21-.605-.21-1.184-.447-1.79-.632-.92-.29-1.815-.632-2.763-.87C50.342 1 49.394.843 48.446.71 47.394.555 46.316.5 45.262.397a26.83 26.83 0 0 0-2.026-.184C42.236.16 41.21.16 40.21.134c-.5-.027-1.026-.08-1.526-.053-.763.026-1.526.105-2.29.21-.736.08-1.473.21-2.183.317-.867.105-1.735.158-2.604.264-.816.106-1.658.264-2.473.396-.29.053-.58.158-.87.21-.63.132-1.288.185-1.92.396-1.13.344-2.263.74-3.368 1.16-1.027.422-2.027.87-3 1.397-1 .552-1.948 1.21-2.895 1.844a45.325 45.325 0 0 0-2.66 1.923c-.84.66-1.63 1.397-2.394 2.135-.42.42-.763.922-1.158 1.396-.657.765-1.315 1.502-1.947 2.293-.524.66-1 1.344-1.5 2.03-.893 1.21-1.656 2.502-2.366 3.794-.29.527-.553 1.054-.816 1.58-.395.79-.816 1.555-1.184 2.372-.264.554-.474 1.16-.632 1.766-.367 1.292-.736 2.61-1.078 3.9-.316 1.16-.395 2.372-.42 3.558-.027 1.054.078 2.082.183 3.136.027.264-.13.58.184.79-.105.29-.026.45.13.5-.182.29.08.476-.024.74-.027.052.08.157.13.236 0 .08-.025.185 0 .264.028.237.133.474.133.738 0 .184.157.395.21.58.026.078 0 .21-.053.263-.158.184-.132.342.105.448.133.342.08.5.054.66.052.236-.027.315 0 .368.21.422.29.896.315 1.37 0 .106.053.212.106.343.026 0 0 .5 0 .5.13-.078.237-.104.368-.157.08.342.158.66.263.95.132.21.132.314.08.34.105.474.157.922.34 1.37 0-.5-.05-1-.13-1.475.368.132.684.263.895.263.027-.08.053-.184.08-.237-.158-.157-.29-.394-.448-.552.053.21 0 .29 0 .37-.105-.054-.237-.107-.368-.16.105-.13.21-.263.368-.42 0-.238-.13-.45-.5-.423.158-.052.316-.13.5-.184.29-.157-.026-.447-.026-.816.026-.447-.237-.895-.316-1.37-.132-.737-.105-1.844-.184-2.582-.158-.132-.29.21-.316.237.08.632.158 1.264.21 1.897-.157-.527-.263-1.107-.394-1.74-.027.185-.053.264-.053.37-.13.13-.026.29.053.474-.184-.08-.395-.052-.395-.052v.738c-.262-.264-.34-.474-.473-.66-.052-.21-.08-.42-.13-.63.05-.133 0-.212 0-.29a15.968 15.968 0 0 1-.08-.634c.026-.026-.026-.42-.026-.42.21.025.343.05.474.05-.263-.34-.08-.552.027-.763.053-.106.237-.13.29-.238.21-.395.553-.71.553-1.212 0-.237.08-.5.105-.738.053-.448.105-.896.13-1.344.054-.58 0-1.16.133-1.713.212-.92.475-1.843.764-2.766.21-.66.448-1.29.71-1.95.395-1.028.764-2.056 1.264-3.03.71-1.424 1.526-2.794 2.316-4.19.5-.87 1.026-1.687 1.58-2.53.525-.817 1.05-1.66 1.657-2.425a21.452 21.452 0 0 1 2.79-2.978c1.053-.948 2.053-1.923 3.184-2.793a32.218 32.218 0 0 1 4.685-3.005c1.343-.71 2.737-1.266 4.132-1.793.895-.342 1.868-.5 2.79-.79 1.052-.343 2.105-.5 3.21-.527.71-.027 1.395-.106 2.105-.185.632-.05 1.263-.104 1.948-.183-.08.105-.106.158-.132.21-.288.422-.604.844-.894 1.265-.237.343-.5.712-.737 1.054-.422.555-.87 1.108-1.264 1.688-.605.87-1.158 1.766-1.79 2.635-.63.843-1.315 1.634-1.973 2.45-.868 1.134-1.684 2.293-2.552 3.426-.79 1.08-1.63 2.11-2.394 3.19-.684.947-1.29 1.95-1.948 2.923-.973 1.45-1.947 2.872-2.92 4.322a271.93 271.93 0 0 1-2.316 3.294c-.053.08-.132.104-.21.157-.21.342-.21.527-.29.685-.21.395-.42.79-.658 1.16-.132.21-.316.394-.474.605-.026-.316.42-.474.21-.87-.13.212-.263.396-.394.607l-.316.63c.105.08.29.133.105.29-.08.133-.158.29-.237.423a.954.954 0 0 0 .29-.264c0 .29-.158.526-.29.763-.105.21-.368.37-.552.527.026.027.21.106.237.132.237-.08.316-.21.343-.132.08-.105.158-.184.184-.263.104-.264.262-.474.525-.58.106-.053.184-.132.263-.21.79-.818 1.606-1.608 2.316-2.478 1.106-1.345 2.106-2.74 3.16-4.11.446-.58.973-1.16 1.446-1.714.078.606.026 1.185 0 1.74-.08.974-.132 1.95-.21 2.95-.027.395 0 .79-.027 1.186 0 .105-.08.184-.08.29 0 .263.08.553.08.817-.08.975-.186 1.923-.265 2.898-.027.21.078.422.13.607-.13 1.422.16 2.925-.078 4.427.184-.29.237-.474.237-.658.025-.158 0-.316 0-.5v-.264c.025-.475.13-.975.078-1.45-.053-.527-.053-1.027.053-1.528.053-.21-.026-.474.106-.738v.395c-.026 1.5.027 3.003-.183 4.505-.027.132.08.37-.21.343-.238.474.052.817-.21 1.08-.054.053.05.29.077.448-.106.317-.106.317.052.343.026.58.08 1.106.105 1.66.42-1 .21-2.03.396-3.058.026.422.053.844.026 1.29 0 .687-.026 1.345-.052 2.03 0 .132-.027.264-.053.396-.08.37-.105.738-.237 1.08-.105.264-.052.66-.052.975v1.003c.105.448-.027.685.052.948-.08.265-.105.344-.08.423l.08.395c.527-.053.29.343.5.553-.158.212-.105.29-.105.397 0 .237-.025.448-.052.685 0 .606-.026 1.212-.026 1.792 0 .08.026.157.026.236 0 .054-.026.74-.026.74.053.078 0 .157-.08.236-.025 0-.104-3.347-.104-3.347h-.395c-.052 1.58.08 3.003-.21 4.48-.316.025-.42.078-.764.078-.816 0-1.632 0-2.448.026-.974 0-1.92.026-2.895.026-.472 0-.972.054-1.446.054-.632 0-1.29-.08-1.92-.08-.975 0-1.922.08-2.896.106-.71.026-1.42.026-2.13.053-.475.025-.95.05-1.422.104-.21.026-.395.105-.658.184-.08 0-.263-.026-.42 0-.265.053-.5.21-.765.264-.395.08-.5.184-.448.58v.263c-.026.052.58-.08.58-.08-.054 0-.08.158-.16.29.212-.08.343-.132.475-.184.395.185.737.08 1.052.16 1.026.262 2.078.37 3.13.473.685.053 1.343.08 2.027.105.973.053 1.947.106 2.92.106.816 0 1.606-.08 2.42-.08 1.13 0 2.264.052 3.395.08.237 0 .5-.028.763-.028h1.92c1.712-.052 3.422-.08 5.133-.13.975-.028 1.975-.08 2.948-.107l3-.08c1.158-.026 2.316-.026 3.448-.05.868 0 1.71-.03 2.58-.055.972-.026 1.972-.105 2.946-.157.527-.027 1.054-.08 1.58-.132.632-.052 1.29-.13 1.92-.157.948-.054 1.922-.08 2.87-.133 1.184-.078 2.368-.183 3.578-.21 1.106-.052 2.237-.026 3.343-.052.974-.027 1.948-.08 2.948-.106l1.66-.08s1.104-.026 1.657-.08c.947-.052 1.894-.157 2.842-.183.604-.027 1.21 0 1.815-.027.973-.026 1.973-.08 2.947-.08.367 0 .762.054 1.236.08-.21.185-.342.29-.5.422.105.026.21.08.316.132a.71.71 0 0 1-.42.13c-.054.133-.107.186-.16.45h.474c-.184 0-.342.237-.526.395-.21-.054-.395 0-.5.29.184.104.158.183.132.29-.316.104-.553.21-.42.552-.107.052-.238.105-.37.184-.13.21-.368.263-.316.553.106.025.21.08.29.104-.132.053-.263.132-.395.184-.473.29-.262.422-.157.554-.08.053-.158.105-.237.132.052.237.13.29.157.29a9.3 9.3 0 0 0-.395.316c-.08.237-.185.342-.29.5s-.158.37-.29.527c-.552.607-.947 1.32-1.657 1.793-.264.185-.5.422-.737.66-.474.447-.895.948-1.395 1.37a29.595 29.595 0 0 1-2.052 1.554 151.56 151.56 0 0 1-2.604 1.792c-.474.315-1 .552-1.5.842s-.974.554-1.474.843c-.316.21-.606.5-.948.66-.868.37-1.79.685-2.684 1.028-.87.37-1.5.685-2.158.922-.605.21-1.237.37-1.868.5-.21.054-.448 0-.685.027-.448.08-.895.186-1.343.238-1.158.158-2.316.264-3.473.422-.685.08-1.343.21-2.027.29-.473.026-.973-.026-1.447-.026-.342 0-.71.08-1.053.027-.552-.08-1.105-.21-1.658-.316-.13-.026-.316-.08-.42-.026-.21.106-.396-.052-.607 0-.13.027-.262-.08-.394-.08-.106-.025-.238.028-.37 0-.29-.078-.552-.183-.87-.157-.313.026-.63-.132-.97-.21-.475-.106-.92-.21-1.396-.317a2.38 2.38 0 0 1-.525-.237c-.685 0-1.133-.026-1.554-.185-.368-.13-.71-.315-1.105-.262-.104.026-.183-.026-.29-.026-.08-.106-.157-.317-.235-.317-.526.027-.842-.42-1.29-.553-.236-.08-.42-.343-.657-.422-.58-.237-1.052-.737-1.71-.816-.21-.027-.42-.132-.658-.21.08.104.13.183.21.262-.763-.37-1.473-.79-2.184-1.186-.104-.026-.183-.13-.262-.184l-.71-.474c-.395.08-.553-.08-.66-.132-.71-.5-1.525-.817-2.21-1.37-.29-.238-.63-.396-.84-.686-.37-.448-.817-.764-1.317-1.027-.394-.21-.762-.448-1.13-.685-.185-.132-.37-.29-.37-.58 0-.185-.078-.37-.315-.264-.105-.158-.21-.342-.342-.395-.316-.13-.526-.37-.763-.58s-.42-.5-.71-.605c-.527-.21-.843-.658-1.158-1.027-.738-.87-1.396-1.82-2.08-2.74-.053-.08-.158-.133-.237-.212.105.29.237.527.368.79-.262-.105-.446-.29-.604-.474-.027.027 1.815 3.057 1.815 3.057.16.237.29.475.448.712a.813.813 0 0 1-.79-.422c-.236-.42-.5-.684-1.026-.63a4.588 4.588 0 0 1-.13-.58c-.107 0-.185 0-.37-.027.37.58.685 1.08 1.027 1.66-.133-.08-.21-.132-.265-.158.473.5.815 1.133 1.42 1.45.132.605.816.895.974 1.475-.13-.027-.238-.053-.37-.08-.21-.263-.447-.526-.683-.816.052.184.13.342.236.474.316.395.606.79.974 1.133.132.134.316.187.316.424.21.105.29.13.368.13.054.16-.025.397.29.344.21.395.42.395.71.264.343.343.528.37.764.16 0 .13.026.262.026.368.105-.053.08-.132.08-.264.13.105.21.158.262.21.263.37.5.712.868 1.002.5.422.948.87 1.42 1.265.922.765 1.95 1.398 2.975 1.977 1.264.712 2.475 1.476 3.764 2.16 1.552.818 3.21 1.372 4.92 1.767.632.132 1.237.263 1.87.42.55.16 1.104.397 1.657.528.842.185 1.71.343 2.552.5.183.027.37.054.58.08.235.053.524-.053.577.027.132.21.237.104.395.078.184-.053.395-.053.605-.053.737.026 1.447.184 2.184.132.16 0 .396-.133.528.13.236-.105.368-.105.473-.13.028.236 0 .236-.05.262-.054.026-.133.053-.238.132.947.184 1.842.21 2.63 0 1.37.105 2.554-.053 3.686-.448.105.132.184.316.342.053.052-.08.184-.107.29-.133.236-.053.526-.158.736-.08.238.08.317-.13.5-.13.317 0 .606-.027.896-.08.158-.026.316-.105.5-.158a1.285 1.285 0 0 0-.58-.133c.317-.158.606-.29.896-.42-.053.078-.106.183-.21.183h.367c-.08 0-.185.237-.316.395.946-.237 1.814-.448 2.657-.66-.29-.552.315-.367.526-.684-.263.08-.526.158-.79.21.895-.447 1.816-.842 2.71-1.237-.13.158-.29.237-.525.37.158.025.263.025.342.05.42.133.316-.262.447-.5.5 0 .71-.078.947-.158.263-.08.526-.158.79-.263.42-.184.815-.42 1.236-.63.08-.028.21 0 .316 0 .29-.186.394-.344.473-.318.37.053.63-.08.736-.42.184-.133.316-.238.447-.318.578-.316 1.13-.632 1.71-.948.21 0 .316 0 .368-.027.344-.16.66-.342.975-.527a2.258 2.258 0 0 1-.263-.13c.262-.054.34-.08.5-.133.63-.74 1.5-1.24 2.157-1.82.29-.026.29-.105.29-.157.104-.132.21-.29.34-.396.58-.527 1.21-.975 1.737-1.528a37.16 37.16 0 0 0 2.184-2.374c.63-.738 1.264-1.475 1.79-2.292.737-1.133 1.368-2.293 2.026-3.48.474-.842.895-1.685 1.37-2.528.05-.08.157-.185.236-.185.71-.08 1.422-.13 2.106-.21.158-.026.342-.13.5-.21-.08-.132-.132-.29-.21-.422-.106-.16-.264-.29-.37-.45-.104-.13-.183-.29-.262-.447-.08-.13-.158-.236-.237-.37a9.7 9.7 0 0 1-.45-.894c-.026-.08-.08-.21-.052-.29.474-1.027.658-2.134 1.105-3.162.447-1.054.58-2.24.79-3.373.184-1.08.29-2.16.42-3.24.08-.764.185-1.502.21-2.266.16-1.212.106-2.346.08-3.48-.026-1-.08-2.028-.13-3.03zM12.685 66.405c-.184-.21-.342-.448-.526-.658l.08-.08c.287.317.577.633.866.976-.158-.08-.342-.132-.42-.238zm.42.238c.08-.027.16-.027.238-.053.08.132.132.29.21.448-.368-.027-.552-.185-.447-.395zm27.37 10.883v-.08c.5-.052.973-.105 1.473-.157v.077c-.5.08-.973.13-1.473.158zm6.63-.685c-.367.08-.762.133-1.13.186-.132.026-.29.158-.342-.08-.053.027-.106.027-.158.054.13.394.447.078.71.236-.58.08-1.13.132-1.684.21v-.052c.16-.026.343-.053.5-.08v-.078a7.743 7.743 0 0 0-.79-.053c-.077 0-.183.106-.262.132-.105.026-.21.053-.342.053-.447.026-.894.026-1.316.052-.027 0-.08-.026-.106-.026v-.08c1.763-.236 3.5-.473 5.263-.71.027.052.027.105.053.157-.158 0-.263.055-.395.08zm.396-.262c.606-.08 1.16-.132 1.738-.21-1.21.342-1.605.394-1.737.21zM24.58 23.374c.84-1.16 1.71-2.32 2.552-3.505.263-.345.473-.714.736-1.056.08-.106.185-.158.316-.264l-.026-.05c.105-.133.21-.24.263-.344.134-.21.213-.448.318-.685a.385.385 0 0 1 .105-.103c.37.184.37-.21.5-.343.237-.264.474-.553.684-.817.158-.21.316-.395.448-.632.026-.08-.053-.21-.08-.317h-.078c.08-.052.158-.13.237-.184.026 0 .026 0 .052-.026.158-.238.316-.475.474-.686.315-.42.657-.842 1.025-1.21-.052.13-.105.263-.158.368.027 0 .027.027.053.027.316-.422.658-.817.974-1.24-.027-.025-.053-.052-.08-.052-.13.132-.236.264-.368.396-.026-.027-.052-.053-.08-.053.265-.343.528-.685.79-1.08.053.08.106.184.21.395.107-.263.212-.447.29-.632-.078.08-.183.158-.262.238l-.08-.08.474-.71c.5-.712 1-1.45 1.5-2.162.185-.263.42-.474.58-.738.5-1 1.29-1.792 1.894-2.714.132-.184.316-.342.474-.5.13-.16.237-.106.342.026.71.896 1.42 1.818 2.13 2.714.528.66 1.054 1.29 1.554 1.976.605.844 1.184 1.687 1.79 2.53.684.975 1.368 1.95 2.026 2.95 1 1.477 1.947 2.953 2.947 4.428.737 1.08 1.474 2.135 2.184 3.215h-1.344c-1.236-.025-2.5-.13-3.736-.078-1.684.08-3.394.264-5.078.396-2.132.185-4.29.21-6.42.21-.765 0-1.528.107-2.29.16-.922.052-1.817.105-2.738.13-1.08.054-2.13.08-3.21.107-.606.026-1.237 0-1.895 0zm30.183 12.12v.238c-.026 0-.052.027-.105.027-.105-.37-.21-.766-.342-1.135-.263-.765-.553-1.53-1.027-2.214-.528-.737-1-1.5-1.528-2.265-.13-.185-.316-.343-.474-.5-.553-.607-1.106-1.24-1.816-1.687a21.485 21.485 0 0 0-3.29-1.688 7.374 7.374 0 0 1-.92-.474h.63l4.5-.08c.974-.025 1.922-.025 2.895-.078.236 0 .368.08.5.29.236.395.473.79.736 1.186.027.052.08.13.08.21 0 .58 0 1.186.026 1.766.025.606.08 1.186.104 1.792 0 .606-.053 1.238-.026 1.87.027.897.053 1.82.053 2.74zM26.447 26.67c1.237-.053 2.42-.132 3.632-.185.945-.053 1.92-.08 2.866-.132.395-.025.764-.05 1.158 0-.42.212-.842.423-1.21.686-.474.316-.92.737-1.395 1.08-.475.342-.896.764-1.29 1.212-.5.605-1.053 1.132-1.58 1.712-.37.422-.79.817-1.105 1.265-.447.58-.842 1.21-1.263 1.87.132-2.504.29-4.98.184-7.51zm17.185 25.35c-.843.21-1.71.448-2.58.553-.736.106-1.5.08-2.263.08a25.42 25.42 0 0 1-2.028-.08c-.763-.078-1.526-.157-2.263-.5-.633-.29-1.29-.553-1.92-.87-.634-.316-1.265-.684-1.74-1.264-.34-.423-.815-.765-1.236-1.134.08.316.263.58.553.764-.132.158-.316.08-.58-.343-.078.053-.157.08-.21.106.08-.185.158-.37.237-.527-.105-.21-.237-.448-.342-.66-.21-.342-.42-.71-.605-1.053-.053-.08-.053-.158-.105-.237a5.893 5.893 0 0 1-.37-.475c-.21-.315-.394-.657-.657-.974 0 .08.027.158.027.264-.027 0-.053.026-.053.026l-.554-1.344c-.026 0-.026 0-.052.026l.473 1.74c-.026 0-.052.025-.08.025-.077-.104-.156-.21-.21-.34-.052-.212-.21-.212-.34-.133-.08.053-.133.237-.106.316.185.448.395.896.606 1.344.052.158.105.29.184.448.027.053.106.105.106.184.106.21.185.42.316.606.237.316.5.632.737.948.235.316.445.66.656.975.026.053.105.053.13.08.133.395.58.684.896.526.08.606.737.817 1 1.397a11.957 11.957 0 0 1-.763-.343c-.027.026-.027.052-.054.105.316.158.632.316.92.5.265.16.528.317.765.5.316.29.685.45 1.13.554a.282.282 0 0 0-.05-.107c.736.343 1.5.712 2.078 1-2.737.054-5.658.107-8.685.16 0-.5-.026-.975-.026-1.476 0-.21.052-.395.025-.606-.08-1.21-.08-2.424-.237-3.61-.157-1.264-.157-2.503-.13-3.77.025-.683-.027-1.394-.054-2.08 0-.922 0-1.82.028-2.74 0-.132.053-.237.106-.37h.08c.025.054 0 .133.05.16.08.08.212.21.265.184.157-.106.394-.21.447-.37.13-.315.184-.658.184-.974 0-.236.106-.394.21-.553.054-.08.08-.158.133-.263-.105-.08-.21-.132-.342-.237.106-.29.08-.633.475-.79.052-.027.052-.16.08-.238.025-.213.05-.45.078-.66.052.08.08.105.13.157a.42.42 0 0 1 .054-.08c0-.104-.026-.315 0-.315.316-.053.184-.395.342-.553.025-.028-.027-.107-.027-.16 0-.052 0-.13.026-.13.367-.08.315-.475.552-.66.08-.053.105-.13.21-.263.21.368-.158.553-.184.816.446-.263.578-.895.315-1.08.105-.08.21-.184.29-.29.29-.316.604-.606.868-.922.185-.236.29-.526.474-.763.106-.132.316-.237.474-.317.474-.262.92-.552 1.21-1 .053-.053.132-.105.21-.158.08-.053.238-.053.264-.132.027-.052-.052-.184-.105-.263.104-.053.21-.158.42-.264-.08.158-.105.264-.158.37l.13.13c.238-.184.606-.394.843-.552 0-.025-.132-.13-.132-.13-.157.08-.394.21-.63.316.05-.08.05-.132.08-.158.367-.237.735-.474 1.13-.66.92-.42 1.842-.842 2.763-1.237.158-.08.37-.026.553-.026.078 0 .13 0 .21-.026.42-.132.842-.264 1.263-.37.183-.052.393-.078.58-.078.787.025 1.577.025 2.366.078.342.026.658.105.974.21a9.88 9.88 0 0 1 1.184.5c.447.24.868.502 1.29.792.763.5 1.473 1.054 2.236 1.502.737.448 1.316 1.054 1.79 1.74.58.816 1.237 1.554 1.5 2.555l.394 1.74c.08.316.264.632.185 1-.133.66-.238 1.345-.343 2.004-.052.265-.105.53-.078.79.05.82-.265 1.53-.58 2.268-.106.237-.264.475-.395.738a.798.798 0 0 0 .21.106l.237-.474c.027 0 .027 0 .053.027-.132.368-.237.764-.37 1.133-.314.817-.63 1.66-1.025 2.45-.21.448-.58.817-.842 1.24-.262.368-.473.763-.736 1.106-.237.29-.473.58-.79.79-.71.527-1.447 1.054-2.21 1.476-.473.29-1.026.448-1.552.58zm-14.027-1.4l-.026.027c-.055-.026-.134-.052-.186-.105l-.632-.95c-.052-.078-.08-.157-.052-.262.29.448.58.87.895 1.29zm16.37 3.61c1.183-.5 2.157-1.21 3.05-2.028.133-.132.264-.263.422-.37 1.106-.684 1.92-1.633 2.658-2.687.842-1.212 1.395-2.582 2.08-3.873a2.73 2.73 0 0 1 .157-.29c-.053 3.004.29 5.955.684 8.933-2.973.105-6 .21-9.052.316zm26.683-.79c-.026.053-.08.106-.105.16-.027-.054-.027-.133-.053-.24-.158.423-.5.212-.737.212-1.42.027-2.868.027-4.29.027-1.368 0-2.762 0-4.13.024-.448 0-.922.105-1.37.132-1.078.052-2.157.08-3.236.105-.08 0-.158-.13-.29-.236a1.81 1.81 0 0 1-.158.237c-.028-.052-.08-.104-.133-.183-.026.08-.053.158-.08.21H58c-.053-.368-.158-.71-.158-1.08 0-.79.08-1.58.105-2.372.027-.368 0-.71 0-1.054.106.08.185.133.29.21.052-.103.105-.182.158-.26 0 0-.053-.028-.106-.08.05-.027.104-.08.104-.106.026-.08.08-.158.08-.21 0-.185-.054-.343-.08-.5.026 0 .052 0 .08-.028l.157.79h.08c-.106-.183.236-.342-.053-.552-.026-.027.026-.185.026-.264-.08-.157-.13-.315-.21-.526.026-.026.105-.053.184-.08-.105-.052-.184-.104-.263-.13.263-.238.263-.37.026-.633.054-.025.106-.025.106-.05 0-.238 0-.475-.052-.71-.053-.266.08-.58-.316-.74a.79.79 0 0 0 .105.21s-.08.027-.158.08c-.342-.317-.13-.74-.21-1.213.184.053.316.106.447.16-.053-.186-.184-.397-.263-.634h-.107v-1.74c0 .027.184.027.29.054 0-.027.025-.053.025-.08-.08-.105-.185-.21-.29-.342l.053-.053c-.21-.262-.105-.63-.105-.71V39.4c.264.264-.13.606.264.764v-.263h-.027c-.026-.395-.026-.79-.052-1.186h-.052c-.027.054-.027.08-.054.133h-.052l.158-6.298c.263.342.552.66.736 1 .606 1.108 1.395 2.057 2.132 3.058.632.87 1.21 1.818 1.79 2.714.71 1.08 1.394 2.16 2.105 3.24a81.41 81.41 0 0 0 1.63 2.426c.5.71 1.028 1.396 1.554 2.082.446.606.92 1.212 1.367 1.818.527.738 1.053 1.475 1.58 2.187.262.368.552.737.84 1.106.16.21.396.37.554.5-.025 0-.052 0-.104-.026.08.105.13.184.184.237.29.158.316.316.158.554zM74 46.854v-.185c0 .052.026.13 0 .184zm.895-11.62c-.027 0-.184-.16-.21-.186-.027.08 0 .158-.053.264-.027-.078-.21-.052-.21-.13-.027.368.157.737.13 1.106.08-.053.395-.08.474-.158.027.026.08.052.106.052-.527.396-.395.79-.158 1.24.052.104.21.315.052.526-.052.053.027.21.053.343h.077v.05l-.237.08c-.052-.08-.367-.236-.367-.37v1.346c.263.08.263.448.368.633a.768.768 0 0 0 .107-.21l.027.024c-.027.158-.053.316-.106.475-.052.236-.105.447-.13.684 0 .026.05.08.05.105-.288.66-.13 1.396-.235 2.08-.08.5 0 1.03-.053 1.556-.054.448-.16.922-.264 1.37-.027.08-.08.105-.21.158.052-.316.026-.527-.027-.817-.028 0-.37-.184-.397-.184 0 .37.21.87.29 1.29-.08-.026-.395-.21-.42-.21-.054.316-.054.738-.08 1.08-.027.264-.263.5-.29.79 0 .16.184.264.158.528h.21c0-.526.238-1 .238-1.554h.078c.027.053.106.106.08.132-.053.29-.16.606-.132.896 0 .158.13.316.08.5-.054.16-.08.317-.107.554-.027-.132-.053-.184-.053-.263-.026 0-.263-.027-.29-.027-.026.158.185.316.158.448-.026.026-.052.026-.105.053l-.868-1.266c-.686-1-1.37-2.003-2.054-3.03a6.312 6.312 0 0 1-.475-.79 37.09 37.09 0 0 0-2.71-4.033c-.762-.974-1.37-2.03-2.08-3.055-.656-.975-1.314-1.924-1.972-2.9-.237-.315-.526-.605-.737-.948-.683-1.08-1.29-2.187-1.972-3.267-.58-.897-1.21-1.767-1.816-2.636-.21-.29-.42-.607-.632-.923a.37.37 0 0 1-.052-.182c-.053-.58-.106-1.16-.132-1.713 0-.527.053-1.054.053-1.608v-.474c0-.132.025-.237.025-.37.025-.025.052-.078.078-.104-.763 0-1.553-.028-2.316 0-.5.025-.763-.186-1.105-.555-1-1.133-1.737-2.424-2.605-3.636a162.42 162.42 0 0 0-2.5-3.427c-.685-.922-1.37-1.818-2.053-2.74-.764-1.054-1.5-2.108-2.29-3.162a381.983 381.983 0 0 0-2.895-3.794c-.45-.58-.95-1.133-1.45-1.74.343.054.66.106.975.133l1.264.08c.947.077 1.894.13 2.84.26.79.107 1.58.265 2.396.396 1.738.29 3.448.765 5.106 1.318.974.316 1.92.738 2.87 1.133 2.13.87 4.157 1.924 6.157 3.03.63.343 1 .896 1.472 1.397.685.712 1.37 1.423 2.027 2.16.762.87 1.472 1.766 2.21 2.662.657.79 1.34 1.58 2 2.372.21.237.37.527.552.79.42.633.895 1.24 1.263 1.924.262.502.42 1.082.604 1.635.262.817.526 1.607.79 2.424.183.606.34 1.24.472 1.87.106.423.08.87.21 1.29.16.556 0 1.16.16 1.715.025.053.05.132.078.185.105.104.184.21.026.368-.025.026-.025.13 0 .21.054-.052.08-.105.133-.184 0 .053.025.08.025.105 0 .104-.027.21 0 .315 0 .052.052.13.078.184.053-.054.105-.08.21-.16.237.897.264 1.793.264 2.715 0 .87.157 1.74-.21 2.583.078-.29-.106-.555-.027-.818z"/><path d="M58.08 45.482c.025 0 .052.027.052.027l-.027-.03c0-.025 0-.025-.026 0zm4.157 26.036c-.29.21-.58.395-.948.474-.028-.026-.028-.053-.054-.08.29-.184.605-.368.895-.553.027.05.08.104.106.157zM12.895 35.81c.29-.367.58-.736.894-1.105.025.026.235.08.262.105-.29.37-.685.87-.974 1.265-.054-.053-.133-.237-.185-.264zM5.42 48.725c-.21-.448-.42-.923-.63-1.37a.91.91 0 0 1 .236-.106c.29.42.42.92.632 1.37 0 0-.21.105-.237.105zm6.712-12.65c-.158.238-.316.502-.474.74-.026-.028-.316.104-.342.078.158-.237.552-.66.71-.896.027.026.053.053.106.08zM59.422 72.6c.025 0 .025-.026.052-.026.184.026.394.052.605.052-.344.237-.555.21-.66-.026zm-47.24-35.418c.028-.08.08-.158.133-.237.052 0 .13-.027.13-.027.107-.184.107-.316.212-.474-.026-.026-.053-.026-.08-.053-.157.108-.315.24-.473.345.053.052.053.08.053.132-.21-.027-.29.08-.395.368-.026.08-.158.106-.29.21-.026.054-.052.186-.105.317l.027.028c-.053.053-.132.08-.132.08-.158.157-.342.29-.5.447-.026.08-.052.158-.052.237.185-.184.5-.527.737-.738l.027.027c.105-.158.184-.316.29-.474.025.026.025.052.052.08-.08.21-.158.446-.237.657-.055.026-.134.08-.134.053-.105.08-.184.184-.29.263l-.473.316c-.263.237-.526.447-.816.685-.184.29-.368.553-.58.896.317-.08.396.053.37.317.368.052.395-.237.5-.448.026-.054.053-.16.105-.186.237-.21.5-.394.763-.605.053-.053.053-.16.053-.238 0-.026-.133-.026-.212-.053.237-.264.58-.71.816-1 .132-.08.263-.186.263-.265-.026-.29.158-.368.37-.474-.106-.08-.133-.157-.133-.183z"/><path d="M12.71 36.892c-.105.184-.21.342-.315.527l-.158-.08c-.105.605-.474 1.132-.842 1.237.105.053.21.106.29.08.078-.027.13-.16.183-.238l.71-1.028.238-.396-.105-.105zM3.948 48.46c.132 0 .264.026.42.026 0-.105.133-.08.133-.184h.08c0 .132.026.237.026.37h-.552c-.027-.027-.132-.186-.106-.212zm-.21-1.212c-.08-.08-.21-.158-.21-.237-.027-.104.052-.235.13-.367.054.184.08.342.132.527-.027.025-.053.052-.053.078zm.658-1.687c.105.266.21.556.316.82a.798.798 0 0 0-.21.105c-.105-.264-.237-.554-.342-.817a.652.652 0 0 1 .237-.106zm58.58 25.194c.13-.052.288-.08.5-.13-.238.183-.422.315-.58.473-.027-.026-.053-.053-.08-.053.053-.105.106-.184.16-.29zM30.63 15.074c.157-.106.29-.185.447-.29l.052.052c-.16.21-.29.42-.475.685-.026-.183-.026-.29-.053-.42-.026 0 0 0 .027-.026zm7.71 13.333c.237-.106.474-.21.763-.343-.026.158-.026.264-.026.37a.927.927 0 0 0-.264-.054c-.158.027-.448.238-.58.264-.025 0 .106-.21.106-.237zm19.74 22.346c.052.263.552.395.052.658.08.055.157.08.236.134a.2.2 0 0 1-.052.106c-.053.025-.158.078-.21.05-.027 0-.08-.104-.08-.157 0-.237.027-.474.053-.79z"/></g></symbol><symbol viewBox="0 0 24 24" id="powerpoint" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5M8 11v2h1v6H8v1h4v-1h-1v-2h2a3 3 0 0 0 3-3 3 3 0 0 0-3-3H8m5 2a1 1 0 0 1 1 1 1 1 0 0 1-1 1h-2v-2h2z" fill="#d14524"/></symbol><symbol viewBox="0 0 67.47 70" id="powershell" xmlns="http://www.w3.org/2000/svg"><path d="M18.545 12.4c-3.014 0-6.08 2.34-6.873 5.248L1.91 53.438c-.793 2.908.996 5.248 4.01 5.248h42.887c3.014 0 6.08-2.34 6.873-5.248l9.761-35.79c.794-2.908-.993-5.248-4.007-5.248h-42.89zm4.848 6.243c.652.04 1.29.33 1.76.86l7.96 9.013-3.957 3.246 3.957-3.244 4.832 5.47c.037.042.06.088.094.131.026.034.057.06.082.096.02.028.032.057.05.086.057.087.105.176.15.267.028.06.055.117.08.178a2.546 2.546 0 0 1 .171.764c.005.073.01.146.008.219-.002.09-.01.178-.021.267a2.53 2.53 0 0 1-.036.217 2.56 2.56 0 0 1-.07.252c-.024.076-.048.15-.08.224a2.547 2.547 0 0 1-.111.22 2.503 2.503 0 0 1-.133.218 2.546 2.546 0 0 1-.147.187c-.058.07-.118.137-.185.202-.027.026-.048.057-.076.082-.037.032-.077.054-.116.084-.038.03-.07.065-.11.093L16.8 52.271a2.552 2.552 0 0 1-3.563-.626 2.553 2.553 0 0 1 .63-3.563l18.349-12.853-3.06-3.467-7.839-8.873a2.549 2.549 0 0 1 .225-3.608 2.546 2.546 0 0 1 1.85-.638zm22.441 28.214c1.377 0 2.255 1.083 1.969 2.43-.287 1.347-1.627 2.433-3.004 2.434l-9.957.006c-1.378 0-2.256-1.083-1.969-2.43.287-1.347 1.626-2.433 3.004-2.434l9.957-.006z" fill="#03a9f4" stroke-width="5.342" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 210 210" id="prettier" xmlns="http://www.w3.org/2000/svg"><title>prettier-icon-dark</title><g transform="matrix(.9 0 0 .9 10.5 10.5)" fill="none" fill-rule="evenodd"><rect fill="#56B3B4" x="165" y="40" width="20" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="200" width="60" height="10" rx="5"/><rect fill="#BF85BF" x="135" y="120" width="40" height="10" rx="5"/><rect fill="#EA5E5E" x="75" y="120" width="50" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="120" width="50" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="160" width="60" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="80" width="60" height="10" rx="5"/><rect fill="#F7BA3E" x="65" y="20" width="110" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="20" width="40" height="10" rx="5"/><rect fill="#F7BA3E" x="55" y="180" width="20" height="10" rx="5"/><rect fill="#56B3B4" x="55" y="60" width="20" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="180" width="30" height="10" rx="5"/><rect fill="#F7BA3E" x="15" y="60" width="30" height="10" rx="5"/><rect fill="#56B3B4" x="95" y="100" width="90" height="10" rx="5"/><rect fill="#F7BA3E" x="45" y="100" width="40" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="100" width="20" height="10" rx="5"/><rect fill="#BF85BF" x="105" y="40" width="50" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="40" width="80" height="10" rx="5"/><rect fill="#F7BA3E" x="45" y="140" width="100" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="140" width="20" height="10" rx="5"/><rect fill="#EA5E5E" x="135" y="60" width="60" height="10" rx="5"/><rect fill="#F7BA3E" x="135" y="80" width="60" height="10" rx="5"/><rect fill="#56B3B4" x="15" width="130" height="10" rx="5"/></g></symbol><symbol viewBox="0 0 80 80" id="protractor" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="hxa"><path transform="scale(1 -1)" fill="#564b55" stroke-width="27.224" d="M-2.983-69.251h69.412v67.108H-2.983z"/></clipPath></defs><g transform="matrix(1.13039 0 0 -1.13039 5.714 82.137)" clip-path="url(#hxa)"><g transform="scale(.1)"><path d="M1180.54 92.324c-5.53 0-9.93-1.797-13.23-5.39-3.29-3.614-5.22-8.594-5.81-14.97h36.02c0 6.583-1.47 11.622-4.4 15.126-2.93 3.496-7.12 5.234-12.58 5.234zm2.84-62.656c-10.19 0-18.22 3.086-24.11 9.297-5.88 6.21-8.83 14.824-8.83 25.84 0 11.101 2.73 19.922 8.21 26.464 5.45 6.524 12.81 9.805 22.02 9.805 8.63 0 15.46-2.851 20.48-8.523 5.03-5.676 7.55-13.157 7.55-22.461v-6.613h-47.45c.21-8.086 2.26-14.22 6.12-18.418 3.89-4.18 9.34-6.29 16.38-6.29 7.42 0 14.76 1.563 22 4.669V34.14c-3.68-1.602-7.18-2.746-10.48-3.438-3.28-.684-7.24-1.035-11.89-1.035M1272.34 30.918v44.57c0 5.606-1.28 9.805-3.82 12.559-2.56 2.773-6.56 4.16-12.02 4.16-7.2 0-12.49-1.953-15.84-5.851-3.34-3.895-5.03-10.32-5.03-19.286V30.918h-10.42v68.887h8.47l1.71-9.422h.5c2.14 3.387 5.14 6.023 8.99 7.887 3.85 1.867 8.15 2.804 12.88 2.804 8.29 0 14.54-2.011 18.73-6.015 4.19-3.985 6.28-10.391 6.28-19.192V30.918h-10.43M1328.96 38.406c7.1 0 12.27 1.938 15.48 5.813 3.22 3.879 4.81 10.129 4.81 18.758v2.199c0 9.765-1.62 16.726-4.87 20.898-3.25 4.18-8.44 6.25-15.56 6.25-6.11 0-10.79-2.383-14.04-7.129-3.26-4.746-4.88-11.472-4.88-20.136 0-8.797 1.61-15.45 4.84-19.93 3.23-4.484 7.97-6.723 14.22-6.723zm20.85 1.762h-.56c-4.83-7.004-12.02-10.5-21.62-10.5-9.01 0-16.03 3.066-21.04 9.238-5 6.153-7.5 14.922-7.5 26.27 0 11.355 2.51 20.176 7.54 26.465 5.03 6.289 12.03 9.433 21 9.433 9.34 0 16.5-3.398 21.49-10.195h.81l-.43 4.96-.25 4.845v28.039h10.43V30.918h-8.49l-1.38 9.25M1434.91 38.27c1.85 0 3.63.136 5.34.421 1.72.274 3.09.547 4.1.84v-7.976c-1.15-.559-2.81-.996-5.01-1.36-2.18-.351-4.17-.527-5.94-.527-13.32 0-19.97 7.012-19.97 21.055V91.71h-9.88v5.027l9.88 4.336 4.38 14.707h6.04V99.805h20V91.71h-20V51.16c0-4.15.98-7.333 2.96-9.56 1.97-2.206 4.67-3.331 8.1-3.331M1463.81 65.43c0-8.809 1.76-15.508 5.27-20.118 3.53-4.609 8.69-6.906 15.53-6.906s12.01 2.297 15.56 6.875c3.53 4.602 5.3 11.301 5.3 20.149 0 8.75-1.77 15.41-5.3 19.953-3.55 4.539-8.77 6.824-15.69 6.824-6.82 0-11.99-2.246-15.47-6.73-3.46-4.48-5.2-11.16-5.2-20.047zm52.47 0c0-11.23-2.83-20-8.48-26.309-5.66-6.309-13.47-9.453-23.44-9.453-6.17 0-11.64 1.445-16.42 4.336-4.78 2.89-8.46 7.031-11.06 12.45-2.59 5.401-3.88 11.73-3.88 18.976 0 11.23 2.8 19.968 8.41 26.242 5.61 6.258 13.4 9.402 23.38 9.402 9.64 0 17.3-3.222 22.97-9.62 5.69-6.415 8.52-15.087 8.52-26.024M1591.71 92.324c-5.54 0-9.94-1.797-13.23-5.39-3.3-3.614-5.24-8.594-5.81-14.97h36c0 6.583-1.46 11.622-4.39 15.126-2.93 3.496-7.13 5.234-12.57 5.234zm2.83-62.656c-10.19 0-18.22 3.086-24.11 9.297-5.89 6.21-8.83 14.824-8.83 25.84 0 11.101 2.74 19.922 8.2 26.464 5.46 6.524 12.81 9.805 22.04 9.805 8.62 0 15.45-2.851 20.48-8.523 5.03-5.676 7.54-13.157 7.54-22.461v-6.613h-47.45c.21-8.086 2.25-14.22 6.13-18.418 3.87-4.18 9.33-6.29 16.36-6.29 7.43 0 14.77 1.563 22.01 4.669V34.14c-3.69-1.602-7.17-2.746-10.46-3.438-3.3-.684-7.27-1.035-11.91-1.035M1683.5 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12.01 4.16-7.2 0-12.48-1.953-15.83-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.48l1.69-9.422h.51c2.14 3.387 5.14 6.023 8.99 7.887 3.84 1.867 8.15 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M1740.11 38.406c7.12 0 12.28 1.938 15.49 5.813 3.21 3.879 4.81 10.129 4.81 18.758v2.199c0 9.765-1.62 16.726-4.87 20.898-3.25 4.18-8.43 6.25-15.56 6.25-6.12 0-10.8-2.383-14.05-7.129-3.24-4.746-4.88-11.472-4.88-20.136 0-8.797 1.64-15.45 4.85-19.93 3.22-4.484 7.96-6.723 14.21-6.723zm20.87 1.762h-.57c-4.82-7.004-12.03-10.5-21.62-10.5-9.01 0-16.02 3.066-21.03 9.238-5 6.153-7.52 14.922-7.52 26.27 0 11.355 2.52 20.176 7.55 26.465 5.02 6.289 12.02 9.433 21 9.433 9.34 0 16.5-3.398 21.48-10.195h.83l-.44 4.96-.25 4.845v28.039h10.43V30.918h-8.49l-1.37 9.25M1846.07 38.27c1.85 0 3.64.136 5.36.421 1.7.274 3.07.547 4.08.84v-7.976c-1.13-.559-2.8-.996-5-1.36-2.2-.351-4.18-.527-5.94-.527-13.33 0-19.99 7.012-19.99 21.055V91.71h-9.86v5.027l9.86 4.336 4.4 14.707h6.04V99.805H1855V91.71h-19.98V51.16c0-4.15.98-7.333 2.95-9.56 1.97-2.206 4.68-3.331 8.1-3.331M1894.26 92.324c-5.53 0-9.94-1.797-13.22-5.39-3.31-3.614-5.25-8.594-5.83-14.97h36.01c0 6.583-1.45 11.622-4.38 15.126-2.95 3.496-7.13 5.234-12.58 5.234zm2.83-62.656c-10.19 0-18.22 3.086-24.1 9.297-5.9 6.21-8.84 14.824-8.84 25.84 0 11.101 2.73 19.922 8.2 26.464 5.47 6.524 12.81 9.805 22.03 9.805 8.63 0 15.46-2.851 20.49-8.523 5.03-5.676 7.55-13.157 7.55-22.461v-6.613h-47.46c.22-8.086 2.26-14.22 6.13-18.418 3.87-4.18 9.33-6.29 16.37-6.29 7.42 0 14.75 1.563 22 4.669V34.14c-3.7-1.602-7.17-2.746-10.47-3.438-3.28-.684-7.25-1.035-11.9-1.035M1983.36 49.727c0-6.426-2.4-11.368-7.18-14.844-4.77-3.477-11.47-5.215-20.11-5.215-9.13 0-16.26 1.445-21.37 4.336v9.687a51.32 51.32 0 0 1 10.65-3.964c3.79-.977 7.45-1.457 10.97-1.457 5.46 0 9.64.87 12.57 2.609 2.95 1.738 4.41 4.394 4.41 7.95 0 2.694-1.17 4.98-3.5 6.894-2.32 1.914-6.85 4.152-13.6 6.757-6.41 2.383-10.97 4.473-13.67 6.25-2.71 1.778-4.72 3.81-6.04 6.067-1.31 2.254-1.98 4.96-1.98 8.113 0 5.606 2.29 10.04 6.86 13.281 4.57 3.25 10.84 4.883 18.79 4.883 7.42 0 14.66-1.515 21.74-4.531l-3.71-8.496c-6.9 2.851-13.17 4.277-18.79 4.277-4.94 0-8.67-.77-11.18-2.324-2.52-1.543-3.78-3.691-3.78-6.406 0-1.844.48-3.418 1.42-4.707.95-1.309 2.46-2.54 4.56-3.711 2.09-1.184 6.11-2.871 12.07-5.086 8.16-2.98 13.69-5.98 16.55-8.996 2.87-3.02 4.32-6.809 4.32-11.367M2021.28 38.27c1.85 0 3.64.136 5.35.421 1.71.274 3.09.547 4.09.84v-7.976c-1.14-.559-2.81-.996-5.01-1.36-2.18-.351-4.18-.527-5.93-.527-13.33 0-19.99 7.012-19.99 21.055V91.71h-9.87v5.027l9.87 4.336 4.4 14.707h6.02V99.805h20V91.71h-20V51.16c0-4.15 1-7.333 2.97-9.56 1.98-2.206 4.67-3.331 8.1-3.331M2053.61 30.918h-10.42v68.887h10.42zm-11.31 87.559c0 2.39.59 4.14 1.76 5.253 1.18 1.106 2.65 1.661 4.42 1.661 1.67 0 3.1-.567 4.32-1.7 1.22-1.132 1.82-2.871 1.82-5.214 0-2.344-.6-4.09-1.82-5.247-1.22-1.16-2.65-1.726-4.32-1.726-1.77 0-3.24.566-4.42 1.726-1.17 1.157-1.76 2.903-1.76 5.247M2121.59 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12 4.16-7.21 0-12.49-1.953-15.84-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.49l1.69-9.422h.5c2.15 3.387 5.14 6.023 8.99 7.887 3.85 1.867 8.16 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M2159.29 77.742c0-4.812 1.35-8.465 4.08-10.926 2.72-2.48 6.51-3.71 11.37-3.71 10.19 0 15.28 4.953 15.28 14.831 0 10.344-5.16 15.532-15.47 15.532-4.9 0-8.67-1.32-11.31-3.965-2.63-2.649-3.95-6.555-3.95-11.762zm-5.67-58.387c0-3.73 1.58-6.55 4.72-8.488 3.14-1.922 7.65-2.879 13.52-2.879 8.75 0 15.24 1.309 19.45 3.926 4.21 2.617 6.31 6.172 6.31 10.652 0 3.723-1.15 6.32-3.45 7.754-2.31 1.457-6.65 2.168-13.01 2.168h-12.51c-4.74 0-8.43-1.12-11.06-3.386-2.65-2.266-3.97-5.508-3.97-9.747zm54.94 80.45v-6.582l-12.76-1.512c1.18-1.477 2.23-3.39 3.15-5.754.91-2.371 1.37-5.039 1.37-8.02 0-6.746-2.29-12.128-6.91-16.152-4.61-4.012-10.93-6.023-18.98-6.023-2.05 0-3.98.156-5.78.5-4.45-2.356-6.67-5.305-6.67-8.871 0-1.883.77-3.282 2.34-4.176 1.54-.902 4.21-1.36 7.97-1.36h12.2c7.46 0 13.19-1.574 17.19-4.707 4-3.144 6-7.714 6-13.71 0-7.618-3.06-13.426-9.17-17.43C2192.38 2.004 2183.46 0 2171.72 0c-9 0-15.95 1.68-20.82 5.027-4.88 3.352-7.34 8.079-7.34 14.211 0 4.18 1.35 7.813 4.03 10.88 2.68 3.046 6.45 5.116 11.32 6.21-1.77.8-3.24 2.031-4.44 3.711-1.19 1.68-1.78 3.633-1.78 5.84 0 2.52.66 4.707 2.01 6.602 1.34 1.882 3.44 3.71 6.34 5.468-3.56 1.465-6.46 3.953-8.71 7.48-2.23 3.516-3.35 7.54-3.35 12.06 0 7.55 2.26 13.37 6.79 17.452 4.52 4.082 10.93 6.133 19.22 6.133 3.6 0 6.86-.429 9.75-1.27h23.82M2284.61 91.71h-17.54V30.919h-10.43v60.793h-12.31v4.707l12.31 3.766v3.839c0 16.922 7.4 25.391 22.19 25.391 3.65 0 7.93-.73 12.82-2.195l-2.7-8.364c-4.03 1.301-7.46 1.946-10.31 1.946-3.93 0-6.85-1.309-8.73-3.926-1.89-2.617-2.84-6.816-2.84-12.598v-4.472h17.54V91.71M2302.87 65.43c0-8.809 1.76-15.508 5.28-20.118 3.52-4.609 8.7-6.906 15.52-6.906 6.84 0 12.02 2.297 15.57 6.875 3.54 4.602 5.3 11.301 5.3 20.149 0 8.75-1.76 15.41-5.3 19.953-3.55 4.539-8.78 6.824-15.69 6.824-6.83 0-11.99-2.246-15.46-6.73-3.48-4.48-5.22-11.16-5.22-20.047zm52.48 0c0-11.23-2.82-20-8.47-26.309-5.67-6.309-13.48-9.453-23.46-9.453-6.15 0-11.62 1.445-16.4 4.336-4.77 2.89-8.47 7.031-11.06 12.45-2.59 5.401-3.9 11.73-3.9 18.976 0 11.23 2.81 19.968 8.43 26.242 5.6 6.258 13.4 9.402 23.38 9.402 9.63 0 17.28-3.222 22.97-9.62 5.68-6.415 8.51-15.087 8.51-26.024M2403.79 101.074c3.07 0 5.8-.254 8.22-.761l-1.43-9.676c-2.86.633-5.37.933-7.55.933-5.58 0-10.33-2.261-14.3-6.785-3.95-4.531-5.94-10.156-5.94-16.902V30.918h-10.43v68.887h8.62l1.19-12.754h.5c2.56 4.48 5.63 7.949 9.23 10.37 3.61 2.423 7.56 3.653 11.89 3.653M2500.33 69.766l-10.68 28.476c-1.39 3.594-2.81 8.028-4.28 13.262-.93-4.024-2.24-8.438-3.96-13.262l-10.81-28.476zm14.77-38.848l-11.44 29.227h-36.83l-11.32-29.227h-10.81l36.34 92.273h8.98l36.13-92.273h-11.05M2583.07 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12 4.16-7.21 0-12.49-1.953-15.84-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.48l1.69-9.422h.51c2.14 3.387 5.14 6.023 8.99 7.887 3.84 1.867 8.15 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M2620.76 77.742c0-4.812 1.36-8.465 4.08-10.926 2.73-2.48 6.53-3.71 11.37-3.71 10.2 0 15.28 4.953 15.28 14.831 0 10.344-5.15 15.532-15.45 15.532-4.91 0-8.68-1.32-11.32-3.965-2.64-2.649-3.96-6.555-3.96-11.762zm-5.66-58.387c0-3.73 1.57-6.55 4.71-8.488 3.15-1.922 7.65-2.879 13.53-2.879 8.75 0 15.23 1.309 19.44 3.926 4.21 2.617 6.31 6.172 6.31 10.652 0 3.723-1.14 6.32-3.45 7.754-2.31 1.457-6.64 2.168-13 2.168h-12.51c-4.74 0-8.43-1.12-11.07-3.386-2.63-2.266-3.96-5.508-3.96-9.747zm54.94 80.45v-6.582l-12.76-1.512c1.18-1.477 2.22-3.39 3.14-5.754.92-2.371 1.38-5.039 1.38-8.02 0-6.746-2.3-12.128-6.92-16.152-4.61-4.012-10.92-6.023-18.97-6.023-2.05 0-3.99.156-5.78.5-4.46-2.356-6.67-5.305-6.67-8.871 0-1.883.78-3.282 2.33-4.176 1.55-.902 4.21-1.36 7.98-1.36h12.2c7.46 0 13.18-1.574 17.18-4.707 4.01-3.144 6-7.714 6-13.71 0-7.618-3.06-13.426-9.17-17.43C2653.87 2.004 2644.94 0 2633.2 0c-9 0-15.95 1.68-20.83 5.027-4.88 3.352-7.33 8.079-7.33 14.211 0 4.18 1.35 7.813 4.02 10.88 2.69 3.046 6.47 5.116 11.32 6.21-1.77.8-3.23 2.031-4.43 3.711-1.19 1.68-1.79 3.633-1.79 5.84 0 2.52.66 4.707 2.01 6.602 1.35 1.882 3.45 3.71 6.35 5.468-3.56 1.465-6.47 3.953-8.71 7.48-2.23 3.516-3.35 7.54-3.35 12.06 0 7.55 2.25 13.37 6.79 17.452 4.52 4.082 10.92 6.133 19.21 6.133 3.62 0 6.86-.429 9.75-1.27h23.83M2692.7 99.805V55.117c0-5.605 1.27-9.805 3.83-12.566 2.56-2.766 6.57-4.145 12.01-4.145 7.2 0 12.47 1.965 15.81 5.903 3.33 3.945 4.99 10.379 4.99 19.304v36.192h10.44V30.918h-8.62l-1.5 9.25h-.58c-2.13-3.41-5.1-5.988-8.88-7.793-3.8-1.809-8.13-2.707-12.99-2.707-8.37 0-14.65 1.992-18.81 5.977-4.18 3.964-6.26 10.351-6.26 19.101v45.059h10.56M2760.61 30.918h10.43v97.805h-10.43zM2810.67 38.27c6.5 0 11.6 1.789 15.31 5.343 3.71 3.575 5.56 8.555 5.56 14.961v6.23l-10.44-.448c-8.3-.286-14.27-1.583-17.94-3.868-3.66-2.273-5.5-5.82-5.5-10.644 0-3.781 1.14-6.64 3.42-8.613 2.29-1.973 5.48-2.961 9.59-2.961zm23.57-7.352l-2.07 9.805h-.51c-3.44-4.305-6.86-7.227-10.27-8.77-3.42-1.523-7.68-2.285-12.8-2.285-6.83 0-12.17 1.758-16.05 5.273-3.87 3.528-5.81 8.536-5.81 15.032 0 13.906 11.12 21.199 33.37 21.875l11.7.359v4.277c0 5.418-1.17 9.395-3.5 11.985-2.32 2.566-6.03 3.855-11.15 3.855-5.74 0-12.24-1.758-19.49-5.273l-3.21 7.988c3.4 1.836 7.11 3.281 11.16 4.324a47.81 47.81 0 0 0 12.16 1.575c8.23 0 14.3-1.817 18.27-5.461 3.96-3.66 5.93-9.5 5.93-17.54V30.919h-7.73M2893.6 101.074c3.07 0 5.8-.254 8.25-.761l-1.46-9.676c-2.84.633-5.35.933-7.54.933-5.56 0-10.33-2.261-14.3-6.785-3.96-4.531-5.93-10.156-5.93-16.902V30.918h-10.44v68.887h8.61l1.19-12.754h.5c2.57 4.48 5.65 7.949 9.25 10.37 3.6 2.423 7.56 3.653 11.87 3.653M2901.63 6.727c-3.94 0-7.04.558-9.31 1.691v9.121c2.97-.84 6.08-1.25 9.31-1.25 4.14 0 7.3 1.25 9.45 3.77 2.16 2.507 3.24 6.132 3.24 10.859v91.895h10.69V31.797c0-7.95-2.01-14.121-6.04-18.496-4.02-4.383-9.8-6.574-17.34-6.574M2999.96 55.371c0-8.086-2.93-14.394-8.8-18.918-5.87-4.52-13.83-6.785-23.88-6.785-10.9 0-19.27 1.406-25.14 4.219v10.3c3.77-1.59 7.88-2.847 12.31-3.765 4.45-.93 8.85-1.399 13.21-1.399 7.12 0 12.49 1.36 16.09 4.063 3.59 2.695 5.4 6.465 5.4 11.277 0 3.196-.63 5.805-1.91 7.832-1.29 2.024-3.42 3.907-6.42 5.625-2.99 1.711-7.56 3.664-13.67 5.84-8.55 3.059-14.66 6.692-18.32 10.871-3.66 4.2-5.51 9.668-5.51 16.407 0 7.089 2.68 12.714 7.99 16.914 5.32 4.191 12.36 6.289 21.12 6.289 9.13 0 17.54-1.68 25.2-5.032l-3.32-9.304c-7.59 3.183-14.96 4.785-22.13 4.785-5.66 0-10.07-1.223-13.26-3.652-3.19-2.43-4.78-5.809-4.78-10.118 0-3.191.59-5.8 1.76-7.832 1.17-2.031 3.14-3.886 5.95-5.597 2.78-1.688 7.04-3.563 12.79-5.625 9.63-3.426 16.26-7.118 19.89-11.063 3.62-3.937 5.43-9.043 5.43-15.332M741.648 375.406h30c28.965 0 50.227 5.039 63.774 15.117 13.531 10.079 20.32 25.821 20.32 47.247 0 19.832-6.074 34.628-18.191 44.402-12.141 9.758-31.028 14.641-56.692 14.641h-39.211zm172.192 64.246c0-36.062-11.809-63.691-35.434-82.898-23.621-19.219-57.234-28.82-100.847-28.82h-35.911V198.73h-56.445v345.329h99.438c43.14 0 75.457-8.829 96.961-26.465 21.496-17.637 32.238-43.614 32.238-77.942M1099.26 464.691c11.17 0 20.39-.789 27.63-2.371l-5.43-51.718c-7.88 1.894-16.07 2.832-24.57 2.832-22.2 0-40.19-7.246-53.97-21.731-13.78-14.48-20.66-33.301-20.66-56.453V198.73h-55.514v261.227h43.464l7.32-46.055h2.83c8.66 15.594 19.96 27.95 33.9 37.09 13.93 9.141 28.93 13.699 45 13.699M1206.88 329.82c0-60.308 22.28-90.465 66.85-90.465 44.08 0 66.13 30.157 66.13 90.465 0 59.688-22.21 89.512-66.61 89.512-23.31 0-40.2-7.707-50.67-23.144-10.47-15.43-15.7-37.54-15.7-66.368zm190.13 0c0-42.672-10.95-75.972-32.83-99.898-21.89-23.945-52.35-35.918-91.41-35.918-24.41 0-45.97 5.508-64.7 16.543-18.75 11.016-33.16 26.836-43.23 47.48-10.08 20.625-15.11 44.551-15.11 71.793 0 42.364 10.86 75.43 32.58 99.2 21.73 23.777 52.36 35.671 91.89 35.671 37.79 0 67.7-12.156 89.75-36.492 22.05-24.328 33.06-57.121 33.06-98.379M1558.11 238.887c13.54 0 27.07 2.129 40.62 6.386v-41.816c-6.13-2.676-14.05-4.922-23.73-6.738-9.69-1.797-19.73-2.715-30.12-2.715-52.59 0-78.88 27.715-78.88 83.144v140.778h-35.68v24.558l38.26 20.325 18.9 55.261h34.26v-58.113h74.39v-42.031h-74.39v-139.84c0-13.379 3.34-23.242 10.03-29.629 6.69-6.387 15.48-9.57 26.34-9.57M1783.44 464.691c11.17 0 20.38-.789 27.62-2.371l-5.43-51.718c-7.88 1.894-16.06 2.832-24.56 2.832-22.2 0-40.2-7.246-53.97-21.731-13.78-14.48-20.66-33.301-20.66-56.453V198.73h-55.52v261.227h43.46l7.34-46.055h2.82c8.66 15.594 19.95 27.95 33.9 37.09 13.92 9.141 28.93 13.699 45 13.699M1925.05 236.523c20.15 0 36.32 5.625 48.52 16.895 12.21 11.25 18.31 27.051 18.31 47.344v22.676l-33.54-1.407c-26.13-.937-45.16-5.312-57.04-13.105-11.89-7.793-17.82-19.727-17.82-35.781 0-11.661 3.45-20.665 10.39-27.051 6.91-6.387 17.32-9.571 31.18-9.571zm82.66-37.793l-11.11 36.387h-1.87c-12.62-15.918-25.29-26.738-38.04-32.48-12.74-5.742-29.13-8.633-49.13-8.633-25.67 0-45.7 6.934-60.1 20.801-14.41 13.847-21.62 33.457-21.62 58.808 0 26.934 10 47.246 30 60.934 19.99 13.691 50.45 21.172 91.41 22.441l45.09 1.414v13.938c0 16.699-3.88 29.16-11.68 37.441-7.79 8.262-19.88 12.383-36.25 12.383-13.39 0-26.23-1.953-38.5-5.891a294.638 294.638 0 0 1-35.44-13.933l-17.94 39.668c14.17 7.41 29.68 13.035 46.52 16.894 16.85 3.868 32.77 5.789 47.72 5.789 33.22 0 58.31-7.246 75.22-21.726 16.94-14.492 25.4-37.246 25.4-68.262V198.73h-39.68M2220.04 194.004c-39.52 0-69.55 11.543-90.1 34.609-20.55 23.067-30.82 56.172-30.82 99.321 0 43.925 10.74 77.707 32.23 101.339 21.5 23.614 52.56 35.418 93.18 35.418 27.56 0 52.35-5.117 74.41-15.359l-16.78-44.641c-23.46 9.133-42.82 13.704-58.1 13.704-45.19 0-67.79-29.993-67.79-89.981 0-29.293 5.63-51.305 16.89-66.031 11.26-14.707 27.76-22.09 49.48-22.09 24.72 0 48.11 6.152 70.15 18.437v-48.417c-9.92-5.84-20.5-10-31.76-12.52-11.26-2.52-24.93-3.789-40.99-3.789M2451.52 238.887c13.54 0 27.08 2.129 40.63 6.386v-41.816c-6.15-2.676-14.05-4.922-23.73-6.738-9.69-1.797-19.73-2.715-30.12-2.715-52.6 0-78.9 27.715-78.9 83.144v140.778h-35.66v24.558l38.26 20.325 18.9 55.261h34.26v-58.113h74.39v-42.031h-74.39v-139.84c0-13.379 3.34-23.242 10.03-29.629 6.69-6.387 15.47-9.57 26.33-9.57M2585.92 329.82c0-60.308 22.28-90.465 66.84-90.465 44.09 0 66.15 30.157 66.15 90.465 0 59.688-22.22 89.512-66.62 89.512-23.31 0-40.2-7.707-50.67-23.144-10.47-15.43-15.7-37.54-15.7-66.368zm190.13 0c0-42.672-10.94-75.972-32.83-99.898-21.89-23.945-52.36-35.918-91.4-35.918-24.42 0-45.98 5.508-64.72 16.543-18.74 11.016-33.14 26.836-43.22 47.48-10.07 20.625-15.12 44.551-15.12 71.793 0 42.364 10.87 75.43 32.59 99.2 21.74 23.777 52.36 35.671 91.89 35.671 37.79 0 67.7-12.156 89.75-36.492 22.04-24.328 33.06-57.121 33.06-98.379M2972.33 464.691c11.18 0 20.38-.789 27.63-2.371l-5.43-51.718c-7.87 1.894-16.05 2.832-24.57 2.832-22.2 0-40.19-7.246-53.96-21.731-13.78-14.48-20.67-33.301-20.67-56.453V198.73h-55.51v261.227h43.46l7.33-46.055h2.83c8.66 15.594 19.96 27.95 33.89 37.09 13.94 9.141 28.94 13.699 45 13.699" fill="#100f0d"/><path d="M610.11 372.83c0-170.584-138.257-308.862-308.846-308.862-170.602 0-308.846 138.278-308.846 308.863 0 170.576 138.244 308.846 308.846 308.846 170.59 0 308.846-138.27 308.846-308.846" fill="#e53935" stroke-width="1.029"/><path d="M460.694 521.792l-105.04.958-61.415 61.415-72.096-47.883 12.445-12.438-29.207.26-99.129-166.817H67.357l24.39-24.402-24.57-41.363L294.66 64.049c2.192-.04 4.399-.08 6.603-.08 170.416 0 308.585 138.055 308.846 308.408L460.694 521.792" fill="#d51c2f" stroke-width="1.029"/><path d="M149.093 350.258c0 84.048 68.13 152.151 152.171 152.151 84.028 0 152.139-68.103 152.139-152.151zm342.063-7.017v14.046h44.015c-1.75 59.337-25.556 113.104-63.54 153.419L438.75 477.81l-9.925 9.94 32.875 32.887c-40.314 37.983-94.081 61.79-153.41 63.527l-.015-44.003h-14.035v44.003c-59.34-1.737-113.096-25.556-153.41-63.527l32.887-32.887-9.945-9.92-32.883 32.875c-37.975-40.315-61.781-94.082-63.53-153.419h44.002l-.008-14.034H67.176v-51.511h468.176v51.5h-44.196" fill="#f5f5f5" stroke-width="1.029"/></g></g></symbol><symbol id="pug" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:#c1272d}.hyst1{fill:#efcca3}.st2{fill:#ed1c24}.hyst3{fill:#ccac8d}.hyst4{fill:#fff}.st5{fill:#ff931e}.st6{fill:#ffb81e}.hyst7{fill:#56332b}.hyst8{fill:#442823}.hyst9{fill:#7f4a41}.hyst10{fill:#331712}.st11{fill:#fc6}.st12{fill:#ccc}.st13{fill:#b3b3b3}.st14{fill:#989898}.st15{fill:#323232}.st16{fill:#1e1e1e}.st17{fill:#4c4c4c}.st18{fill:#e6e6e6}.st19{fill:#606060}</style><path class="hyst1" d="M107.4 50.9c-.2-4.4.4-8.3-1.6-11.6-4.8-8.2-16.8-13-40.8-13v.7h-.5.5v-.7c-24 0-36.6 4.8-41.4 13.1-1.9 3.4-1.7 7.2-2 11.6-.2 3.5-1.8 7.2-1.1 11.2.8 5.2 1.1 10.4 1.9 15.2.6 3.9 6 7.2 6.5 10.9 1.4 10.2 12 14.9 36 14.9v.8h-.6.7v-.8c24 0 34.2-4.7 35.5-14.9.5-3.8 5.5-7 6.1-10.9.8-4.8 1.1-10 1.9-15.2.7-4-.9-7.8-1.1-11.3z"/><path class="hyst3" d="M64.6 54.5c4.3.1 7.3 2.8 10.1 5.3 3.3 2.9 8.9 4.9 11.2 7.4 2.3 2.5 5.3 5 6.4 8.9 1.1 3.9 1.4 8.9 1.4 10.2 0 1.3.7 1 2.7 0 4.7-2.3 9.9-8.5 9.9-8.5-.6 3.9-5.7 7.4-6.2 11.1C98.9 99.1 89 104 64.5 104h-.1.6"/><path class="hyst3" d="M80.4 46.7c.9 3.1 4.1 13.6-2.1 10.1 0 0 2.6 1.5 4.2 7.2 1.7 5.7 5.8 6.4 5.8 6.4s6.7 1.3 11.7-3c4.2-3.6 4.9-10 3.1-14.9-1.8-4.8-5-6.3-9.7-7.3-4.7-1.1-14.1-2-13 1.5z"/><circle cx="92.3" cy="58.1" r="8.8"/><circle class="hyst4" cx="90" cy="54.2" r="2.3"/><path class="hyst1" d="M78.9 57.7s7.9 5.4 12.2 10.7c4.3 5.3 4.2 6.3 4.2 6.3l-3.1 1.4s-4.4-8.3-9.8-11.4c-5.5-3.1-6.1-5.7-6.1-5.7l2.6-1.3z"/><path class="hyst3" d="M64.9 54.5c-4.3.1-7.5 2.8-10.4 5.3-3.3 2.9-9.1 4.9-11.4 7.4-2.3 2.5-5.4 5-6.5 8.9-1.1 3.9-1.5 8.9-1.5 10.2 0 1.3.2 1.4-2.7 0-4.7-2.2-9.9-8.5-9.9-8.5.6 3.9 5.7 7.4 6.2 11.1C30.1 99.1 40 104 64.5 104h.5"/><path class="hyst7" d="M88.1 71.4C83.3 65.5 75.6 60 64.9 60h-.1c-10.7 0-18.4 5.5-23.2 11.4-5 6.1-4.6 8.5-4.6 14.3 0 21 7.4 15 12.3 17.6 5 2.5 10.2 1.7 15.5 1.7h.1c5.4 0 10.5.7 15.5-1.8 4.9-2.5 12.3 3.7 12.3-17.3.1-5.8.4-8.4-4.6-14.5z"/><path class="hyst8" d="M64.4 65.2s-.7 9.7-2.1 11.6l2.6-.6-.5-11z"/><path class="hyst8" d="M65.1 65.2s.7 9.7 2.1 11.6l-2.6-.6.5-11z"/><path class="hyst7" d="M56.7 62.9c-1-2.3 2.6-6 8.3-6.1 5.7 0 9.3 3.7 8.3 6.1-1 2.4-4.6 3.1-8.3 3.2-3.6-.1-7.3-.8-8.3-3.2z"/><path d="M65 65.2c0-.4 3.4-.5 5.2-1.7 0 0-3.7 1.2-4.5.7-.8-.4-1-1.6-1-1.6s-.3 1.2-.9 1.6c-.7.4-4.9-.7-4.9-.7s5.6 1.4 5.6 1.7c0 .3-.1 1.3-.1 2 0 2.5 0 8.7.4 9.2.6.9.4-6.7.4-9.2-.1-.8-.1-1.6-.2-2z"/><path class="hyst9" d="M65.2 78.6c1.7 0 4.7 1.2 7.4 3.1-2.6-2.9-5.7-4.9-7.4-4.9-1.8 0-5.6 2.2-8.3 5.4 2.8-2.2 6.4-3.6 8.3-3.6z"/><path class="hyst8" d="M64.5 96.3c-3.8 0-7.5-1.2-10.9-2.1-.7-.2-1.4.3-2.1.1-6.3-2-11.4-5.4-14.5-9.7v1c0 21 7.4 15.1 12.3 17.6 5 2.5 10.2 1.7 15.5 1.7h.1c5.4 0 10.5.7 15.5-1.8 4.9-2.5 12.3 3.6 12.3-17.4 0-.8 0-1.6.1-2.3-2.9 4.7-8.2 8.4-14.8 10.6-.6.2-2-.3-2.6-.2-3.6 1.2-6.8 2.5-10.9 2.5z"/><path class="hyst8" d="M55 85s-2.5 7.5-.8 10.8l-2.3-1s1.7-7.6 3.1-9.8zM74.8 85s2.5 7.5.8 10.8l2.3-1s-1.8-7.6-3.1-9.8z"/><path class="hyst3" d="M48.6 46.7c-.9 3.1-4.1 13.6 2.1 10.1 0 0-2.6 1.5-4.2 7.2s-5.8 6.4-5.8 6.4-6.7 1.3-11.7-3c-4.2-3.6-4.9-10-3.1-14.9s5-6.3 9.7-7.3c4.7-1.1 14-2 13 1.5z"/><path d="M64.9 76.8c2.7 0 11.1 5.8 11.2 12.9v-.4c0-7.4-6.8-13.3-11.2-13.3-4.4 0-11.2 6-11.2 13.3v.4c.1-7.1 8.5-12.9 11.2-12.9z"/><ellipse transform="rotate(-14.465 66.712 61.468)" class="hyst10" cx="66.7" cy="61.5" rx=".8" ry="1.5"/><ellipse transform="rotate(17.235 62.371 61.462)" class="hyst10" cx="62.4" cy="61.5" rx=".8" ry="1.5"/><circle cx="37.2" cy="58.1" r="8.8"/><circle class="hyst4" cx="39.5" cy="54.2" r="2.3"/><path class="hyst9" d="M67.5 58.2c0-.1-2.3 1-2.9 1.1-.6-.1-2.9-1.2-2.9-1.1h5.8z"/><path class="hyst1" d="M50 57.7s-7.9 5.4-12.2 10.7c-4.3 5.3-4.2 6.3-4.2 6.3l3.1 1.4s4.4-8.3 9.8-11.4 6.1-5.7 6.1-5.7L50 57.7z"/><path class="hyst3" d="M32.7 41.7S30 49.1 24 52.2c0 0 9.4-1.1 8.7-10.5zM95.8 41.7s2.7 7.4 8.7 10.5c0 0-9.4-1.1-8.7-10.5zM78.7 55.5s-5.9-6.2-13.8-6.4h.1.1c-8 .2-13.8 6.4-13.8 6.4 6.9-4.8 12.8-4.7 13.8-4.7-.1 0 6.7-.1 13.6 4.7zM71.8 42.5s-3-4.2-7-4.3h.2c-3 .1-6.9 4.3-6.9 4.3 3.4-3.3 6.9-3.2 6.9-3.2s3.3-.1 6.8 3.2zM37.2 73.2s-4.7 2.3-8.1.9H29c-3-1.7-4.5-6.8-4.5-6.8s3 9 12.7 5.9zM92 73.2s4.7 2.3 8.1.9c4-1.7 4.6-6.8 4.6-6.8s-3 9-12.7 5.9z"/><path class="hyst3" d="M42.6 41.2c2.6-.5 6.9-.6 10.3.5 4.3 1.5.8 7 1.7 7.3.9.3 2.1-3.8 10.1-3.4 8.1.4 9 4 10.1 3.4s-1.1-10 11-7.8c0 0-12.7-3.4-12.1 5.8 0 0-7.3-5.6-17.5-.6.1 0 2.7-8.6-13.6-5.2zM86.9 41.2c.2 0 .3.1.4.1.1 0-.1-.1-.4-.1zM86.9 41.2zM39.1 28.9S28.3 42.5 26.7 47.7c-1.6 5.3-2.8 27-4.2 30.1l-5-21.4 9.2-22.3 12.4-5.2zM89.9 28.9s10.8 13.6 12.4 18.8c1.6 5.3 2.8 27 4.2 30.1l5-21.4-9.2-22.3-12.4-5.2z"/><path class="hyst7" d="M89.4 28.9s11.6 9.7 15 20.9c3.4 11.2 2 24.8 4.6 26.5 3.7 2.4 7.9-11.9 9.3-13.4 2.2-2.4 9.5-8.5 10-9.6.5-1.1-14.8-17.8-21.5-21.1-8.1-3.8-18.1-4.1-17.4-3.3z"/><path class="hyst8" d="M99.3 34.9s13.7 17.5 13.5 39.3l5.5-11.2c-.1 0-4.9-14.3-19-28.1z"/><path class="hyst7" d="M39.1 28.9s-11.6 9.7-15 20.9-2 24.8-4.6 26.5c-3.7 2.4-7.9-11.9-9.3-13.4C8 60.5.7 54.4.2 53.3-.3 52.2 15 35.5 21.7 32.2c8.1-3.8 18.1-4.1 17.4-3.3z"/><path class="hyst8" d="M29.2 34.9S15.5 52.4 15.7 74.2L10.3 63s4.8-14.3 18.9-28.1z"/><path class="hyst3" d="M21.8 74.6s1 5.4 2.6 7.1.5-1.3.5-1.3-1.7-.9-1.4-7.8-1.7 2-1.7 2zM107.1 74.6s-1 5.4-2.6 7.1-.5-1.3-.5-1.3 1.7-.9 1.4-7.8 1.7 2 1.7 2z"/><g><circle class="hyst8" cx="54.5" cy="70.5" r=".8"/><circle class="hyst8" cx="49.9" cy="75.3" r=".8"/><circle class="hyst8" cx="48.4" cy="70.5" r=".8"/></g><g><circle class="hyst8" cx="74" cy="70.5" r=".8"/><circle class="hyst8" cx="78.6" cy="75.3" r=".8"/><circle class="hyst8" cx="80.1" cy="70.5" r=".8"/></g></symbol><symbol viewBox="0 0 50 50" id="puppet" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -247)" fill="#fbc02d"><path stroke-width=".283" d="M11.559 249.467h13.587v13.587H11.559zM27.435 265.056h13.587v13.587H27.435zM11.559 281.074h13.587v13.587H11.559z"/><path stroke-width=".256" d="M16.62 251.615l18.305 18.305-3.236 3.236-18.305-18.305z"/><path stroke-width=".256" d="M37.834 271.331L19.53 289.636l-3.237-3.237 18.305-18.304z"/></g></symbol><symbol viewBox="0 0 100 99.999997" id="purescript" xmlns="http://www.w3.org/2000/svg"><path clip-path="url(#SVGID_2_)" d="M98.079 38.548L79.22 19.68l-5.087 5.088L90.447 41.09 74.134 57.41l5.087 5.087 18.858-18.86a3.59 3.59 0 0 0 1.055-2.55 3.578 3.578 0 0 0-1.055-2.54M25.483 42.794l-5.09-5.089L1.53 56.568a3.566 3.566 0 0 0-1.05 2.545c0 .961.373 1.863 1.05 2.542L20.394 80.52l5.089-5.086L9.162 59.113z" fill="#42a5f5" stroke-width="1.192"/><path clip-path="url(#SVGID_2_)" transform="matrix(1.19175 0 0 1.19175 -306.84 -629.047)" fill="#42a5f5" d="M281.841 551.736l6.461 6.037h28.379l-6.461-6.037zM288.302 566.861l-6.463 6.035h28.381l6.463-6.035zM281.838 581.982l6.464 6.035h28.381l-6.463-6.035z"/></symbol><symbol viewBox="0 0 24 24" id="python" xmlns="http://www.w3.org/2000/svg"><path d="M19.14 7.5A2.86 2.86 0 0 1 22 10.36v3.78A2.86 2.86 0 0 1 19.14 17H12c0 .39.32.96.71.96H17v1.68a2.86 2.86 0 0 1-2.86 2.86H9.86A2.86 2.86 0 0 1 7 19.64v-3.75a2.85 2.85 0 0 1 2.86-2.85h5.25a2.85 2.85 0 0 0 2.85-2.86V7.5h1.18m-4.28 11.79c-.4 0-.72.3-.72.89 0 .59.32.71.72.71a.71.71 0 0 0 .71-.71c0-.59-.32-.89-.71-.89m-10-1.79A2.86 2.86 0 0 1 2 14.64v-3.78A2.86 2.86 0 0 1 4.86 8H12c0-.39-.32-.96-.71-.96H7V5.36A2.86 2.86 0 0 1 9.86 2.5h4.28A2.86 2.86 0 0 1 17 5.36v3.75a2.85 2.85 0 0 1-2.86 2.85H8.89a2.85 2.85 0 0 0-2.85 2.86v2.68H4.86M9.14 5.71c.4 0 .72-.3.72-.89 0-.59-.32-.71-.72-.71-.39 0-.71.12-.71.71s.32.89.71.89z"/><path d="M9.264 22.379c-.895-.24-1.581-.799-1.947-1.582-.228-.489-.237-.606-.238-2.957-.001-2.745.057-3.074.666-3.785.193-.226.568-.517.833-.648.47-.23.579-.239 3.839-.288 3.131-.048 3.386-.065 3.814-.264.626-.291 1.07-.687 1.4-1.247.27-.46.278-.522.311-2.29l.034-1.82.932.051c1.075.058 1.504.211 2.098.748.853.77.869.841.869 3.957 0 2.434-.02 2.783-.18 3.075a3.365 3.365 0 0 1-1.337 1.33l-.517.273-3.95.031-3.951.031.068.274c.037.151.164.377.282.503.209.224.262.229 2.433.229h2.22v1.05c0 1.653-.394 2.437-1.54 3.072l-.545.302-2.644.018c-1.455.01-2.782-.018-2.95-.063zm6.12-1.692c.22-.222.253-.325.206-.675-.07-.523-.278-.73-.732-.73-.467 0-.672.217-.735.78-.042.372-.012.496.163.672.3.3.77.28 1.097-.047z" fill="#fc0" stroke="#fc0" stroke-width=".102"/><path d="M9.349 22.38c-.911-.15-1.936-1.074-2.176-1.963-.073-.273-.101-1.279-.079-2.868.033-2.317.047-2.473.27-2.926.13-.263.401-.623.603-.8.674-.592.87-.63 3.484-.675 4.399-.076 4.927-.166 5.705-.967.642-.662.706-.9.774-2.883l.061-1.784.951.055c.523.031 1.11.122 1.304.204.54.225 1.358 1.042 1.472 1.47.153.572.243 3.18.16 4.617-.071 1.23-.093 1.327-.395 1.78-.193.288-.577.647-.966.903l-.647.425-3.922.008c-2.157.004-3.942.028-3.966.052-.115.115.354.82.587.883.14.038 1.181.073 2.314.079l2.06.01v.91c0 1.739-.326 2.446-1.454 3.162l-.631.4-2.543-.011c-1.398-.007-2.733-.043-2.966-.081zm5.98-1.718c.285-.256.313-.328.251-.658-.09-.483-.301-.682-.722-.682-.436 0-.625.193-.715.73-.065.384-.044.453.2.663.358.308.595.295.985-.053z" fill="#fdd835" stroke-width=".102"/><path d="M4.281 17.396c-.88-.215-1.714-.935-2.024-1.747-.149-.389-.168-.804-.142-3.041.027-2.26.054-2.638.215-2.962.259-.519.851-1.092 1.392-1.346.437-.206.632-.217 4.408-.245l3.95-.03-.067-.275a1.367 1.367 0 0 0-.282-.504c-.21-.224-.263-.23-2.433-.23h-2.22l.002-1.143c.003-1.338.157-1.795.84-2.493.746-.763 1.103-.838 4.025-.838 2.961 0 3.28.06 4.067.768.37.333.572.621.728 1.037.201.539.213.735.183 3.072-.035 2.777-.045 2.824-.78 3.598-.787.829-.76.824-4.59.883-3.812.06-3.797.057-4.61.806-.765.706-.917 1.2-.964 3.133l-.04 1.653-.677-.01c-.371-.007-.813-.045-.98-.086zM9.59 5.551c.237-.204.286-.326.286-.72 0-.547-.201-.763-.71-.763-.502 0-.765.248-.765.724 0 .492.141.782.439.902.345.14.444.12.75-.143z" fill="#3c78aa"/></symbol><symbol viewBox="0 0 24 24" id="r" xmlns="http://www.w3.org/2000/svg"><path d="M11.956 4.05c-5.694 0-10.354 3.106-10.354 6.947 0 3.396 3.686 6.212 8.531 6.813v2.205h3.53V17.82c.88-.093 1.699-.259 2.475-.497l1.43 2.692h3.996l-2.402-4.048c1.936-1.263 3.147-3.034 3.147-4.97 0-3.841-4.659-6.947-10.354-6.947m1.584 2.712c4.349 0 7.558 1.45 7.558 4.753 0 1.77-.952 3.013-2.505 3.779a1.081 1.081 0 0 1-.228-.156c-.373-.165-.994-.352-.994-.352s3.085-.227 3.085-3.302-3.23-3.127-3.23-3.127h-7.092v7.413c-2.64-.766-4.462-2.392-4.462-4.255 0-2.63 3.52-4.753 7.868-4.753m.156 4.12h2.143s.983-.05.983.974c0 1.004-.983 1.004-.983 1.004h-2.143v-1.977m-.031 4.566h.952c.186 0 .28.052.445.207.135.103.28.3.404.476-.57.073-1.17.104-1.801.104z" fill="#1976d2" stroke-width="1.035"/></symbol><symbol viewBox="0 0 24 24" id="raml" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="razor" xmlns="http://www.w3.org/2000/svg"><path d="M15.45 11.91c-.11-2.21-1.75-3.54-3.73-3.54h-.08c-2.29 0-3.55 1.8-3.55 3.84 0 2.29 1.53 3.74 3.54 3.74 2.25 0 3.72-1.65 3.83-3.59m-3.81-5.97c1.53 0 2.97.68 4.02 1.74 0-.51.33-.89.83-.89h.11c.74 0 .89.7.89.92v7.9c-.04.52.54.78.87.44 1.27-1.29 2.78-6.69-.79-9.81-3.33-2.92-7.8-2.44-10.18-.8-2.52 1.74-4.14 5.61-2.57 9.22 1.71 3.95 6.61 5.13 9.52 3.95 1.48-.59 2.15 1.4.65 2.05-2.34.99-8.77.89-11.78-4.32-2.03-3.52-1.93-9.71 3.46-12.92C10.81 1.42 16.24 2.1 19.5 5.5c3.45 3.6 3.25 10.3-.1 12.91-1.51 1.18-3.76.03-3.74-1.7l-.02-.56a5.611 5.611 0 0 1-3.99 1.66C8.63 17.81 6 15.15 6 12.13c0-3.05 2.63-5.74 5.65-5.74z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="react" xmlns="http://www.w3.org/2000/svg"><path d="M12 10.11c1.03 0 1.87.84 1.87 1.89 0 1-.84 1.85-1.87 1.85-1.03 0-1.87-.85-1.87-1.85 0-1.05.84-1.89 1.87-1.89M7.37 20c.63.38 2.01-.2 3.6-1.7-.52-.59-1.03-1.23-1.51-1.9a22.7 22.7 0 0 1-2.4-.36c-.51 2.14-.32 3.61.31 3.96m.71-5.74l-.29-.51c-.11.29-.22.58-.29.86.27.06.57.11.88.16l-.3-.51m6.54-.76l.81-1.5-.81-1.5c-.3-.53-.62-1-.91-1.47C13.17 9 12.6 9 12 9c-.6 0-1.17 0-1.71.03-.29.47-.61.94-.91 1.47L8.57 12l.81 1.5c.3.53.62 1 .91 1.47.54.03 1.11.03 1.71.03.6 0 1.17 0 1.71-.03.29-.47.61-.94.91-1.47M12 6.78c-.19.22-.39.45-.59.72h1.18c-.2-.27-.4-.5-.59-.72m0 10.44c.19-.22.39-.45.59-.72h-1.18c.2.27.4.5.59.72M16.62 4c-.62-.38-2 .2-3.59 1.7.52.59 1.03 1.23 1.51 1.9.82.08 1.63.2 2.4.36.51-2.14.32-3.61-.32-3.96m-.7 5.74l.29.51c.11-.29.22-.58.29-.86-.27-.06-.57-.11-.88-.16l.3.51m1.45-7.05c1.47.84 1.63 3.05 1.01 5.63 2.54.75 4.37 1.99 4.37 3.68 0 1.69-1.83 2.93-4.37 3.68.62 2.58.46 4.79-1.01 5.63-1.46.84-3.45-.12-5.37-1.95-1.92 1.83-3.91 2.79-5.38 1.95-1.46-.84-1.62-3.05-1-5.63-2.54-.75-4.37-1.99-4.37-3.68 0-1.69 1.83-2.93 4.37-3.68-.62-2.58-.46-4.79 1-5.63 1.47-.84 3.46.12 5.38 1.95 1.92-1.83 3.91-2.79 5.37-1.95M17.08 12c.34.75.64 1.5.89 2.26 2.1-.63 3.28-1.53 3.28-2.26 0-.73-1.18-1.63-3.28-2.26-.25.76-.55 1.51-.89 2.26M6.92 12c-.34-.75-.64-1.5-.89-2.26-2.1.63-3.28 1.53-3.28 2.26 0 .73 1.18 1.63 3.28 2.26.25-.76.55-1.51.89-2.26m9 2.26l-.3.51c.31-.05.61-.1.88-.16-.07-.28-.18-.57-.29-.86l-.29.51m-2.89 4.04c1.59 1.5 2.97 2.08 3.59 1.7.64-.35.83-1.82.32-3.96-.77.16-1.58.28-2.4.36-.48.67-.99 1.31-1.51 1.9M8.08 9.74l.3-.51c-.31.05-.61.1-.88.16.07.28.18.57.29.86l.29-.51m2.89-4.04C9.38 4.2 8 3.62 7.37 4c-.63.35-.82 1.82-.31 3.96a22.7 22.7 0 0 1 2.4-.36c.48-.67.99-1.31 1.51-1.9z" fill="#00bcd4"/></symbol><symbol viewBox="0 0 24 24" id="readme" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="reason" xmlns="http://www.w3.org/2000/svg"><path d="M3 3v18h18V3H3zm5.119 8.993h2.798c.382 0 .71.025.985.075.275.05.534.159.774.326.244.168.435.386.577.654.145.265.218.598.218 1 0 .552-.112 1.001-.335 1.35-.22.348-.536.638-.947.87l2.16 3.203H12.31l-1.763-2.742h-.77v2.742H8.12v-7.478zm6.594 0h4.676v1.447h-3.018v1.29h2.802v1.447h-2.802v1.848h3.018v1.446h-4.676v-7.478zM9.778 13.37v2.014h.513c.266 0 .49-.014.67-.044.18-.03.329-.1.45-.207a.96.96 0 0 0 .253-.34c.055-.128.082-.297.082-.508 0-.187-.034-.35-.1-.483a.698.698 0 0 0-.343-.317 1.086 1.086 0 0 0-.395-.095 6.012 6.012 0 0 0-.526-.02h-.604z" fill="#f44336" stroke-width="1.067"/></symbol><symbol viewBox="0 0 172 193" id="restql" xmlns="http://www.w3.org/2000/svg"><title>Group</title><g transform="translate(14.767 16.713) scale(.82795)" fill="none"><path d="M171.39 55.799c-.975-6.147-4.673-11.642-10.15-14.805L96.381 3.546C93.217 1.72 89.615.756 85.964.756s-7.253.964-10.415 2.788L10.69 40.992A20.896 20.896 0 0 0 .272 59.035v74.89a20.894 20.894 0 0 0 10.416 18.042l64.859 37.446c3.165 1.827 6.767 2.791 10.417 2.791s7.252-.964 10.415-2.79l64.859-37.445c5.479-3.166 9.178-8.66 10.152-14.808zm-16.516 85.147L90.017 178.39a8.104 8.104 0 0 1-8.108 0l-64.857-37.444a8.109 8.109 0 0 1-4.053-7.021v-74.89a8.109 8.109 0 0 1 4.053-7.021l64.857-37.446c1.254-.725 2.654-1.086 4.054-1.086s2.8.361 4.054 1.086l64.857 37.446a8.106 8.106 0 0 1 4.053 7.021v74.89a8.109 8.109 0 0 1-4.053 7.021z" fill="#83e8c2"/><path d="M158.93 59.035a8.109 8.109 0 0 0-4.053-7.021L90.02 14.568c-1.254-.725-2.654-1.086-4.054-1.086s-2.8.361-4.054 1.086L17.055 52.014a8.106 8.106 0 0 0-4.053 7.021v74.89a8.109 8.109 0 0 0 4.053 7.021l64.857 37.444a8.104 8.104 0 0 0 8.108 0l64.857-37.444a8.109 8.109 0 0 0 4.053-7.021zm-46.766 31.681c.119-.069.242-.118.365-.149.044-.012.088-.01.131-.018.076-.012.152-.029.228-.029l.015.001c.02.001.038.005.059.006.093.005.184.019.273.04l.1.03c.077.025.15.057.223.095.028.014.057.027.084.043.094.057.184.122.263.199.007.008.013.017.021.024.07.071.133.15.188.235.018.029.033.059.05.09.04.072.072.148.099.229a1.512 1.512 0 0 1 .081.46v16.209l-3.278 1.893a1.548 1.548 0 0 0-.678.83 1.533 1.533 0 0 0-.098.514v3.785l-14.038 8.104-.01.004a1.55 1.55 0 0 1-.354.146c-.045.012-.09.011-.135.018-.074.012-.15.029-.225.029l-.014-.001c-.02-.001-.039-.005-.059-.006a1.463 1.463 0 0 1-.273-.041c-.034-.008-.066-.019-.1-.03a1.318 1.318 0 0 1-.223-.094c-.029-.015-.057-.027-.084-.044a1.45 1.45 0 0 1-.263-.198c-.009-.008-.015-.019-.023-.027a1.495 1.495 0 0 1-.185-.232c-.019-.029-.034-.06-.051-.09a1.422 1.422 0 0 1-.098-.229 1.702 1.702 0 0 1-.033-.101 1.487 1.487 0 0 1-.048-.358l-.001-.002v-20.053a1.446 1.446 0 0 1 .727-1.255zM85.24 31.369a1.449 1.449 0 0 1 1.452 0l45.741 26.41a1.45 1.45 0 0 1 0 2.512l-17.366 10.027a1.457 1.457 0 0 1-1.452 0l-15.49-8.943 1.727-.996a1.552 1.552 0 0 0 0-2.688l-13.111-7.57c-.239-.139-.508-.207-.775-.207s-.535.068-.775.207l-3.278 1.893-14.038-8.104a1.451 1.451 0 0 1 0-2.513zM57.59 47.558c.251 0 .501.065.726.194l15.489 8.942-1.727.997a1.552 1.552 0 0 0 0 2.688l1.727.996-15.488 8.943a1.457 1.457 0 0 1-1.452 0L39.499 60.291a1.45 1.45 0 0 1 0-2.512l17.366-10.027c.225-.129.475-.194.725-.194zm-9.56 92.328c-.241 0-.489-.062-.724-.196l-17.365-10.026a1.45 1.45 0 0 1-.726-1.256V75.59c0-.847.694-1.453 1.452-1.453.242 0 .49.062.724.197l17.366 10.025c.449.26.726.738.726 1.257v17.886l-1.727-.997a1.552 1.552 0 0 0-2.327 1.344v15.139c0 .555.295 1.067.775 1.344l3.278 1.894v16.209a1.45 1.45 0 0 1-1.452 1.451zm29.828 14.929a1.452 1.452 0 0 1-2.177 1.257l-17.365-10.026a1.452 1.452 0 0 1-.726-1.257v-17.885l1.726.996c.25.145.515.211.773.211.811 0 1.554-.648 1.554-1.555v-1.993l15.489 8.942c.449.26.726.738.726 1.257zm0-32.768c0 .127-.02.246-.049.36-.009.035-.021.067-.032.101-.026.08-.059.157-.099.229-.017.03-.032.061-.05.09a1.48 1.48 0 0 1-.188.235l-.021.025a1.51 1.51 0 0 1-.264.199c-.026.016-.055.028-.082.043a1.597 1.597 0 0 1-.324.124 1.362 1.362 0 0 1-.278.041c-.018.001-.036.006-.055.006l-.015.001c-.077 0-.155-.018-.233-.03-.043-.007-.084-.005-.125-.017a1.484 1.484 0 0 1-.366-.149l-14.035-8.104v-3.784a1.545 1.545 0 0 0-.776-1.343l-3.276-1.892V91.976c0-.127.02-.246.049-.361.009-.034.021-.066.032-.1a1.33 1.33 0 0 1 .099-.229c.017-.03.032-.062.051-.091.054-.084.116-.163.187-.234l.021-.025c.079-.076.168-.142.263-.199.027-.016.056-.029.084-.043a1.476 1.476 0 0 1 .601-.166c.019 0 .036-.005.055-.005l.015-.001c.078 0 .157.018.236.03.04.007.081.005.122.017.124.031.246.08.366.149l17.361 10.023a1.456 1.456 0 0 1 .726 1.259zm-9.984-45.373a1.448 1.448 0 0 1-.544-.55 1.466 1.466 0 0 1 0-1.413c.121-.219.303-.41.544-.55l14.038-8.104 3.277 1.892c.48.276 1.071.276 1.551 0l3.278-1.893 14.038 8.105a1.45 1.45 0 0 1 0 2.513L86.691 86.7a1.447 1.447 0 0 1-1.452 0zm74.842 51.733c0 .518-.276.997-.726 1.256l-45.741 26.409a1.452 1.452 0 0 1-2.177-1.257v-20.053c0-.519.277-.997.727-1.257l15.488-8.941v1.992c0 .906.743 1.555 1.553 1.555.26 0 .523-.066.774-.21l13.11-7.57a1.55 1.55 0 0 0 .776-1.344v-3.784l14.038-8.105a1.452 1.452 0 0 1 2.177 1.257v20.052zm0-32.764c0 .519-.276.997-.726 1.256l-15.489 8.943v-1.993c0-.906-.744-1.554-1.554-1.554a1.519 1.519 0 0 0-.773.21l-1.727.996V85.616c0-.519.277-.997.727-1.257l17.365-10.025c.234-.135.482-.197.724-.197.758 0 1.453.606 1.453 1.453z" fill="#111d5a"/><g fill="#83e8c2"><path d="M59.402 90.568zM94.485 123.06zM94.771 123.29zM77.775 122.51zM77.072 123.33zM77.418 123.09zM77.856 122.05zM76.749 123.45zM94.119 122.41zM77.131 133.51l-15.489-8.942v1.993c0 .906-.743 1.555-1.554 1.555a1.53 1.53 0 0 1-.773-.211l-1.726-.996v17.885c0 .519.276.997.726 1.257l17.365 10.026a1.452 1.452 0 0 0 2.177-1.257v-20.053a1.454 1.454 0 0 0-.726-1.257zM94.25 122.74zM110.28 111.42zM94.494 100.98c.088-.089.189-.168.303-.232l17.365-10.026-17.365 10.026a1.392 1.392 0 0 0-.303.232zM77.627 122.83zM58.027 90.936zM58.374 90.693zM59.044 90.521l-.015.001c.083-.001.167.015.251.029-.079-.012-.158-.03-.236-.03zM57.819 91.195zM58.696 90.568zM57.589 91.977zM76.043 123.46zM57.67 91.516zM75.677 123.31l-14.035-8.11zM76.401 123.5l.015-.001c-.082.001-.166-.016-.248-.029.078.012.156.03.233.03zM112.16 90.716zM77.662 101.27zM113.64 90.734zM96.237 123.31zM113.33 90.597zM112.89 90.52c-.075 0-.151.018-.228.029.081-.014.162-.029.242-.028l-.014-.001zM141.26 74.137c-.241 0-.489.062-.724.197l-17.365 10.025c-.449.26-.727.738-.727 1.257v17.885l1.727-.996c.25-.145.515-.211.773-.21.81 0 1.554.647 1.554 1.554v1.993l15.489-8.943a1.45 1.45 0 0 0 .726-1.256V75.59c0-.847-.695-1.453-1.453-1.453zM112.96 90.526zM95.523 123.5c.074 0 .15-.018.225-.029-.08.013-.159.028-.238.028l.013.001zM95.451 123.5zM85.238 86.7zM95.078 123.43zM141.26 106.9c-.241 0-.489.062-.724.196l-14.038 8.105v3.784c0 .555-.296 1.067-.776 1.344l-13.11 7.57c-.251.144-.515.21-.774.21-.81 0-1.553-.648-1.553-1.555v-1.992l-15.488 8.941c-.449.26-.727.738-.727 1.257v20.053a1.452 1.452 0 0 0 2.177 1.257l45.741-26.409a1.45 1.45 0 0 0 .726-1.256v-20.053a1.454 1.454 0 0 0-1.454-1.452zM67.871 41.396a1.451 1.451 0 0 0 0 2.513l14.038 8.104 3.278-1.893c.24-.139.508-.207.775-.207s.536.068.775.207l13.111 7.57a1.552 1.552 0 0 1 0 2.688l-1.727.996 15.49 8.943a1.457 1.457 0 0 0 1.452 0l17.366-10.027a1.45 1.45 0 0 0 0-2.512l-45.741-26.41a1.449 1.449 0 0 0-1.452 0zM39.497 57.779a1.45 1.45 0 0 0 0 2.512l17.366 10.027a1.457 1.457 0 0 0 1.452 0l15.488-8.943-1.727-.996a1.552 1.552 0 0 1 0-2.688l1.727-.997-15.489-8.942a1.458 1.458 0 0 0-1.451 0zM49.481 138.43v-16.209l-3.278-1.894a1.55 1.55 0 0 1-.775-1.344v-15.139c0-.906.743-1.555 1.554-1.554.259 0 .523.065.773.21l1.727.997V85.611a1.45 1.45 0 0 0-.726-1.257L31.39 74.33a1.436 1.436 0 0 0-.724-.197c-.758 0-1.452.606-1.452 1.453v52.817c0 .518.276.997.726 1.256l17.365 10.026a1.45 1.45 0 0 0 2.176-1.255zM114.34 108.18l-3.278 1.893 3.278-1.893V91.971zM114.11 91.193zM114.16 91.283z"/></g><g fill="#de5941"><path d="M94.494 100.98a1.45 1.45 0 0 0-.424 1.023v20.053l.001.002c0 .126.02.244.048.358.01.034.021.066.033.101.026.08.059.156.098.229.017.03.032.061.051.09.055.084.115.162.185.232.009.009.015.02.023.027.079.077.169.142.263.198.027.017.055.029.084.044a1.46 1.46 0 0 0 .596.165c.02.001.039.005.059.006.079 0 .158-.016.238-.028.045-.007.09-.006.135-.018.119-.031.238-.08.354-.146l.01-.004 14.038-8.104v-3.785c0-.18.04-.35.098-.514.122-.343.353-.643.678-.83l3.278-1.893V91.977c0-.127-.021-.246-.049-.361-.009-.033-.021-.065-.032-.099a1.266 1.266 0 0 0-.099-.229c-.017-.031-.032-.061-.05-.09a1.425 1.425 0 0 0-.188-.235l-.021-.024a1.41 1.41 0 0 0-.263-.199c-.027-.016-.056-.029-.084-.043a1.509 1.509 0 0 0-.323-.125 1.591 1.591 0 0 0-.273-.04c-.021-.001-.039-.005-.059-.006-.08-.001-.161.015-.242.028-.043.008-.087.006-.131.018-.123.031-.246.08-.365.149l-17.365 10.026a1.447 1.447 0 0 0-.302.233zM77.13 100.74L59.769 90.717a1.424 1.424 0 0 0-.366-.149c-.041-.012-.082-.01-.122-.017-.084-.015-.168-.03-.251-.029-.019 0-.036.005-.055.005-.095.005-.188.02-.278.041-.034.009-.065.02-.099.03a1.406 1.406 0 0 0-.224.095c-.028.014-.057.027-.084.043a1.515 1.515 0 0 0-.263.199l-.021.025c-.07.071-.133.15-.187.234-.019.029-.034.061-.051.091-.04.073-.072.149-.099.229a1.463 1.463 0 0 0-.081.461v16.206l3.276 1.892a1.547 1.547 0 0 1 .776 1.343v3.784l14.035 8.104c.119.068.242.117.366.149.041.012.082.01.125.017.082.014.166.03.248.029.019 0 .037-.005.055-.006.095-.004.188-.019.278-.041.034-.008.065-.019.099-.029.077-.025.152-.058.225-.095.027-.015.056-.027.082-.043.095-.058.185-.123.264-.199l.021-.025c.07-.071.133-.15.188-.235.018-.029.033-.06.05-.09.04-.072.072-.149.099-.229a1.448 1.448 0 0 0 .081-.461v-20.047a1.456 1.456 0 0 0-.726-1.259zM86.689 86.7l17.365-10.026a1.45 1.45 0 0 0 0-2.513l-14.038-8.105-3.278 1.893a1.556 1.556 0 0 1-1.551 0l-3.277-1.892-14.038 8.104c-.241.14-.423.331-.544.55a1.466 1.466 0 0 0 0 1.413c.121.218.303.41.544.55L85.238 86.7a1.447 1.447 0 0 0 1.451 0z"/></g></g></symbol><symbol viewBox="0 0 24 24" id="riot" xmlns="http://www.w3.org/2000/svg"><defs><path d="M13.26 3.04l.58.05.54.07.52.09.49.11.46.13.44.14.41.16.39.17.36.19.33.21.32.22.29.23.26.25.22.22.2.22.19.24.17.24.15.25.15.26.12.27.12.28.1.29.08.31.07.31.05.32.04.34.02.35.01.37v.05l-.02.51-.05.49-.09.48-.13.45-.15.43-.19.4-.22.39-.26.37-.28.34-.31.33-.33.3-.37.28-.39.27-.41.24-.44.22L21 21h-7.04l-3.48-5.14H9.17V21H3V3h9.01l.64.01.61.03zm-4.09 8.52h2.66l.99-.11.75-.35.47-.55.16-.74v-.05l-.17-.75-.47-.54-.74-.32-.96-.11H9.17v3.52z" id="ija"/></defs><use xlink:href="#ija" fill="#ff1744"/><use xlink:href="#ija" fill-opacity="0" stroke="#000" stroke-opacity="0"/></symbol><symbol viewBox="0 0 24 24" id="robot" xmlns="http://www.w3.org/2000/svg"><path d="M12.05 2.804a1.787 1.787 0 0 1 1.788 1.788c0 .661-.357 1.242-.893 1.546v1.135h.893a6.256 6.256 0 0 1 6.256 6.256h.894a.894.894 0 0 1 .893.893v2.681a.894.894 0 0 1-.893.894h-.894v.894a1.787 1.787 0 0 1-1.787 1.787H5.795a1.787 1.787 0 0 1-1.787-1.787v-.894h-.894a.894.894 0 0 1-.894-.894v-2.68a.894.894 0 0 1 .894-.894h.894a6.256 6.256 0 0 1 6.255-6.256h.894V6.138a1.773 1.773 0 0 1-.894-1.546 1.787 1.787 0 0 1 1.788-1.788m-4.022 9.83a2.234 2.234 0 0 0-2.234 2.235 2.234 2.234 0 0 0 2.234 2.234 2.234 2.234 0 0 0 2.234-2.234 2.234 2.234 0 0 0-2.234-2.234m8.043 0a2.234 2.234 0 0 0-2.234 2.234 2.234 2.234 0 0 0 2.234 2.234 2.234 2.234 0 0 0 2.235-2.234 2.234 2.234 0 0 0-2.235-2.234z" fill="#ff5722" stroke-width=".894"/></symbol><symbol viewBox="100 100 800 800" id="rollup" xmlns="http://www.w3.org/2000/svg"><style>.ilst0{fill:url(#ilXMLID_4_)}.ilst1{fill:url(#ilXMLID_5_)}.ilst2{fill:url(#ilXMLID_8_)}.ilst3{fill:url(#ilXMLID_9_)}.ilst4{fill:url(#ilXMLID_11_)}.ilst5{opacity:.3;fill:url(#ilXMLID_16_)}</style><g id="ilXMLID_14_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_4_" x1="444.47" x2="598.47" y1="526.05" y2="562.05" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_15_" class="ilst0" d="M721 410c0-33.6-8.8-65.1-24.3-92.4-41.1-42.3-130.5-52.1-152.7-.2-22.8 53.2 38.3 112.4 65 107.7 34-6-6-84-6-84 52 98 40 68-54 158S359 779 345 787c-.6.4-1.2.7-1.9 1h368.7c6.5 0 10.7-6.9 7.8-12.7l-96.4-190.8c-2.1-4.1-.6-9.2 3.4-11.5C683 540.6 721 479.8 721 410z" fill="url(#ilXMLID_4_)"/></g><g id="ilXMLID_2_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_5_" x1="420.38" x2="696.38" y1="475" y2="689" gradientUnits="userSpaceOnUse"><stop stop-color="#BF3338" offset="0"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_10_" class="ilst1" d="M721 410c0-33.6-8.8-65.1-24.3-92.4-41.1-42.3-130.5-52.1-152.7-.2-22.8 53.2 38.3 112.4 65 107.7 34-6-6-84-6-84 52 98 40 68-54 158S359 779 345 787c-.6.4-1.2.7-1.9 1h368.7c6.5 0 10.7-6.9 7.8-12.7l-96.4-190.8c-2.1-4.1-.6-9.2 3.4-11.5C683 540.6 721 479.8 721 410z" fill="url(#ilXMLID_5_)"/></g><linearGradient id="ilXMLID_8_" x1="429.39" x2="469.39" y1="517.16" y2="559.16" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_3_" class="ilst2" d="M329.82 813.46c15.58-8.903 122.41-220.34 227.02-320.5s117.96-66.771 60.094-175.83c0 0-221.46 310.49-301.58 464.06" fill="url(#ilXMLID_8_)" stroke-width="1.113"/><g id="ilXMLID_7_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_9_" x1="502.11" x2="490.11" y1="589.46" y2="417.46" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_12_" class="ilst3" d="M373 537c134.4-247.1 152-272 222-272 36.8 0 73.9 16.6 97.9 46.1-32.7-52.7-90.6-88-156.9-89H307.7c-4.8 0-8.7 3.9-8.7 8.7V691c13.6-35.1 36.7-85.3 74-154z" fill="url(#ilXMLID_9_)"/></g><linearGradient id="ilXMLID_11_" x1="450.12" x2="506.94" y1="514.21" y2="552.85" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FBB040" offset="0"/><stop stop-color="#FB8840" offset="1"/></linearGradient><path id="ilXMLID_6_" class="ilst4" d="M556.84 492.96c-104.61 100.16-211.44 311.6-227.02 320.5s-41.732 10.016-55.643-5.564c-14.801-16.582-37.837-43.401 86.802-272.65 149.57-274.99 169.15-302.7 247.05-302.7 40.953 0 82.24 18.473 108.95 51.302 1.447 2.337 2.893 4.785 4.34 7.233-45.738-47.074-145.23-57.98-169.93-.222-25.373 59.204 42.622 125.08 72.335 119.85 37.837-6.677-6.677-93.48-6.677-93.48 57.757 108.95 44.403 75.563-60.205 175.72z" fill="url(#ilXMLID_11_)" stroke-width="1.113"/><linearGradient id="ilXMLID_16_" x1="508.33" x2="450.33" y1="295.76" y2="933.76" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFF" offset="0"/><stop stop-color="#FFF" stop-opacity="0" offset="1"/></linearGradient><path id="ilXMLID_13_" class="ilst5" d="M373.22 547.49c149.57-274.99 169.15-302.7 247.05-302.7 33.719 0 67.661 12.575 93.48 35.277-26.708-30.492-66.326-47.519-105.72-47.519-77.9 0-97.486 27.71-247.05 302.7-124.64 229.25-101.6 256.07-86.802 272.65 2.114 2.337 4.563 4.34 7.122 6.01-13.02-18.919-18.807-62.877 91.922-266.42z" fill="url(#ilXMLID_16_)" opacity=".3" stroke-width="1.113"/></symbol><symbol viewBox="0 0 24 24" id="ruby" xmlns="http://www.w3.org/2000/svg"><path d="M16 9h3l-5 7m-4-7h4l-2 8M5 9h3l2 7m5-12h2l2 3h-3m-5-3h2l1 3h-4M7 4h2L8 7H5m1-5L2 8l10 14L22 8l-4-6H6z" fill="#f44336"/></symbol><symbol viewBox="0 0 144 144" id="rust" xmlns="http://www.w3.org/2000/svg"><path d="M68.252 26.206a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0M25.766 58.451a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m84.97.166a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m-74.661 4.88a3.252 3.252 0 0 0 1.651-4.29l-1.58-3.574h6.214v28.01H29.823a43.847 43.847 0 0 1-1.42-16.738zm25.994.688v-8.256h14.798c.764 0 5.397.883 5.397 4.347 0 2.877-3.553 3.908-6.475 3.908zm-20.203 44.452a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m52.769.166a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m1.101-8.076a3.246 3.246 0 0 0-3.856 2.498l-1.787 8.342a43.847 43.847 0 0 1-36.566-.175l-1.787-8.342a3.246 3.246 0 0 0-3.854-2.497l-7.365 1.581a43.847 43.847 0 0 1-3.808-4.488h35.834c.406 0 .676-.074.676-.443V84.527c0-.369-.27-.442-.676-.442h-10.48V76.05h11.335c1.035 0 5.532.296 6.97 6.045.45 1.768 1.44 7.519 2.116 9.36.674 2.065 3.417 6.19 6.34 6.19h18.501a43.847 43.847 0 0 1-4.06 4.7zm19.898-33.468a43.847 43.847 0 0 1 .093 7.612h-4.499c-.45 0-.631.296-.631.737v2.066c0 4.863-2.742 5.92-5.145 6.19-2.288.258-4.825-.958-5.138-2.358-1.35-7.593-3.6-9.214-7.152-12.016 4.409-2.8 8.996-6.93 8.996-12.457 0-5.97-4.092-9.729-6.881-11.572-3.914-2.58-8.246-3.096-9.415-3.096H39.336A43.847 43.847 0 0 1 63.867 28.52l5.484 5.753a3.243 3.243 0 0 0 4.59.105l6.137-5.869a43.847 43.847 0 0 1 30.017 21.38l-4.201 9.487a3.256 3.256 0 0 0 1.652 4.29zm10.477.154l-.143-1.467 4.327-4.036c.88-.82.55-2.472-.574-2.891l-5.532-2.068-.433-1.428 3.45-4.792c.704-.974.058-2.53-1.127-2.724l-5.833-.949-.7-1.31 2.45-5.38c.502-1.095-.43-2.496-1.636-2.45l-5.92.206-.935-1.135 1.36-5.766c.275-1.17-.913-2.36-2.084-2.085l-5.765 1.359-1.136-.935.207-5.92c.046-1.198-1.357-2.135-2.45-1.637l-5.379 2.452-1.31-.703-.95-5.833c-.193-1.183-1.75-1.83-2.723-1.128l-4.796 3.45-1.425-.432-2.068-5.532c-.42-1.127-2.072-1.452-2.89-.576l-4.036 4.33-1.467-.143-3.117-5.036c-.63-1.02-2.318-1.02-2.946 0l-3.117 5.036-1.467.143-4.037-4.33c-.819-.876-2.47-.551-2.89.576l-2.069 5.532-1.426.432-4.795-3.45c-.974-.703-2.53-.055-2.723 1.128l-.951 5.833-1.31.703-5.379-2.452c-1.093-.5-2.496.439-2.45 1.637l.206 5.92-1.136.935-5.765-1.36c-1.171-.272-2.36.915-2.086 2.086l1.358 5.766-.933 1.135-5.92-.206c-1.193-.035-2.134 1.355-1.637 2.45l2.453 5.38-.703 1.31-5.832.949c-1.185.192-1.827 1.75-1.128 2.724l3.45 4.792-.433 1.428-5.532 2.068c-1.123.42-1.452 2.07-.574 2.891l4.328 4.036-.143 1.467-5.035 3.116c-1.02.63-1.02 2.318 0 2.946l5.035 3.117.143 1.467-4.328 4.037c-.878.818-.549 2.468.574 2.89l5.532 2.068.433 1.428-3.45 4.793c-.701.976-.056 2.532 1.129 2.723l5.831.948.703 1.312-2.453 5.378c-.5 1.093.444 2.5 1.638 2.451l5.917-.207.935 1.136-1.358 5.768c-.275 1.168.915 2.355 2.086 2.08l5.765-1.357 1.137.932-.207 5.921c-.046 1.199 1.357 2.136 2.45 1.636l5.379-2.45 1.31.702.95 5.83c.193 1.187 1.75 1.829 2.725 1.13l4.792-3.453 1.427.435 2.069 5.53c.42 1.123 2.072 1.454 2.89.574l4.037-4.328 1.467.146 3.117 5.035c.628 1.016 2.316 1.018 2.946 0l3.117-5.035 1.467-.146 4.036 4.328c.818.88 2.47.549 2.89-.574l2.068-5.53 1.428-.435 4.793 3.453c.974.699 2.53.055 2.722-1.13l.952-5.83 1.31-.703 5.378 2.451c1.093.5 2.493-.435 2.45-1.636l-.206-5.92 1.135-.933 5.765 1.357c1.171.275 2.36-.912 2.085-2.08l-1.358-5.768.932-1.136 5.92.207c1.194.048 2.138-1.358 1.636-2.451l-2.45-5.378.7-1.312 5.833-.948c1.187-.19 1.831-1.747 1.127-2.723l-3.45-4.793.433-1.428 5.532-2.068c1.125-.422 1.454-2.072.574-2.89l-4.327-4.037.143-1.467 5.035-3.117c1.02-.628 1.021-2.315.001-2.946z" fill="#ff7043" stroke-width="1.146"/></symbol><symbol viewBox="0 0 500 500" id="sass" xmlns="http://www.w3.org/2000/svg"><path d="M422.676 96.573c-12.192-47.839-91.508-63.557-166.575-36.892-44.68 15.877-93.029 40.786-127.81 73.311-41.349 38.675-47.943 72.328-45.216 86.395 9.583 49.622 77.585 82.069 105.535 106.126v.144c-8.246 4.05-68.565 34.584-82.684 65.799-14.893 32.932 2.372 56.556 13.804 59.742 35.424 9.859 71.764-7.866 91.311-37.01 18.853-28.12 17.28-64.422 9.086-82.487 11.3-2.976 24.476-4.314 41.218-2.36 47.248 5.52 56.517 35.017 54.747 47.366-1.77 12.35-11.681 19.14-14.998 21.186-3.317 2.045-4.326 2.766-4.05 4.287.405 2.215 1.94 2.137 4.758 1.652 3.894-.656 24.804-10.042 25.709-32.828 1.14-28.933-26.587-61.302-75.684-60.45-20.216.354-32.933 2.268-42.123 5.69-.681-.774-1.363-1.547-2.084-2.307-30.35-32.382-86.46-55.285-84.088-98.824.866-15.823 6.372-57.5 107.817-108.052 83.104-41.415 149.637-30.009 161.135-4.76 16.427 36.08-35.554 103.137-121.858 112.812-32.88 3.684-50.198-9.059-54.498-13.804-4.536-4.995-5.204-5.218-6.909-4.287-2.753 1.533-1.01 5.938 0 8.574 2.583 6.712 13.15 18.603 31.176 24.515 15.863 5.205 54.459 8.063 101.156-9.99 52.283-20.255 93.12-76.523 81.125-123.548zM200.213 340.34c3.92 14.5 3.487 28.016-.564 40.248a65.289 65.289 0 0 1-3.225 7.97c-3.12 6.477-7.316 12.534-12.442 18.132-15.653 17.069-37.507 23.532-46.88 18.092-10.122-5.874-5.048-29.944 13.083-49.11 19.52-20.636 47.602-33.903 47.602-33.903l-.039-.079 2.465-1.35z" fill="#ec407a" stroke="#ec407a" stroke-width="16.286552999999998"/></symbol><symbol viewBox="0 0 300 300" id="sbt" xmlns="http://www.w3.org/2000/svg"><path d="M105.46 209.517c-7.875 0-13.452-7.521-13.452-15.37v-.327c0-7.848 5.578-13.735 13.452-13.735h164.05c1.476-4.905 2.625-11.446 3.281-17.986h-137.81c-7.875 0-14.273-6.05-14.273-13.898s6.398-13.898 14.273-13.898h137.31c-.82-6.54-1.969-13.081-3.773-17.986h-104.01c-7.875 0-14.273-6.05-14.273-13.898s6.398-13.898 14.273-13.898h91.87c-21.327-37.607-60.864-61.315-106.14-61.315-67.918 0-123.04 54.448-123.04 122.3 0 67.856 55.122 123.28 123.04 123.28 46.59 0 87.112-25.507 107.95-63.114h-152.73z" fill="#0277bd" stroke-width="1.638"/></symbol><symbol viewBox="0 0 256 256" id="scala" xmlns="http://www.w3.org/2000/svg"><path fill="#f44336" fill-rule="evenodd" stroke-width=".3" d="M59.607 50.647l149.097-21.982v49.488L59.607 100.135zM59.593 114.08L208.69 92.098v49.488L59.593 163.568zM59.587 177.358l149.097-21.982v49.488L59.587 226.846z"/><path fill="#f44336" fill-rule="evenodd" stroke-width=".3" d="M62.425 91.414l95.605 30.923-2.832 8.757-95.605-30.922zM113.084 61.13l95.604 30.922-2.832 8.757-95.605-30.922zM62.425 154.79l95.605 30.922-2.833 8.758-95.604-30.923zM113.097 124.408l95.604 30.923-2.832 8.757-95.605-30.922z"/></symbol><symbol viewBox="0 0 24 24" id="settings" xmlns="http://www.w3.org/2000/svg"><path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97 0-.33-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1 0 .33.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.66z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="shaderlab" xmlns="http://www.w3.org/2000/svg"><path d="M9.11 17H6.5l-4.91-5L6.5 7h2.61l1.31-2.26L17.21 3l1.87 6.74L17.77 12l1.31 2.26L17.21 21l-6.79-1.74L9.11 17m.14-.25l5.13 1.38L11.42 13H5.5l3.75 3.75m6.87.38L17.5 12l-1.38-5.13L13.15 12l2.97 5.13M9.25 7.25L5.5 11h5.92l2.96-5.13-5.13 1.38z" fill="#1976d2"/></symbol><symbol viewBox="0 0 24 24" id="slim" xmlns="http://www.w3.org/2000/svg"><path d="M6.959 2.5a4.605 4.605 0 0 0-4.615 4.615v9.957a4.605 4.605 0 0 0 4.615 4.615h9.957a4.605 4.605 0 0 0 4.615-4.615V7.115A4.605 4.605 0 0 0 16.916 2.5zm4.938 2.691a6.811 6.811 0 0 1 6.81 6.813H13.43L9.938 7.287l.699 4.717H5.086a6.811 6.811 0 0 1 6.81-6.813z" fill="#f57f17"/></symbol><symbol id="smarty" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.iust0{fill:#ffce00}</style><path class="iust0" d="M9.14 20.606c0 .556.398.953.954.953h3.812c.556 0 .953-.397.953-.953v-.953H9.141zM12 2.5c-3.653 0-6.671 3.018-6.671 6.671 0 2.303 1.112 4.289 2.859 5.48v2.144c0 .556.397.953.953.953h5.718c.556 0 .953-.397.953-.953V14.65c1.747-1.191 2.86-3.177 2.86-5.48 0-3.653-3.019-6.671-6.672-6.671zm2.7 10.563l-.794.555v2.224h-3.812v-2.224l-.794-.555A4.712 4.712 0 0 1 7.235 9.17 4.78 4.78 0 0 1 12 4.405a4.78 4.78 0 0 1 4.765 4.765 4.712 4.712 0 0 1-2.065 3.892z"/></symbol><symbol viewBox="0 0 200 200" id="snyk" xmlns="http://www.w3.org/2000/svg"><title>Group 2</title><g transform="translate(15.255 18.22) scale(1.8477)" fill="none" fill-rule="evenodd"><path d="M65.161 24.997c-1.656 5.974-5.255 23.587-5.255 23.587s-6.618-2.464-14.148-2.476h-.055c-.413.002-.822.012-1.23.026v41.649h6.677v.003h5.815v-.003h20.858c.111-8.177-2.036-27.066-2.036-27.066-1.088-2.279.46-7.668.46-7.668-8.869-9.092-11.086-28.051-11.086-28.051zm-3.357 43.958c5.476 0 1.381 4.64.9 5.168H52.35c.944-1.18 4.504-5.168 9.453-5.168z" fill="#607d8b" stroke-width="1.6"/><path d="M26.366 24.995s-2.217 18.961-11.087 28.053c0 0 1.548 5.391.46 7.669 0 0-2.15 18.895-2.038 27.066h19.273v.003h7.079v-.003h5.744V46.107h-.025c-7.532.013-14.151 2.478-14.151 2.478s-3.6-17.615-5.255-23.59zm3.264 43.96c4.95 0 8.51 3.987 9.452 5.168H28.73c-.479-.528-4.573-5.168.9-5.168z" fill="#90a4ae" stroke-width="1.6"/><g transform="translate(23.76 77.45) scale(1.5998)"><g transform="translate(17.526)"><path d="M7.357.06H.177v.075C.177 2.64 2.345 4.67 4.89 4.67 7.431 4.67 9.6 2.64 9.6.135V.059z" fill="#455a64"/><path d="M1.972.06v.075a2.692 2.692 0 1 0 5.386 0V.059z" fill="#fff"/><path d="M5.496.06H4.234c-.012 0-.023.005-.034.007.157.033.243.388.21.624a.721.721 0 0 1-.71.617c.102.471.487.85.997.922a1.188 1.188 0 0 0 1.35-1.007C6.112.743 5.881.06 5.495.06z" fill="#37474f"/></g><path d="M7.552.06H.372v.075c0 2.505 2.17 4.535 4.712 4.535 2.544 0 4.712-2.03 4.712-4.535V.059z" fill="#455a64"/><path d="M2.168.06v.075a2.692 2.692 0 1 0 5.385 0V.059z" fill="#fff"/><path d="M5.692.06H4.428c-.01 0-.022.005-.032.007.156.033.242.388.21.624a.72.72 0 0 1-.712.617c.104.471.488.85.999.922A1.187 1.187 0 0 0 6.24 1.223C6.308.743 6.078.06 5.69.06z" fill="#37474f"/></g><path d="M25.514-.27l-4.202 7.697C19.838 10.17 6.858 34.465 6.858 43.243v.516L12.8 59.573c-.8 7.258-2.203 21.643-1.78 28.21h5.73c-.354-3.787.648-17.008 1.903-28.25l.076-.677-1.075-2.892c3.694-3.868 6.285-9.193 8.073-14.261l.174 1.235 5.869 9.629 2.291-.983c.058-.024 5.935-2.523 11.643-2.523 5.672 0 11.646 2.5 11.702 2.525l2.29.976 5.86-9.626.23-1.608c1.769 5.117 4.358 10.536 8.07 14.49l-1.127 3.035.076.678c1.259 11.286 2.266 24.564 1.916 28.252h5.677c.406-6.567-1.05-20.952-1.848-28.208l5.838-15.817v-.514c0-8.779-12.876-33.074-14.347-35.816L65.923-.27l-5.897 41.229-2.723 4.478c-2.628-.882-7.1-2.11-11.603-2.11-4.498 0-8.94 1.225-11.557 2.108l-2.722-4.476-2.07-14.452a.832.832 0 0 0 .006-.071l-.016-.004zm-3.166 18.39l1.206 8.407c-.46 3.143-2.561 15.47-8.198 23.24l-2.598-6.99c.325-4.554 5.067-15.462 9.59-24.656zm46.763 0c4.523 9.194 9.267 20.104 9.592 24.657L76.166 49.6c-6.09-8.553-8-22.459-8.166-23.73z" fill="#607d8b" stroke-width="1.6"/></g></symbol><symbol viewBox="0 0 24 24" id="solidity" xmlns="http://www.w3.org/2000/svg"><path d="M5.8 14.05l6.253 8.61 6.252-8.61-6.254 3.807z" fill="#0288d1" stroke-width="4.553" stroke-linejoin="round"/><path d="M12.051 1.347L5.8 11.833l6.252 3.807 6.254-3.807z" fill="#0288d1" stroke-width="5.025" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 120 120" id="sonar" xmlns="http://www.w3.org/2000/svg"><style>.a,.b{fill:#fff}.b{stroke:#fff;stroke-miterlimit:10}</style><path d="M115.45 23.033S97.961 33.27 97.534 33.412c-.427.284-.852.57-1.137.854-1.422 1.421-1.848 3.41-1.422 5.26.285.852.711 1.849 1.422 2.56.711.71 1.564 1.137 2.559 1.422 1.848.426 3.84 0 5.262-1.422.426-.427.709-.853.851-1.28l.143-.427 2.56-4.692zm-39.102 9.242c-27.441 0-31.99 13.08-31.99 29.29 0 3.838.569 7.962-1.99 11.942-3.84 5.972-8.957 5.828-10.236 5.828-1.706 0-7.962-.993-8.246-2.841h.994c6.682 0 11.658-5.404 11.658-12.655v-2.56h-5.686c-4.123 0-7.82 1.849-10.238 5.12-2.417-3.271-6.113-5.12-10.236-5.12h-5.83v2.56c0 7.11 5.688 12.795 12.797 12.795h1.848c0 4.124 5.687 20.332 47.63 20.332 16.352 0 40.665-2.843 40.665-33.697 0-5.829-1.848-11.23-4.691-15.78-.996.284-1.992.568-3.13.568a8.92 8.92 0 0 1-8.956-8.957c0-.995.141-1.991.425-2.986-4.265-2.702-8.53-3.838-14.787-3.838z" fill="#1e88e5" stroke-width="1.422"/></symbol><symbol viewBox="0 0 412 395" id="stylelint" xmlns="http://www.w3.org/2000/svg"><title>stylelint-icon-white</title><g transform="translate(31.478 29.499) scale(.84775)" fill="#cfd8dc" fill-rule="evenodd"><path d="M208.8 393.05c45.057-161.12 43.75-161.85 76.32-276.73l7.832 4.523c4.255 2.458 7.738.448 7.738-4.455V61.602c8.643-30.27 15.416-53.66 17.4-60.693h35.287l58.618 54.304-38.498 33.27 29.11 31.473-191.86 273.09c-.938 1.542-2.244 1.19-1.947 0zm20.96-347.28c1.733 0 3.148.958 3.148 2.147v28.077c0 1.186-1.415 2.15-3.147 2.15h-47.396c-1.742 0-3.153-.96-3.153-2.15V47.917c0-1.185 1.41-2.147 3.153-2.147h47.396z"/><path d="M288.26 14.688l-52.14 30.1c.605.92.973 1.98.973 3.136v28.078c0 1.457-.565 2.77-1.496 3.83l52.663 30.402c3.59 2.073 6.535.377 6.535-3.764V18.456c0-4.145-2.944-5.836-6.535-3.768zM175.02 76V47.923c0-1.15.368-2.21.966-3.13l-52.14-30.105c-3.588-2.068-6.53-.376-6.53 3.768v88.013c0 4.14 2.938 5.84 6.53 3.76l52.66-30.405c-.926-1.06-1.487-2.37-1.487-3.827z"/><path d="M201.25 393.05h1.947c-45.05-161.12-43.753-161.85-76.32-276.73l-7.833 4.523c-4.253 2.458-7.737.448-7.737-4.455V61.602C102.662 31.332 95.892 7.942 93.902.909H58.619L.002 55.213l38.494 33.27-29.11 31.473z"/><circle cx="204.57" cy="122.54" r="14.231"/><circle cx="204.57" cy="207.16" r="14.231"/><circle cx="204.57" cy="291.78" r="14.23"/></g></symbol><symbol viewBox="0 0 412 395" id="stylelint_light" xmlns="http://www.w3.org/2000/svg"><title>stylelint-icon-black</title><g transform="translate(31.478 29.499) scale(.84775)" fill="#546e7a" fill-rule="evenodd"><path d="M208.8 393.05c45.057-161.12 43.75-161.85 76.32-276.73l7.832 4.523c4.255 2.458 7.738.448 7.738-4.455V61.602c8.643-30.27 15.416-53.66 17.4-60.693h35.287l58.618 54.304-38.498 33.27 29.11 31.473-191.86 273.09c-.938 1.542-2.244 1.19-1.947 0zm20.96-347.28c1.733 0 3.148.958 3.148 2.147v28.077c0 1.186-1.415 2.15-3.147 2.15h-47.396c-1.742 0-3.153-.96-3.153-2.15V47.917c0-1.185 1.41-2.147 3.153-2.147h47.396z"/><path d="M288.26 14.688l-52.14 30.1c.605.92.973 1.98.973 3.136v28.078c0 1.457-.565 2.77-1.496 3.83l52.663 30.402c3.59 2.073 6.535.377 6.535-3.764V18.456c0-4.145-2.944-5.836-6.535-3.768zM175.02 76V47.923c0-1.15.368-2.21.966-3.13l-52.14-30.105c-3.588-2.068-6.53-.376-6.53 3.768v88.013c0 4.14 2.938 5.84 6.53 3.76l52.66-30.405c-.926-1.06-1.487-2.37-1.487-3.827z"/><path d="M201.25 393.05h1.947c-45.05-161.12-43.753-161.85-76.32-276.73l-7.833 4.523c-4.253 2.458-7.737.448-7.737-4.455V61.602C102.662 31.332 95.892 7.942 93.902.909H58.619L.002 55.213l38.494 33.27-29.11 31.473z"/><circle cx="204.57" cy="122.54" r="14.231"/><circle cx="204.57" cy="207.16" r="14.231"/><circle cx="204.57" cy="291.78" r="14.23"/></g></symbol><symbol viewBox="0 0 200.00001 200.00001" id="stylus" xmlns="http://www.w3.org/2000/svg"><path d="M126.814 155.9c14.64-17.51 16.362-35.595 5.024-69.18-7.177-21.24-19.09-37.602-10.334-50.807 9.329-14.065 29.135-.43 12.63 18.371l3.301 2.297c19.806 2.296 29.566-24.83 14.783-32.58C113.179 3.621 79.02 42.803 94.09 88.156c6.458 19.232 15.5 39.613 8.18 55.83-6.314 13.923-18.514 22.103-26.695 22.39-17.079.862-5.74-38.32 13.922-48.08 1.722-.861 4.162-2.01 1.866-4.88-24.256-2.727-38.464 8.468-46.645 24.112-23.825 45.497 45.21 62.29 82.095 18.371z" fill="#c0ca33" stroke-width="1.435"/></symbol><symbol viewBox="0 0 24 24" id="swc" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="jba"><stop offset="0" stop-color="#791223"/><stop offset="1" stop-color="#d92f3c"/></linearGradient><linearGradient xlink:href="#jba" id="jbb" x1="12.356" y1="21.559" x2="12.356" y2="2.949" gradientUnits="userSpaceOnUse"/></defs><path d="M6 3c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6 3 6.5V19a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6.5c0-.5-.17-.93-.46-1.27l-1.39-1.68C18.88 3.21 18.47 3 18 3H6zm-.07 1h12l.94 1H5.12l.81-1z" fill="url(#jbb)"/><path style="line-height:125%" d="M11.053 11.918h-.008c-.244.022-.475.054-.676.11a2.9 2.9 0 0 0-.856.412 3.399 3.399 0 0 0-.67.683 9.36 9.36 0 0 0-.586.95c-.07.131-.134.244-.201.365v.001h-.002l-.768 1.372-.003-.001c-.136.253-.264.485-.38.686-.123.212-.26.39-.411.539a1.599 1.599 0 0 1-.52.34c-.04.016-.092.024-.138.036h-.567v1.383H5.834v-.001c.245-.02.477-.053.679-.11a2.9 2.9 0 0 0 .856-.411c.245-.185.469-.413.67-.683.195-.275.39-.591.585-.95.07-.131.135-.244.202-.366l.004.001.002-.002.02-.038H10.948v-1.378h-.19v-.001H9.624c.125-.234.246-.452.355-.64.123-.21.259-.39.41-.538.152-.148.325-.26.52-.34.04-.015.091-.024.136-.035h.57V13.3h-.002v-1.381h-.56v-.001z" font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#fff"/></symbol><symbol viewBox="0 0 24 24" id="swift" xmlns="http://www.w3.org/2000/svg"><path d="M17.09 19.72c-2.36 1.36-5.59 1.5-8.86.1A13.807 13.807 0 0 1 2 14.5c.67.55 1.46 1 2.3 1.4 3.37 1.57 6.73 1.46 9.1 0-3.37-2.59-6.24-5.96-8.37-8.71-.45-.45-.78-1.01-1.12-1.51 8.28 6.05 7.92 7.59 2.41-1.01 4.89 4.94 9.43 7.74 9.43 7.74.16.09.25.16.36.22.1-.25.19-.51.26-.78.79-2.85-.11-6.12-2.08-8.81 4.55 2.75 7.25 7.91 6.12 12.24-.03.11-.06.22-.05.39 2.24 2.83 1.64 5.78 1.35 5.22-1.21-2.39-3.48-1.65-4.62-1.17z" fill="#fe5e2f"/></symbol><symbol viewBox="0 0 24 24" id="table" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5m4 7.5h-4v2h1l-2 1.67L10 13h1v-2H7v2h1l3 2.5L8 18H7v2h4v-2h-1l2-1.67L14 18h-1v2h4v-2h-1l-3-2.5 3-2.5h1v-2z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 200 200" id="terraform" xmlns="http://www.w3.org/2000/svg"><g transform="translate(177.03 -58.705) scale(.92881)" fill="#5c6bc0" stroke="#b0aff5" stroke-linejoin="round"><g stroke-width=".288"><path transform="skewY(26.439) scale(.89541 1)" d="M-203.8 170.95h64.714v51.88H-203.8zM-124.37 171.04h64.714v51.88h-64.714zM-124.37 236.09h64.714v51.88h-64.714z"/></g><path transform="skewY(-22.59) scale(-.92328 1)" stroke-width=".284" d="M-19.172 128.27h62.76v51.88h-62.76z"/></g></symbol><symbol viewBox="0 0 24 24" id="test-js" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="test-jsx" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#00bcd4"/></symbol><symbol viewBox="0 0 24 24" id="test-ts" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#0288d1"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="tex" xmlns="http://www.w3.org/2000/svg"><g font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#42a5f5" stroke-linejoin="miter"><text style="line-height:125%" x="9.914" y="364.919"><tspan x="9.914" y="364.919" font-size="287.5">T</tspan></text><text style="line-height:125%" x="136.374" y="435.558"><tspan x="136.374" y="435.558" font-size="287.5">E</tspan></text><text style="line-height:125%" x="307.819" y="361.201"><tspan x="307.819" y="361.201" font-size="287.5">X</tspan></text></g></symbol><symbol viewBox="0 0 24 24" id="todo" xmlns="http://www.w3.org/2000/svg"><path d="M3 5h6v6H3V5m2 2v2h2V7H5m6 0h10v2H11V7m0 8h10v2H11v-2m-6 5l-3.5-3.5 1.41-1.41L5 17.17l4.59-4.58L11 14l-6 6z" fill="#42a5f5"/></symbol><symbol id="travis" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><style id="jkstyle2">.jkst0{fill:#cb3349}.jkst1{fill:#f4edae}.jkst2{fill:#e6ccad}.jkst3{fill:#656c67}.jkst4{fill:#e5caa3}.jkst5{fill:#c7b39a}.jkst6{fill:#ebd599}.jkst7{fill:#2d3136}.jkst8{fill:#edf6fa}.jkst9{opacity:.8}.jkst10{opacity:.75;fill:#ebd599}</style><g id="jkg99" transform="translate(11.017 12.484) scale(.8858)"><g id="jkg10"><path class="jkst0" d="M47.781 86.572s-31.118 21.903-32.335 30.247l2.335-.48S55.045 91.64 84.584 88.628l.669-3.749z" id="jkpath4" fill="#cb3349"/><path class="jkst0" d="M96.629 83.442l-24.511 17.385 1.325 1.063c.999-.806 43.539-13.798 43.539-13.798l8.969-5.623c-6.018.749-29.322.973-29.322.973z" id="jkpath6" fill="#cb3349"/><path class="jkst0" d="M117.932 104.469c17.405 0 43.495-17.046 43.495-17.046l-8.434-1.605c-.417.417-13.6-.462-13.6-.462l-6.258-1.738-14.951 17.036-1.217 2.956c1.075-.437.965.859.965.859z" id="jkpath8" fill="#cb3349"/></g><path class="jkst0" d="M174.728 158.832l-5.377 1.514-24.843-.537-15.541-12.085-18.784 4.7-21.726-1.88-12.166 13.294-22.828 6.819-11.398-3.534-.574-.494 5.116 12.527s11.588 12.424 18.061 13.885c6.472 1.461 18.165-.105 26.935-1.463 8.769-1.357 15.764-4.489 18.582-9.603 2.818-5.117 3.236-6.578 3.236-6.578s8.353 11.797 15.556 13.155c7.203 1.357 28.605-5.952 28.605-5.952s13.051-3.549 15.346-8.038c2.297-4.489 8.353-19.209 8.353-19.209zM44.456 169.038l-.361-.166-2.013-1.736z" id="jkpath12" fill="#cb3349"/><g id="jkg97"><path class="jkst1" d="M195.832 70.085a48.125 48.125 0 0 0-.21-2.009 26.472 26.472 0 0 0-.215-1.424c-1.793-1.509-3.831-2.851-5.952-4.071-2.299-1.343-4.704-2.546-7.159-3.663-2.438-1.15-4.942-2.191-7.461-3.207a134.313 134.313 0 0 0-3.798-1.477c-1.269-.495-2.55-.956-3.835-1.424 2.697.447 5.366 1.059 8.015 1.741 1.723.446 3.437.945 5.14 1.477-12.112-31.655-41.07-52.27-72.687-52.27-31.622 0-60.577 20.615-72.686 52.27a109.044 109.044 0 0 1 5.137-1.477c2.653-.682 5.323-1.294 8.018-1.741-1.289.468-2.567.929-3.84 1.424-1.267.472-2.536.967-3.798 1.477-2.519 1.016-5.016 2.057-7.46 3.207-2.45 1.117-4.857 2.32-7.156 3.663-2.121 1.219-4.157 2.562-5.957 4.071-.075.457-.151.951-.21 1.424a51.768 51.768 0 0 0-.21 2.009 51.354 51.354 0 0 0-.177 4.061 59.216 59.216 0 0 0 .5 8.11c.37 2.692.864 5.366 1.595 7.951.36 1.295.768 2.572 1.24 3.808.237.617.495 1.225.764 1.816.134.294.274.585.413.864l.172.328c.199.101.408.204.607.3l1.204.575c.671.305 1.6.746 2.368 1.09.043-.037.086-.075.123-.114l-2.235-8.513c.474-.13 4.718-1.225 12.032-2.617a38.816 38.816 0 0 1-1.772-.381c-1.665-.414-3.309-.919-4.899-1.564a22.415 22.415 0 0 1-2.309-1.115c-.742-.426-1.472-.908-2.037-1.548 8.036 2.622 24.64 1.434 39.399-.091 13.499-1.391 27.029-2.293 40.63-2.32 13.602.027 27.137.929 40.63 2.32 14.766 1.525 31.37 2.713 39.405.091-.564.64-1.293 1.123-2.035 1.548a22.5 22.5 0 0 1-2.308 1.115c-1.592.645-3.234 1.15-4.899 1.564-.247.059-.496.113-.743.166 8.02 1.488 12.689 2.697 13.188 2.831l-2.138 8.11c.43-.194.864-.381 1.29-.574l1.202-.575c.2-.097.403-.199.607-.3l.166-.328c.146-.279.286-.57.419-.864.27-.591.528-1.199.764-1.816a42.235 42.235 0 0 0 1.241-3.808c.731-2.585 1.225-5.259 1.595-7.951.345-2.685.526-5.398.501-8.11a50.874 50.874 0 0 0-.179-4.059z" id="jkpath14" fill="#f4edae"/><path class="jkst2" d="M116.787 182.661c-1.064.16-2.128.295-3.186.375-.682.033-1.404.102-2.059.102l-.242.005c.822-1.837 1.446-3.26 1.919-4.339.963 1.08 2.188 2.417 3.568 3.857z" id="jkpath16" fill="#e6ccad"/><path class="jkst2" d="M119.101 185.018c3.304 3.272 7.398 5.146 11.904 5.479-7.569 3.074-14.702 4.26-20.197 4.63-5.478.367-11.032-.279-16.474-1.771.456-.082.79-.14 1.193-.189.447-.054 10.206-1.327 14.605-7.868l.413.009 1.08-.009c.731 0 1.395-.06 2.094-.087a43.69 43.69 0 0 0 4.878-.703c.167.171.333.338.504.509z" id="jkpath18" fill="#e6ccad"/><path class="jkst3" d="M128.464 87.071a98.82 98.82 0 0 1-1.048 1.343c-1.933 2.444-4.614 5.57-7.794 8.627a369.585 369.585 0 0 0-11.404-.177c-6.46 0-12.655.171-18.537.457 8.311-3.449 18.296-6.818 29.109-8.842a113.323 113.323 0 0 1 9.674-1.408z" id="jkpath20" fill="#656c67"/><path class="jkst3" d="M79.821 90.792c-2.966 2.084-6.317 4.744-9.566 7.971a360.155 360.155 0 0 0-21.567 2.81c9.207-4.232 19.713-8.127 31.133-10.781z" id="jkpath22" fill="#656c67"/><path class="jkst3" d="M181.48 107.969l-3.384 23.679-16.212 11.355-42.283-4.807-6.365-20.961a1.383 1.383 0 0 0-1.108-.971c-1.567-.253-2.953-.382-4.108-.382-1.16 0-2.541.129-4.115.382-.522.086-.95.461-1.106.971l-6.209 20.45-42.047 9.357-16.662-11.672-3.283-26.572c.715-.404 1.441-.806 2.176-1.209 1.031-.222 2.191-.457 3.475-.704l3.094 25.073c.048.392.264.741.586.967l11.462 8.032a1.425 1.425 0 0 0 1.101.213l34.57-7.692c.119-.027.237-.069.344-.124a1.39 1.39 0 0 0 .682-.827l6.225-20.498c1.67-.43 5.947-1.429 9.706-1.429 3.749 0 8.03.999 9.701 1.429l6.225 20.498c.161.532.624.912 1.176.977l34.57 3.927c.335.037.677-.05.952-.242l11.469-8.025c.31-.22.52-.566.573-.946l3.062-21.421c2.301.444 4.224.846 5.733 1.172z" id="jkpath24" fill="#656c67"/><path class="jkst3" d="M185.751 93.119l-2.976 11.29c-6.086-1.342-19.456-3.975-37.654-5.747 5.946-2.535 12-5.715 17.531-9.69 10.829 1.53 18.78 3.169 23.099 4.147z" id="jkpath26" fill="#656c67"/><g id="jkg32"><path class="jkst4" d="M63.841 128.441c2.357-1.274 5.021-1.085 9.19-1.079.447.011.908.005 1.39-.005.41-.005.822-.011 1.258-.022 4.296-.042 7.869.366 7.806-6.381-.065-6.746-3.062-12.198-7.354-12.155-4.297.037-8.454 5.564-8.197 12.306.07 1.756.328 3.023.742 3.937-3.745.938-4.777 3.254-4.835 3.399zm51.657-27.749a46.634 46.634 0 0 1-5.249 3.712l-6.097 3.68a52.065 52.065 0 0 0-7.331 1.467 1.216 1.216 0 0 0-.317.14 1.406 1.406 0 0 0-.629.794l-6.209 20.46-33.185 7.38-10.452-7.321-3.041-24.634c5.936-1.09 13.874-2.352 23.41-3.42a56.802 56.802 0 0 0-2.955 3.855l-5.677 8.149 8.266-5.511c.123-.086 5.387-3.549 13.998-7.761a377.407 377.407 0 0 1 35.468-.99z" id="jkpath28" fill="#e5caa3"/><path class="jkst4" d="M151.835 125.675c-.042-.16-.945-2.873-4.942-2.397.461-1.003.666-2.356.521-4.21-.528-6.731-4.443-12.08-8.735-11.931-4.292.152-7.042 5.731-6.805 12.478.236 6.741 3.84 6.694 8.132 6.543 5.77-.107 8.939-1.88 11.829-.483zm21.18-19.385l-2.992 20.944-10.539 7.379-33.141-3.766-6.183-20.363a1.41 1.41 0 0 0-.945-.934c-.205-.06-4.308-1.23-8.659-1.607l.795-.053c.687-.049 12.118-1.451 25.767-6.157 15.115 1.161 27.458 3.02 35.897 4.557z" id="jkpath30" fill="#e5caa3"/></g><g id="jkg38"><path class="jkst5" d="M63.841 128.441c2.357-1.274 5.021-1.085 9.19-1.079.447.011.908.005 1.39-.005.41-.005.822-.011 1.258-.022 4.296-.042 7.869.366 7.806-6.381-.065-6.746-3.062-12.198-7.354-12.155-4.297.037-8.454 5.564-8.197 12.306.07 1.756.328 3.023.742 3.937-3.745.938-4.777 3.254-4.835 3.399zm51.657-27.749a46.634 46.634 0 0 1-5.249 3.712l-6.097 3.68a52.065 52.065 0 0 0-7.331 1.467 1.216 1.216 0 0 0-.317.14 1.406 1.406 0 0 0-.629.794l-6.209 20.46-33.185 7.38-10.452-7.321-3.041-24.634c5.936-1.09 13.874-2.352 23.41-3.42a56.802 56.802 0 0 0-2.955 3.855l-5.677 8.149 8.266-5.511c.123-.086 5.387-3.549 13.998-7.761a377.407 377.407 0 0 1 35.468-.99z" id="jkpath34" fill="#c7b39a"/><path class="jkst5" d="M151.835 125.675c-.042-.16-.945-2.873-4.942-2.397.461-1.003.666-2.356.521-4.21-.528-6.731-4.443-12.08-8.735-11.931-4.292.152-7.042 5.731-6.805 12.478.236 6.741 3.84 6.694 8.132 6.543 5.77-.107 8.939-1.88 11.829-.483zm21.18-19.385l-2.992 20.944-10.539 7.379-33.141-3.766-6.183-20.363a1.41 1.41 0 0 0-.945-.934c-.205-.06-4.308-1.23-8.659-1.607l.795-.053c.687-.049 12.118-1.451 25.767-6.157 15.115 1.161 27.458 3.02 35.897 4.557z" id="jkpath36" fill="#c7b39a"/></g><path class="jkst2" d="M187.481 115.502c.508.419.911 1.504.456 6.558-.559 6.188-3.16 17.049-4.771 18.8-1.778.344-5.505-.064-7.778-.595.393-1.559.505-2.306.822-3.9l3.975-2.781c.317-.22.526-.566.58-.941l2.778-19.466c1.686.912 3.421 1.899 3.938 2.325z" id="jkpath40" fill="#e6ccad"/><path class="jkst2" d="M40.937 140.908c.199.704.408 1.407.624 2.1-2.139.628-6.495 1.23-8.465.886-1.633-1.645-4.679-12.966-5.345-18.978-.543-4.871-.162-5.924.333-6.334.575-.483 2.728-1.708 4.593-2.707l2.519 20.449c.048.393.257.741.586.967z" id="jkpath42" fill="#e6ccad"/><path class="jkst2" d="M121.347 141.194l-.151 1.305s-4.581 4.248-11.956 5.199c-7.375.95-13.171-3.582-13.171-3.582.242.788.586 2.567 2.256 4.086a53.184 53.184 0 0 0-6.313-.393c-.804 0-1.616.023-2.401.061-4.539.237-10.924 7.1-15.414 14.014-2.203.697-9.089 2.883-17.06 5.237-7.44-10.309-11.098-20.842-11.469-21.932l.005-.006c-.15-.419-.301-.839-.441-1.268l1.913 1.338v.005l4.726 3.309 1.58 1.101c.236.167.515.253.794.253.102 0 .204-.011.305-.031l43.435-9.67a1.385 1.385 0 0 0 1.025-.95l6.194-20.39c1.069-.145 2.008-.22 2.814-.22.801 0 1.746.075 2.815.22l6.374 20.997c.162.532.624.919 1.171.977z" id="jkpath44" fill="#e6ccad"/><path class="jkst2" d="M170.926 140.066l1.402-.984c-.232.973-.484 1.94-.747 2.896-1.949 6.248-4.25 11.774-6.805 16.656-.565.039-1.161.061-1.8.061-1.972 0-3.986-.167-6.215-.371-3.868-.355-10.007-1.058-11.946-1.283-1.67-1.332-7.385-5.873-12.14-9.615-.187-.151-.348-.291-.505-.42-.837-.708-1.789-1.513-3.717-1.513-1.751 0-4.308.638-10.489 2.508 3.212-2.401 3.233-5.5 3.233-5.5l.151-1.305 40.748 4.629a1.41 1.41 0 0 0 .955-.241l4.094-2.868z" id="jkpath46" fill="#e6ccad"/><path class="jkst6" d="M140.937 54.337c.124 3.625.033 10.194-1.655 16.345a1.335 1.335 0 0 0 0 .704 259.298 259.298 0 0 0-6.446-.591c2.412-5.054 2.938-10.436 3.052-12.332 1.852-1.317 3.696-2.896 5.049-4.126z" id="jkpath48" fill="#ebd599"/><path class="jkst6" d="M79.456 58.462c.112 1.896.638 7.267 3.046 12.317-2.149.171-4.297.37-6.441.596a1.328 1.328 0 0 0 0-.694c-1.686-6.139-1.772-12.714-1.654-16.345 1.353 1.231 3.19 2.81 5.049 4.126z" id="jkpath50" fill="#ebd599"/><path class="jkst7" d="M151.835 125.675c-2.89-1.396-6.059.377-11.828.484-4.292.151-7.896.198-8.132-6.543-.237-6.747 2.513-12.326 6.805-12.478 4.292-.15 8.207 5.2 8.735 11.931.145 1.854-.06 3.207-.521 4.21 3.996-.477 4.899 2.235 4.941 2.396zm-13.488-9.878a2.203 2.203 0 0 0 2.154-2.235 2.186 2.186 0 0 0-2.235-2.153 2.194 2.194 0 0 0 .081 4.388z" id="jkpath52" fill="#2d3136"/><circle transform="rotate(-1.049 138.093 113.428)" class="jkst8" cx="138.307" cy="113.602" id="jkellipse54" r="2.194" fill="#edf6fa"/><path class="jkst7" d="M83.484 120.953c.063 6.747-3.509 6.339-7.806 6.381-.435.011-.848.016-1.258.022-.482.011-.944.016-1.39.005-4.168-.005-6.833-.194-9.19 1.079.058-.145 1.09-2.461 4.835-3.4-.414-.914-.673-2.181-.742-3.937-.257-6.741 3.9-12.269 8.197-12.306 4.292-.042 7.289 5.411 7.354 12.156zm-6.634-3.529a2.195 2.195 0 1 0-.122-4.388 2.195 2.195 0 0 0 .122 4.388z" id="jkpath56" fill="#2d3136"/><circle transform="rotate(-1.473 76.78 115.216)" class="jkst8" cx="76.79" cy="115.23" id="jkellipse58" r="2.195" fill="#edf6fa"/><g class="jkst9" id="jkg64" opacity=".8"><path class="jkst6" d="M50.691 75.155s.667-8.692 2.03-12.023c.702-1.717 4.996-2.81 8.276-3.591 3.278-.78 8.508-2.342 9.524 2.264 1.015 4.606 2.653 7.963 3.746 9.446l-1.404-18.97-22.562 5.464-1.484 16.786.703 1.327 1.171-.703" id="jkpath60" fill="#ebd599"/><path class="jkst6" d="M164.855 75.155s-.666-8.692-2.029-12.023c-.703-1.717-4.997-2.81-8.275-3.591-3.28-.78-8.51-2.342-9.526 2.264-1.013 4.606-2.654 7.963-3.748 9.446l1.407-18.97 22.562 5.464 1.483 16.786-.703 1.327-1.171-.703" id="jkpath62" fill="#ebd599"/></g><path class="jkst10" d="M132.965 18.378s-.598 45.49-11.224 45.49h-14.875-12.752c-10.626 0-11.484-45.47-11.484-45.47l-5.22 15.438.085 21.183 3.707 2.947 1.685 9.096 2.357 5.307 45.482.084 2.105-3.791 1.769-6.4.254-4.043 5.023-14.341z" id="jkpath66" opacity=".75" fill="#ebd599"/><path class="jkst10" d="M166.429 60.794s2.187 15.692 7.974 18.522c5.788 2.829 0 0 0 0l-8.103-2.444z" id="jkpath68" opacity=".75" fill="#ebd599"/><path class="jkst10" d="M48.908 60.794s-2.187 15.692-7.975 18.522c-5.788 2.829 0 0 0 0l8.104-2.444z" id="jkpath70" opacity=".75" fill="#ebd599"/><path class="jkst7" d="M167.987 76.8c2.755.902 5.526 1.858 8.036 3.325-1.343-.532-2.729-.913-4.126-1.257a70.385 70.385 0 0 0-4.201-.924c-2.82-.531-5.65-.982-8.498-1.327-2.841-.37-5.687-.682-8.546-.924-2.858-.241-5.709-.483-8.573-.65-11.446-.704-22.924-.88-34.41-.892-11.483.006-22.962.221-34.409.897-2.862.166-5.715.409-8.572.651-2.857.241-5.71.548-8.546.923-2.847.345-5.678.796-8.498 1.327-1.407.264-2.81.57-4.206.919-1.391.344-2.783.725-4.126 1.257 2.509-1.466 5.28-2.427 8.041-3.331.232-.075.467-.139.703-.214-.015-.059-.032-.113-.043-.177-.048-.317-1.069-7.859.709-18.645.086-.516.456-.935.962-1.075l2.917-.831c.634-22.625 9.952-33.266 10.243-33.594-8.326 13.397-8.25 29.286-8.106 32.986l18.128-5.152c.016-.005.026-.005.042-.01.076-.016.151-.027.226-.032.021 0 .049-.006.075-.006a1.19 1.19 0 0 1 .297.027c.015 0 .031.011.053.016.075.016.145.042.224.075.033.016.054.033.086.049.058.033.119.07.177.112.016.011.034.016.049.033l.032.032c.016.016.037.027.054.044.012.016.494.493 1.262 1.209-.182-5.973.102-23.108 8.262-37.31-.172.498-6.646 19.428-4.415 40.645.724.58 1.486 1.149 2.229 1.649.359.247.58.655.585 1.09.006.07.161 6.833 3.148 12.586.042.086.074.177.102.268 7.429-.505 14.878-.709 22.312-.714 7.436.005 14.88.22 22.307.731.027-.097.06-.193.109-.285 2.986-5.753 3.142-12.516 3.142-12.586.01-.436.231-.843.591-1.09.741-.5 1.493-1.069 2.224-1.649 2.234-21.217-4.24-40.147-4.411-40.645 8.153 14.201 8.444 31.336 8.262 37.31a62.536 62.536 0 0 0 1.261-1.209c.016-.016.039-.027.053-.044.012-.01.018-.021.033-.032.016-.016.033-.022.049-.033.06-.042.119-.079.177-.118.028-.01.054-.027.081-.043.081-.033.155-.059.236-.08.016 0 .033-.011.049-.011.096-.021.2-.032.296-.027.027 0 .049.006.07.006.075.005.156.016.231.032.012.006.028.006.042.01l18.129 5.152c.146-3.7.221-19.59-8.104-32.986.289.328 9.609 10.969 10.237 33.594l2.922.831c.499.14.875.559.962 1.075 1.777 10.786.752 18.328.708 18.645-.01.065-.026.124-.042.182.239.07.47.139.707.215zm-3.297-.968c.14-1.207.789-7.809-.591-16.801l-20.52-5.833c.184 3.475.265 11.012-1.707 18.199a1.619 1.619 0 0 1-.101.258c.203.021.408.037.606.064 5.769.661 11.511 1.584 17.189 2.83 1.712.398 3.426.823 5.124 1.283zm-25.409-5.151c1.688-6.15 1.779-12.72 1.655-16.345-1.353 1.23-3.197 2.809-5.049 4.125-.114 1.896-.64 7.278-3.052 12.332 2.149.173 4.298.366 6.446.591a1.33 1.33 0 0 1 0-.703zm-56.78.098c-2.408-5.05-2.934-10.422-3.046-12.317-1.858-1.316-3.696-2.895-5.049-4.125-.119 3.631-.032 10.206 1.654 16.345.065.237.058.473 0 .694 2.145-.227 4.292-.425 6.441-.597zm-8.933.864a1.65 1.65 0 0 1-.098-.247c-1.975-7.187-1.889-14.723-1.712-18.199L51.244 59.03c-1.38 8.982-.736 15.583-.597 16.797 1.703-.462 3.411-.887 5.131-1.284 2.835-.628 5.693-1.154 8.556-1.638 2.869-.478 5.747-.843 8.626-1.192.205-.027.404-.042.608-.07z" id="jkpath72" fill="#2d3136"/><g id="jkXMLID_1_"><g id="jkg78"><path class="jkst7" d="M129.293 18.973v17.025h-12.068v-4.974h-2.72v22.981h4.109v12.85H97.505v-12.85h4.092v-22.98h-2.711v4.974h-12.06V18.973zm-3.626 13.408v-9.789H90.443v9.789h4.816v-4.974h9.964v30.225h-4.1v5.606h13.865v-5.606h-4.1V27.407h9.964v4.974z" id="jkpath74" fill="#2d3136"/><path class="jkst0" id="jkpolygon76" fill="#cb3349" d="M101.123 57.632h4.1V27.407h-9.964v4.974h-4.816v-9.79h35.224v9.79h-4.816v-4.974h-9.964v30.225h4.1v5.606h-13.864z"/></g></g><path class="jkst3" d="M30.694 93.119c1.759-.399 4.136-.907 7.051-1.47a104.37 104.37 0 0 0-6.222 4.597z" id="jkpath83" fill="#656c67"/><path class="jkst5" d="M95.111 139.78s.492 3.165-3.938 4.519c-4.428 1.355-32.482 9.716-35.682 9.263-3.199-.451-11.319-5.874-11.319-5.874l-1.969-7.004 12.016 7.492z" id="jkpath85" fill="#c7b39a"/><path class="jkst5" d="M120.242 139.167s-.354 3.182 4.131 4.345c4.484 1.161 32.875 8.295 36.05 7.704 3.176-.591 11.053-6.361 11.053-6.361l1.663-7.084-11.045 6.588z" id="jkpath87" fill="#c7b39a"/><path class="jkst5" d="M28.412 133.956s3.887 7.775 10.166 5.083l4.485 1.645-.448 3.29-9.419 1.195-2.541-1.494z" id="jkpath89" fill="#c7b39a"/><path class="jkst5" d="M187.551 131.822s-6.353 8.115-12.632 5.424l-2.019 1.302.448 3.289 9.419 1.196 2.54-1.495z" id="jkpath91" fill="#c7b39a"/><path class="jkst5" d="M89.279 192.904s23.03 11.611 49.106-4.188l-8.374-.571s-18.272 7.232-32.738 3.235z" id="jkpath93" fill="#c7b39a"/><path class="jkst7" d="M112.626 171.509l1.594 1.899c.036.046 3.577 4.26 7.906 8.552 2.879 2.853 6.357 4.297 10.343 4.297 1.361 0 2.791-.175 4.235-.523 1.34-.326 2.796-.673 4.287-1.03 5.384-1.287 11.482-2.749 14.438-3.577.585-.166 1.238-.315 1.925-.472 3.935-.909 9.329-2.163 12.187-7.889 2.149-4.297 5.047-9.874 7.197-13.961-1.863.859-3.816 1.79-5.203 2.52-2.138 1.123-4.938 1.667-8.558 1.667-2.152 0-4.266-.181-6.605-.389-4.675-.43-12.586-1.361-12.667-1.372l-.606-.067-.478-.383c-.071-.052-7.003-5.575-12.606-9.981-.227-.186-.434-.358-.621-.513-.59-.503-.59-.503-.942-.503-1.797 0-7.02 1.62-18.462 5.167l-.703.223-.689-.26c-.078-.026-7.585-2.81-16.581-2.81-.736 0-1.47.019-2.185.056-.901.046-5.958 2.448-12.425 12.68l-.419.657-.741.238c-.107.037-11.238 3.63-23.042 7.005l-.766.218-.725-.337c-.077-.031-4.696-2.174-9.091-4.194 2.397 3.541 5.462 7.958 8.159 11.422 4.711 6.067 10.649 11.674 22.034 11.674 1.428 0 2.945-.088 4.503-.265 11.581-1.309 14.563-1.837 16.168-2.117.543-.092.973-.171 1.522-.238.088-.011 9.571-1.237 12.232-7.206 2.744-6.134 3.298-7.595 3.319-7.651l.968-2.583s.12-.669.317-.877c0 .005 0 .005.005.005l.019.016c.305.219.757.902.757.902zM40.499 55.71c-2.516 1.014-5.016 2.06-7.46 3.209-2.449 1.119-4.856 2.32-7.155 3.66-2.121 1.222-4.157 2.563-5.954 4.076-.077.455-.149.952-.211 1.423a51.357 51.357 0 0 0-.388 6.068c-.026 2.713.16 5.426.502 8.112.372 2.692.864 5.369 1.594 7.952a41.963 41.963 0 0 0 1.243 3.804c.233.623.492 1.228.762 1.818.134.294.274.585.413.864l.172.326c.201.104.409.207.605.3l1.206.574c.673.311 1.6.751 2.366 1.093.046-.037.088-.078.124-.114l-2.231-8.511c.471-.129 4.717-1.227 12.032-2.619a33.744 33.744 0 0 1-1.775-.379 36.704 36.704 0 0 1-4.898-1.563 22.857 22.857 0 0 1-2.309-1.119c-.741-.425-1.471-.905-2.035-1.547 8.035 2.624 24.637 1.433 39.398-.088 13.501-1.393 27.028-2.293 40.628-2.325 13.6.031 27.138.931 40.63 2.325 14.77 1.522 31.374 2.713 39.406.088-.564.642-1.293 1.122-2.034 1.547-.739.42-1.522.782-2.309 1.119a36.965 36.965 0 0 1-4.903 1.563c-.244.056-.492.114-.741.166 8.02 1.486 12.689 2.697 13.186 2.832l-2.138 8.107c.43-.192.864-.377 1.288-.574l1.207-.574c.196-.094.404-.196.606-.3l.166-.326c.144-.279.284-.57.419-.864.27-.591.528-1.196.767-1.818.471-1.231.879-2.51 1.236-3.804.731-2.583 1.228-5.26 1.595-7.952.346-2.686.528-5.4.502-8.112a52.755 52.755 0 0 0-.176-4.059 51.573 51.573 0 0 0-.213-2.009 29.83 29.83 0 0 0-.213-1.423c-1.797-1.513-3.831-2.853-5.954-4.076-2.299-1.34-4.704-2.541-7.159-3.66-2.438-1.149-4.943-2.195-7.46-3.209a140.105 140.105 0 0 0-3.801-1.476c-1.267-.491-2.552-.956-3.835-1.423 2.696.445 5.369 1.06 8.013 1.739 1.724.446 3.444.948 5.141 1.481-12.11-31.658-41.07-52.272-72.685-52.272-31.622 0-60.576 20.614-72.684 52.272a107.832 107.832 0 0 1 5.135-1.481c2.651-.678 5.322-1.294 8.02-1.739-1.29.466-2.568.931-3.842 1.423-1.268.47-2.535.967-3.799 1.475zm159.43 18.316a53.972 53.972 0 0 1-.258 8.733 55.462 55.462 0 0 1-1.619 8.605c-.4 1.414-.86 2.811-1.404 4.198a38.295 38.295 0 0 1-.89 2.071c-.161.341-.331.678-.523 1.025l-.284.512a8.975 8.975 0 0 1-.348.574l-.294.457-.461.237c-.492.254-.895.445-1.342.653l-1.298.585a88.22 88.22 0 0 1-2.62 1.065c-.611.239-1.15.457-1.662.674l-1.444 5.487c-.036-.009-.471-.12-1.283-.315l-.078.574c1.594.833 4.726 2.522 5.793 3.403 2.148 1.775 2.299 4.587 1.823 9.841-.244 2.697-1.139 7.946-2.381 12.767-2.144 8.298-3.283 9.273-4.753 9.649-.746.192-1.894.383-3.008.383-2.266 0-5.353.063-7.429-.439-.533 1.888-2.055 6.812-5.068 12.962.151-.073.3-.135.435-.207 3.717-1.952 10.861-5.064 11.162-5.199l5.643-2.452-2.89 5.435c-.067.118-6.264 11.773-10.059 19.383-3.769 7.538-10.835 9.179-15.065 10.151-.637.151-1.241.291-1.733.425-3.035.854-9.18 2.319-14.599 3.623-.064.016-.13.033-.197.042a64.057 64.057 0 0 1-10.955 5.411c-14.568 5.518-29.923 5.208-43.844.092a647.05 647.05 0 0 1-9.193 1.097 45.12 45.12 0 0 1-4.985.291c-13.264 0-20.294-6.736-25.425-13.331-5.493-7.062-12.212-17.546-12.497-17.985L31 158.426l6.585 2.961c3.152 1.419 12.524 5.757 15.205 7 .217-.061.43-.124.642-.186-4.457-6.357-8.112-13.605-10.695-21.634-2.195.662-5.576 1.175-8.206 1.175-.961 0-1.822-.072-2.484-.228-1.471-.336-3.148-1.754-5.431-9.795-1.325-4.668-2.314-9.764-2.603-12.387-.57-5.121-.466-7.864 1.662-9.636 1.283-1.071 5.611-3.344 6.507-3.809l-.192-1.58c-13.75 8.08-21.991 15.22-22.157 15.366L0 134.302l7.005-11.047c5.544-8.755 11.948-15.832 17.84-21.284-.244-.098-.471-.196-.71-.294l-1.299-.585a34.907 34.907 0 0 1-1.34-.653l-.461-.237-.295-.457c-.166-.249-.238-.388-.347-.574l-.29-.512c-.181-.347-.358-.684-.518-1.025a30.878 30.878 0 0 1-.89-2.071 44.74 44.74 0 0 1-1.404-4.198 54.745 54.745 0 0 1-1.62-8.605 54.664 54.664 0 0 1-.259-8.733c.078-1.455.218-2.909.419-4.354.104-.725.213-1.45.358-2.17.15-.734.296-1.418.518-2.221l.155-.564.404-.317c2.294-1.802 4.768-3.163 7.284-4.369a78.87 78.87 0 0 1 6.311-2.616c5.943-16.493 16.162-31.118 29.591-41.311C74.337 5.57 90.664 0 107.671 0s33.334 5.57 47.218 16.106c13.43 10.193 23.649 24.819 29.588 41.307a78.282 78.282 0 0 1 6.316 2.62c2.515 1.206 4.99 2.567 7.283 4.369l.404.317.156.564c.227.803.372 1.487.517 2.221.146.72.26 1.445.357 2.17.203 1.443.348 2.897.419 4.352zm-11.995 48.031c.456-5.052.058-6.139-.455-6.554-.513-.43-2.247-1.412-3.935-2.329l-2.779 19.464a1.39 1.39 0 0 1-.58.942l-3.977 2.781c-.315 1.593-.429 2.345-.817 3.903 2.273.528 5.999.938 7.775.595 1.612-1.748 4.214-12.61 4.768-18.802zm-5.161-17.648l2.977-11.29c-4.318-.978-12.27-2.615-23.1-4.148-5.53 3.976-11.582 7.155-17.53 9.691 18.199 1.771 31.57 4.406 37.653 5.747zm-4.68 27.237l3.385-23.676a240.127 240.127 0 0 0-5.731-1.169l-3.059 21.422a1.415 1.415 0 0 1-.575.943l-11.472 8.023c-.27.192-.616.28-.947.243l-34.572-3.929a1.391 1.391 0 0 1-1.176-.973l-6.227-20.5c-1.668-.431-5.949-1.43-9.696-1.43-3.764 0-8.041.999-9.708 1.43l-6.228 20.5a1.388 1.388 0 0 1-1.025.947l-34.572 7.692a1.483 1.483 0 0 1-.306.033 1.36 1.36 0 0 1-.792-.25l-11.467-8.029a1.396 1.396 0 0 1-.585-.968l-3.091-25.072c-1.284.249-2.443.487-3.479.703-.734.405-1.46.809-2.174 1.213l3.281 26.568 16.666 11.675 42.047-9.354 6.207-20.449a1.389 1.389 0 0 1 1.108-.975c1.574-.253 2.95-.382 4.116-.382 1.153 0 2.536.129 4.105.382.528.083.957.461 1.108.975l6.366 20.956 42.282 4.808zm-8.07-4.411l2.992-20.948c-8.439-1.536-20.78-3.394-35.897-4.554-13.647 4.707-25.077 6.108-25.766 6.155l-.797.057c4.353.374 8.454 1.544 8.66 1.605.452.135.804.481.944.933l6.186 20.366 33.138 3.764zm2.303 11.845l-1.404.983-3.779 2.651-4.095 2.868c-.279.192-.621.28-.954.243l-40.746-4.633-2.966-.337a1.39 1.39 0 0 1-1.171-.977l-6.377-20.998c-1.066-.145-2.014-.219-2.81-.219-.809 0-1.751.073-2.817.219l-6.192 20.392a1.383 1.383 0 0 1-1.025.946l-43.435 9.672c-.103.02-.206.03-.305.03-.279 0-.559-.083-.798-.253l-1.578-1.098-4.726-3.307v-.011l-1.91-1.335c.135.43.289.85.441 1.268l-.006.006c.368 1.092 4.028 11.622 11.467 21.929a873.96 873.96 0 0 0 17.057-5.234c4.488-6.917 10.877-13.777 15.418-14.014a51.12 51.12 0 0 1 2.402-.061c2.221 0 4.344.16 6.31.393-1.671-1.517-2.013-3.298-2.256-4.085 0 0 5.793 4.53 13.17 3.584 7.378-.953 11.959-5.204 11.959-5.204s-.021 3.102-3.236 5.503c6.182-1.869 8.739-2.511 10.489-2.511 1.931 0 2.883.808 3.717 1.519.161.129.322.268.507.419a3519.302 3519.302 0 0 1 12.141 9.614c1.936.227 8.075.926 11.943 1.283 2.23.201 4.245.372 6.217.372.637 0 1.233-.026 1.797-.063 2.558-4.88 4.857-10.411 6.808-16.653.261-.96.516-1.928.743-2.901zm-15.034-51.593c-.01-.006-.02-.012-.031-.012a551.624 551.624 0 0 0-9.826-.651 905.6 905.6 0 0 0-13.667-.668 72.95 72.95 0 0 1-1.574 2.225c-2.479 3.355-7.398 9.51-13.704 14.729 8.926-1.6 24.409-5.56 37.803-14.905.336-.238.668-.486.999-.718zm-29.876.926c.377-.471.729-.926 1.044-1.34-3.281.331-6.512.808-9.67 1.408-10.814 2.024-20.801 5.389-29.11 8.837a383.259 383.259 0 0 1 18.54-.455c3.908 0 7.708.067 11.404.176 3.179-3.056 5.861-6.182 7.792-8.626zm3.587 102.085c-4.503-.332-8.598-2.205-11.903-5.477a271.86 271.86 0 0 0-.502-.512 44.25 44.25 0 0 1-4.881.704c-.698.026-1.361.087-2.091.087l-1.083.011-.413-.011c-4.396 6.539-14.159 7.813-14.605 7.87-.403.046-.734.103-1.191.186 5.442 1.491 10.996 2.138 16.474 1.77 5.492-.367 12.627-1.558 20.195-4.628zm-17.4-7.461a45.604 45.604 0 0 0 3.184-.378 138.958 138.958 0 0 1-3.568-3.857 398.441 398.441 0 0 1-1.92 4.339h.243c.658.001 1.378-.071 2.061-.104zm-3.354-78.632c1.827-1.103 3.582-2.366 5.249-3.712a422.33 422.33 0 0 0-7.278-.072c-10.137 0-19.606.415-28.189 1.061-8.61 4.209-13.875 7.672-13.998 7.76l-8.268 5.514 5.679-8.149a52.452 52.452 0 0 1 2.956-3.857c-9.536 1.066-17.477 2.329-23.41 3.422l3.038 24.632 10.453 7.321 33.184-7.378 6.212-20.464c.104-.337.331-.621.627-.793.098-.063.202-.109.315-.14.192-.052 3.51-.999 7.336-1.465zm3.816-18.788c-2.31-.036-4.623-.057-6.933-.062h-.005c-3.39.005-6.787.041-10.189.109l-6.269 2.971c-.005.005-.041.021-.088.048-.942.46-9.174 4.613-16.919 12.021 6.943-3.65 17.146-8.418 29.153-12.115a144.186 144.186 0 0 1 11.25-2.972zM70.251 98.761c3.251-3.225 6.605-5.886 9.567-7.967-11.415 2.651-21.923 6.543-31.128 10.778a360.846 360.846 0 0 1 21.561-2.811zm2.159-9.949a150.122 150.122 0 0 1 11.813-2.796c-5.798.212-11.6.481-17.393.808-3.366.186-6.715.414-10.065.667-1.678.129-3.345.263-5.007.445-.476.046-.942.098-1.418.16-4.369 2.614-21.127 13.134-32.631 26.889 11.179-7.769 30.654-19.443 54.701-26.173zm-30.85 54.197a68.861 68.861 0 0 1-.621-2.102l-5.162-3.612a1.391 1.391 0 0 1-.586-.969l-2.516-20.449c-1.864.999-4.017 2.225-4.592 2.707-.497.409-.875 1.46-.336 6.332.668 6.01 3.712 17.333 5.348 18.979 1.968.347 6.327-.258 8.465-.886zm-3.815-51.36a229.005 229.005 0 0 0-7.051 1.47l.829 3.127a103.93 103.93 0 0 1 6.222-4.597z" id="jkpath95" fill="#2d3136"/></g></g></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="tune" xmlns="http://www.w3.org/2000/svg"><path d="M6.85 2.852h-2v6h2v-6m12 0h-2v10h2v-10m-16 10h2v8h2v-8h2v-2h-6v2m12-6h-2v-4h-2v4h-2v2h6v-2m-4 14h2v-10h-2v10m4-6v2h2v4h2v-4h2v-2h-6z" fill="#fbc02d" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 50 50" id="twig" xmlns="http://www.w3.org/2000/svg"><path d="M9.727 47.556c-.125-.223-.297-2.168-.183-2.087.034.025.171.267.304.537.132.27.282.487.332.482.123-.011.075-1.196-.1-2.454-.331-2.398-1.176-4.435-2.358-5.69-.2-.212-.344-.4-.319-.419.093-.067 1.327.843 1.842 1.359.293.293.735.825.981 1.181.328.474.465.618.51.534.078-.147-.21-9.903-.376-12.701-.074-1.255.063-1.023.61 1.035 1.064 4.006 1.858 7.922 2.342 11.55.086.637.173 1.172.195 1.19.022.016.092.001.157-.034.888-.483 1.524-.667 2.55-.736.727-.048.945.062.35.178-1.15.222-1.99 1.013-2.344 2.201-.315 1.061-.327 2.707-.024 3.434.152.366.037.426-1.067.56-.716.088-.977.096-1.202.037-.356-.092-1.118-.098-1.195-.008-.031.036-.243.066-.47.066-.38 0-.423-.017-.535-.215zm1.974-3.233c.152-.205.072-.41-.204-.522-.225-.09-.263-.088-.437.025-.21.137-.252.43-.08.554.18.13.607.096.72-.057zm1.248.086a.763.763 0 0 0 .214-.203c.241-.33-.352-.622-.745-.366-.406.265.08.785.531.569zm2.288 3.094c-.033-.039.117-.387.334-.775.216-.387.411-.665.433-.618.07.152-.201 1.28-.33 1.372-.15.108-.354.117-.437.02zM8.2 47.092c-.29-.343-.221-.434.14-.182.176.123.321.263.321.31 0 .165-.279.087-.46-.128zm8.649-.145c0-.053.102-.18.227-.282.25-.204.312-.113.143.207-.095.18-.37.236-.37.075zm8.065-.827c-.243-.025-.48-.088-.527-.141-.11-.125-.114-3.043-.004-3.043.045 0 .132.149.193.331.127.38.228.42.31.124.094-.337.065-3.472-.039-4.297-.449-3.55-1.865-6.124-4.342-7.89-1.086-.774-2.653-1.436-4.047-1.711-.764-.15-.522-.224.598-.182 2.364.089 4.167.706 5.847 2.001a11.046 11.046 0 0 1 2.32 2.502c.453.682.64.854.64.584 0-.07.063-.882.139-1.805.679-8.26 2.396-15.1 4.984-19.86 1.86-3.422 5.108-6.817 7.885-8.244 1.397-.718 2.539-.988 4.02-.952.933.023 1.01.036 1.77.307a6.822 6.822 0 0 1 1.363.662c.612.407 1.309 1.004 1.235 1.058-.026.018-.343-.165-.705-.407-2.657-1.771-5.062-1.52-7.12.742-1.108 1.22-2.651 3.53-3.634 5.443-2.828 5.503-4.541 11.464-5.291 18.413-.163 1.509-.282 3.76-.195 3.703.032-.022.266-.52.518-1.108 1.597-3.723 3.578-6.428 5.79-7.908.672-.449 1.612-.904 1.715-.83.022.016-.172.22-.432.454-1.957 1.754-3.248 3.76-4.232 6.572-.938 2.68-1.366 5.588-1.368 9.3-.002 1.741.188 4.385.366 5.101.125.505.08.546-.585.546-.55 0-2.306.138-3.416.27-.414.05-.817.04-1.609-.036-.58-.056-1.129-.119-1.218-.14-.165-.037-.18-.014-.2.302-.01.186-.098.203-.728.139zm2.507-6.725c.294-.11.375-.22.375-.517 0-.63-1.309-.706-1.524-.088-.074.211.13.51.42.616.297.108.413.106.73-.011zm2.369-.052c.277-.222.318-.364.174-.611-.4-.691-1.755-.307-1.428.404.121.266.299.35.738.354.227 0 .387-.045.516-.147zm3.011 6.681c-.027-.05.088-.268.256-.484.879-1.135 1.22-1.544 1.284-1.544.04 0 .056.037.036.082l-.423.964c-.212.485-.445.924-.519.977-.169.122-.57.125-.634.005zm2.446-.596c0-.121.853-.683.896-.59.018.04-.056.209-.166.376-.168.259-.238.305-.464.305-.164 0-.266-.035-.266-.091zm-13.04-.124c-.177-.159-.493-.656-.462-.725.018-.038.248.1.512.309.264.207.457.405.428.438-.075.088-.371.074-.478-.022z" fill="#9bb92f" stroke-width=".078"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="typescript" xmlns="http://www.w3.org/2000/svg"><path d="M49 51h408v408H49V51zm246.669 314.879l19.463-1.702c.922 7.8 3.067 14.199 6.435 19.198 3.368 4.998 8.597 9.04 15.688 12.124 7.09 3.085 15.067 4.627 23.93 4.627 7.87 0 14.819-1.17 20.845-3.51 6.027-2.34 10.512-5.548 13.455-9.625 2.942-4.077 4.413-8.526 4.413-13.348 0-4.892-1.418-9.164-4.254-12.816-2.836-3.651-7.516-6.718-14.039-9.2-4.183-1.63-13.436-4.165-27.759-7.604s-24.355-6.683-30.099-9.732c-7.445-3.899-12.993-8.739-16.644-14.517-3.652-5.779-5.478-12.249-5.478-19.41 0-7.871 2.234-15.227 6.701-22.069 4.467-6.842 10.99-12.036 19.569-15.581 8.58-3.546 18.116-5.318 28.61-5.318 11.557 0 21.75 1.861 30.577 5.584 8.828 3.722 15.617 9.199 20.368 16.432 4.75 7.232 7.303 15.421 7.657 24.568l-19.782 1.489c-1.064-9.856-4.662-17.301-10.795-22.335-6.133-5.034-15.191-7.551-27.174-7.551-12.479 0-21.573 2.286-27.281 6.86-5.707 4.573-8.561 10.086-8.561 16.538 0 5.602 2.021 10.21 6.062 13.826 3.971 3.617 14.34 7.321 31.109 11.115 16.769 3.793 28.273 7.108 34.513 9.944 9.076 4.183 15.776 9.483 20.101 15.9 4.325 6.417 6.488 13.809 6.488 22.175 0 8.296-2.375 16.113-7.126 23.452-4.751 7.338-11.575 13.046-20.474 17.123-8.898 4.077-18.913 6.116-30.045 6.116-14.11 0-25.933-2.056-35.47-6.169-9.537-4.112-17.017-10.299-22.441-18.559-5.424-8.26-8.278-17.602-8.562-28.025zm-65.728 50.094V278.454h51.583v-18.399H157.938v18.399h51.37v137.519h20.633z" fill="#0288d1"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="typescript-def" xmlns="http://www.w3.org/2000/svg"><path d="M457 459H49V51h408v408zM69 71v368h368V71H69z" fill="#0288d1"/><text x="342.219" y="344.544" font-family="ArialMT" font-size="12" fill="#0288d1" transform="translate(-6058.94 -5838) scale(18.1514)"><tspan style="-inkscape-font-specification:sans-serif" font-family="sans-serif" font-weight="400">TS</tspan></text></symbol><symbol viewBox="0 0 24 24" id="url" xmlns="http://www.w3.org/2000/svg"><path d="M16 6h-3v1.9h3a4.1 4.1 0 0 1 4.1 4.1 4.1 4.1 0 0 1-4.1 4.1h-3V18h3a6 6 0 0 0 6-6c0-3.32-2.69-6-6-6M3.9 12A4.1 4.1 0 0 1 8 7.9h3V6H8a6 6 0 0 0-6 6 6 6 0 0 0 6 6h3v-1.9H8c-2.26 0-4.1-1.84-4.1-4.1M8 13h8v-2H8v2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="verilog" xmlns="http://www.w3.org/2000/svg"><path d="M17.282 17.08H6.718V6.513h10.564m4.226 4.226V8.627h-2.113V6.514c0-1.173-.95-2.113-2.113-2.113H15.17V2.288h-2.113v2.113h-2.112V2.288H8.83v2.113H6.718c-1.173 0-2.113.94-2.113 2.113v2.113H2.492v2.113h2.113v2.113H2.492v2.113h2.113v2.113a2.113 2.113 0 0 0 2.113 2.113H8.83v2.113h2.113v-2.113h2.112v2.113h2.113v-2.113h2.113a2.113 2.113 0 0 0 2.113-2.113v-2.113h2.113v-2.113h-2.113V10.74m-6.339 2.113h-2.112V10.74h2.112m2.113-2.113H8.831v6.34h6.338z" fill="#ff7043" stroke-width="1.056"/></symbol><symbol viewBox="0 0 24 23.999999" id="vfl" xmlns="http://www.w3.org/2000/svg"><defs><style>.jra{fill:#f05223}.jrb{fill:url(#jra)}</style><radialGradient id="jra" cx="205.45" cy="208.29" r="225.35" gradientTransform="matrix(.04556 0 0 .0456 2.888 2.88)" gradientUnits="userSpaceOnUse"><stop stop-color="#ffd104" offset="0"/><stop stop-color="#faa60e" offset=".35"/><stop stop-color="#f05023" offset="1"/></radialGradient></defs><title>houdinibadge</title><g stroke-width=".046"><path class="jra" d="M19.97 3H4.03A1.03 1.031 0 0 0 3 4.031v4.135C4.548 6.977 6.563 6.21 8.948 6.21c5.107.003 8.35 3.574 8.348 8.081 0 3.13-1.46 5.485-3.746 6.71h6.42A1.03 1.031 0 0 0 21 19.968V4.031a1.03 1.031 0 0 0-1.03-1.03z" fill="#f4511e"/><path class="jrb" d="M3 17.722v2.247A1.03 1.031 0 0 0 4.03 21h1.837C4.474 20.21 3.49 19 3 17.722z" fill="url(#jra)"/><path class="jra" d="M8.948 8.231c-2.586-.09-4.598.86-5.948 2.264v3.163c.918-2.654 3.447-3.87 5.565-3.85 2.647.027 4.689 2.025 4.7 4.284.012 2.159-.892 3.748-3.33 4.14-1.33.213-3.411-.567-3.318-2.578.046-1.037.854-1.622 1.777-1.58-.905 1.213.293 2.102 1.139 1.921 1.048-.224 1.475-1.156 1.475-1.878 0-.762-.718-1.994-2.498-1.951-2.204.052-3.591 1.639-3.638 3.602-.056 2.468 2.253 4.091 4.622 4.121 3.48.046 5.543-2.24 5.539-5.586-.005-3.029-2.434-5.946-6.085-6.072z" fill="#f05223"/></g></symbol><symbol viewBox="0 0 24 24" id="virtual" xmlns="http://www.w3.org/2000/svg"><path d="M21 14H3V4h18m0-2H3c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h7l-2 3v1h8v-1l-2-3h7a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 281.25 281.25" id="visualstudio" xmlns="http://www.w3.org/2000/svg"><path d="M196.18 101.74l-52.778 42.444 52.778 40.889V101.74m-136.67 110l-30-18.889v-100L62.843 81.74l47.778 37 96.666-89.222 44.444 27.778v172.22l-55.555 22.222-85.111-81.555-51.555 41.555m3.333-48.889l20.667-19.111-20.667-19.778z" fill="#ab47bc" stroke-width="11.111"/></symbol><symbol viewBox="0 0 300 300" id="vscode" xmlns="http://www.w3.org/2000/svg"><defs><style>.icon-canvas-transparent{fill:#f6f6f6;opacity:0}.icon-white{fill:#fff}</style></defs><title>BrandVisualStudioCode</title><path d="M218.62 29.953l-105.41 96.92L54.301 82.47 29.955 96.64l58.068 53.359-58.068 53.359 24.346 14.212 58.909-44.402 105.41 96.878 51.424-24.976V54.93zm0 63.744v112.6l-74.719-56.302z" fill="#2196f3" stroke-width="17.15"/></symbol><symbol viewBox="0 0 24 24" id="vue" xmlns="http://www.w3.org/2000/svg"><path d="M1.821 4.15l10.21 17.618L22.24 4.235V4.15h-7.692L12.113 8.33 9.691 4.15H1.82z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179H5.937z" fill="#35495e"/></symbol><symbol viewBox="0 0 420 419" id="watchman" xmlns="http://www.w3.org/2000/svg"><g stroke="#fff" stroke-linecap="round" stroke-linejoin="bevel"><path d="M166.95 145.32a93.935 123.23 0 0 1 92.934 3.263" fill="none" stroke-width="18.467"/><path d="M162.92 137.96L44.63 256.25a174.07 173.93 0 0 0 5.705 16.486l123.68-123.68-11.096-11.096zM266.54 144.04l-11.096 11.096 117.16 117.16a174.07 173.93 0 0 0 5.691-16.5l-111.76-111.76zm170.65 170.65v22.193l17.1 17.1 11.096-11.098-28.195-28.195z" fill="#fff" stroke-width="1.963"/><path d="M167.52 273.36a93.935 123.23 0 0 1 92.934-3.263" fill="none" stroke-width="18.467"/><path d="M49.516 144.56a174.07 173.93 0 0 0-.809 2.213 174.07 173.93 0 0 0-4.757 14.344 174.07 173.93 0 0 0-.016.055l119.56 119.56 11.098-11.096-125.07-125.07zM454.87 64.703l-17.668 17.668v22.191l28.764-28.764-11.096-11.096zm-80.984 80.984l-117.86 117.86 11.098 11.096 112.18-112.18a174.07 173.93 0 0 0-5.416-16.777z" fill="#fff" stroke-width="1.963"/></g><image x="21.229" y="20.262" width="378" height="377.1" preserveAspectRatio="none" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaQAAAGjCAYAAABjSWGNAAAgAElEQVR4AeydB3hUVdrH/+fOpNMF JAFUmivFXtZuIBRRQUUTil1RV3et6Lr6rSu6rg3B1dXVtXeBCCioCASIimLDhkFsgAIJSAkhPZmZ 8z3vjReGMJlMueeWmfc88MzMLaf8zp3855zznvcV4MQEkoxA7sVr01PrUrv4pdZNBNA1AHQRkJ0g ZAcJrYOA7Aipv8/Q6JgUKdBkG0iZISDSDVwSaAfAY3z+/bVSAD56L+mfwA5INAqgSkrUCKA2IOQO IcQOIUU5pNwhNZQLia2Q2OyH3OJBxqaiwk4VzfLlj0wg4QmIhG8hNzDJCEgxJH9DjgfevgGBAyCx vxSyh5DoAWA/CHQH0MEFUOoBbJbAeiHEBiED6wMSv0DgF02In1J2Vq+ZP78fXcOJCSQMARakhOnK 5GrICaO3tE3P8B0kERgkgP6Qop8E+gqgL4Bdo5gEphKAwHpIrIHAjxL4DpCrAo1yVfHsHhsSuN3c tAQmwIKUwJ2bGE2TYsjY33qLQOBwCHk4JA6HhgE08kmM9ilpRQUgvwPE1xD4MiC0r2R67dfFz/eq U1IaZ8oETCLAgmQSSM7GHAIj8td38sN7HAROkpDHATgMTWs15hSQvLn4JLBaQHwqEPgQQiwvmpHz XfLi4JY7kQALkhN7JYnqlDd2w4GQnuMk5AkCOBHAQQD4ubTgGZDAFg1ieQCBj6Dhg7SKmhW8LmUB eC6iRQL8xW8RDZ8wn4AUQ8eVHib9YjCELj4nAOhqfjmcY4wE6iGxAkIsCwDvSel5v7iwa1WMefFt TCBqAixIUSPjG6IhkDtmQw/N4xkKTQ4TEkNZgKKhZ/u1DZD4CAKLIMSik/p3WzF5sgjYXiuuQMIS YEFK2K61p2GTJ0tt2Xebj5EyMArAab+vAdlTGS7VVAI0xSeAd6QQb2uBtIW8V8pUvJwZz9XzM2AG AdpoKqpTh3uEdlYA8gwBdDEjX87D0QQaALlMCLzha5Rz2NTc0X3lmsrxCMk1XeWsig6/YFOWr0Ge JoBzICWNhNo6q4ZcGwsJkFOKT4TUZiOgzSqate8aC8vmohKIAAtSAnWm6qaMHPljWl2bzBGapk2A lKMBZKguk/N3JYHPpMCrHuGbuWj6fqWubAFX2hYCLEi2YHdToVIMKSg7SQAXABgDoJObas91tZWA H5DvCeCV2rrUwg/ndqm0tTZcuOMJsCA5vovsqeDwszd19af4LwbEZQAOtKcWXGoCEaiCwAwB7emi Gd0+TqB2cVNMJMCCZCJMt2dFFnIflJQNh8BEgKbkRIrb28T1dySBlULIpzyBwCsLCntud2QNuVK2 EGBBsgW7swqlvUIer+cSQE7UPWI7q3pcm8QlUAtgtibFU4sKu70PCJm4TeWWRUKABSkSSgl5DTkt 3XSqJuVfJDAiRFyfhGw1N8qxBH4AxJP+hrpnit/otcOxteSKKSXAgqQUr/My18216wMXCOB6AH9w Xg25RklOoEoI8bzw+R5ZNKvnj0nOIumaz4KUJF0+YsyWbL/Xdx0gr5BAxyRpNjfTvQQCkPIdDdqD iwqz33NvM7jm0RBgQYqGlguvbfKmrd2MJrPtNBc2gauc5ASElJ8EIO4/eWD2m+xLL7EfBhakBO3f wfmlR2oCtwI4G4CWoM3kZiURASmwGpAPVLTPeXnFk6IxiZqeNE1lQUqwrs4bW3Y0AoF/QIjT2Vdh gnUuN8cgsFYK3LNPoPzFwsKBDcZBfnU/ARYk9/eh3oIh+WXHaELeIZs8bCdIq7gZTCAsgXUQuK9T oPw5FqawnFxzkgXJNV0VuqJD8zcdDCHvlvpG1tDX8FEmkOAE1gohJp/Yv9vLvMbk7p5mQXJp/w0Z u7mPkP7JACbwGpFLO5GrbS4BiRII/H3xzOw3eZOtuWityo0FySrSJpWTO760s8cv7wQEeVVINSlb zoYJJBAB+bGQ4uaiwpxlCdSopGgKC5JLuvm4/PUZmcJ7NSD/DqCDS6rN1WQCdhKY45f+vxYX9vzJ zkpw2ZETYEGKnJVNV1L4h9JxAuI+9jNnUxdwsW4mQFZ4//E31N/NLomc340sSA7uo2H5Gw7zC+3f AjjFwdXkqjEBNxDYKgVuPbl/9rNs+ODc7mJBcmDf5J61toMnNfVfgLgCgNeBVeQqMQF3EpD4XGja NRyTyZndx4LkqH6RYkj+pouEkFMAdHZU1bgyTCBxCEgIPFvvabx52av7lydOs9zfEhYkh/Th0HM2 95Ye//8ADHVIlbgaTCDRCWwWENcWzcyemegNdUv7WJBs7qncXOnVum66XoBMuZFpc3W4eCaQdAQE xFyfz//n4tk9NiRd4x3WYBYkGzuEjBak0J6WwJE2VoOLZgJMANgJIW49qX+3J9jowb7HgQXJBva0 pyhLeO+QkJPYaMGGDuAimUDLBJYJgSuKZuR81/IlfEYVARYkVWRbyHfouLJcBPCUhOzbwiV8mAkw AXsJ1EOKe3Z07HYvh7mwtiNYkCzinXvx2nRvTdq9EriOw0JYBJ2LYQLxEfgCUlywuDB7VXzZ8N2R EmBBipRUHNcNGVt6hJB4CcCAOLLhW5kAE7CeQB1tqF0yI/thdtiqHj4LkkLG+fnSUy7KbpWQ/wBE isKiOGsmwASUEpBLPBouXji9+3qlxSR55ixIih6A3DEbemhe7WV2+6MIMGfLBKwnsA3AxMUzc96w vujkKJEFSUE/D87fOFoT4lkA+yjInrNkAkzAVgLyv/7MhknFz/eqs7UaCVg4C5KJnTpy5I9p9W3b TBGQf2HDBRPBclZMwHkEVnr8KFg4K2e186rm3hqxIJnUd0PGlO4vPHgdAkeZlCVnwwSYgLMJVEHI yxfP6D7d2dV0T+1YkEzoq6H5ZadLIV/gKToTYHIWTMBlBIQQj6bsrLpp/vx+9S6ruuOqy4IUR5fo VnRa2Z1S4jaeoosDJN/KBNxP4DMhcW5RYc6v7m+KfS1gQYqRvR6zKCXtVQiMjDELvo0JMIEEIiCB LR4p8hcVZr+XQM2ytCmapaUlSGF5+WUDvKnpn7EYJUiHcjOYgAkEBNAlIAKLho7deI0J2SVlFjxC irLbh+SXni0EXgTQJspb+XImwASShICAeC6lsuoqXleKrsNZkKLglVdQeiuAf/F6URTQ+FImkLwE lnkatXMWzun2W/IiiK7lLEgR8KL9RQ1tsyia60URXM6XMAEmwAQMAmsDHjFq6WvZJcYBfm2ZAAtS y2z0M7njSzt7/JgD4MRWLuXTTIAJMIFQBCqkkAVLZnRfGOokH9tNgI0adrPY611u/vq+Xr9YzmK0 Fxo+wASYQOQE2gsp3h4ytnRi5Lck55UsSC30+5D8smM8wvMhB9JrARAfZgJMIBoCXiHxVN7YjXcC kmemWiDHYEKAyRu76QzIALkDyQpxmg8xASbABGImQBZ4vt+6XVFcLHwxZ5KgN7IgNevYvPyNl0EI MmDwNDvFH5kAE2AC5hCQmJ9Zh3PnzcupMSfDxMiFp+yC+nFIQdlNEOIpFqMgKPyWCTAB8wkIjKxJ xyLy+GJ+5u7NkUdIet9JkVdQRvuLaJ8RJybABJiAVQS+9kvvqcWFXTdZVaCTy0l6QZo8WWrLVpU9 JoE/ObmjuG5MgAkkJgEB8ZNPw7Di6dnrErOFkbcqqQWJvHVvF2UU2fXCyJHxlUyACTAB0wn86pf+ vOLCnj+ZnrOLMkzaNaTcXOndLja9zGLkoqeVq8oEEpfAfh7hfW/4OaUHJW4TW29ZUo6Q8vNLUreh 42tCYEzriPgKJsAEmIBlBDZDiiGLC7NXWVaigwpKOkEiMSoXnQol5GgH9QNXhQkwASagE6C4SprU 8ooKu61MNiRJNWVH03Q0MmIxSrbHnNvLBNxDgOIqSREoGjq2tL97am1OTZNmhEQGDNtE6asCosAc dJwLE2ACTEAlAVkGIXMXz+jxg8pSnJR3UoyQfreme57FyEmPHteFCTCB8ARENqS2mJw8h78ucc4m gSBJUS7KHgVwfuJ0G7eECTCBJCHQwyM8i3LHbOiRDO1NeEEaMrbsHt70mgyPMreRCSQsgQM8Hu3d kfllXRK2hb83LKEFaUjBxluExN8SvRO5fUyACSQ4AYGBjQjMG5q/vX0itzRhBSlvbOmVAuLeRO48 bhsTYALJQ0AK8UcpamfT1pVEbXVCCtLg/I2jIfEYgKSxIkzUB5TbxQSYQDABMWS76Phsogb5SzhB Gjxu0x81IV7jEBLBDzG/ZwJMIIEInJc3tuyhBGrPrqYklCCReaQIBOYByNzVQn7DBJgAE0g0AhLX Dc0vTbj18YSZ0iILlEaBjyRk0tjsJ9p3jNvDBJhAVAQkpBy/uLD7jKjucvDFCTFCokW+Bsg3WIwc /KRx1ZgAEzCbgIAQz9IyhdkZ25VfAgiSFPoin8DxdkHkcpkAE2ACNhHI1AKBuYmycdb1gjS0oOz/ AJxn08PAxTIBJsAE7CbQ1ePV3kmEPUquFqS8/LJzJXCX3U8Dl88EmAATsJnAwVLUv+R2c3CPzRBj Ln5o/qaDIeRbABJ2k1jMcPhGJsAEkpHAH3oPrNTWlkxb6tbGu9LKLu/sDfsgRfsEQB+3gud6MwEm wAQUEJBC4NyiGTmzFeStPEvXTdlRKAmkaLTxlcVI+ePBBTABJuAyAkJKvKDPILms4lRd1wlSudj0 LwDDXMiaq8wEmAATsIJAG6kFZrnRyMFVgkQ+6iTkX63oUS6DCTABJuBaAhL9Aqhznc871wjS0HM2 99aEeIEdprr2K8IVZwJMwEICQmBMXsGmGy0sMu6iXGHUkHvx2nRPddpHEDg87hZzBkyACTCBpCEg G4UUQ4oKc5a5ocmuGCF5a9IeYjFyw+PEdWQCTMBZBESKFGKGbpnsrIqFrI3jBWno2NIxHII8ZN/x QSbABJhABARkDlI8z7lh06yjBWnImNL9IfF0BMT5EibABJgAE2iRgByVl192bYunHXLCsWtItN9o O8reBztNdcijwtVgAkzA5QTqhSaPK5re/UuntsOxI6RyUXo7i5FTHxuuFxNgAi4kkBaQ4tVRo0od G8DUkYI0JL/sGAlBXrw5MQEmwASYgEkEhMRBtZnifpOyMz0bx03ZDb9gU5a/PvAFgANNby1nyASY ABNgAlIKeeqSGd0XOg2F40ZI/nr/AyxGTntMuD5MgAkkEAEhpHh2RP76Tk5rk6MEacjYjcMBcZXT IHF9mAATYAIJRqC7X3gedVqbHDNld8LoLW3T0xu/BbCf0yBxfZgAE2ACiUhASoxZUpgzxyltc8wI KT29cQqLkVMeC64HE2ACyUBACPnYiRN+6eiUtjpCkIbll50C4AqnQOF6MAEmwASSg4DITvN7pzml rbZP2ZFNfE0GvuGAe055JLgeTIAJJB0BiZGLC3Petbvdto+QajPEP1iM7H4MuHwmwASSmoDA407Y MGurIOWNX3+IRMBV8TqS+qHlxjMBJpCoBA6oyRB32N042wRp8mSpwe99AhApdkPg8pkAE2ACTEDe OHjshkPt5GCbIC0r2XQlII+zs/FcNhNgAkyACewi4NWkeEIfLOw6ZO0bWwRpZH5ZFynkPdY2lUtj AkyACTCB8ATEse9/V3Zp+GvUnbVFkBqaxKiDumZxzkyACTABJhATAYl7cs9aa8vfZ8sFadi40qMA 2KbAMXUQ38QEmAATSBICAuiipabfZUdzLRYkKQIB+R8AFpdrB1oukwkwASbgTgIC8uph+RsOs7r2 lgrDkPxNFwHiWKsbyeUxASbABJhAVAQ8AWgPRXWHCRdb5qnhd48MPwDobkK9OQsmwASYABNQTEAI eVbRjO5vKi5mV/aWjZBq0sVNLEa7uPMbJsAEmIDjCUgpHjjyCmnZXlFLBGnEmC3ZEPJmx9PnCjIB JsAEmEAwgQM7lJddHXxA5XtLBMmX0vhPAG1UNoTzZgJMgAkwAQUEBG63KkSFckEafk7pQZC4SAEm zpIJMAEmwATUE9gn1Z/yV/XFWGB+7feARkdeKxrDZTABJsAEmID5BITENXnjN+9rfs575qh0hPT7 Jthz9iySPzEBJsAEmIDLCGSJQODvquusVJACAZC/OstMy1XD4vyZABNgAslKQEp5xfD8Tb1Utl+Z IA0pKD0ZwDCVlee8mQATYAJMwDICqX7NTwFVlSVlgiQgbQ/2pIwaZ8wEmAATSEYCUpw/ZOzmPqqa rkSQ8saVngSIIaoqzfkyASbABJiALQS8Aj5la0lKBAkBOdkWVFwoE2ACTIAJqCUgxflDz9ncW0Uh pgvS0PzSE3l0pKKrOE8mwASYgCMIeOFRY3FnuiBB4FZHIONKMAEmwASYgBICEvK84eM29jQ7c1MF KW/8+kMkMNLsSnJ+TIAJMAEm4CgCqb6AuMHsGpkqSPB7yL0E7zsyu5c4PybABJiAwwgI4PIR+es7 mVkt0wQpd1zZAQDGmlk5zosJMAEmwAQcS6CNT3j/bGbtTBMkLYAb2WedmV3DeTEBJsAEnE5AXntc /voMs2ppitPT3LPWdhCQl5hVKc6HCdhNwOsVaN9WQ1amQFaGQEaGhox0gdQUAaEBaali19x0Q6PU j1OdG30S/gDQ0CDh9wPVtQFUVUlUVgdQXRNATa20u2lcPhMwk0DnLOE5H8BTZmRqiiBpqekTAcnx jszoEc7DUgIkLL3396JvrxT03T8FPXK8yNnXgy77eJTUo7pG4tdSH9Zv9OGXDT78vK4Rq39uRFV1 QEl5nCkTUE1AAtcD8mlAxP1rK24DhNxc6fV0LfsZwH6qG875M4F4CXTqoOHQAWk4dGAqBv0hFQf0 NOU3WbzV0gXqu58asXJ1A75YWY/NW/xx58kZMAGrCAjg1KKZOQviLS/ub6O3a9mZksUo3n7g+xUR SEkROKR/Kv54eJr+v3t23I+8kpr27O4F/R9+StN0/MZNPnzxTQM++7oeK76pR31D3D8+ldSbM2UC REBKXAUgbkGKe4SUl1+6FAK53C1MwCkEaK3n2CPSMfiEdBx/VLpTqhVXPZZ9WocPP6vDxyvq9fWo uDLjm5mA+QQCwu/pVzRr3zXxZB2XIOXllw2AkCXxVIDvZQJmEKCR0PFHpWHw8Rk48ZjEEKGWuJAw LXq/Fp98WY/GRh45tcSJj1tLQEDcXzQz+2/xlBqfIBVsfBQQptqhx9MYvjf5CPQ5IAUjB2fgrFOz kq/xAN4qqsE7i2vww5rGpGw/N9pRBLZ2kuXdCwsHNsRaq5gFadSo0syaDGwE0CHWwvk+JhALAbKM yzsxA6OGZ6Jfr5RYski4e0iQ5i2qwdIPa1FXz6OmhOtglzRISHleUWH3V2OtbsyCNLRg46US4plY C+b7mEC0BPbt4sGoYZkYdybvMAjHbtbb1Zi7qAYby3zhLuNzTMB8AhIfLC7MoWjhMaWYBElKeeCM udULXp1TeQDtq+DEBFQSGHRQKkYPy8SQE03bEK6yuo7J+73ldZi7sBpfr4p5BsUxbeGKuIOApkE+ PLnTjQMOSv93LDWOWpBIjAB8T4XNW1iDp1/bCRalWNDzPa0ROO7IdH1a7pjD0lq7lM+HIfDNqgZ9 xFT8UW2Yq/gUE4iPAE2fP35fZyOTvwsh/mV8iPQ1FkG6F8AuSwoWpUhR83WREsg9PkMfER0yIDXS W/i6CAjQxtt5C6t1Cz3JExsREONLIiXQe78UPDlllxgZt3mFEFHt8I5KkKSU5E9lr4lpFiWDP7/G Q+CUY2lElIXDBrIQxcMxknsfeqoCbxfVRHIpX8MEwhJoQYzontOEEPPD3tzsZLSCdCqAkAWQKP3v 5Z1s4dMMMH9snQBNyZHFHE3RcbKOwKofGnTLPNrTxIkJxEIgjBhRdjOFEFGFJIpWkF4EcEFLFSdR eviZipZO83EmsAeBA3unYPTwTJw6OHOP4/zBWgLkmohMxskbBCcmECkB8gP59INdWru8vRBiZ2sX GecjFiQpJf3VqDZubOmVRaklMnzcINCxvaabbp9zuns3s1IYCWMdJjNDoLau6XNKCnaFojDa65ZX GimRVd53P/ImW7f0mV31pC0Y40a30Wc2WqnDJUKI51u5ZtfpaASJhl7Td90Z5g2LUhg4SXxKCGD8 WW1w6bi2jqZAISHWl/qwcZMf5OR0yzY/tu8IoKIygIqdgV1CFK4RXg/QsYMHnTtq6NTRg25dPdi/ hxf75XhBZuxOTrSPqfCtamzdHtV6tJObxHUzkUAUYkSlLhJCDI+0+GgE6U0AoyPNmEUpUlLJcR15 Vrj1Guc59aA/urRP59vVDbr7nTW/+pT7hyNh7pHtxYADU3BQ31TdiKNnjvO8kD8/sxKvzK6KSICT 4ynmVkYpRgawbCHEJuNDuNeIBElK2RHA9nAZhTrHohSKSnIdoz+8Z52a6Shfcx9/UY9PvqjDFysb 9BGQE3qka2cPjjg4DUcflgayNnRSuu/RHSj6gA0fnNQndtQlRjGiql4jhHg0kjpHKkgUnvzZSDJs fg2LUnMiyfP5vDFtcMlYZ0zP0R/UpR/V4qtvGxwfW4g8lx91SBpOOCYNp+Y6w+CDnLjSd/nnX3h9 KXm+wbtb2r6thosL2kayZrT7pt3vioUQg3d/bPldpIL0NtmUt5xN+DMsSuH5JNpZp3hYIKuxhe/V 6kHu3BqmgcSJggtS4D4nxHZ66fUqvPh6JU/jJdqXNkx7sjIFJo5vF6sYGTnvK4T4zfjQ0murgiSl bA9gR0sZRHqcRSlSUu69zuMBLhvXDgWj7bOeW7fBh/lLavQpJjJASKTUrq2GYSdl4KqL2tnerH8+ VI73PmYzcds7QnEFTBIjquWVQognW6tuJII0HkDM7sSDK8CiFEwjsd4POzkDt/zZPqMFGg3NXViD L1bWJxbYEK0ho4hDB6TqXi3sXG+a/U41XplTpVsehqgmH3I5ARPFiEgsFEKMaA1JJII0E0B+axlF ep5FKVJS7riOwoVfPLYtzjnNnlHRzHnVmLeoGmWbk9NEOWdfjy5M+WfYw5+e0keeqdB/DLjjieVa RkKA9tZdPiHuabrmRXUQQoT1nNCqIAUCcqYQ5gkS1ZBFqXk/ufOznaOiFwsrMXt+DaqqE2taLtYn gf6AnD0yyzYjkjcX1GD2/GqOwRRrBzrsvusuax/vmlGoFh0phPgi1AnjWKuCNOutquvHnJ71kHGD Wa8sSmaRtD4fWiuiX0/n2vCr/IXCSsycW+14Sznre6WpRK9XYMxpmbjiPHvWmZ54aSdef6tVhy52 4eFyIyCgSIyoZE0IEdbPPHnvDpu2+f5y7uqfGo/PO8nc4Gh/6JOCju09+OTLxJ/zDwvYZSfJ0uvZ aV0w4EBrvQ2QEP3ffeX63iF/cs7ORfSkBAJAyfeN+tpOba3EkYdYG0vqqEPT0KG9B6Wb/dhZyaPX iDrNQRepEqM/3bK14sWnMqesWnVnfILUa9Ckh0s3+Tuv/qkRLEoOenJsqArtKbpuIhldWpdmzK3C P6aU47Ov6sFCFDl38rNX8kMjXn2jGo0+icMHWSdM9GPzrFOzUF0r2S9e5F1m+5WqxOiav28lLyjp dahatGbV1F/DNTTsCGnI2M19hJR3UQbk14tFKRzKxD3X94AUfVOclc5Q58yvxgOPV2Dph3U8PRfH o0UjppXfNWDGvGpoAjjYQj96Rx+ahvbtPPh5nU93PhtHM/hWxQRUidH1d2zDqh9+30wtsGltydQl 4ZoSVpB6DbxxrADOMDIgUfplgw+nHMfTdwaTRH+lX7p33dwR/XqnWNLU5Svq8PgLO/HGuzU85WMi cRpdfvltAxYva9o71L+fNVOuB/VNAVkA/rYtgJ/WsZcHE7vUtKyuubQ9Ro8w3yMIiRH5iAxKaWtL pj4d9Hmvt2GNGobkl84SAmOa30V7H26/gdzbmZvY0MFcnvHmRhswrTTnfvQ5EiJnLoiTFdthA9NA 01H7dfcip5sHbTI1tMnSdMwNjVIfBZBn8A2lPtAG3a9K6rH2170CLMfbLabcT/uYRg/PwinHWec3 j4wdyOiBk3MIkBidaY0YUaP9XunvuqCwZ4t+UVsUpPx86dkuyrYCCLnbUZUokfnof54Na6runN5M 0JrQegMFzjvpj9b8saJ1ohdmVoH+qDsppaUKDD4hA8NPzsAhA2IbUWwvD+CDT+vwzmJn+oEbNSzT 0nXBjz6v04MB0pogJ3sJqBKjG+7YhpV7jox2N1SK/MWF2a/vPrDnuxYFKS9/w3EQ2kd7Xr7nJxal PXkkwieaXrnyAmtMht9bTt4VqvXwD05iRzvUzz29DS44t42p1fr863p9Ayn9UXZS0jToa4QTzja3 veHa+NyMptAW4a7hc+oIqBKj2+7djk/D/dgQeHLxjJwrW2pZi2tIvQbedJEQGNLSjXSc1pNUrCnR vDMthn7KJuHh8Jt+jqboLjjXGu/cT76yU18r2rzFOTbcNCKiP8r33baP7prHbMA53bz6iOsPfVJR XRvAxjJntJ0s8mh9icJy0B6zvr3UrxfSKJy+4zSlSdF3OVlH4M8Xt1MSDqZVMWpqYoe1JVMfaam1 LY6QhuaXLpACEUX645FSS3jdcZz2FNHUDXleUJ30Hf3vVDsmDhG1lzaTjh1tvZcDGimR/z0aOTkp 0ZoC/YK2Kt37nx1YvIzjLVnBm8SIPHqYnSIUI71YKf09lhT23BiqDiFHSLm50ivaVP0XQEQT5zxS CoXWHcdGDc/EnZM6os/+6n8VP/x0BV6aVYXKKmdsmKTRwLgz22DaHftYuk/HeDIoSuzQkzJ0I4kd lQE4ZbT4/c+NmPVONVJThCUboGmt0uMR+KpkD4ssAxO/mkTACWJETdEgVqxZNW1lqGaFFKRex151 tJDy6lA3tHSMRaklMs49ftn4tpg4Qf16EVnO0eZWChXuhETesmmt7N93ddajtNpdpwN6pmBEbia6 dfVie7kfW7fbL9iNjdBHbr+W+nGyBRFsDx63vWsAACAASURBVOmfqntuIevE6hqewjP7mVQlRpOn lmP5iihH+EJsXVsy9a1QbQwpSH37T5oAgWGhbgh3jEUpHB3nnOvYXsOV57eDFRtdyWKSgrrV1tn/ R4aEiPZVPfqvziAXN05LfQ5IwWl5mejU0YMt2wIor7BfmNat9+mjWlpfG/SHiCZMYsZKJvW0zWB9 qR9ULidzCKgSI4qJ9cEnMRnoZK0tmfpYqNaFXEPKKyibC8hRoW6I5BivKUVCyZ5rjjksDffc2kl5 4bQ2MnNuFTY5wGiBhIj+0N9wuXXrImYApvW2eQur9T1NZuQXbx4nHJ2OO28yf/9hqHqRN/cXX68K dYqPRUHgTxeocYIcb4DGVCm6zi/M3tK8KSFHSL0GTnpYADGvfPFIqTlmZ3ymxcxbrwm5rczUCj79 WiWeea0SVQ6YeqHQ3/+7vwsorLrbElmbjh6RpW++JWG321np+tKm0VJ6qoaBikdLhw5MA0XI/ea7 BvicYYzotscHThUjAtkoRPG6kqk/Noe6lyANz9/USwp5W/MLo/3MohQtMbXXTxzfFpeOU2vSvWRZ LR5+Zifo1e405MQMkH8uFRZFVreN3PzQVGN6uoYNZfavsaxYWY9fNvpwyrFqrTIP6puqm+GvXN2I Tb+xKkXz3CkTo3+bE7peCPnz2pJpxc3btJcgHXDwDacC4tzmF8bymUUpFmrm3pOWJnD1Re2Vxy4i bwsPP70Tv2219w8HucK59rL2utFC1857Pd7mwrU4NxqV0BoLWb+tXe9DXb1963L03SaHrRlpAqr9 4tEot6IyALL+49Q6AVViNOVxMs+Pac0oRKVFw9qSqS81P7HXN7b3oJsmAji2+YWxfmZRipVc/PfR lM+F57bF6UPNd5wYXLsHn6jQg+YFH7P6PcVposXb8We1Qbcuez3WVldHaXmDDkpFwag2ujB9v6YR ZBFnRyKHrZ99XY/ynQEce4TaKdE/Hp6u7xejDbycWiYwcUJbFIw23+MGidGCYlNnPjpflP/gA8XF e8ZH2suoIa9g43JAmCZIBjo2dDBIWPOad2KG8vUicoY7Y16VrdMpZC1HfvdIkJI1vTK7Cq+9UWXr iClnXw/yz2ijIuz1Ht1Khh5Pv7rTEVabe1TMAR9IjGhfndlJgRjpVfT40X/hrJzVwfXd46ckOVSt FVX/BmD6LkkeKQVjV/ueHkrVgfTojyB5bq6qtmfaiJydXj6haR8VbTBN5kR7eMjlEfXEdz82gmIg WZ0qq6Ue/ZnqQF7RVSUa9Xfr6sGOioDt08Oq2hhLvm4TI72NQi5vvkF2D0HKOfjKgwBcHwuQSO5h UYqEUnzXkPHChflqjRfuf2yHvpM/vprGdnf/vim4dFw7XH1RO9CGUjsTTR9R8Luff/FhQ5kfASn1 zZ121YmE4PwxbdDogx5M0w5h+mZVA35c68OQE9QZPPTaLwWnDs7E9h0BikRqF27HlKtKjMizyjtL TJ2m24OZEFi7pmTaouCDe0zZ5Y0tPQ8SLwdfoOL9iNwM3HyV+ebHyR66QtUGOOMZoCm619+2xw9d v14p+nTQaUPUrocZbW3p1fDYTYEEySlp89Q2S9MNSM4bY/7USfOyWvtMDmxnvV1tS+j3lBShj2DH nBbz7pHWmqeff3lWFZ6fWRnRtYl4kSoxeuSZCt3PokpmUorFSwqzhwaXsccIqffASRcBOD74AhXv KaTx5q1+0EY7M1Oyegnvso8Hl09ohzNPVfflnzm3Gv99YaflfugO6OnVR3yTrmwPEiW7Erk9euqV nfr+KtqP01KimE7kk418wdEIhabT7EpHHpKGC85pq6+30FSelYnaTgYPZAlI9VCVaOqW9iuFDXmg qnCb8724oK0+VWt2NawQI6qzEGi/tmTqA8H133OEVFBKw6c9FCv4YrPf80gpfqL0B2/a5H3izyhM DnZEcqV1IfJArvoXdphm66dKvm/QfynG6o2a3DSRqfa4s+wfMVE/vrmgOuTIrjUO8Zy3IuAjjd4L 36pC6WZ7tx3Ewymae0mMzj/H/GfKKjEy2iok9i8qzPl112fjDb3mFWwsBUR28DHV71WJ0pz51Xjs +cQOl2yFJd3N/9ymx8pR/RwY+dOUF5luF4xWN9ozymrt9YH/7sDC98yZQ9+3iwdnjshCwSj72/XQ UxV4u6imteabep6CAF51oZrQB8EVjSYMQvB9bnqfKGKkM5cYubgw512D/64puxMn/NLRG/DcbZyw 6lXV9B1t1mvbRkOihkomx6g3XKHONxv5orvlX9vx68aWp6fMfkbox8kjd3dW7pamtXrTH2zyTk7P plmJPFiv+KYeRR/U6lN5FIPKrkRulAb8IVWfygs3/Whm/Wi9jb6LZHBxxMHqpvDyTsrAth0B/Jig xg6qxOh/L+3EnPnW/kjRny8hvlpbMnW58aztEqQDD/rrURC41Dhh5SuLUnS06aGk0BGqEnldePyF naD1EKsSmS3/5RJ1AhtJO2hK6//u367UcovMo8kwYulHTUYRqr0ctNTunH29GHx8Bsj4wMrNpt+u brJKpLJVJRJcenLJ4i+REhnKXKTAgpbEqPCtaptQiQ1rS6bONQrfJUi9Dp40EsAZxgmrX1mUIiNO bkFUrkfQw0lB9KxMNNq74jz1cZlaahO1+dZ7t+um0i1dY/ZxcpRKI4YPP6unxV0c2Nseg42DD0rV nbeSAYJViUZlbxXVIC1NA4WcUJHIBD4zQ8Pn31jXLhXtMPIkMbpkrPk/Qu0VI2qdrF9bMu0Zo527 BKn3oEkXmOkyyCggmlcWpfC0/nJJO6WL/HdOK8f8peasmYRvye6zfQ9IsSykwe5Sm96RR3ISom+/ t9YCLbgeFPPo4y/qdXGiUOoUE8nqRKO01FSBL1ZaN6Kg+FiffFkP8rWoKs4STYt2bO9ByQ+Nlo72 ze6/xBUjIiXarS2Zep/BbJcg9Rk46RoA/YwTdr2yKO1Nnhb6aUGYFsVVJLJQuu/RHVi52ro/SEY7 aPqxn8Wjg+dmkBCV61M6ofYSGXWz8pWixH74WR2+WtUACoZn9aZf8o9nbFy3st0kghSm5OjD1Kwr 0QisXRtND3hIG2ndllSJ0bPTKzFjrl3TdHv0QlrvQ/76xNpvH9Qrs0uQeg2c9A8Anfe41KYPLEq7 we/X3YsJZ6nzETb7nWrQ2okdsXbItFvFBund9PZ890JhJf7vvnJ9zcQOLwZ71ib0p81b/Hj/kzpQ yIWMdIH9e1jnFql9O49pVoWhWxf6KO2RIiexZDWqItEPnjOGZeplbCxzj1m4KjGiH2SvzrF2Wj5c v2oy8Maakmnr6RpdkJp82FVOBcQugQqXgRXnWJSAwwel4rF7OiubZ6fQ4k+/at8ud/rCDein3tqM /O7d/sB2fP51gy1eC2L5vlD8n/eW12HVj43IzBCwwl8f+Yhb9UOjLXt5SCjeXlyL9FSh7Hknwftt WwA/rbNvijbSZyH/jCxMnGD+uiqJEX0fHJWE9t6akqlfU510Aeox8KoDpMCNjqokoJvdqvDo4AaT 8NzjM3D3X9WFGievC9PftPfBJFdH7dtqyh676W9UYfK0cny8ot62EA3xNo42epJF3k/rfPo2hpxu akdM6ekCxcvNinkTXeuNdSUKRKgqIi15hSfvEbSu5NREYnTlBUkiRmTWAJSsLZm6lPpDf7r98PcF 9nDa4Ji+MmJwmD21Y0QSdeLmWfJQoNJb9z8p6qNNf3SMB8vrgbJf/TPnVYOmIrdud8/0jMGlpdeP Pq8D/acfKqOHZYJc5qhIJx6Trlv92bm29uTLO1G+w6/kjzIxu+L8droFnhN94CWbGFF/aEAf41nW BUlqYn9h3ZYTo+yIX5NJlFQ9kAbsa2/fhlU/WG+8YJRvvHbsYP7sMDkSJQ8dm7YkjhAZvIzX4o9q Qf8piuqo4Vkg7+dmp306emwXc9oXQ/14x40dzW6enh+53UlPE3oIFSUFxJCpqu8+OaB13DRdEB8p sb/xUf+r0HvQjWcC4hTjoBNfk2FNidZUVMwbU3+SJR25VdlQZp73gXieE/pjkD/KHF9cNBp64L8V IH9zZLGVDIlCXsxfUqOviZgdnHDRB7Uod4BFGnkJ+fDzemiK9mmRWTgZcnz6pf17lc4ckYmrLzZ/ Y7grvKELYG3J1Ifoe6uPkITUekp9b7Ozv8qJPFJS5RKEetSJfv0aTJjCp82VJLQ//2JCZs5+9Fus 3btLa7CguAan5WXihsvN/4PWYsEWnfh5XaPuZd7nB+iPttmJ8iTBe+H1Sj3on9n5R5IfRTy+5lLz +84VYtQEKCc3V3qLi4VPHyEdMOCma4RA70jg2X1NIo6UKKYJuc5RkWio/uQr9lnStdQm2ogZT7hl cm1EFkO0sZQTdN9t5GFjZ6XEMYfHt6eHhN4JIySjX/1+6KMYVcYOtFcpI03Tpwgrdlr7PJEYXXtZ UosRdbOW0rby6Z+/nbZTN3ESQvY0Ot8NrzRSomiGZicydCDLLyuT7groTDVi9NQrlfofbSvbw2XZ R4AMEd54txpTHt9hXyUUlkzGDuRdQ0UaNTwTz0ztAnKlZFVSJUZkPetEg41wXGUA+9F5w+a2R7iL nXhu3qIaUOwOs5OVokTid+4Zarwv/PupCpCTVE5MIJEIvPZGFR58wvzvvcHooTv3gdlrckbewa8q xcjOvYXBbYzmfUBqetgjbdSoUpqYNX9yNpraxHgthUhwqyjRnLFheh5j81u87Z8PlevOK1u8gE8w ARcToHUzitOlKt11c0cMO1mN1wiqM4VZUTFNRyMjN4oRMZEIdKNXrTrTY2lAPrMfIpWiRNNpZidy B3PdZe2VLNBSXW+6axve+9iejY1ms+L8mEBLBChkxgXX/qYbtbR0TTzHb/lzB9AoxuykKiCpm8WI GAsNXejVK4Xs7OQ9SJE8ECRKlMz+1WFMpz3xkjmRZ7MyBSaObwearzY7kbXZ7Hersd7CgHpmt4Hz YwLRECjb7Mdjz1cgINVY4NHfE/JcMdMkJ6QsRmF6V4ocOuvVAgFXj5CMJjpdlLp29mD8mWqcpJIY Pf3aTlBUUk5MIF4CNbXWWprFU18yB//PsxXw+SQorpbZieJ0kfd18vsYT1IlRq+/Ve3aabpmPHXH 3l4BdEmUP2NOFaVuXTwYO1qNGJFVFXnr5sQEzCIg3aNHu5r8+Is7dR91tLnc7ERRWj2aiNlyjdw9 me36jNpIYmTW7I3ZzKLPT+xL93ghRQe4fc4uqPVOE6Xe+6XgySlqonok1gMZ1In8lgnEQID2pdXU Slx+nvmRVcnVkNeLqEcjpxybjr9f1yGG1oS/JfG++1KHpEnR9CZ88911lkRJhfUdrSlFY+jQr5c6 MaJFzMT5deSu54tr61wCtNVh2pNqzMJpI3c0338So9tvMN8XX+KJkf486aA0IPEEiZpntyhRBM7H 71MzMiKXIG4173TunzKuWaIQeGdxDf4xpVxJcyL9UapKjMgNWIL+EG0aIQkI8yVcyaMQfaZ2iRIF 1vv3nftEX+EI7qBpCbftwo6gWXwJEzCVAIXquO4favYqkSiF8+hysqKRkRN9UprYaem5F69NJ08N 5jtSMrGW8WZltSgdc1gaptyuRoz+99JOR7uRj7ev+H4mYCaBku8bcOmNW5TsVaJN7aEcotL3/x8K pukSXIyaur06q4NXAubv/jTzqTIhLxIlSqr3KdHDeM+taqK8PvxMhZIvlgl4OQsm4FgCFMLi+cIm /3dm7/8zPIXTd5OSqu9/UogRgFQEMryQSHdosFhTH3LVovTFynplYnT/Yzuw6P1aU3lwZkwgWQiQ B+9HnlWzgdYQueUr6pR8/5NFjOhZDABtvBCIz1e9i55qlaJkeHUwG8fkqeVY9im7AjKbK+eXXATI EzptoA0EpOk+JEmUDGEyk+qbC2rw2PPJs8fQ70EmBehL6DWk5g+IKlFqXo4ZnynC66df2R/N0oy2 cB5MwAkE6A88xVdS9QPSrDaSGJGAJlMSgUAaCVLSjJCMznWDKN04eRu++a7BqDK/MgEmYBIBMpuu b5BQ4dXBjComoxgRN02KtmRll24GRLflQaLk1OHw9XewGLnteeL6uosAbZ+g/XxOS8kqRkY/0Agp aRMtGHo9wJUKwkzECvWKm7diza+Nsd7O9zEBJhAhAdrP1+iTuGSs+a6GIqzCHpeRk+Rkm6YLBiAR aJPUgkQwCt+q1pnYLUr0ML65sBrr1vuC+4jfMwEmoJDAK7Or0NAgbf9RSt9/w3xcYXMdnbUAPCRI zvh5YCMqu0WJHsbpc6uweYvfRgpcNBNITgL0/acwFuG8L6gkw2K0my4JEq0jJX2yS5ToYXx5dhW2 lbMYJf1DyABsI0DT936/NH3zfGsNYjHak1DST9kF47BalOhh5MB6wT3A75mAfQTI0Ims71TELgrV Khajvanw6KgZExIl8hmnOrEYqSbM+TOB6AksKK7FPx9S4yk8uDb0/f/fy+r/zgSX6Yb3JEgujA+p Fq1qUWIxUtt/nDsTiIfAex/XKRUl4/tfV58osbrjob3nvSRITZ4H9zye1J+yMgVy9lU3m0luRkYO yUxqxtx4JuBkAocMUOcvgL7/+WeYH2rdyTwjrZu6v7qR1sBh15EYTRzfTolvquCmUuRJEWR2Hnwu Gd5npFPrOTEB5xG47rL2yr//FBKdEsc2293/EvCzIO3mAavEyCiS9j4JAcyc17QXyjieDK8eXr1M hm52VRszMwQun6D+x6gBhUXJINH0KqBV0Z8FdiUNWC5GRldccX47x/rUMurIr0wg0Ql0bK9ZKkYG TxKliwuSfiuogQM0Qkp6d9JWj4x20f/9DbkuSfEKHr43BxPmcy0vCIehw6eiIdCtiwdjR7dRPk3X Up14pNREJiBkJQlScvk4b/ZU0DDdijWjZsXu9ZEeSq8XePpVtjHZC06IA7SJkRMTiJdA7/1S8OSU zvFmE/f99P0nv3rkyihZk9S0eg0yuUdIVs4Zt/agjTuzDSZO4OF7a5z4PBMwg0D/fs4QI6MtNFPi 1JAYRh1Vvnr8qNEgkncNyQprmmg7kESJLPA4MQG7CGRkJL4F5CEDUvGfu+0fGTXv42QWJQ2o0gSQ lNuFnShGxsNJ0SztcvRo1IFf3UugPs64jhr9VUjgdPxR6Zh2xz6ObWGyilIDtNqkXENyshgZ35Kz R2aB/jAkc3wUgwW/RkegsZHX11oiNuzkDNzy5w4tnXbMcRIlSkm1ppRVvUOTkOodNzmmmwFVYvT6 73GVzGzqmSMy9fqmpyX2L1YzmXFeTKAlAqOHZyoTI4pAa3YiUco/I8vsbJ2aX13x873qvIDY4dQa ml0vVWJ0273b8elXTdbzNN1mZiI3I5QK36pC6WYOUWEmW84reQgUjMoC7flTkcgZK/m/o2SMbMwq xwgcakQiMCtfB+aj65AmZHIIkhVi9MRLO6FipESi9OIjXXFQ3xQHPkdcJSbgbAIXntvGEjGi6bWX Z5lvtk2ilAQjJX2mToOQCT9CskKMjK8kiZIqV0CP/qszjj5MndNHow38ygQShQBto7gwX81Witsf 2D0yMniRbzoWJYNGNK9NAyNNAluiuc1t11opRgabJ1/eielvmv9LifK/99ZOGHJChlEUvzIBJtAC Ado+QdsoVKS/3bMdy1eE9rqmUpTMXhJQwSa2POVmuk8LaFpZbBk4/y47xMigQh4XVPxSovxvu7YD Rg3j8BUGa35lAs0JXHNpe6j64339Hdvw+dfhPa6pEiUSWTLOSMC0ldrkFVLobxKtgXaKkcGSHkpy B2L2Qiflf93E9qANjDPnJp+ncIMvvzKB5gQ6ddBwwTltlfmlu/KWrfh5XWPzYkN+pu8/JcNXXciL Yjh47WXt9bso5HrCJCFLqS3erBp/WU2CzQA5QYyMB4UWOn0+4PLzzJ/HvuK8dsjK0KDC5NSoP78y AbcQ+EOfFDx2jxrvCxTldfa71Vi/0RcVDhIl8lFp9tRhoomSDDQtHWnz5uWQzCaM1DpJjIwnd8bc KvzvJTUOMcj31VUXqjFnNerPr0zA6QTI+4JKMZo+typqMTKY0fS9ijVlEqVEmb4T0DYRLyNM2gYD nptfad7Y2LdjZjuC9xnFmi/tI3jkGTWO1c85PQvU9pQU3kAba//wfe4lcFpeJu66uaOSBtDI6OnX dmLzlvj2AKoUpTOGun9NSRMB3ZZBjxgrpVgvhDxQSY9alCn9QSbPBmanux4q37XpNd68ac63vkHi 5qvMd11CbScXZLPnV2N9aXTTCvG2i+9nAnYRoDhGKqbDqT1vLqjBo89VQJrkickILWP29N31l7fX 16oXFNfa1Q1xlys0/EqZ6CMkoQXWx52jjRmoEiPagf3+7zuwzWoePTQkcioSjQ6fe6gLDh/Ee5VU 8OU8nUWAjIVUidGc+dW6H0mzxMggp2qkRD9yR+S61hjA37ApRx8hNU3ZSbhWkFSKkeEOxHiYzHol kbv5n9vNym6vfKbc3glDT3Ltw7lXe/gAE2hO4OqL2imLHUTeVh57Xs2aL7WDREmFRxcXi1JpcbHQ p3V0QZJC6MOl5p3u9M9uFCOD6Zff1uNPt2zFO4vV2JP87S8dcM5p5vrVM+ruhFdaxD7qEB4JOqEv rKxD504e3eHwGEXP9qtzqkDeVlQnVW7GXClKQQMifQ0JkL8C7loQd7MYGQ/7T+sa8eLrVfD7ocQY 46qL2qFDew3PvGa+J2KjDXa9nnB0Ouh/8Ue1mLuoBt+sijMIkF0NMbFcjwf6jxBVTkRNrGpMWR06 IBVTFcYxou0TVoZ7MITP7A28JEqNPmDJMpesKQUNiHRB8gY8P/pFIKaHxI6bEkGMDG5bt/vx3xeb fpGpsBAcf1YbZKQLPPqc+l99RpusfM09PgP0f+F7tZi3sBrf/RTZpkUr66i6LCEAGjEksvk/rY/Q H1pVibZl2OFRW5Uo3XZNB/h9cpcXclXczMlXrjHy8dCbIwZOqawVlbcCQv9snHTiayKJkcE3EAA+ +bIebdto6N8v1Ths2utBfVPRsb0Hm7b4UbHTGT88AhIgsTQr9TkgBWT+26mjB1u2BVBe4Yx2mtW+ UPmQEJ11ahZ0p7uHmjd9+eaCauxwyHNC7R53Vhtcc0mTd4JQHOI99u+nKvDGu2qmziOp2+ff1CMz Q8OAA8397p9yXAZ+2eDT/0dSD7uuEZDPrymZ9iWVrwvQqlV3yt4DbzofgHPj+gL6XhsVpt3B8Uzs 6hQq97Ov1DyYlDftYj9zRBbW/OrDr1HuNlfBhKYUzj2jjel7pw7snaL7+WvXVmsS4MrEFKbTh2bi v/d2xjGHmydERj/TNHJdvUm2zkamMb6SN5ILzjXvh0vzakyeWo6iD+yf2iJRUvGD1BWipImpa7+d qtsx7BoR9Rk4aSSAfs07zCmfE3FkFIotPZgej8Ah/c39tWSURdNbldUSqx0wtdW/Xwp65vy+jGlU 0KRXGhWSAGdladj0mx87qxJDmIafkoHrr1DnXLeqOoDnZ6rxVB9N12ZmCPzpwvagTd+q0rW3b8OK b8I7SVVVdqh86QdpMoqSP+C9bd2qKfpDt0uQeg2adAyAY0OBsvtYsoiRwfmrkgb9F+qRiqzIjjks TRc9KsfOJCFw8rHpSqswoF+qPq2Vnq5hQ5kP1TXO+OUfbaPph8S1l7bX14q6dNr1tY02m1avX/5F vel771ottNkFBx+UiosK2mLkYPM3ulNR5H2BRkY0neW0pFKUflzr078DDmvzzqWF3f5u1GnXk917 0KT9AZxhnHDKa7KJkcG95IdG/LY1gOOPVvMHm0ZgNK21YmW9aTvRjbpH+kpThxeca77T2VDlD/xD qm6BlpoisHa9zzFTUqHqGnzsxGPS8eeLm+L67Ntl19c1+BJT39Pifumm+NzkxFMhcoMzeVJH9NpP TXRk8r7wyLMVqKl17g8TVaJEcdRoZmSjjf0b4tn4Zm3J1KeM47ue8D4DJmVA4FLjhBNek1WMDPZk Fk4PUJ6iTa40rUW/trduD2BbufVTWrQLnkYsVkbBHXRQKgpGNa1d/bCmEY0ONcr74xFpuPrC9nro gpx91UxrGs+Z8frdj436pk3js9Wv5HlBpck6bUb97wvusDZVJUr0t8RhorR4bcnUN4xnbZcg9Rt4 fVVAaLcYJ+x+/csl7fSpFrPr4RQDhkjbRb9mPvi0DgJCN0yI9L5Ir+vbKwW0QL55qx8/r7N+CoP+ CLZv58FBfdX8Im6JA00LkZUfrddRHWgvmBPSUYem4U8XtsPFBW3RI9saITLa/ez0Sqz5xfpngJwC k+cF+qGgKlGwzKddth+PREnFd8NJoiQhX1tbMu1Do993CdLPqx6q7T3wxisBYc0cilGDEK80RXH2 SPMXM+9/bAeWfBg67HCIajjm0I6KAFaubkCbTE2JKFFDaZMppa9t2GBK0Tc7tPMoa1u4jqSpSwrh QRM4q35oBJng25EOGZCKyye0xcQJ7ZQZeoRr18x51SicZ32wx4EHpuLisW11k/1w9Yvn3JMv78Qr c+w31IilDZ9+mdiipEnt32tWTf3RYLNLkOhArwE3jxQCvY2TdryqEqMpj+/QN0/a0SYzyiQzaZV7 laiOhw5M09eVSJSsHDHQ1N2n9GuwrfUjJaNvDhuYpk+PEWea0rBKmPr3TcGl49rpI4QDelo7SjTa To5En/h9c7ZxzIrXkUOawkb03l9du+/9zw68VWTfHiMzOKoUpa9WNcQdWiOeNgrg1jWrpu6Ky7OH IPUeNOkQAMfHU0A896oUIze7Zg9mSsN4r1fgYEVm4bSuRCOGb75r0PfxBJet+j198eoaJFRZF0ZS /yMObhKm2jqJ739uVGbw0Wf/FN2SjEIH0KZeuxK5ynnyFetdS106ri2uPF9tYMnrbt+m/4izi62Z 5aoSpRG5mfji2wb8ttWWOettiwtzCjVkfQAAIABJREFU/i+Y056CNHBSVwBjgi+w6j2LUeSkv/y2 AVWKjQGGn5Jpy36lku8b8frb1boQqBLdSEjTWs4F57RFTZ3U15giuSeSa2jf1QXntMHNV3cAbeK1 Ky1fUYfHX9iJtxU5922pXZ06aLj8vHYw239bcHlk1v23e7Y70cQ5uJpRv1clSqcOtkeUpBTL166a +mIwiD0Eqc+gSWRz9OfgC6x4z2IUPWVaiCevC7Q/RVWi/UoZGZrlmwdp2oxEd96iGgT8AFnG2ZVI mC48ty12VjWNmGKtR86+Hpx/Tlvcek0HJe6hIq0X+fp7bnqlbk1ntfkvmbBTmHHyGqIqGaEjGhqc a9YdT9tJlMgNmNkM7RAlTcjZa0qmLQrmsYcgHTngwfJaUXUTAHVPTHDppH6KDBhozShRpumaIdv1 kfbxqLTAo4Jo0blrZ3tMw8l9DU0nLHivVl/Tor1EdiVy0UPCtG1HAD+uidxWvMs+TUL09+s7mu6r LFoW0/5XgUeeqcDPNljSnX9OG9D0pMr0YmElnplu/fSjyjaFypvWkhNBlITEY2tWTVsZ3Ma9Yk7k FWxcDghLPDawGAV3RezvyckmsSRHmyoT/TGjMOx2pe7ZXowelqnUnUwkbaNRBnkWX/R+bYtrTB3b a/pGXHIManci56E0NWd29NNI2kWbry/KbwsVPiiDy3/46Qp9RB18LNHfX3dZeyVha668ZSt+Xhf5 j65YOQuBAUUzcr4Lvn9vQRpb+m9IXBd8kYr3LEbmU504vq3uGdn8nHfnSFMihsv83Uetfdeze5Mw qdgaEG1LSJRopEpGEB4N6NhBQ98DUkBTfXYnCjlCnrvtECJqO7mF+scNHZVj+L/7tieM8UK0sFSJ 0hU3b8WaX5WKUsVJA7I7TZ68Z9yjEIK0cRykeC1aMNFcz2IUDa3orqXF4j9doNZ6iUK70wjBbl94 ZKlGMaTI3Qyn3QTI/Y9hGLL7qLXvaGMvTdOpTGS88PLsKmwrt8VCTGXTosrbpaK0aPHMnOHNG7rH GhKd7HPQLTXQpLIREotR8y4w9zNt7ly3wQdyO68qHdDDC7LCa2gEvv3ePgetFPPo4y/q9bAdZApv p/m0KtbR5EuRgW+9dzu+/V7pL9uwVdqvu1f3MpE/Su30MW3kJTdANDJN9kTfQRWb5unH3rJP69XE FhPylbUl04qb991eIyS6YEhB6W8C6NL84ng/sxjFSzDy+7t38+Lc07OUzDEH14KcVU5/swpbttn/ K5W8HdAak0rLw+C2O+X9C4WVeO2Navh89v5xpj9g9GtddbIruqvqdsWTf1amwMTx7ZR835VM3wlt 1OIZ3d5q3ua9Rkh0Qe+BN51EMd2aXxzPZxajeOhFf29lVUCfV8/KND8SZXBtyAcdCd9v2wIgZ7B2 ps1b/Hj/kzqQp3QK206/1hM5kX82GhF9sbLBMs8SoXimpQndKSo5R1Wd/jGlPOGtZ2NhSE6CVY6U Fr5Xq+99jKVuoe5Jlbj+p1VT97KQakGQbuwJiGGhMorlGDlOHHOa+UP4ZDDtjoV38D3kJ67BB5AH ApXp+KPS9TDMX5XUg8KT25nKNvtRvLwOZA1Hgd5UBQG0q43kXeH2B7brU5VWungK1V4ajf7v/s6g uFMqE60X3fVQue7WSWU5bs5bpSileIW+FGBSPLHvFhbmTAnFOqQg9Rn0Vz8gJ4a6IdpjtMCuIuoj mbLOX2J/6OFoedhx/berG/S9J4MVbqKldg04MFXf/PnLRp8jgp9RXJ+lH9Xhp3U+PRJnTjd3j5im v1GFO6eVY/mKekeEzZg4oa3ug0/1M134VjUee36na4MrquYTnD+JEq0jZ2WY64iZNuKmmidKs9eW TN1ruo7aEVKQ9usyZZOWVTUJQFw/e0iMVLgIof0wtIufU+QE1pf68M6SWqSlqgljEVyTU47NAE3j kLcFJyRqe9EHtfi11I8O7TRYEejOzHbTAv49/9mBDz6pc0RgwWOPSMNVF7XHqbnqrRtpi8FLr7vT U7eZz0A0eTU0Sn00QwJipkcHs0RJSPlQ8w2xRvtCCtK6dXcG+gyYdAoE+hgXRvuqUozs3JwZLQcn XU9RMmmXN8WfoXhAKtOgP6Tqng0oTLRTQkWvW+/TvT5s2uJHp44ePTihSgbx5j3r7Wrc/98KFH9U 65jRAe11u3Zie0tiNd3yr+1Y/AHPgsTyHNHUGlnbOlGU/H55/brvpoWMlBhSkAjAAQMn7S8EhsQC g8UoFmrW3UMjF4oSe9xRasKjB7eEzM8zMzR8/Z29C+/BdSLXOfOX1FjGILjsSN6/8W41pjxeoY/q yDjFCYnWCGktmLxDq07U/kl3bQeNbDnFTkC1KJERRdQRlwXWLH29+90ttapFQeo98AYfhLispRtb Os5i1BIZZx3/cW2jbpGmKhJtcGv1taUxbWyLShtcl+D3xOClWbSxMoDjjlQvzsFlh3pPcXumPlGh W5FV7HSGENH+LgoaSBGcaSuB6vTcjErQfiq7jTVUt9Oq/FWKEu19ilqUBApbWj8iJi0K0lEDp5bV iqprAUT8TWUxsuoxM6ecHTubTMPT0zRY4biUotKSb7MNZT49tIU5rYg/F3KWSsK0s1KCnKhancik lox0SJBos69T0ojcDDx+b2dLng1qM5l0v2NxOAynsFZZDxKlX0t9GGNyFG5aU4pWlATEA2tKppa0 1N6QG2ONi/MKSmcDONv4HO6VxSgcHeefOy0vEzdeoX5To0Hi6dcqQVZjTkvkqJb2VV2p2P0StZvW huYuqsE3NoSND8edfAWeOTxTubNeow40RUcjI/rDyUkdgd77peDJKZ1NL4BM8p9+LSIryIBX+rss KOy5vaVKtDhCohv6DLipEwROb+lm4ziLkUHCva80fbXkwzp4NHMtc1oiQvuiaCqPDC2ctlZAZrOv zKlCQ4Oa/VsffV6nmzFPf7Pa1vDRofrmgnPb4I4bO4IiB1uR6IfJs9Mro1+LsKJyCVYGjb7JFRB5 1DAzGSOllasbQLHMwqTPFxX2eCTMeYQdIeXmr+/rEZ4fw2XAYhSOjjvP0Y57CmNuVZozv1p3Bkqe FpyWPB6A9m+dPjQzbstEmpKjX5M//2KvR4tQjIeckKH/oVJtfWmU7RQHvUZ9kulV5Ujp4WcqwqG8 Z/HMPUOWN784rCDRxXkFpd8DOLD5jfSZxSgUlcQ4RtE9J09SHzogmBYFWCPvzQHnLKMEVw8d2ms4 8uA0HH5wKg7okYIe2R60ydL2uMb4QKbltFb2/U+NesTdVT80wOc8vdVDZYwengmasrUq0b6qJ18O afVrVRWSvhw7RElInFRUmLMsHPxIBOlBALRJdo/EYrQHjoT8QPuVaDf+OQrcPoUDZncgwHB1a36O XBNlpGtITxfw+yRq6qTugbqx0dnrISSk487MwrgzrRsJE7up/yMPK7ypvflzZMfnfr1S8Ph9ataU QoyUtneS2V0LC0XYn2Vh15AIUu+BN9H8wkXBwFiMgmkk7nsaqZAvvPKdARx7RMTGlnED+eMR6Ti4 f6rulYCC3zk50Zw5hUCg/UJVNRL1DdKxIzziqGnAhLPb4P7/64RBijdHB/cbGS7c8eAOfPOdM7x3 BNctWd9v3xHAF9824NTB5o6OaU2JQqzTJvygNOutwnazgj6HfNuqIHU64cGN6XW7zb9ZjEJyTOiD P/zciIXv18LjESDv3lak7K5e5B6Xgb69UnQT8dLNYX9YWVElV5dB1oMU4v4/d3fG4YOsNW0n9z/P z6zi2EUOfIJ+2+q3RJSkEHevLZm6qjUErU7ZUQZDC0pnSiCfpm9UDPEp0Nbsd6pbqyufdwABKyLS hmqmU02kQ9XVScdIiGh96IbLrTPpN9pPBhxvLqwGuWzi5GwCNFr+9537mF5Jipf2n2d3NAqZ0aWo sFNYiwcqPCJByhu7cdzE8e1eUyFGHGzL9GdAeYb79/DirBHqg/+FasiyT+t0x7orvtljOiDUpUl9 jIRoxCmZuOkq64WIwD/1SiVmzHXePrOkfihaabwqUVr6Ue36ISdk7tdK8frpiASp9LfGU7O7eOdH kmE017AYRUPLeddSWJGrLmxnS8U+/6ZeN6H+8LM6W8p3aqFkpj5qWJbu6seOOvKoyA7q5pV5+KBU TLnd/JESgH5CiJ9aq2lEgiSlfA3AuNYyi+Y8i1E0tJx7bbcuHj3ECK1P2JHW/NqoC9O7xbVwumWb Sj5tszR9H9Gl49RHbW2pHbRW9PpbPPXeEh+3HD/msDTcc2sns6vbTQixubVMIxWkCwG80FpmkZ5n MYqUlHuus9r1UCgy09+swluLakB7gJIl0X4S2nk/api5llLR8CMLOgqi58SNzdG0g6/dTcBkUSoW QgzenXvL7yIVpA4AylvOJvIzLEaRs3LblakpAhcXtEXBaHtGSwavT76ox4L3an6PrOrs/UBGnaN5 peCHucel47QhmZY5Pm2pfryvqCUy7j9uoihdI4R4NBIiEQkSZSSlfBPA6EgybekaFqOWyCTW8aMO SdN/tZN3b7sTWfkUfVCD1T81QrpYm8hI4ZD+qcg7McNSrwot9R+NRskHnVO9arRUbz4eHQGTRClb CLEpkpKjEaSxAKZHkmmoa1iMQlFJ7GMFo7Jwxfn2GD2EIkvrGxQGfNWPDa4QJxKhAf1SccIx6SCW TkhLljV5KP92NW9wdUJ/WFEH+mF5500xuxFbJIQYHmk9oxEkmqSOacWSxSjS7ki869q20XDe2W10 wwcnta7og1p8+mU9Vqysh1OC4RGfrEyBwwam4Y9HpOlTck5i9uhzO0HrRZySj8Apx6bj9htiEqVL hBDPR0osYkGiDKWULwK4INLM6ToWo2hoJe61hw5IxejhWTjlOPun8ZpTXrfBh5XfNej/v1/TiNJN PstGUB3bayBXKxQgkTwoWOUJozmDcJ9fnVOFFwo5ims4RslwLkZRai+EiNiTbrSCdCqAiPcjsRgl w2MaXRuHnZyBW/5MNjLOTuRzjTwMbNzkQ+kmP0o3+0ARdmMZTZGT2k4dNHTu6EGPHA96ZnuxXw+v 7mm7a+dWvXfZBmreoqZwGWRaz4kJEIEoRalQCFEQDbloBYm+PRH5AWExiqYbkutaWhspGNUGl59n 356ZWIn7/UBFZUDf80SOVBt9TZYSXo8Atcv4nJmu6Y5MMzMFaI+QmxJZKc5dVA165cQEmhOIQpRO F0K80/z+cJ+jEiTKSEp5L4C/hcuUxSgcHT5nEKCRw4Sz2oCilHJyBoEHn6jAu0s5PIQzesO5tYhw psMrRPhwE81bGIsg/QHA6uYZGZ/Zh5VBgl8jJdC+rYb8M7Iw7iwWpkiZmX0dGSy8uaDasrUzs+vP +VlPYERuBm6+qsXp9/uEELdGW6uoBYkKkFJ+AODE5oU9N6MSr8xmh4rNufDnyAjQesroYZksTJHh MuUqms14/W0WIlNgJmEmYUTpICEERRuPKsUqSJcCeCa4JBajYBr8Ph4CZHlGUWp5xBQPxfD3khDN nl8NWhPjxATiIRBClJYJIU6KJc9YBYl26e0aCrEYxYKe72mNAIXZPuvUTN0dUWvX8vnICDz8dAXe XlzDHhYiw8VXRUigmSidJ4R4NcJb97gsJkGiHKSU1wJ4+LHnd2LOfN4stwdV/mAqAa9X4PS8DFxz qT2xfUxtjA2Zfb2qAXMXVuP9j+t4jcgG/slSZJssbdurj+57aVaWmBtrm2MWJCrw7oe3n7j0wzpa T+LEBJQTILPqIw9Jw+jhmTj+KOdtsFUOIMoCFhTXYt6iat2PX5S38uVMIGoCAuL+opnZYS2wW8s0 LkGizPPyS5dCILe1gvg8EzCTABlAnDE0ExPOZsu85lzJ6ek7S2qwoyLQ/BR/ZgKqCASE39OvaNa+ a+IpwARBKjsXQhbGUwm+lwnESsDrAY45PB3DT8nAicck76jpvY/r9FhQX5XU87RcrA8T3xczAQEx t2hm9pkxZ/D7jXELUm6u9Hq6lv0MIKKY6fFWmO9nAi0RoP1Mg0/I0GMFDTootaXLEub4N6sasPjD Wn1tqLKKR0MJ07EubIgATi2ambMg3qrHLUhUgSEFZTcJyCnxVobvZwJmEdinowcn/TEdJx+brscR Mitfu/MhH3sffV6H95bXYcs2ttm2uz+4fJ3AqsUzswcBIu6IY6YIUu5Zazt4UtPWA+AJfX5CHUeA zMePPCQVfzw8HUcflgba5+SmRNNxH6+ow8df1INHQm7queSoqwCuKJqZ85QZrTVFkKgiQwrKHhGQ 15hRKc6DCagk0LO7F4f2T8XB/VP10VOXfZzjcZsct67+sRG0FvTFtw1Y84u7I92q7EfO2xEEttZI /37LC3vWmlEb0wRpeP6mXn4R+AGA14yKcR5MwCoC7dtpOLB3CvockIJ+vVLQM8eD7vt6kZZm2tcj ZFPKNvvx60Yfftnow/c/NYBiMW36jafhQsLig84kIMUdiwuz7zKrcqZ+4/IKSl8GcJ5ZleN8mICd BMhIonMnD9q1FaBpP4p+2yZT6EKV4hWgDbspv//8CgRoszjg+X2wVVsndW8IdQ0S9fUSOysD+nTb zqqAbo69dbsfPtYeO7uXy46fQJVX+vdfUNhze/xZNeVg7mjG438Afs8EAKYKnVmN5XyYQDQEaPqM /nNiAkxgbwISeMpMMaISTF3dXfxaz29EFBFl924iH2ECTIAJMAEXEGjwavIhs+tpqiBR5aSG+8yu JOfHBJgAE2ACziEgIF5ZOL07WVabmkwXpMXTcz4A5BJTa8mZMQEmwASYgFMI+ODX7lZRGdMFiSop Ie5UUVnOkwkwASbABGwmIOTL8fqsa6kFSgRpycyc9wEsbalQPs4EmAATYAKuJODzBwL/UlVzJYJE lZXAZFWV5nyZABNgAkzAFgKvFBf2/ElVycoE6fdR0iJVFed8mQATYAJMwFICDR6pKV2OUSZIhEnT cFvTYMlSaFwYE2ACTIAJmE5APrWwsNta07MNylCpIC2anvM5gFlB5fFbJsAEmAATcB+BGq8vVdna kYFDqSBRIR4/bgfgMwrkVybABJgAE3AXASnwyILZXcpU11q5IC2clbMaAi+obgjnzwSYABNgAkoI bGvwND6gJOdmmSoXJCrP25hCo6TqZmXzRybABJgAE3A4ASlw97JX9y+3opqWCBIN9SQER5S1oke5 DCbABJiAWQQEfqxon/2YWdm1lo8lgkSVyKqlEOeitLUK8XkmwASYABNwBgEZwC0rnhSNVtXGMkGa Ny+nRorA361qGJfDBJgAE2ACsROQwHtLCnPmxJ5D9HdaJkhUtZP757wACTIF58QEmAATYALOJeD3 yMD1VlfP8kB6eWPLjoaUH5sdi8lqcFweE2ACTCBRCUghH18yo/vVVrfP0hESNW7xjOzPADxrdUO5 PCbABJgAE2idgAS2BOobyMuO5clyQaIWpkpBjd1heWu5QCbABJgAEwhPQOC24jd62fL32RZBml+Y vQVC/F94KnyWCTABJsAErCQgpPzk5P7Zts1g2SJIBPik/t2eAPS1JCt5c1lMgAkwASYQmoDPr8kr J08WgdCn1R+13KghuEmDx244VJMaWd15g4/zeybABJgAE7CYgJBTFs/o/leLS92jONtGSFSLpTN6 fA2IaXvUiD8wASbABJiA1QTWeVI9SmMdRdIgWwWJKphZKwmC0hgbkYDga5gAE2ACyUpACnH1wpe6 2e5v1HZBIg8Omha4nAP5JetXgdvNBJiAzQReXjIje77NddCLt12QqBaLpvdYDOBJJwDhOjABJsAE kojAJq/0X+eU9jpCkAhGXV3KzQB+dQoYrgcTYAJMINEJSImrFxT23O6UdjpGkD6c26VSCslTd055 MrgeTIAJJDgBMd1q56mtAXWMIFFFl8zovhCQj7dWaT7PBJgAE2AC8RAQpV7p+3M8Oai411GCRA30 pHnIDv4HFY3lPJkAE2ACTABSQF7qpKk6o08cJ0hkehjQtAsB+IxK8isTYAJMgAmYRUD+t2hmzgKz cjMzH8cJEjVu6fRun0DIe8xsKOfFBJgAE0h2AlJgdWatsNUbQ7g+cKQgUYX9m3P+CYmPwlWezzEB JsAEmEDEBBoAnEd7PyO+w+ILHStIxcXCJ/2YIIByi5lwcUyACTCBxCMg8dclM3K+cHLDHCtIBG3J 7JxfpBRXOBkg140JMAEm4AICby8uzH7E6fV0tCARvMWF2a8L4Amng+T6MQEmwAScSUCU+j24GBDS mfXbXSvHCxJVtVr6b4TEl7urze+YABNgAkwgAgI+aHJc8Ws5WyO41vZLXCFIywt71krNkw+gwnZi XAEmwASYgEsISIhbF0/P+cAl1YUrBIlgLpmx789CyIvYK7hbHi2uJxNgAjYTmLNkZrepNtchquJd I0jUqqIZ3d+EkA9G1UK+mAkwASaQbAQEfhQy/RI3rBsFd42rBIkq3imQc6uUgsJVcGICTIAJMIG9 CVSLgHZOUWEn1y1xuE6QCguFPw0YD4Ff9u4HPsIEmAATSGoCUgAXFRV2W+lGCq4TJII8vzB7SwCB MwE4dsexGx8GrjMTYALuJiCBfxXNzJnl1la4UpAI9tIZPb6WQlzMRg5uffS43kyACZhKQMq3Th6Q fYepeVqcmbC4PNOLyysoux2Qd5meMWfIBJgAE3APgZVCpp/kxnWjYMSuFyRAiryCspfIaWBww/g9 E2ACTCAZCEhgS8AXOKJ4do8Nbm+va6fsdoMXspMsvxSQH+8+xu+YABNgAklBoEZq2qhEECPqrQQY ITU9dCPzy7o0CnwkIfsmxWPIjWQCTCDZCUgJed6Smd1fSxQQCTBCauoKsrzzSd9IGr4mSudwO5gA E2ACLREQErclkhhROxNmhGR02uBxm/6oBQJLAGQax/iVCTABJpBIBKSQjy+Z0f3qRGoTtSVhRkhG x1D484CU4ynorHGMX5kAE2ACCUTglX0COdckUHt2NSXhBIlatrSw+1wJ/IX3KO3qZ37DBJhAQhCQ S8iIizzWJERzmjUi4absgts3NL/0b1Lg3uBj/J4JMAEm4FICnwqZPtzte43CsU/IEZLR4KLCnPsE xP3GZ35lAkyACbiUwKpUKc5IZDGifknoEVLTg6dvnH0cwJUufRC52kyACSQzAYFf/I2BExNlr1G4 rkzoEVJTw2njbPafAbwSDgSfYwJMgAk4kMAGf8A/NBnEiNgnwQip6RHLz5eebaL0VQFR4MCHjqvE BJgAE2hGQJZByNzFM3r80OxEwn5MGkGiHjzyCpnSvrxsuhAYk7A9yg1jAkzA9QRog78mcErRjJzv XN+YKBqQBFN2u2mseFI07oPy8QJi7u6j/I4JMAEm4BwCuhhJLS/ZxIh6IKkEiRpcWDiwoaPcni8l ZjvnEeSaMAEmwAR0ApulRwx2a8TXePswqabsgmHl5kqvZ99NL0PKscHH+T0TYAJMwB4CotTjl3kL Z+Wstqd8+0tNuhGSgby4WPg6BbpRDKWXjWP8ygSYABOwicB6v/SdksxiRNyTVpCo8eR+o5PMpjDo /7PpIeRimQATSHICAuInvyZOLi7s+VOSo0ges+/wHS3FkLFl9wiJv4W/js8yASbABEwkIMU3fnhG FBd23WRirq7NKqlHSLt7TcglM3JuFVL8lR2y7qbC75gAE1BIQOKj+pSGXBaj3YyT1qhhN4I93w0Z WzpRSDwBwLPnGf7EBJgAEzCNwLuZtThn3rycGtNyTICMWJBCdOKQgo2jBASFBc4KcZoPMQEmwARi JiAgnivv0O1K2hcZcyYJeiMLUgsdS5FnRSAwTwBdWriEDzMBJsAEoiQg/7l4Zs4dgJBR3pgUl7Mg henm3Pz1fb3CO19C9g1zGZ9iAkyACbRGwC+Aq4pm5jzV2oXJfJ4FqZXezx1f2tnjxxwAJ7ZyKZ9m AkyACYQiUCGAsUUzcxaEOsnHdhNgK7vdLEK+K34tZ2tqZfVQ3kAbEg8fZAJMIDyBtQGPOIHFKDwk 4yyPkAwSEbzmFZTeCuBfyRS2IwIsfAkTYAIhCYjlnkZx1sI53X4LeZoP7kWABWkvJOEPDMkvPVsI vAigTfgr+SwTYAJJS0DI51N31vxp/vx+9UnLIIaGsyDFAG342E2DAlLOYWOHGODxLUwgsQn4hJA3 Fs3o/p/Ebqaa1rEgxcj1xAm/dEzzpbwK4NQYs+DbmAATSCACehwjTRQUTc8uTqBmWdoUNmqIEfey V/cv7ySzzwBwL7sbihEi38YEEoWAxOfw4WgWo/g6lEdI8fHT7x6aX3a6FPIFAPuYkB1nwQSYgIsI CCEeTdlZdROvF8XfaSxI8TPUcxiev6mXH4GZEDjKpCw5GybABJxNoAoCf1o8I+cVZ1fTPbXjKTuT +mphYbe1qVXVJ9KvJZ7CMwkqZ8MEnEtgpcePo1mMzO0gHiGZy1PPbejYjWdKKZ7hKTwFcDlLJmAz ASnk44GMhhuLn+9VZ3NVEq54FiRFXZo7ZkMPzau9LIBTFBXB2TIBJmAtge1SYuKSwhxyJcZJAQGe slMAlbIsnt1jwz4yOw8Q/wAku5lXxJmzZQIWEVgqJA5nMVJLm0dIavnquQ8bV3pUIACywhtgQXFc BBNgAuYRqBPAbScOyH548mQRMC9bzikUARakUFQUHDsuf31Gpua5FxLXsi88BYA5SyZgNgGJLwFx /uLC7FVmZ835hSbAghSai7KjeQVlgwXw5P+3d26xVVRRGP7/PbW2oqYgSIGC8a6QoPLgJYKppYL1 Fi+p0AcUrw/6oDHxHhU1XuOLiYmJqVGjRvAIGjERQ9GCGNEgmGgUlYhSORRRqQhKy5lZZho1EUs9 9zMz5386ObPXWnutb03yZ86Zvbe2HSoZYgUWgUIJDBj48K8NjQ/rVNdCUebmL0HKjVdRrAeflliz ALBbANQUJaiCiIAIFEzAgA/DPS8CAAAGbElEQVRovF5PRQWjzCuABCkvbMVxap275RQL2AlgWnEi KooIiECeBHYCvGvG5Man9V9RngSL4CZBKgLEQkI0N1uNO7z3ZsIeAFBfSCz5ioAI5EOAS/2Mf0P4 Zmw+3vIpHgEJUvFYFhSp9bJtRwUueIa0mQUFkrMIiEC2BLbB7KYVqQmLsnWQXWkJSJBKyzfH6MaW Oen5ND4OYHSOzjIXARHIjoABfK7GMre+k5r4S3YusioHAQlSOSjnOMfs9p5RGboHAV6vlx5yhCdz ERiewDrS3di1qHHN8GYarQQBCVIlqGc55+BLDz6fBDEjSxeZiYAIDE3gJxrunj5lXKdeWhgaUBSu SpCi0IVhczC2XJ6eS/BRAJOGNdWgCIjAvgQGQDzl9/c/2P3GkX37Dup7tAhIkKLVj/1mE65dGgHv JiNuB9CwX0MNiIAI/E3gdd/827pTEzf+fUGf0SYgQYp2f/6TXXNHerTn2/0ArgN4wH8MdEEEqpwA zT6C5+7QceLxuxEkSPHr2WDGze09x3j0FgDoAKBd22PaR6VdVAJfkLina9G41wFaUSMrWFkISJDK grl0k8zs6JmKjHsI5AWlm0WRRSDCBIjvYbxvlDW+lErRj3CmSu1/CEiQ/gdQXIZb5/SebkFwL4i2 uOSsPEWgQAKbaXx0JH55NpWaMlBgLLlHgIAEKQJNKGYKLe1bTyXtXgDnFzOuYolAhAh8R8MjI7Hj eQlRhLpShFQkSEWAGMUQ4aGAvo87SVys/5ii2CHllAeBrwh7fEfD+Bd1LEQe9GLgIkGKQZMKSfHs 9i3HO8dbYZgHoLaQWPIVgQoR+JjEY9NPHPeGFrVWqANlmlaCVCbQlZ7mnLmbxwfm3QzjdVrHVOlu aP4sCAQElhn4xIpXx72Xhb1MEkBAgpSAJuZSwqx5vSP8Absy3OUYwHG5+MpWBMpAYDdgL4D25IpF TV+XYT5NESECEqQINaOcqSxYYG7lF73nerAbDZgNwCvn/JpLBP5FgPjGjM8EA3s6tcXPv8hU1RcJ UlW1e+hiW9vTkwLyKsKu1n55QzPS1ZIQ6AfwGh07uxY2rtRi1pIwjlVQCVKs2lXaZMOnplVf9s5G YNeSdqG2Jiot7yqO/hkMz9bAf1HnEVXxXTBE6RKkIaDoEjCzY9tY8zPzSV4Dw7FiIgIFEtgF8FWY 37ki1fRhgbHknlACEqSENrZ4ZYXHX2ydQeIKGi41YGTxYitSwgkEAFbC7OXa2rrU2y8ftjPh9aq8 AglIkAoEWE3ubW3fHDhwyIg2g3UQvBBAfTXVr1qzJGBYC+AV52UWLl84KZ2ll8xEABIk3QR5ETjz ou2H1B04cB7Jyww4D8CIvALJKQkEDLCPaG4JAre4a/HYb5NQlGooPwEJUvmZJ27G8PDAg+iFr45f YkAbgTGJK1IF7UsgA9gqI5cGe4PXupc0/bCvgb6LQK4EJEi5EpP9sATa28372UufxoAXwHg+aFOH ddBgnAj8BGAZzN4i6pd1pUb9GqfklWv0CUiQot+jWGfYfOkPTTU1nGVw5xhspp6eYtXO8EiHNQSW +84tH+2PXavzhmLVv9glK0GKXcvim3C4zmn1hvRJ8F2rITgL5HTtqxepfvowrIfDKgZ8NwNvZXfq 8F2RylDJJJqABCnR7Y12ceHPezu89FTzeRaIGQBOBzAh2lknKrvdMKwDsdrI9/v/qFn9wZtjfktU hSomVgQkSLFqV/KTbWnvmeCcdxrMzjDwVAAnAzg0+ZWXvMKMARsIfGLAGs+CNXu3T/i8u5uZks+s CUQgSwISpCxByaxSBIwtc348CgimATaNNihQk7Xn3rD92EmzL438FMT6gO7Tg3cHny1dOv73Yb00 KAIVJiBBqnADNH1+BMJ1ULUH+ZO9wJ9ixAk0d7yZHQ3iaAB1+UWNlZcBCF+13mjARho2mLPPXcAN Xanxm2NViZIVgb8ISJB0KySMgHHW3HSTH7hjBp+iDEcYrYmGpsHvxMSY/AQY7oS9zYAewLYQDD83 G7DJkRsz9f2bup8/ck/CmqdyqpyABKnKb4BqLL95/qa62j21Y3xzjQDHGvwxNDfKYA2ObDCzBpg1 GFy9ozWY8QA4Oxhm9QT/efraz75+vxMIxQTh9gUg+mDYS2CXGcKxPwJaH8k+GnfArC8g+hyw3Rx+ 9PZie+DqtmqNTzXemar5T7boKrYfCqI6AAAAAElFTkSuQmCC"/></symbol><symbol viewBox="0 0 24 24" id="webpack" xmlns="http://www.w3.org/2000/svg"><path d="M19.376 15.988l-7.709 4.45-7.708-4.45V7.087l7.708-4.45 7.709 4.45z" fill="#fff" fill-opacity=".785" stroke-width="0"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18.21 0 .41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.939v2.104h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07l7-3.94zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83-4.9-2.83zm-5 5.08v3.58l4 2.308v-3.58l-4-2.308zm10 0l-4 2.308v3.58l4-2.308v-3.58z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83-4.9-2.83zm-5 5.08v3.58l4 2.308v-3.58l-4-2.308zm10 0l-4 2.308v3.58l4-2.308v-3.58z" fill="#1c78c0"/></symbol><symbol viewBox="0 0 24 24" id="wolframlanguage" xmlns="http://www.w3.org/2000/svg"><title>wolframLanguage</title><g transform="scale(.12121)" fill="none" fill-rule="evenodd"><circle cx="99.197" cy="98.946" r="83.28" fill="#212121" stroke-width=".841"/><path d="M182.529 98.828a83.406 83.406 0 0 1-39.14 70.721.064.064 0 0 1-.038.019l-28.62-35.665 23.71 2.612s11.385 1.177 13.978 0c2.373-.938 15.175-18.963 15.175-18.963s-36.75-23.23-49.312-36.032c1.434-21.575-1.656-50.269-1.656-50.03-9.251 9.234-10.429 10.669-19.68 19.203-4.028-13.04-5.923-17.547-9.95-30.588-12.104 9.95-21.337 26.799-27.977 46.48a78.68 78.68 0 0 0-4.23 5.094 109.774 109.774 0 0 0-2.667 3.66 114.558 114.558 0 0 0-5.132 8.002 172.555 172.555 0 0 0-3.403 6.051c-7.706 14.475-14.034 31.066-19.515 46.001a.858.858 0 0 1-.092-.184c-14.988-30.912-9.502-67.85 13.822-93.072 23.325-25.223 59.723-33.575 91.71-21.045 31.988 12.53 53.029 43.382 53.017 77.736z" fill="#e53935"/><path d="M101.452 69.178s-1.416-8.295-2.373-11.367c6.401-6.18 7.357-7.118 13.52-13.04.477 11.845.238 18.006-.479 32.481-3.55-3.568-10.668-8.074-10.668-8.074zm-27.737 40.778s-6.64-4.029-11.624-4.728c1.435-3.329 5.223-7.596 6.18-8.773-1.913.699-15.653 6.86-17.087 12.084a74.804 74.804 0 0 1 11.385 3.79 35.993 35.993 0 0 0-8.774 20.158s21.815-3.33 38.185-1.196c.283.168.609.251.938.24l8.534.239 27.111 45.136.221.35c-.037.018-.055.037-.073.037-51.133 18.485-88.085-15.543-95.976-27.443.034-.102.058-.206.074-.313 7.1-30.017 15.855-65.939 30-76.552 7.356-12.82 9.49-31.783 22.751-41.734 3.33 9.951 8.553 30.588 12.103 40.539 15.653 15.652 39.361 35.094 55.234 43.15 1.656.956 3.79 7.596 3.79 7.596l-6.401 8.056-68.276-6.879a54.462 54.462 0 0 0-4.58-.183 86.848 86.848 0 0 0-14.144 1.36c3.311-8.295 10.43-14.935 10.43-14.935zm22.054-8.774c3.789-.46 7.817.956 12.323 3.568 4.267-1.195 4.745-1.434 9.013-2.612-5.463-4.028-11.386-8.295-19.442-7.118a47.249 47.249 0 0 0-1.894 6.162z" fill="#fff" stroke-width=".936"/></g></symbol><symbol viewBox="0 0 24 24" id="word" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5M7 13l1.5 7h2l1.5-3 1.5 3h2l1.5-7h1v-2h-4v2h1l-.9 4.2L13 15h-2l-1.1 2.2L9 13h1v-2H6v2h1z" fill="#01579b"/></symbol><symbol viewBox="0 0 24 24" id="xaml" xmlns="http://www.w3.org/2000/svg"><path d="M18.93 12l-3.47 6H8.54l-3.47-6 3.47-6h6.92l3.47 6m4.84 0l-4.04 7L18 18l3.46-6L18 6l1.73-1 4.04 7M.23 12l4.04-7L6 6l-3.46 6L6 18l-1.73 1-4.04-7z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="xml" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m.12 13.5l3.74 3.74 1.42-1.41-2.33-2.33 2.33-2.33-1.42-1.41-3.74 3.74m11.16 0l-3.74-3.74-1.42 1.41 2.33 2.33-2.33 2.33 1.42 1.41 3.74-3.74z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 24 24" id="yaml" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="yang" xmlns="http://www.w3.org/2000/svg"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2m0 2a8 8 0 0 0-8 8 8 8 0 0 0 8 8 4 4 0 0 1-4-4 4 4 0 0 1 4-4 4 4 0 0 0 4-4 4 4 0 0 0-4-4m0 2.5A1.5 1.5 0 0 1 13.5 8 1.5 1.5 0 0 1 12 9.5 1.5 1.5 0 0 1 10.5 8 1.5 1.5 0 0 1 12 6.5m0 8a1.5 1.5 0 0 0-1.5 1.5 1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5 1.5 1.5 0 0 0-1.5-1.5z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 289.99999 290.00001" id="yarn" xmlns="http://www.w3.org/2000/svg"><path d="M250.733 218.418c-12.39 2.943-18.661 5.653-33.993 15.641-24.004 15.487-50.176 22.688-50.176 22.688s-2.168 3.252-8.44 4.723c-10.84 2.633-51.647 4.878-55.364 4.956-9.988.077-16.105-2.555-17.809-6.66-5.188-12.388 7.434-17.809 7.434-17.809s-2.788-1.703-4.414-3.252c-1.471-1.47-3.02-4.413-3.484-3.33-1.936 4.724-2.943 16.261-8.13 21.45-7.125 7.2-20.598 4.8-28.573.619-8.75-4.646.62-15.564.62-15.564s-4.724 2.788-8.518-2.942c-3.407-5.266-6.582-14.248-5.73-25.32 1.084-12.777 15.176-25.011 15.176-25.011s-2.477-18.661 5.653-37.787c7.356-17.422 27.179-31.437 27.179-31.437s-16.648-18.352-10.454-35c4.027-10.84 5.653-10.763 6.97-11.227 4.645-1.781 9.136-3.717 12.466-7.356 16.648-17.964 37.864-14.557 37.864-14.557s9.911-30.431 19.203-24.469c2.865 1.859 13.163 24.778 13.163 24.778s10.996-6.426 12.235-4.026c6.659 12.931 7.433 37.632 4.49 52.654-4.955 24.778-17.344 38.096-22.3 46.459-1.161 1.936 13.319 8.053 22.456 33.373 8.44 23.152.929 42.587 2.245 44.756.232.387.31.542.31.542s9.679.774 29.114-11.228c10.376-6.427 22.688-13.628 36.703-13.783 13.55-.232 14.247 15.719 4.104 18.12z" fill="#2c8ebb" stroke-width=".774"/></symbol><symbol viewBox="0 0 24 24" id="zip" xmlns="http://www.w3.org/2000/svg"><path d="M14 17h-2v-2h-2v-2h2v2h2m0-6h-2v2h2v2h-2v-2h-2V9h2V7h-2V5h2v2h2m5-4H5c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#afb42b"/></symbol></svg>
diff --git a/app/assets/images/gitlab_logo.png b/app/assets/images/gitlab_logo.png
index ca30b459019..12525056939 100644
--- a/app/assets/images/gitlab_logo.png
+++ b/app/assets/images/gitlab_logo.png
Binary files differ
diff --git a/app/assets/images/logo.svg b/app/assets/images/logo.svg
index f4e19b67008..c650177c960 100644
--- a/app/assets/images/logo.svg
+++ b/app/assets/images/logo.svg
@@ -1,26 +1,10 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg width="210px" height="210px" viewBox="0 0 210 210" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
- <!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch -->
- <title>Slice 1</title>
- <desc>Created with Sketch.</desc>
- <defs></defs>
- <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
- <g id="logo" sketch:type="MSLayerGroup" transform="translate(0.000000, 10.000000)">
- <g id="Page-1" sketch:type="MSShapeGroup">
- <g id="Fill-1-+-Group-24">
- <g id="Group-24">
- <g id="Group">
- <path d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" id="Fill-4" fill="#E24329" class="tanuki-shape"></path>
- <path d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-8" fill="#FC6D26" class="tanuki-shape"></path>
- <path d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" id="Fill-12" fill="#FCA326" class="tanuki-shape"></path>
- <path d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" id="Fill-16" fill="#E24329" class="tanuki-shape"></path>
- <path d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-18" fill="#FC6D26" class="tanuki-shape"></path>
- <path d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" id="Fill-20" fill="#FCA326" class="tanuki-shape"></path>
- <path d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" id="Fill-22" fill="#E24329" class="tanuki-shape"></path>
- </g>
- </g>
- </g>
- </g>
- </g>
- </g>
+<svg width="200" height="192" class="tanuki-logo" viewBox="0 0 50 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path class="tanuki-shape tanuki" d="m49.014 19-.067-.18-6.784-17.696a1.792 1.792 0 0 0-3.389.182l-4.579 14.02H15.651l-4.58-14.02a1.795 1.795 0 0 0-3.388-.182l-6.78 17.7-.071.175A12.595 12.595 0 0 0 5.01 33.556l.026.02.057.044 10.32 7.734 5.12 3.87 3.11 2.351a2.102 2.102 0 0 0 2.535 0l3.11-2.352 5.12-3.869 10.394-7.779.029-.022a12.595 12.595 0 0 0 4.182-14.554Z"
+ fill="#E24329"/>
+ <path class="tanuki-shape right-cheek" d="m49.014 19-.067-.18a22.88 22.88 0 0 0-9.12 4.103L24.931 34.187l9.485 7.167 10.393-7.779.03-.022a12.595 12.595 0 0 0 4.175-14.554Z"
+ fill="#FC6D26"/>
+ <path class="tanuki-shape chin" d="m15.414 41.354 5.12 3.87 3.11 2.351a2.102 2.102 0 0 0 2.535 0l3.11-2.352 5.12-3.869-9.484-7.167-9.51 7.167Z"
+ fill="#FCA326"/>
+ <path class="tanuki-shape left-cheek" d="M10.019 22.923a22.86 22.86 0 0 0-9.117-4.1L.832 19A12.595 12.595 0 0 0 5.01 33.556l.026.02.057.044 10.32 7.734 9.491-7.167L10.02 22.923Z"
+ fill="#FC6D26"/>
</svg>
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif
deleted file mode 100644
index 3f4ef31947b..00000000000
--- a/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif
deleted file mode 100644
index 387628f831c..00000000000
--- a/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/gitlab_footer_logo.gif b/app/assets/images/mailers/gitlab_footer_logo.gif
deleted file mode 100644
index 3f4ef31947b..00000000000
--- a/app/assets/images/mailers/gitlab_footer_logo.gif
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/gitlab_header_logo.gif b/app/assets/images/mailers/gitlab_header_logo.gif
deleted file mode 100644
index 387628f831c..00000000000
--- a/app/assets/images/mailers/gitlab_header_logo.gif
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/gitlab_header_logo.png b/app/assets/images/mailers/gitlab_header_logo.png
deleted file mode 100644
index 35ca1860887..00000000000
--- a/app/assets/images/mailers/gitlab_header_logo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/gitlab_logo.png b/app/assets/images/mailers/gitlab_logo.png
new file mode 100644
index 00000000000..12525056939
--- /dev/null
+++ b/app/assets/images/mailers/gitlab_logo.png
Binary files differ
diff --git a/app/assets/images/mailers/gitlab_logo_black_text.png b/app/assets/images/mailers/gitlab_logo_black_text.png
new file mode 100644
index 00000000000..ed8f05a633e
--- /dev/null
+++ b/app/assets/images/mailers/gitlab_logo_black_text.png
Binary files differ
diff --git a/app/assets/images/mailers/gitlab_tanuki_2x.png b/app/assets/images/mailers/gitlab_tanuki_2x.png
deleted file mode 100644
index 551dd6ce2ce..00000000000
--- a/app/assets/images/mailers/gitlab_tanuki_2x.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/gitlab-logo-gray-rgb.png b/app/assets/images/mailers/in_product_marketing/gitlab-logo-gray-rgb.png
deleted file mode 100644
index 31083af512e..00000000000
--- a/app/assets/images/mailers/in_product_marketing/gitlab-logo-gray-rgb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/msapplication-tile.png b/app/assets/images/msapplication-tile.png
deleted file mode 100644
index 1e0e2ed73ce..00000000000
--- a/app/assets/images/msapplication-tile.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/touch-icon-ipad-retina.png b/app/assets/images/touch-icon-ipad-retina.png
deleted file mode 100644
index 516dc2f4710..00000000000
--- a/app/assets/images/touch-icon-ipad-retina.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/touch-icon-ipad.png b/app/assets/images/touch-icon-ipad.png
deleted file mode 100644
index b2093d015b8..00000000000
--- a/app/assets/images/touch-icon-ipad.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/touch-icon-iphone-retina.png b/app/assets/images/touch-icon-iphone-retina.png
deleted file mode 100644
index 438654e0d20..00000000000
--- a/app/assets/images/touch-icon-iphone-retina.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/touch-icon-iphone.png b/app/assets/images/touch-icon-iphone.png
deleted file mode 100644
index e5f87fbbcf6..00000000000
--- a/app/assets/images/touch-icon-iphone.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/twitter_card.jpg b/app/assets/images/twitter_card.jpg
new file mode 100644
index 00000000000..6b998ab731c
--- /dev/null
+++ b/app/assets/images/twitter_card.jpg
Binary files differ
diff --git a/app/assets/javascripts/access_tokens/components/projects_token_selector.vue b/app/assets/javascripts/access_tokens/components/projects_token_selector.vue
index a746f62b3a1..4843c52fcbb 100644
--- a/app/assets/javascripts/access_tokens/components/projects_token_selector.vue
+++ b/app/assets/javascripts/access_tokens/components/projects_token_selector.vue
@@ -148,7 +148,7 @@ export default {
</template>
<template #dropdown-footer>
<gl-intersection-observer v-if="projects.pageInfo.hasNextPage" @appear="loadMoreProjects">
- <gl-loading-icon v-if="isLoadingMoreProjects" size="md" />
+ <gl-loading-icon v-if="isLoadingMoreProjects" class="gl-mb-3" size="sm" />
</gl-intersection-observer>
</template>
</gl-token-selector>
diff --git a/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue b/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue
new file mode 100644
index 00000000000..7f6e5dc4f35
--- /dev/null
+++ b/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue
@@ -0,0 +1,51 @@
+<script>
+import { GlListbox } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
+
+export default {
+ name: 'BackgroundMigrationsDatabaseListbox',
+ i18n: {
+ database: s__('BackgroundMigrations|Database'),
+ },
+ components: {
+ GlListbox,
+ },
+ props: {
+ databases: {
+ type: Array,
+ required: true,
+ },
+ selectedDatabase: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ selected: this.selectedDatabase,
+ };
+ },
+ methods: {
+ selectDatabase(database) {
+ visitUrl(setUrlParams({ database }));
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-center" data-testid="database-listbox">
+ <label id="label" class="gl-font-weight-bold gl-mr-4 gl-mb-0">{{
+ $options.i18n.database
+ }}</label>
+ <gl-listbox
+ v-model="selected"
+ :items="databases"
+ right
+ :toggle-text="selectedDatabase"
+ aria-labelledby="label"
+ @select="selectDatabase"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/background_migrations/index.js b/app/assets/javascripts/admin/background_migrations/index.js
new file mode 100644
index 00000000000..4ddd8f17c9a
--- /dev/null
+++ b/app/assets/javascripts/admin/background_migrations/index.js
@@ -0,0 +1,38 @@
+import Vue from 'vue';
+import * as Sentry from '@sentry/browser';
+import Translate from '~/vue_shared/translate';
+import BackgroundMigrationsDatabaseListbox from './components/database_listbox.vue';
+
+Vue.use(Translate);
+
+export const initBackgroundMigrationsApp = () => {
+ const el = document.getElementById('js-database-listbox');
+
+ if (!el) {
+ return false;
+ }
+
+ const { selectedDatabase } = el.dataset;
+ let { databases } = el.dataset;
+
+ try {
+ databases = JSON.parse(databases).map((database) => ({
+ value: database,
+ text: database,
+ }));
+ } catch (e) {
+ Sentry.captureException(e);
+ }
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(BackgroundMigrationsDatabaseListbox, {
+ props: {
+ databases,
+ selectedDatabase,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/admin/statistics_panel/components/app.vue b/app/assets/javascripts/admin/statistics_panel/components/app.vue
index 1f0db422807..f250bdae4f5 100644
--- a/app/assets/javascripts/admin/statistics_panel/components/app.vue
+++ b/app/assets/javascripts/admin/statistics_panel/components/app.vue
@@ -29,7 +29,7 @@ export default {
<div class="gl-card">
<div class="gl-card-body">
<h4>{{ __('Statistics') }}</h4>
- <gl-loading-icon v-if="isLoading" size="md" class="my-3" />
+ <gl-loading-icon v-if="isLoading" size="lg" class="my-3" />
<template v-else>
<p
v-for="statistic in getStatistics(statisticsLabels)"
diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue
index 929f5d10956..37a6ea16018 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -178,8 +178,8 @@ export default {
serverErrorMessage: '',
isErrorAlertDismissed: false,
sort: 'STARTED_AT_DESC',
- statusFilter: [],
- filteredByStatus: '',
+ statusFilter: ALERTS_STATUS_TABS[0].filters,
+ filteredByStatus: ALERTS_STATUS_TABS[0].status,
alerts: {},
alertsCount: {},
sortBy: 'startedAt',
@@ -283,13 +283,17 @@ export default {
<paginated-table-with-search-and-tabs
:show-error-msg="showErrorMsg"
:i18n="$options.i18n"
- :items="alerts.list || []"
+ :items="
+ alerts.list || [] /* eslint-disable-line @gitlab/vue-no-new-non-primitive-in-template */
+ "
:page-info="alerts.pageInfo"
:items-count="alertsCount"
:status-tabs="$options.statusTabs"
:track-views-options="$options.trackAlertListViewsOptions"
:server-error-message="serverErrorMessage"
- :filter-search-tokens="['assignee_username']"
+ :filter-search-tokens="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
+ 'assignee_username',
+ ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
filter-search-key="alerts"
@page-changed="pageChanged"
@tabs-changed="statusChanged"
@@ -305,7 +309,11 @@ export default {
<template #table>
<gl-table
class="alert-management-table"
- :items="alerts ? alerts.list : []"
+ :items="
+ alerts
+ ? alerts.list
+ : [] /* eslint-disable-line @gitlab/vue-no-new-non-primitive-in-template */
+ "
:fields="$options.fields"
:show-empty="true"
:busy="loading"
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
index 902bad780ad..1f970ef1846 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -196,7 +196,7 @@ export default {
.then(
({ data: { httpIntegrationResetToken, prometheusIntegrationResetToken } = {} } = {}) => {
const [error] =
- httpIntegrationResetToken?.errors || prometheusIntegrationResetToken?.errors;
+ httpIntegrationResetToken?.errors || prometheusIntegrationResetToken.errors;
if (error) {
return createFlash({ message: error });
}
diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql
index d4f4f244759..babcdea935d 100644
--- a/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql
@@ -3,8 +3,6 @@
mutation createHttpIntegration($projectPath: ID!, $name: String!, $active: Boolean!) {
httpIntegrationCreate(input: { projectPath: $projectPath, name: $name, active: $active }) {
errors
- # We have ID in a deeply nested fragment
- # eslint-disable-next-line @graphql-eslint/require-id-when-available
integration {
...HttpIntegrationItem
}
diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql
index caa258e0848..05bf8eab524 100644
--- a/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql
@@ -1,10 +1,8 @@
#import "../fragments/http_integration_item.fragment.graphql"
-mutation destroyHttpIntegration($id: ID!) {
+mutation destroyHttpIntegration($id: AlertManagementHttpIntegrationID!) {
httpIntegrationDestroy(input: { id: $id }) {
errors
- # We have ID in a deeply nested fragment
- # eslint-disable-next-line @graphql-eslint/require-id-when-available
integration {
...HttpIntegrationItem
}
diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql
index 2f30f9abb5c..65245bfb914 100644
--- a/app/assets/javascripts/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql
@@ -1,10 +1,8 @@
#import "../fragments/http_integration_item.fragment.graphql"
-mutation resetHttpIntegrationToken($id: ID!) {
+mutation resetHttpIntegrationToken($id: AlertManagementHttpIntegrationID!) {
httpIntegrationResetToken(input: { id: $id }) {
errors
- # We have ID in a deeply nested fragment
- # eslint-disable-next-line @graphql-eslint/require-id-when-available
integration {
...HttpIntegrationItem
}
diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/reset_prometheus_token.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/reset_prometheus_token.mutation.graphql
index 8f34521b9fd..99179fca1f9 100644
--- a/app/assets/javascripts/alerts_settings/graphql/mutations/reset_prometheus_token.mutation.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/mutations/reset_prometheus_token.mutation.graphql
@@ -1,6 +1,6 @@
#import "../fragments/integration_item.fragment.graphql"
-mutation resetPrometheusIntegrationToken($id: ID!) {
+mutation resetPrometheusIntegrationToken($id: IntegrationsPrometheusID!) {
prometheusIntegrationResetToken(input: { id: $id }) {
errors
integration {
diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql
index 2cf56613673..cc9e841ffb9 100644
--- a/app/assets/javascripts/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql
@@ -1,10 +1,12 @@
#import "../fragments/http_integration_item.fragment.graphql"
-mutation updateHttpIntegration($id: ID!, $name: String!, $active: Boolean!) {
+mutation updateHttpIntegration(
+ $id: AlertManagementHttpIntegrationID!
+ $name: String!
+ $active: Boolean!
+) {
httpIntegrationUpdate(input: { id: $id, name: $name, active: $active }) {
errors
- # We have ID in a deeply nested fragment
- # eslint-disable-next-line @graphql-eslint/require-id-when-available
integration {
...HttpIntegrationItem
}
diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/update_prometheus_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/update_prometheus_integration.mutation.graphql
index 62761730bd2..95d72e4af91 100644
--- a/app/assets/javascripts/alerts_settings/graphql/mutations/update_prometheus_integration.mutation.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/mutations/update_prometheus_integration.mutation.graphql
@@ -1,6 +1,10 @@
#import "../fragments/integration_item.fragment.graphql"
-mutation updatePrometheusIntegration($id: ID!, $apiUrl: String!, $active: Boolean!) {
+mutation updatePrometheusIntegration(
+ $id: IntegrationsPrometheusID!
+ $apiUrl: String!
+ $active: Boolean!
+) {
prometheusIntegrationUpdate(input: { id: $id, apiUrl: $apiUrl, active: $active }) {
errors
integration {
diff --git a/app/assets/javascripts/alerts_settings/graphql/queries/get_http_integration.query.graphql b/app/assets/javascripts/alerts_settings/graphql/queries/get_http_integration.query.graphql
index 7299e6836d4..5f6ab27cae9 100644
--- a/app/assets/javascripts/alerts_settings/graphql/queries/get_http_integration.query.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/queries/get_http_integration.query.graphql
@@ -1,6 +1,6 @@
#import "ee_else_ce/alerts_settings/graphql/fragments/http_integration_payload_data.fragment.graphql"
-query getHttpIntegration($projectPath: ID!, $id: ID) {
+query getHttpIntegration($projectPath: ID!, $id: AlertManagementHttpIntegrationID!) {
project(fullPath: $projectPath) {
id
alertManagementHttpIntegrations(id: $id) {
diff --git a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
index b2b033de75d..b151e1605da 100644
--- a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
+++ b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
@@ -7,6 +7,7 @@ import {
GlDropdownSectionHeader,
GlDropdownItem,
GlSearchBoxByType,
+ GlTruncate,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { filterBySearchTerm } from '~/analytics/shared/utils';
@@ -28,6 +29,7 @@ export default {
GlDropdownSectionHeader,
GlDropdownItem,
GlSearchBoxByType,
+ GlTruncate,
},
props: {
groupId: {
@@ -212,30 +214,29 @@ export default {
<gl-dropdown
ref="projectsDropdown"
class="dropdown dropdown-projects"
- toggle-class="gl-shadow-none"
+ toggle-class="gl-shadow-none gl-mb-0"
:loading="loadingDefaultProjects"
:show-clear-all="hasSelectedProjects"
show-highlighted-items-title
highlighted-items-title-class="gl-p-3"
+ block
@clear-all.stop="onClearAll"
@hide="onHide"
>
<template #button-content>
- <gl-loading-icon v-if="loadingDefaultProjects" class="gl-mr-2" />
- <div class="gl-display-flex gl-flex-grow-1">
- <gl-avatar
- v-if="isOnlyOneProjectSelected"
- :src="selectedProjects[0].avatarUrl"
- :entity-id="getEntityId(selectedProjects[0])"
- :entity-name="selectedProjects[0].name"
- :size="16"
- :shape="$options.AVATAR_SHAPE_OPTION_RECT"
- :alt="selectedProjects[0].name"
- class="gl-display-inline-flex gl-vertical-align-middle gl-mr-2"
- />
- {{ selectedProjectsLabel }}
- </div>
- <gl-icon class="gl-ml-2" name="chevron-down" />
+ <gl-loading-icon v-if="loadingDefaultProjects" class="gl-mr-2 gl-flex-shrink-0" />
+ <gl-avatar
+ v-if="isOnlyOneProjectSelected"
+ :src="selectedProjects[0].avatarUrl"
+ :entity-id="getEntityId(selectedProjects[0])"
+ :entity-name="selectedProjects[0].name"
+ :size="16"
+ :shape="$options.AVATAR_SHAPE_OPTION_RECT"
+ :alt="selectedProjects[0].name"
+ class="gl-display-inline-flex gl-vertical-align-middle gl-mr-2 gl-flex-shrink-0"
+ />
+ <gl-truncate :text="selectedProjectsLabel" class="gl-min-w-0 gl-flex-grow-1" />
+ <gl-icon class="gl-ml-2 gl-flex-shrink-0" name="chevron-down" />
</template>
<template #header>
<gl-dropdown-section-header>{{ __('Projects') }}</gl-dropdown-section-header>
diff --git a/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
index 1a3544e7677..6ac1bce4032 100644
--- a/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
+++ b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
@@ -1,6 +1,6 @@
<script>
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
-import { flatten, isEqual } from 'lodash';
+import { flatten, isEqual, keyBy } from 'lodash';
import createFlash from '~/flash';
import { sprintf, s__ } from '~/locale';
import { METRICS_POPOVER_CONTENT } from '../constants';
@@ -28,6 +28,23 @@ const fetchMetricsData = (reqs = [], path, params) => {
);
};
+const extractMetricsGroupData = (keyList = [], data = []) => {
+ if (!keyList.length || !data.length) return [];
+ const kv = keyBy(data, 'identifier');
+ return keyList.map((id) => kv[id] || null).filter((obj) => Boolean(obj));
+};
+
+const groupRawMetrics = (groups = [], rawData = []) => {
+ return groups.map((curr) => {
+ const { keys, ...rest } = curr;
+ return {
+ data: extractMetricsGroupData(keys, rawData),
+ keys,
+ ...rest,
+ };
+ });
+};
+
export default {
name: 'ValueStreamMetrics',
components: {
@@ -52,13 +69,24 @@ export default {
required: false,
default: null,
},
+ groupBy: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
metrics: [],
+ groupedMetrics: [],
isLoading: false,
};
},
+ computed: {
+ hasGroupedMetrics() {
+ return Boolean(this.groupBy.length);
+ },
+ },
watch: {
requestParams(newVal, oldVal) {
if (!isEqual(newVal, oldVal)) {
@@ -76,6 +104,11 @@ export default {
return fetchMetricsData(this.requests, this.requestPath, this.requestParams)
.then((data) => {
this.metrics = this.filterFn ? this.filterFn(data) : data;
+
+ if (this.hasGroupedMetrics) {
+ this.groupedMetrics = groupRawMetrics(this.groupBy, this.metrics);
+ }
+
this.isLoading = false;
})
.catch(() => {
@@ -86,14 +119,35 @@ export default {
};
</script>
<template>
- <div class="gl-display-flex gl-flex-wrap" data-testid="vsa-metrics">
+ <div class="gl-display-flex gl-mt-6" data-testid="vsa-metrics">
<gl-skeleton-loading v-if="isLoading" class="gl-h-auto gl-py-3 gl-pr-9 gl-my-6" />
- <metric-tile
- v-for="metric in metrics"
- v-show="!isLoading"
- :key="metric.identifier"
- :metric="metric"
- class="gl-my-6 gl-pr-9"
- />
+ <template v-else>
+ <div v-if="hasGroupedMetrics" class="gl-flex-direction-column">
+ <div
+ v-for="group in groupedMetrics"
+ :key="group.key"
+ class="gl-mb-7"
+ data-testid="vsa-metrics-group"
+ >
+ <h4 class="gl-my-0">{{ group.title }}</h4>
+ <div class="gl-display-flex gl-flex-wrap">
+ <metric-tile
+ v-for="metric in group.data"
+ :key="metric.identifier"
+ :metric="metric"
+ class="gl-mt-5 gl-pr-10"
+ />
+ </div>
+ </div>
+ </div>
+ <div v-else class="gl-display-flex gl-flex-wrap gl-mb-7">
+ <metric-tile
+ v-for="metric in metrics"
+ :key="metric.identifier"
+ :metric="metric"
+ class="gl-mt-5 gl-pr-10"
+ />
+ </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js
index 2ac144ceb5e..38d05552783 100644
--- a/app/assets/javascripts/analytics/shared/constants.js
+++ b/app/assets/javascripts/analytics/shared/constants.js
@@ -55,4 +55,40 @@ export const METRICS_POPOVER_CONTENT = {
commits: {
description: s__('ValueStreamAnalytics|Number of commits pushed to the default branch'),
},
+ 'time-to-restore-service': {
+ description: s__(
+ 'ValueStreamAnalytics|Median time an incident was open on a production environment in the given time period.',
+ ),
+ },
+ time_to_restore_service: {
+ description: s__(
+ 'ValueStreamAnalytics|Median time an incident was open on a production environment in the given time period.',
+ ),
+ },
+ 'change-failure-rate': {
+ description: s__(
+ 'ValueStreamAnalytics|Percentage of deployments that cause an incident in production.',
+ ),
+ },
+ change_failure_rate: {
+ description: s__(
+ 'ValueStreamAnalytics|Percentage of deployments that cause an incident in production.',
+ ),
+ },
};
+
+const KEY_METRICS_TITLE = s__('ValueStreamAnalytics|Key metrics');
+const KEY_METRICS_KEYS = ['lead_time', 'cycle_time', 'issues', 'commits', 'deploys'];
+
+const DORA_METRICS_TITLE = s__('ValueStreamAnalytics|DORA metrics');
+const DORA_METRICS_KEYS = [
+ 'deployment_frequency',
+ 'lead_time_for_changes',
+ 'time_to_restore_service',
+ 'change_failure_rate',
+];
+
+export const VSA_METRICS_GROUPS = [
+ { key: 'key_metrics', title: KEY_METRICS_TITLE, keys: KEY_METRICS_KEYS },
+ { key: 'dora_metrics', title: DORA_METRICS_TITLE, keys: DORA_METRICS_KEYS },
+];
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 09dfcddcb73..dfe94aeb884 100644
--- a/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue
+++ b/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue
@@ -122,7 +122,7 @@ export default {
<div>
<h3>{{ $options.i18n.yAxisTitle }}</h3>
<gl-alert v-if="loadingError" variant="danger" :dismissible="false" class="gl-mt-3">
- {{ this.$options.i18n.loadUserChartError }}
+ {{ $options.i18n.loadUserChartError }}
</gl-alert>
<chart-skeleton-loader v-else-if="isLoading" />
<gl-alert v-else-if="!chartUserData.length" variant="info" :dismissible="false" class="gl-mt-3">
@@ -132,12 +132,12 @@ export default {
v-else
:option="options"
:include-legend-avg-max="true"
- :data="[
+ :data="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
{
name: $options.i18n.yAxisTitle,
data: chartUserData,
},
- ]"
+ ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
/>
</div>
</template>
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 64812e52849..8d46ea76be1 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -93,6 +93,7 @@ const Api = {
groupNotificationSettingsPath: '/api/:version/groups/:id/notification_settings',
notificationSettingsPath: '/api/:version/notification_settings',
deployKeysPath: '/api/:version/deploy_keys',
+ secureFilePath: '/api/:version/projects/:project_id/secure_files/:secure_file_id',
secureFilesPath: '/api/:version/projects/:project_id/secure_files',
dependencyProxyPath: '/api/:version/groups/:id/dependency_proxy/cache',
@@ -857,6 +858,14 @@ const Api = {
});
},
+ tag(id, tagName) {
+ const url = Api.buildUrl(this.tagPath)
+ .replace(':id', encodeURIComponent(id))
+ .replace(':tag_name', encodeURIComponent(tagName));
+
+ return axios.get(url);
+ },
+
freezePeriods(id) {
const url = Api.buildUrl(this.freezePeriodsPath).replace(':id', encodeURIComponent(id));
@@ -970,6 +979,22 @@ const Api = {
return axios.get(url, { params: { per_page: DEFAULT_PER_PAGE, ...options } });
},
+ uploadProjectSecureFile(projectId, fileData) {
+ const url = Api.buildUrl(this.secureFilesPath).replace(':project_id', projectId);
+
+ const headers = { 'Content-Type': 'multipart/form-data' };
+
+ return axios.post(url, fileData, { headers });
+ },
+
+ deleteProjectSecureFile(projectId, secureFileId) {
+ const url = Api.buildUrl(this.secureFilePath)
+ .replace(':project_id', projectId)
+ .replace(':secure_file_id', secureFileId);
+
+ return axios.delete(url);
+ },
+
async updateNotificationSettings(projectId, groupId, data = {}) {
let url = Api.buildUrl(this.notificationSettingsPath);
diff --git a/app/assets/javascripts/api/integrations_api.js b/app/assets/javascripts/api/integrations_api.js
new file mode 100644
index 00000000000..692aae21a4f
--- /dev/null
+++ b/app/assets/javascripts/api/integrations_api.js
@@ -0,0 +1,21 @@
+import axios from '../lib/utils/axios_utils';
+import { buildApiUrl } from './api_utils';
+
+const JIRA_CONNECT_SUBSCRIPTIONS_PATH = '/api/:version/integrations/jira_connect/subscriptions';
+
+export function addJiraConnectSubscription(namespacePath, { jwt, accessToken }) {
+ const url = buildApiUrl(JIRA_CONNECT_SUBSCRIPTIONS_PATH);
+
+ return axios.post(
+ url,
+ {
+ jwt,
+ namespace_path: namespacePath,
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${accessToken}`, // eslint-disable-line @gitlab/require-i18n-strings
+ },
+ },
+ );
+}
diff --git a/app/assets/javascripts/api/tags_api.js b/app/assets/javascripts/api/tags_api.js
new file mode 100644
index 00000000000..d4ee247ade6
--- /dev/null
+++ b/app/assets/javascripts/api/tags_api.js
@@ -0,0 +1,12 @@
+import axios from '../lib/utils/axios_utils';
+import { buildApiUrl } from './api_utils';
+
+const TAG_PATH = '/api/:version/projects/:id/repository/tags/:tag_name';
+
+export function getTag(id, tagName) {
+ const url = buildApiUrl(TAG_PATH)
+ .replace(':id', encodeURIComponent(id))
+ .replace(':tag_name', encodeURIComponent(tagName));
+
+ return axios.get(url);
+}
diff --git a/app/assets/javascripts/api/user_api.js b/app/assets/javascripts/api/user_api.js
index 09995fad628..c362253f52e 100644
--- a/app/assets/javascripts/api/user_api.js
+++ b/app/assets/javascripts/api/user_api.js
@@ -10,6 +10,9 @@ const USER_PATH = '/api/:version/users/:id';
const USER_STATUS_PATH = '/api/:version/users/:id/status';
const USER_PROJECTS_PATH = '/api/:version/users/:id/projects';
const USER_POST_STATUS_PATH = '/api/:version/user/status';
+const USER_FOLLOW_PATH = '/api/:version/users/:id/follow';
+const USER_UNFOLLOW_PATH = '/api/:version/users/:id/unfollow';
+const CURRENT_USER_PATH = '/api/:version/user';
export function getUsers(query, options) {
const url = buildApiUrl(USERS_PATH);
@@ -69,3 +72,18 @@ export function updateUserStatus({ emoji, message, availability, clearStatusAfte
clear_status_after: clearStatusAfter,
});
}
+
+export function followUser(userId) {
+ const url = buildApiUrl(USER_FOLLOW_PATH).replace(':id', encodeURIComponent(userId));
+ return axios.post(url);
+}
+
+export function unfollowUser(userId) {
+ const url = buildApiUrl(USER_UNFOLLOW_PATH).replace(':id', encodeURIComponent(userId));
+ return axios.post(url);
+}
+
+export function getCurrentUser(options) {
+ const url = buildApiUrl(CURRENT_USER_PATH);
+ return axios.get(url, { ...options });
+}
diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js
index c3c28aeafc0..07fd6dae76a 100644
--- a/app/assets/javascripts/behaviors/copy_to_clipboard.js
+++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js
@@ -43,7 +43,7 @@ function genericSuccess(e) {
}
/**
- * Safari > 10 doesn't support `execCommand`, so instead we inform the user to copy manually.
+ * Safari < 10 doesn't support `execCommand`, so instead we inform the user to copy manually.
* See http://clipboardjs.com/#browser-support
*/
function genericError(e) {
diff --git a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
index 6922ec9c5a5..3b9f6011c6d 100644
--- a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
@@ -53,9 +53,6 @@ function fixElementSource(el) {
// Mermaid doesn't like `<br />` tags, so collapse all like tags into `<br>`, which is parsed correctly.
const source = el.textContent?.replace(/<br\s*\/>/g, '<br>');
- // Remove any extra spans added by the backend syntax highlighting.
- Object.assign(el, { textContent: source });
-
return { source };
}
@@ -78,17 +75,13 @@ function renderMermaidEl(el, source) {
width: '100%',
});
- // Add the original source into the DOM
- // to allow Copy-as-GFM to access it.
- const sourceEl = document.createElement('text');
- sourceEl.textContent = source;
- sourceEl.classList.add('gl-display-none');
-
const wrapper = document.createElement('div');
wrapper.appendChild(iframeEl);
- wrapper.appendChild(sourceEl);
- el.closest('pre').replaceWith(wrapper);
+ // Hide the markdown but keep it "visible enough" to allow Copy-as-GFM
+ // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83202
+ el.closest('pre').classList.add('gl-sr-only');
+ el.closest('pre').parentNode.appendChild(wrapper);
// Event Listeners
iframeEl.addEventListener('load', () => {
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index 6124befd3b6..82229b5aa8f 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -156,7 +156,7 @@ export default class ShortcutsIssuable extends Shortcuts {
static copyBranchName() {
// There are two buttons - one that is shown when the sidebar
// is expanded, and one that is shown when it's collapsed.
- const allCopyBtns = Array.from(document.querySelectorAll('.js-sidebar-source-branch button'));
+ const allCopyBtns = Array.from(document.querySelectorAll('.js-source-branch-copy'));
// Select whichever button is currently visible so that
// the "Copied" tooltip is shown when a click is simulated.
diff --git a/app/assets/javascripts/blob/components/blob_content.vue b/app/assets/javascripts/blob/components/blob_content.vue
index 9832ebbea5c..f032e2e7fb8 100644
--- a/app/assets/javascripts/blob/components/blob_content.vue
+++ b/app/assets/javascripts/blob/components/blob_content.vue
@@ -66,7 +66,7 @@ export default {
</script>
<template>
<div class="blob-viewer" :data-type="activeViewer.type" :data-loaded="!loading">
- <gl-loading-icon v-if="loading" size="md" color="dark" class="my-4 mx-auto" />
+ <gl-loading-icon v-if="loading" size="lg" color="dark" class="my-4 mx-auto" />
<template v-else>
<blob-content-error
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index 8a4fe1a9025..f78d921fa90 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -92,7 +92,7 @@ export default {
</blob-filepath>
</div>
- <div class="gl-sm-display-flex file-actions">
+ <div class="gl-display-flex gl-flex-wrap file-actions">
<viewer-switcher v-if="showViewerSwitcher" v-model="viewer" :doc-icon="blobSwitcherDocIcon" />
<slot name="actions"></slot>
diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js
index e02217d0deb..4f970d657c2 100644
--- a/app/assets/javascripts/blob/file_template_selector.js
+++ b/app/assets/javascripts/blob/file_template_selector.js
@@ -1,10 +1,14 @@
import $ from 'jquery';
+import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
export default class FileTemplateSelector {
constructor(mediator) {
this.mediator = mediator;
this.$dropdown = null;
this.$wrapper = null;
+
+ this.dropdown = null;
+ this.wrapper = null;
}
init() {
@@ -12,18 +16,21 @@ export default class FileTemplateSelector {
this.$dropdown = $(cfg.dropdown);
this.$wrapper = $(cfg.wrapper);
- this.$dropdownIcon = this.$wrapper.find('.dropdown-menu-toggle-icon');
- this.$loadingIcon = $(
- '<div class="gl-spinner gl-spinner-orange gl-spinner-sm gl-absolute gl-top-3 gl-right-3 gl-display-none"></div>',
- ).insertAfter(this.$dropdownIcon);
- this.$dropdownToggleText = this.$wrapper.find('.dropdown-toggle-text');
+
+ this.dropdown = document.querySelector(cfg.dropdown);
+ this.wrapper = document.querySelector(cfg.wrapper);
+
+ this.dropdownIcon = this.wrapper.querySelector('.dropdown-menu-toggle-icon');
+ this.loadingIcon = loadingIconForLegacyJS({ classes: ['gl-display-none'] });
+ this.dropdown.appendChild(this.loadingIcon);
+ this.dropdownToggleText = this.wrapper.querySelector('.dropdown-toggle-text');
this.initDropdown();
this.selectInitialTemplate();
}
selectInitialTemplate() {
- const template = this.$dropdown.data('selected');
+ const template = this.dropdown.dataset.selected;
if (!template) {
return;
@@ -33,11 +40,11 @@ export default class FileTemplateSelector {
}
show() {
- if (this.$dropdown === null) {
+ if (this.dropdown === null) {
this.init();
}
- this.$wrapper.removeClass('hidden');
+ this.wrapper.classList.remove('hidden');
/**
* We set the focus on the dropdown that was just shown. This is done so that, after selecting
@@ -49,36 +56,36 @@ export default class FileTemplateSelector {
* closed anymore.
*/
setTimeout(() => {
- this.$dropdown.focus();
+ this.dropdown.focus();
}, 0);
}
hide() {
- if (this.$dropdown !== null) {
- this.$wrapper.addClass('hidden');
+ if (this.dropdown !== null) {
+ this.wrapper.classList.add('hidden');
}
}
isHidden() {
- return !this.$wrapper || this.$wrapper.hasClass('hidden');
+ return !this.wrapper || this.wrapper.classList.contains('hidden');
}
getToggleText() {
- return this.$dropdownToggleText.text();
+ return this.dropdownToggleText.textContent;
}
setToggleText(text) {
- this.$dropdownToggleText.text(text);
+ this.dropdownToggleText.textContent = text;
}
renderLoading() {
- this.$loadingIcon.removeClass('gl-display-none');
- this.$dropdownIcon.addClass('gl-display-none');
+ this.loadingIcon.classList.remove('gl-display-none');
+ this.dropdownIcon.classList.add('gl-display-none');
}
renderLoaded() {
- this.$loadingIcon.addClass('gl-display-none');
- this.$dropdownIcon.removeClass('gl-display-none');
+ this.loadingIcon.classList.add('gl-display-none');
+ this.dropdownIcon.classList.remove('gl-display-none');
}
reportSelection(options) {
diff --git a/app/assets/javascripts/blob/openapi/index.js b/app/assets/javascripts/blob/openapi/index.js
index a04da98ff77..4c497db9842 100644
--- a/app/assets/javascripts/blob/openapi/index.js
+++ b/app/assets/javascripts/blob/openapi/index.js
@@ -11,6 +11,7 @@ export default () => {
url: el.dataset.endpoint,
dom_id: '#js-openapi-viewer',
deepLinking: true,
+ displayOperationId: true,
});
})
.catch((error) => {
diff --git a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
index b4ca29114cb..f3c542c467a 100644
--- a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
+++ b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
@@ -132,7 +132,7 @@ export default {
<gl-button
ref="goToPipelines"
:href="goToPipelinesPath"
- variant="success"
+ variant="confirm"
:data-track-property="humanAccess"
:data-track-value="$options.goToTrackValuePipelines"
:data-track-action="$options.trackEvent"
diff --git a/app/assets/javascripts/blob/sketch/index.js b/app/assets/javascripts/blob/sketch/index.js
index d257810da65..a92161bbc1b 100644
--- a/app/assets/javascripts/blob/sketch/index.js
+++ b/app/assets/javascripts/blob/sketch/index.js
@@ -27,7 +27,7 @@ export default class SketchLoader {
}
getZipFile() {
- return new JSZip.external.Promise((resolve, reject) => {
+ return new Promise((resolve, reject) => {
JSZipUtils.getBinaryContent(this.container.dataset.endpoint, (err, data) => {
if (err) {
reject(err);
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index ee2f6cfb46c..2ee2e199358 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import { FileTemplateExtension } from '~/editor/extensions/source_editor_file_template_ext';
+import { ToolbarExtension } from '~/editor/extensions/source_editor_toolbar_ext';
import SourceEditor from '~/editor/source_editor';
-import { getBlobLanguage } from '~/editor/utils';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
@@ -36,7 +36,7 @@ export default class EditBlob {
import('~/editor/extensions/source_editor_markdown_ext'),
import('~/editor/extensions/source_editor_markdown_livepreview_ext'),
]);
- this.editor.use([
+ this.markdownExtensions = this.editor.use([
{ definition: MarkdownExtension },
{
definition: MarkdownLivePreview,
@@ -48,7 +48,6 @@ export default class EditBlob {
message: `${BLOB_EDITOR_ERROR}: ${e}`,
});
}
- this.hasMarkdownExtension = true;
addEditorMarkdownListeners(this.editor);
}
@@ -58,8 +57,6 @@ export default class EditBlob {
const fileContentEl = document.getElementById('file-content');
const form = document.querySelector('.js-edit-blob-form');
- this.hasMarkdownExtension = false;
-
const rootEditor = new SourceEditor();
this.editor = rootEditor.createInstance({
@@ -67,21 +64,29 @@ export default class EditBlob {
blobPath: fileNameEl.value,
blobContent: editorEl.innerText,
});
- this.editor.use([{ definition: SourceEditorExtension }, { definition: FileTemplateExtension }]);
+ this.editor.use([
+ { definition: SourceEditorExtension },
+ { definition: FileTemplateExtension },
+ { definition: ToolbarExtension },
+ ]);
fileNameEl.addEventListener('change', () => {
this.editor.updateModelLanguage(fileNameEl.value);
- const newLang = getBlobLanguage(fileNameEl.value);
- if (newLang === 'markdown') {
- if (!this.hasMarkdownExtension) {
- this.fetchMarkdownExtension();
- }
- }
});
form.addEventListener('submit', () => {
fileContentEl.value = insertFinalNewline(this.editor.getValue());
});
+
+ // onDidChangeModelLanguage is part of the native Monaco API
+ // https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneCodeEditor.html#onDidChangeModelLanguage
+ this.editor.onDidChangeModelLanguage(({ newLanguage = '', oldLanguage = '' }) => {
+ if (newLanguage === 'markdown') {
+ this.fetchMarkdownExtension();
+ } else if (oldLanguage === 'markdown') {
+ this.editor.unuse(this.markdownExtensions);
+ }
+ });
}
initFileSelectors() {
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 70ba90bb1d4..10c7a3db2d3 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
@@ -80,17 +80,14 @@ export default {
<template>
<div
- class="board-add-new-list board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal gl-flex-shrink-0"
+ 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-white"
+ class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
>
- <h3
- class="gl-font-size-h2 gl-px-5 gl-py-4 gl-m-0 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
- data-testid="board-add-column-form-title"
- >
+ <h3 class="gl-font-size-h2 gl-px-5 gl-py-5 gl-m-0" data-testid="board-add-column-form-title">
{{ $options.i18n.newList }}
</h3>
@@ -98,7 +95,7 @@ export default {
class="gl-display-flex gl-flex-direction-column gl-h-full gl-overflow-y-auto gl-align-items-flex-start"
>
<div class="gl-px-5">
- <h3 class="gl-font-lg gl-mt-5 gl-mb-2">
+ <h3 class="gl-font-lg gl-mt-3 gl-mb-2">
{{ $options.i18n.scope }}
</h3>
<p class="gl-mb-3">{{ $options.i18n.scopeDescription }}</p>
@@ -147,23 +144,18 @@ export default {
</gl-dropdown>
</gl-form-group>
</div>
- <div
- class="gl-display-flex gl-p-3 gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
- >
- <gl-button
- data-testid="cancelAddNewColumn"
- class="gl-ml-auto gl-mr-3"
- @click="setAddColumnFormVisibility(false)"
- >{{ $options.i18n.cancel }}</gl-button
- >
+ <div class="gl-display-flex gl-mb-4">
<gl-button
data-testid="addNewColumnButton"
:disabled="!selectedId"
variant="confirm"
- class="gl-mr-4"
+ class="gl-mr-3 gl-ml-4"
@click="$emit('add-list')"
>{{ $options.i18n.add }}</gl-button
>
+ <gl-button data-testid="cancelAddNewColumn" @click="setAddColumnFormVisibility(false)">{{
+ $options.i18n.cancel
+ }}</gl-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index 814ff16efec..98ce1ac7f97 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -11,10 +11,12 @@ import { sortBy } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
import { isScopedLabel } from '~/lib/utils/common_utils';
+import { updateHistory } from '~/lib/utils/url_utility';
import { sprintf, __, n__ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { ListType } from '../constants';
+import eventHub from '../eventhub';
import BoardBlockedIcon from './board_blocked_icon.vue';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue';
@@ -174,10 +176,19 @@ export default {
)
);
},
- labelTarget(label) {
+ filterByLabel(label) {
+ if (!this.updateFilters) return;
+
const filterPath = window.location.search ? `${window.location.search}&` : '?';
- const value = encodeURIComponent(label.title);
- return `${filterPath}label_name[]=${value}`;
+ const filter = `label_name[]=${encodeURIComponent(label.title)}`;
+
+ if (!filterPath.includes(filter)) {
+ updateHistory({
+ url: `${filterPath}${filter}`,
+ });
+ this.performSearch();
+ eventHub.$emit('updateTokens');
+ }
},
showScopedLabel(label) {
return this.scopedLabelsAvailable && isScopedLabel(label);
@@ -232,7 +243,7 @@ export default {
:description="label.description"
size="sm"
:scoped="showScopedLabel(label)"
- :target="labelTarget(label)"
+ @click="filterByLabel(label)"
/>
</template>
</div>
@@ -242,7 +253,7 @@ export default {
<div
class="gl-display-flex align-items-start flex-wrap-reverse board-card-number-container gl-overflow-hidden"
>
- <gl-loading-icon v-if="item.isLoading" size="md" class="mt-3" />
+ <gl-loading-icon v-if="item.isLoading" size="lg" class="mt-3" />
<span
v-if="item.referencePath"
class="board-card-number gl-overflow-hidden gl-display-flex gl-mr-3 gl-mt-3"
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 1d6a71aca47..8868b9b2f3e 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -75,7 +75,7 @@ export default {
v-if="!isSwimlanesOn"
ref="list"
v-bind="draggableOptions"
- class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap"
+ class="boards-list gl-w-full gl-py-5 gl-pr-3 gl-white-space-nowrap"
@end="moveList"
>
<board-column
@@ -85,10 +85,11 @@ export default {
:list="list"
:data-draggable-item-type="$options.draggableItemTypes.list"
:disabled="disabled"
+ :class="{ 'gl-xs-display-none!': addColumnFormVisible }"
/>
<transition name="slide" @after-enter="afterFormEnters">
- <board-add-new-column v-if="addColumnFormVisible" />
+ <board-add-new-column v-if="addColumnFormVisible" class="gl-xs-w-full!" />
</transition>
</component>
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index aeb2cee590d..fa0c798ca9d 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -2,7 +2,8 @@
import { pickBy, isEmpty, mapValues } from 'lodash';
import { mapActions } from 'vuex';
import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
-import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { updateHistory, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import {
FILTERED_SEARCH_TERM,
@@ -10,6 +11,7 @@ import {
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { AssigneeFilterType } from '~/boards/constants';
+import eventHub from '../eventhub';
export default {
i18n: {
@@ -33,6 +35,7 @@ export default {
data() {
return {
filterParams: this.initialFilterParams,
+ filteredSearchKey: 0,
};
},
computed: {
@@ -306,12 +309,21 @@ export default {
},
},
created() {
+ eventHub.$on('updateTokens', this.updateTokens);
if (!isEmpty(this.eeFilters)) {
this.filterParams = this.eeFilters;
}
},
+ beforeDestroy() {
+ eventHub.$off('updateTokens', this.updateTokens);
+ },
methods: {
...mapActions(['performSearch']),
+ updateTokens() {
+ const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
+ this.filterParams = convertObjectPropsToCamelCase(rawFilterParams, {});
+ this.filteredSearchKey += 1;
+ },
handleFilter(filters) {
this.filterParams = this.getFilterParams(filters);
@@ -399,6 +411,7 @@ export default {
<template>
<filtered-search
+ :key="filteredSearchKey"
class="gl-w-full"
namespace=""
:tokens="tokens"
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index a874c9e070a..9d972860d06 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -17,6 +17,7 @@ const boardDefaults = {
labels: [],
milestone: {},
iterationCadence: {},
+ iterationCadenceId: null,
iteration: {},
assignee: {},
weight: null,
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 47f25f34d0c..66388f4eb43 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -287,7 +287,7 @@ export default {
:data-board-type="list.listType"
:class="{ 'bg-danger-100': boardItemsSizeExceedsMax }"
draggable=".board-card"
- class="board-list gl-w-full gl-h-full gl-list-style-none gl-mb-0 gl-p-2"
+ class="board-list gl-w-full gl-h-full gl-list-style-none gl-mb-0 gl-p-3 gl-pt-0"
data-testid="tree-root-wrapper"
@start="handleDragOnStart"
@end="handleDragOnEnd"
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 9f70c84931f..a4298eb2544 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -126,7 +126,7 @@ export default {
return this.list.collapsed ? this.$options.i18n.expand : this.$options.i18n.collapse;
},
chevronIcon() {
- return this.list.collapsed ? 'chevron-down' : 'chevron-right';
+ return this.list.collapsed ? 'chevron-right' : 'chevron-down';
},
isNewIssueShown() {
return (this.listType === ListType.backlog || this.showListHeaderButton) && !this.isEpicBoard;
@@ -248,7 +248,6 @@ export default {
<template>
<header
:class="{
- 'has-border': list.label && list.label.color,
'gl-h-full': list.collapsed,
'board-inner gl-rounded-top-left-base gl-rounded-top-right-base': isSwimlanesHeader,
}"
@@ -279,28 +278,6 @@ export default {
@click="toggleExpanded"
/>
<!-- EE start -->
- <span
- v-if="showMilestoneListDetails"
- aria-hidden="true"
- class="milestone-icon"
- :class="{
- 'gl-mt-3 gl-rotate-90': list.collapsed,
- 'gl-mr-2': !list.collapsed,
- }"
- >
- <gl-icon name="timer" />
- </span>
-
- <span
- v-if="showIterationListDetails"
- aria-hidden="true"
- :class="{
- 'gl-mt-3 gl-rotate-90': list.collapsed,
- 'gl-mr-2': !list.collapsed,
- }"
- >
- <gl-icon name="iteration" />
- </span>
<a
v-if="showAssigneeListDetails"
@@ -399,7 +376,7 @@ export default {
<span class="gl-display-inline-flex">
<gl-tooltip :target="() => $refs.itemCount" :title="itemsTooltipLabel" />
<span ref="itemCount" class="gl-display-inline-flex gl-align-items-center">
- <gl-icon class="gl-mr-2" :name="countIcon" />
+ <gl-icon class="gl-mr-2" :name="countIcon" :size="16" />
<item-count
v-if="!isLoading"
:items-size="isEpicBoard ? list.epicsCount : boardList.issuesCount"
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 24071c6f0b4..c559e4cdbd3 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -135,14 +135,14 @@ export default {
:modal-id="$options.modalId"
:title="$options.i18n.modalAction"
size="sm"
- :action-primary="{
+ :action-primary="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: $options.i18n.modalAction,
attributes: [{ variant: 'danger' }],
- }"
- :action-secondary="{
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ :action-secondary="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: $options.i18n.modalCancel,
attributes: [{ variant: 'default' }],
- }"
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@primary="handleModalPrimary"
>
<p>{{ $options.i18n.modalCopy }}</p>
diff --git a/app/assets/javascripts/boards/components/board_top_bar.vue b/app/assets/javascripts/boards/components/board_top_bar.vue
index f90ac1e9079..54a6e3000a4 100644
--- a/app/assets/javascripts/boards/components/board_top_bar.vue
+++ b/app/assets/javascripts/boards/components/board_top_bar.vue
@@ -33,7 +33,7 @@ export default {
class="issues-details-filters filtered-search-block gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row row-content-block second-block"
>
<div
- class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-flex-grow-1 gl-lg-mb-0! mb-md-2 mb-sm-0 gl-w-full"
+ class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-flex-grow-1 gl-lg-mb-0 gl-mb-3 gl-w-full"
>
<boards-selector />
<new-board-button />
@@ -41,7 +41,7 @@ export default {
<issue-board-filtered-search v-else />
</div>
<div
- class="filter-dropdown-container gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-align-items-flex-start"
+ class="filter-dropdown-container gl-md-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-align-items-flex-start"
>
<toggle-labels />
<toggle-epics-swimlanes v-if="swimlanesFeatureAvailable && isSignedIn" />
diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue
index 4746f598ab7..7002fd44294 100644
--- a/app/assets/javascripts/boards/components/config_toggle.vue
+++ b/app/assets/javascripts/boards/components/config_toggle.vue
@@ -1,5 +1,6 @@
<script>
import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
import { formType } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import { s__, __ } from '~/locale';
@@ -14,8 +15,9 @@ export default {
GlModalDirective,
},
mixins: [Tracking.mixin()],
- inject: ['canAdminList', 'hasScope'],
+ inject: ['canAdminList'],
computed: {
+ ...mapGetters(['hasScope']),
buttonText() {
return this.canAdminList ? s__('Boards|Edit board') : s__('Boards|View scope');
},
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 1412411c275..247910301e7 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -91,6 +91,9 @@ export default {
loadMoreProjects() {
this.fetchGroupProjects({ search: this.searchTerm, fetchNext: true });
},
+ setFocus() {
+ this.$refs.search.focusInput();
+ },
},
};
</script>
@@ -107,8 +110,10 @@ export default {
block
menu-class="gl-w-full!"
:loading="initialLoading"
+ @shown="setFocus"
>
<gl-search-box-by-type
+ ref="search"
v-model.trim="searchTerm"
debounce="250"
:placeholder="$options.i18n.searchPlaceholder"
@@ -135,7 +140,7 @@ export default {
<span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span>
</gl-dropdown-text>
<gl-intersection-observer v-if="hasNextPage" @appear="loadMoreProjects">
- <gl-loading-icon v-if="groupProjectsFlags.isLoadingMore" size="md" />
+ <gl-loading-icon v-if="groupProjectsFlags.isLoadingMore" size="lg" />
</gl-intersection-observer>
</gl-dropdown>
</div>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
index 9d19fe57e7a..53e574e9942 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
@@ -130,7 +130,7 @@ export default {
@off-click="handleOffClick"
>
<template #title>
- <span class="gl-font-weight-bold" data-testid="item-title">{{ item.title }}</span>
+ <span data-testid="item-title">{{ item.title }}</span>
</template>
<template #collapsed>
<span class="gl-text-gray-800">{{ item.referencePath }}</span>
diff --git a/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql b/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql
index 81cc7b4d246..0e1d11727cf 100644
--- a/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql
@@ -2,8 +2,6 @@
mutation createBoardList($boardId: BoardID!, $backlog: Boolean, $labelId: LabelID) {
boardListCreate(input: { boardId: $boardId, backlog: $backlog, labelId: $labelId }) {
- # We have ID in a deeply nested fragment
- # eslint-disable-next-line @graphql-eslint/require-id-when-available
list {
...BoardListFragment
}
diff --git a/app/assets/javascripts/boards/graphql/board_list_destroy.mutation.graphql b/app/assets/javascripts/boards/graphql/board_list_destroy.mutation.graphql
index ef3fd36e980..5cb1a74d5c7 100644
--- a/app/assets/javascripts/boards/graphql/board_list_destroy.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/board_list_destroy.mutation.graphql
@@ -1,4 +1,4 @@
-mutation DestroyBoardList($listId: ID!) {
+mutation DestroyBoardList($listId: ListID!) {
destroyBoardList(input: { listId: $listId }) {
errors
}
diff --git a/app/assets/javascripts/boards/graphql/board_list_update.mutation.graphql b/app/assets/javascripts/boards/graphql/board_list_update.mutation.graphql
index 7ea0e2f915a..13327028065 100644
--- a/app/assets/javascripts/boards/graphql/board_list_update.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/board_list_update.mutation.graphql
@@ -1,9 +1,7 @@
#import "./board_list.fragment.graphql"
-mutation UpdateBoardList($listId: ID!, $position: Int, $collapsed: Boolean) {
+mutation UpdateBoardList($listId: ListID!, $position: Int, $collapsed: Boolean) {
updateBoardList(input: { listId: $listId, position: $position, collapsed: $collapsed }) {
- # We have ID in a deeply nested fragment
- # eslint-disable-next-line @graphql-eslint/require-id-when-available
list {
...BoardListFragment
}
diff --git a/app/assets/javascripts/boards/graphql/board_lists.query.graphql b/app/assets/javascripts/boards/graphql/board_lists.query.graphql
index e6e98864aad..06e8c8783de 100644
--- a/app/assets/javascripts/boards/graphql/board_lists.query.graphql
+++ b/app/assets/javascripts/boards/graphql/board_lists.query.graphql
@@ -2,7 +2,7 @@
query BoardLists(
$fullPath: ID!
- $boardId: ID!
+ $boardId: BoardID!
$filters: BoardIssueInput
$isGroup: Boolean = false
$isProject: Boolean = false
@@ -13,8 +13,6 @@ query BoardLists(
id
hideBacklogList
lists(issueFilters: $filters) {
- # We have ID in a deeply nested fragment
- # eslint-disable-next-line @graphql-eslint/require-id-when-available
nodes {
...BoardListFragment
}
@@ -27,8 +25,6 @@ query BoardLists(
id
hideBacklogList
lists(issueFilters: $filters) {
- # We have ID in a deeply nested fragment
- # eslint-disable-next-line @graphql-eslint/require-id-when-available
nodes {
...BoardListFragment
}
diff --git a/app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql b/app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql
index bae3220dfad..f48383624c9 100644
--- a/app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql
+++ b/app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql
@@ -1,4 +1,4 @@
-query BoardList($id: ID!, $filters: BoardIssueInput) {
+query BoardList($id: ListID!, $filters: BoardIssueInput) {
boardList(id: $id, issueFilters: $filters) {
id
issuesCount
diff --git a/app/assets/javascripts/boards/graphql/group_board.query.graphql b/app/assets/javascripts/boards/graphql/group_board.query.graphql
index 8d87b83da96..526e3193efe 100644
--- a/app/assets/javascripts/boards/graphql/group_board.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_board.query.graphql
@@ -1,6 +1,6 @@
#import "ee_else_ce/boards/graphql/board_scope.fragment.graphql"
-query GroupBoard($fullPath: ID!, $boardId: ID!) {
+query GroupBoard($fullPath: ID!, $boardId: BoardID!) {
workspace: group(fullPath: $fullPath) {
id
board(id: $boardId) {
diff --git a/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql
index 1658cf09085..89670760450 100644
--- a/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql
@@ -3,7 +3,7 @@
mutation issueMoveList(
$projectPath: ID!
$iid: String!
- $boardId: ID!
+ $boardId: BoardID!
$fromListId: ID
$toListId: ID
$moveBeforeId: ID
diff --git a/app/assets/javascripts/boards/graphql/lists_issues.query.graphql b/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
index 994ea894be3..bf5329c4a8d 100644
--- a/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
+++ b/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
@@ -2,8 +2,8 @@
query BoardListsEE(
$fullPath: ID!
- $boardId: ID!
- $id: ID
+ $boardId: BoardID!
+ $id: ListID
$filters: BoardIssueInput
$isGroup: Boolean = false
$isProject: Boolean = false
diff --git a/app/assets/javascripts/boards/graphql/project_board.query.graphql b/app/assets/javascripts/boards/graphql/project_board.query.graphql
index 8246d615a6a..2a9142696d2 100644
--- a/app/assets/javascripts/boards/graphql/project_board.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_board.query.graphql
@@ -1,6 +1,6 @@
#import "ee_else_ce/boards/graphql/board_scope.fragment.graphql"
-query ProjectBoard($fullPath: ID!, $boardId: ID!) {
+query ProjectBoard($fullPath: ID!, $boardId: BoardID!) {
workspace: project(fullPath: $fullPath) {
id
board(id: $boardId) {
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 77c5994b5a1..8af7da1e0aa 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -69,7 +69,6 @@ function mountBoardApp(el) {
timeTrackingLimitToHours: parseBoolean(el.dataset.timeTrackingLimitToHours),
issuableType: issuableTypes.issue,
emailsDisabled: parseBoolean(el.dataset.emailsDisabled),
- hasScope: parseBoolean(el.dataset.hasScope),
hasMissingBoards: parseBoolean(el.dataset.hasMissingBoards),
weights: el.dataset.weights ? JSON.parse(el.dataset.weights) : [],
// Permissions
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 82307da2572..a84b678a5d9 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -77,6 +77,7 @@ export default {
milestoneTitle: board.milestone?.title || null,
iterationId: board.iteration?.id || null,
iterationTitle: board.iteration?.title || null,
+ iterationCadenceId: board.iterationCadence?.id || null,
assigneeId: board.assignee?.id || null,
assigneeUsername: board.assignee?.username || null,
labels: board.labels?.nodes || [],
@@ -134,7 +135,7 @@ export default {
variables,
})
.then(({ data }) => {
- const { lists, hideBacklogList } = data[boardType]?.board;
+ const { lists, hideBacklogList } = data[boardType].board;
commit(types.RECEIVE_BOARD_LISTS_SUCCESS, formatBoardLists(lists));
// Backlog list needs to be created if it doesn't exist and it's not hidden
if (!lists.nodes.find((l) => l.listType === ListType.backlog) && !hideBacklogList) {
@@ -429,7 +430,7 @@ export default {
variables,
})
.then(({ data }) => {
- const { lists } = data[boardType]?.board;
+ const { lists } = data[boardType].board;
const listItems = formatListIssues(lists);
const listPageInfo = formatListsPageInfo(lists);
commit(types.RECEIVE_ITEMS_FOR_LIST_SUCCESS, { listItems, listPageInfo, listId });
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index cb31eb4b008..e1891a4d954 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -51,4 +51,18 @@ export default {
isEpicBoard: () => {
return false;
},
+
+ hasScope: (state) => {
+ const { boardConfig } = state;
+ if (boardConfig.labels?.length > 0) {
+ return true;
+ }
+ let hasScope = false;
+ ['assigneeId', 'iterationCadenceId', 'iterationId', 'milestoneId', 'weight'].forEach((attr) => {
+ if (boardConfig[attr] !== null && boardConfig[attr] !== undefined) {
+ hasScope = true;
+ }
+ });
+ return hasScope;
+ },
};
diff --git a/app/assets/javascripts/branches/components/delete_branch_modal.vue b/app/assets/javascripts/branches/components/delete_branch_modal.vue
index 14c2badeb3f..383fa5f7512 100644
--- a/app/assets/javascripts/branches/components/delete_branch_modal.vue
+++ b/app/assets/javascripts/branches/components/delete_branch_modal.vue
@@ -32,17 +32,10 @@ export default {
return sprintf(modalTitle, { branchName: this.branchName });
},
- message() {
- const modalMessage = this.isProtectedBranch
+ modalMessage() {
+ return this.isProtectedBranch
? this.$options.i18n.modalMessageProtectedBranch
: this.$options.i18n.modalMessage;
-
- return sprintf(modalMessage, { branchName: this.branchName });
- },
- unmergedWarning() {
- return sprintf(this.$options.i18n.unmergedWarning, {
- defaultBranchName: this.defaultBranchName,
- });
},
undoneWarning() {
return sprintf(this.$options.i18n.undoneWarning, {
@@ -92,17 +85,15 @@ export default {
i18n: {
modalTitle: s__('Branches|Delete branch. Are you ABSOLUTELY SURE?'),
modalTitleProtectedBranch: s__('Branches|Delete protected branch. Are you ABSOLUTELY SURE?'),
- modalMessage: s__(
- "Branches|You're about to permanently delete the branch %{strongStart}%{branchName}.%{strongEnd}",
- ),
+ modalMessage: s__("Branches|You're about to permanently delete the branch %{branchName}."),
modalMessageProtectedBranch: s__(
- "Branches|You're about to permanently delete the protected branch %{strongStart}%{branchName}.%{strongEnd}",
+ "Branches|You're about to permanently delete the protected branch %{branchName}.",
),
unmergedWarning: s__(
- 'Branches|This branch hasn’t been merged into %{defaultBranchName}. To avoid data loss, consider merging this branch before deleting it.',
+ "Branches|This branch hasn't been merged into %{defaultBranchName}. To avoid data loss, consider merging this branch before deleting it.",
),
undoneWarning: s__(
- 'Branches|Once you confirm and press %{strongStart}%{buttonText},%{strongEnd} it cannot be undone or recovered.',
+ 'Branches|After you confirm and select %{strongStart}%{buttonText},%{strongEnd} you cannot recover this branch.',
),
cancelButtonText: s__('Branches|Cancel, keep branch'),
confirmationText: s__(
@@ -119,13 +110,19 @@ export default {
<gl-modal ref="modal" size="sm" :modal-id="modalId" :title="title">
<gl-alert class="gl-mb-5" variant="danger" :dismissible="false">
<div data-testid="modal-message">
- <gl-sprintf :message="message">
- <template #strong="{ content }">
- <strong> {{ content }} </strong>
+ <gl-sprintf :message="modalMessage">
+ <template #branchName>
+ <strong>
+ <code class="gl-white-space-pre-wrap">{{ branchName }}</code>
+ </strong>
</template>
</gl-sprintf>
<p v-if="!merged" class="gl-mb-0 gl-mt-4">
- {{ unmergedWarning }}
+ <gl-sprintf :message="$options.i18n.unmergedWarning">
+ <template #defaultBranchName>
+ <code class="gl-white-space-pre-wrap">{{ defaultBranchName }}</code>
+ </template>
+ </gl-sprintf>
</p>
</div>
</gl-alert>
@@ -145,7 +142,7 @@ export default {
{{ content }}
</template>
</gl-sprintf>
- <code class="gl-white-space-pre-wrap"> {{ branchName }} </code>
+ <code class="gl-white-space-pre-wrap">{{ branchName }}</code>
<gl-form-input
v-model="enteredBranchName"
name="delete_branch_input"
diff --git a/app/assets/javascripts/captcha/captcha_modal.vue b/app/assets/javascripts/captcha/captcha_modal.vue
index b8b90b04beb..36aa098d5ff 100644
--- a/app/assets/javascripts/captcha/captcha_modal.vue
+++ b/app/assets/javascripts/captcha/captcha_modal.vue
@@ -107,7 +107,9 @@ export default {
ref="modal"
:modal-id="modalId"
:title="__('Please solve the captcha')"
- :action-cancel="{ text: __('Cancel') }"
+ :action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ text: __('Cancel'),
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@shown="shown"
@hide="hide"
@hidden="$emit('hidden')"
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 d70ade36fe9..dbc4565b19d 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
@@ -1,22 +1,48 @@
<script>
-import { GlLink, GlLoadingIcon, GlPagination, GlTable } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlButton,
+ GlIcon,
+ GlLink,
+ GlLoadingIcon,
+ GlModal,
+ GlModalDirective,
+ GlPagination,
+ GlSprintf,
+ GlTable,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import Api, { DEFAULT_PER_PAGE } from '~/api';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { __ } from '~/locale';
+import httpStatusCodes from '~/lib/utils/http_status';
+import { __, s__, sprintf } from '~/locale';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
+ GlAlert,
+ GlButton,
+ GlIcon,
GlLink,
GlLoadingIcon,
+ GlModal,
GlPagination,
+ GlSprintf,
GlTable,
TimeagoTooltip,
},
- inject: ['projectId'],
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ GlModal: GlModalDirective,
+ },
+ inject: ['projectId', 'admin', 'fileSizeLimit'],
docsLink: helpPagePath('ci/secure_files/index'),
DEFAULT_PER_PAGE,
i18n: {
+ deleteLabel: __('Delete File'),
+ uploadLabel: __('Upload File'),
+ uploadingLabel: __('Uploading...'),
pagination: {
next: __('Next'),
prev: __('Prev'),
@@ -26,27 +52,45 @@ export default {
'Use Secure Files to store files used by your pipelines such as Android keystores, or Apple provisioning profiles and signing certificates.',
),
moreInformation: __('More information'),
+ uploadErrorMessages: {
+ duplicate: __('A file with this name already exists.'),
+ tooLarge: __('File too large. Secure Files must be less than %{limit} MB.'),
+ },
+ deleteModalTitle: s__('SecureFiles|Delete %{name}?'),
+ deleteModalMessage: s__(
+ 'SecureFiles|Secure File %{name} will be permanently deleted. Are you sure?',
+ ),
+ deleteModalButton: s__('SecureFiles|Delete secure file'),
},
+ deleteModalId: 'deleteModalId',
data() {
return {
page: 1,
totalItems: 0,
loading: false,
+ uploading: false,
+ error: false,
+ errorMessage: null,
projectSecureFiles: [],
+ deleteModalFileId: null,
+ deleteModalFileName: null,
};
},
fields: [
{
key: 'name',
label: __('Filename'),
- },
- {
- key: 'permissions',
- label: __('Permissions'),
+ tdClass: 'gl-vertical-align-middle!',
},
{
key: 'created_at',
label: __('Uploaded'),
+ tdClass: 'gl-vertical-align-middle!',
+ },
+ {
+ key: 'actions',
+ label: '',
+ tdClass: 'gl-text-right gl-vertical-align-middle!',
},
],
computed: {
@@ -63,6 +107,18 @@ export default {
this.getProjectSecureFiles();
},
methods: {
+ async deleteSecureFile(secureFileId) {
+ this.loading = true;
+ this.error = false;
+ try {
+ await Api.deleteProjectSecureFile(this.projectId, secureFileId);
+ this.getProjectSecureFiles();
+ } catch (error) {
+ Sentry.captureException(error);
+ this.error = true;
+ this.errorMessage = error;
+ }
+ },
async getProjectSecureFiles(page) {
this.loading = true;
const response = await Api.projectSecureFiles(this.projectId, { page });
@@ -72,6 +128,48 @@ export default {
this.projectSecureFiles = response.data;
this.loading = false;
+ this.uploading = false;
+ },
+ async uploadSecureFile() {
+ this.error = null;
+ this.uploading = true;
+ const [file] = this.$refs.fileUpload.files;
+ try {
+ await Api.uploadProjectSecureFile(this.projectId, this.uploadFormData(file));
+ this.getProjectSecureFiles();
+ } catch (error) {
+ this.error = true;
+ this.errorMessage = this.formattedErrorMessage(error);
+ this.uploading = false;
+ }
+ },
+ formattedErrorMessage(error) {
+ let message = '';
+ if (error?.response?.data?.message?.name) {
+ message = this.$options.i18n.uploadErrorMessages.duplicate;
+ } else if (error.response.status === httpStatusCodes.PAYLOAD_TOO_LARGE) {
+ message = sprintf(this.$options.i18n.uploadErrorMessages.tooLarge, {
+ limit: this.fileSizeLimit,
+ });
+ } else {
+ Sentry.captureException(error);
+ message = error;
+ }
+ return message;
+ },
+ loadFileSelctor() {
+ this.$refs.fileUpload.click();
+ },
+ setDeleteModalData(secureFile) {
+ this.deleteModalFileId = secureFile.id;
+ this.deleteModalFileName = secureFile.name;
+ },
+ uploadFormData(file) {
+ const formData = new FormData();
+ formData.append('name', file.name);
+ formData.append('file', file);
+
+ return formData;
},
},
};
@@ -79,16 +177,51 @@ export default {
<template>
<div>
- <h1 data-testid="title" class="gl-font-size-h1 gl-mt-3 gl-mb-0">{{ $options.i18n.title }}</h1>
+ <gl-alert v-if="error" variant="danger" class="gl-mt-6" @dismiss="error = null">
+ {{ errorMessage }}
+ </gl-alert>
+ <div class="row">
+ <div class="col-md-12 col-lg-6 gl-display-flex">
+ <div class="gl-flex-direction-column gl-flex-wrap">
+ <h1 class="gl-font-size-h1 gl-mt-3 gl-mb-0">
+ {{ $options.i18n.title }}
+ </h1>
+ </div>
+ </div>
+
+ <div class="col-md-12 col-lg-6">
+ <div class="gl-display-flex gl-flex-wrap gl-justify-content-end">
+ <gl-button v-if="admin" class="gl-mt-3" variant="info" @click="loadFileSelctor">
+ <span v-if="uploading">
+ <gl-loading-icon size="sm" class="gl-my-5" inline />
+ {{ $options.i18n.uploadingLabel }}
+ </span>
+ <span v-else>
+ <gl-icon name="upload" class="gl-mr-2" /> {{ $options.i18n.uploadLabel }}
+ </span>
+ </gl-button>
+ <input
+ id="file-upload"
+ ref="fileUpload"
+ type="file"
+ class="hidden"
+ data-qa-selector="file_upload_field"
+ @change="uploadSecureFile"
+ />
+ </div>
+ </div>
+ </div>
- <p>
- <span data-testid="info-message" class="gl-mr-2">
- {{ $options.i18n.overviewMessage }}
- <gl-link :href="$options.docsLink" target="_blank">{{
- $options.i18n.moreInformation
- }}</gl-link>
- </span>
- </p>
+ <div class="row">
+ <div class="col-md-12 col-lg-12 gl-my-4">
+ <span data-testid="info-message">
+ {{ $options.i18n.overviewMessage }}
+ <gl-link :href="$options.docsLink" target="_blank">{{
+ $options.i18n.moreInformation
+ }}</gl-link>
+ </span>
+ </div>
+ </div>
<gl-table
:busy="loading"
@@ -112,14 +245,23 @@ export default {
{{ item.name }}
</template>
- <template #cell(permissions)="{ item }">
- {{ item.permissions }}
- </template>
-
<template #cell(created_at)="{ item }">
<timeago-tooltip :time="item.created_at" />
</template>
+
+ <template #cell(actions)="{ item }">
+ <gl-button
+ v-if="admin"
+ v-gl-modal="$options.deleteModalId"
+ v-gl-tooltip.hover.top="$options.i18n.deleteLabel"
+ variant="danger"
+ icon="remove"
+ :aria-label="$options.i18n.deleteLabel"
+ @click="setDeleteModalData(item)"
+ />
+ </template>
</gl-table>
+
<gl-pagination
v-if="!loading"
v-model="page"
@@ -129,5 +271,25 @@ export default {
:prev-text="$options.i18n.pagination.prev"
align="center"
/>
+
+ <gl-modal
+ :ref="$options.deleteModalId"
+ :modal-id="$options.deleteModalId"
+ title-tag="h4"
+ category="primary"
+ :ok-title="$options.i18n.deleteModalButton"
+ ok-variant="danger"
+ @ok="deleteSecureFile(deleteModalFileId)"
+ >
+ <template #modal-title>
+ <gl-sprintf :message="$options.i18n.deleteModalTitle">
+ <template #name>{{ deleteModalFileName }}</template>
+ </gl-sprintf>
+ </template>
+
+ <gl-sprintf :message="$options.i18n.deleteModalMessage">
+ <template #name>{{ deleteModalFileName }}</template>
+ </gl-sprintf>
+ </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/ci_secure_files/index.js b/app/assets/javascripts/ci_secure_files/index.js
index 18b4ac6866e..3944286dc60 100644
--- a/app/assets/javascripts/ci_secure_files/index.js
+++ b/app/assets/javascripts/ci_secure_files/index.js
@@ -1,14 +1,19 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import SecureFilesList from './components/secure_files_list.vue';
export const initCiSecureFiles = (selector = '#js-ci-secure-files') => {
const containerEl = document.querySelector(selector);
const { projectId } = containerEl.dataset;
+ const { admin } = containerEl.dataset;
+ const { fileSizeLimit } = containerEl.dataset;
return new Vue({
el: containerEl,
provide: {
projectId,
+ admin: parseBoolean(admin),
+ fileSizeLimit,
},
render(createElement) {
return createElement(SecureFilesList);
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
index be2366108b3..3af89dc4a2c 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
@@ -26,6 +26,7 @@ import {
AWS_TIP_DISMISSED_COOKIE_NAME,
AWS_TIP_MESSAGE,
CONTAINS_VARIABLE_REFERENCE_MESSAGE,
+ ENVIRONMENT_SCOPE_LINK_TITLE,
EVENT_LABEL,
EVENT_ACTION,
} from '../constants';
@@ -40,6 +41,7 @@ export default {
tokenList: awsTokenList,
awsTipMessage: AWS_TIP_MESSAGE,
containsVariableReferenceMessage: CONTAINS_VARIABLE_REFERENCE_MESSAGE,
+ environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
components: {
CiEnvironmentsDropdown,
GlAlert,
@@ -81,6 +83,7 @@ export default {
'containsVariableReferenceLink',
'protectedEnvironmentVariablesLink',
'maskedEnvironmentVariablesLink',
+ 'environmentScopeLink',
]),
...mapComputed(
[
@@ -109,7 +112,7 @@ export default {
return regex.test(this.variable.secret_value);
},
containsVariableReference() {
- const regex = RegExp(/\$/);
+ const regex = /\$/;
return regex.test(this.variable.secret_value);
},
displayMaskedError() {
@@ -278,12 +281,18 @@ export default {
<gl-form-select id="ci-variable-type" v-model="variable_type" :options="typeOptions" />
</gl-form-group>
- <gl-form-group
- :label="__('Environment scope')"
- label-for="ci-variable-env"
- class="w-50"
- data-testid="environment-scope"
- >
+ <gl-form-group label-for="ci-variable-env" class="w-50" data-testid="environment-scope">
+ <template #label>
+ {{ __('Environment scope') }}
+ <gl-link
+ :title="$options.environmentScopeLinkTitle"
+ :href="environmentScopeLink"
+ target="_blank"
+ data-testid="environment-scope-link"
+ >
+ <gl-icon name="question" :size="12" />
+ </gl-link>
+ </template>
<ci-environments-dropdown
v-if="scopedVariablesAvailable"
class="w-100"
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
index 61636b389da..f078234829a 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
@@ -179,10 +179,7 @@ export default {
</p>
</template>
</gl-table>
- <div
- class="ci-variable-actions gl-display-flex"
- :class="{ 'gl-justify-content-center': isTableEmpty }"
- >
+ <div class="ci-variable-actions gl-display-flex gl-mt-5">
<gl-button
v-gl-modal-directive="$options.modalId"
class="gl-mr-3"
diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js
index 663a912883b..fa55b4d9e77 100644
--- a/app/assets/javascripts/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci_variable_list/constants.js
@@ -31,3 +31,5 @@ export const AWS_TOKEN_CONSTANTS = [AWS_ACCESS_KEY_ID, AWS_DEFAULT_REGION, AWS_S
export const CONTAINS_VARIABLE_REFERENCE_MESSAGE = __(
'Values that contain the %{codeStart}$%{codeEnd} character can be considered a variable reference and expanded. %{docsLinkStart}Learn more.%{docsLinkEnd}',
);
+
+export const ENVIRONMENT_SCOPE_LINK_TITLE = __('Learn more');
diff --git a/app/assets/javascripts/ci_variable_list/index.js b/app/assets/javascripts/ci_variable_list/index.js
index 7c40f8134d4..f771751194c 100644
--- a/app/assets/javascripts/ci_variable_list/index.js
+++ b/app/assets/javascripts/ci_variable_list/index.js
@@ -17,6 +17,7 @@ const mountCiVariableListApp = (containerEl) => {
containsVariableReferenceLink,
protectedEnvironmentVariablesLink,
maskedEnvironmentVariablesLink,
+ environmentScopeLink,
} = containerEl.dataset;
const isGroup = parseBoolean(group);
const isProtectedByDefault = parseBoolean(protectedByDefault);
@@ -34,6 +35,7 @@ const mountCiVariableListApp = (containerEl) => {
containsVariableReferenceLink,
protectedEnvironmentVariablesLink,
maskedEnvironmentVariablesLink,
+ environmentScopeLink,
});
return new Vue({
diff --git a/app/assets/javascripts/clone_panel.js b/app/assets/javascripts/clone_panel.js
index ec831a77bde..79280c13f0f 100644
--- a/app/assets/javascripts/clone_panel.js
+++ b/app/assets/javascripts/clone_panel.js
@@ -17,7 +17,12 @@ export default function initClonePanel() {
$('a', $cloneOptions).on('click', (e) => {
const $this = $(e.currentTarget);
const url = $this.attr('href');
- if (url && (url.startsWith('vscode://') || url.startsWith('xcode://'))) {
+ if (
+ url &&
+ (url.startsWith('vscode://') ||
+ url.startsWith('xcode://') ||
+ url.startsWith('jetbrains://'))
+ ) {
// Clone with "..." should open like a normal link
return;
}
diff --git a/app/assets/javascripts/clusters/agents/components/activity_events_list.vue b/app/assets/javascripts/clusters/agents/components/activity_events_list.vue
index 6567ce203bc..18c6503bfb2 100644
--- a/app/assets/javascripts/clusters/agents/components/activity_events_list.vue
+++ b/app/assets/javascripts/clusters/agents/components/activity_events_list.vue
@@ -28,17 +28,17 @@ export default {
},
i18n: {
emptyText: s__(
- 'ClusterAgents|See Agent activity updates such as tokens created or revoked and clusters connected or not connected.',
+ 'ClusterAgents|See agent activity updates, like tokens created or revoked and clusters connected or not connected.',
),
- emptyTooltip: s__('ClusterAgents|What is GitLab Agent activity?'),
+ emptyTooltip: s__('ClusterAgents|What is agent activity?'),
error: s__(
- 'ClusterAgents|An error occurred while retrieving GitLab Agent activity. Reload the page to try again.',
+ 'ClusterAgents|An error occurred while retrieving agent activity. Reload the page to try again.',
),
today: __('Today'),
yesterday: __('Yesterday'),
},
- emptyHelpLink: helpPagePath('user/clusters/agent/install/index', {
- anchor: 'view-agent-activity',
+ emptyHelpLink: helpPagePath('user/clusters/agent/work_with_agent', {
+ anchor: 'view-an-agents-activity-information',
}),
borderClasses: 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100',
apollo: {
@@ -68,8 +68,8 @@ export default {
},
emptyStateTitle() {
return n__(
- "ClusterAgents|There's no activity from the past day",
- "ClusterAgents|There's no activity from the past %d days",
+ 'ClusterAgents|No activity occurred in the past day',
+ 'ClusterAgents|No activity occurred in the past %d days',
EVENTS_STORED_DAYS,
);
},
@@ -124,7 +124,7 @@ export default {
<template>
<div>
- <gl-loading-icon v-if="isLoading" size="md" />
+ <gl-loading-icon v-if="isLoading" size="lg" />
<div v-else-if="hasEvents">
<div
diff --git a/app/assets/javascripts/clusters/agents/components/revoke_token_button.vue b/app/assets/javascripts/clusters/agents/components/revoke_token_button.vue
new file mode 100644
index 00000000000..7d36cbb170d
--- /dev/null
+++ b/app/assets/javascripts/clusters/agents/components/revoke_token_button.vue
@@ -0,0 +1,201 @@
+<script>
+import {
+ GlButton,
+ GlModalDirective,
+ GlTooltip,
+ GlModal,
+ GlFormGroup,
+ GlFormInput,
+ GlSprintf,
+} from '@gitlab/ui';
+import { s__, __, sprintf } from '~/locale';
+import { REVOKE_TOKEN_MODAL_ID, TOKEN_STATUS_ACTIVE } from '../constants';
+import revokeAgentToken from '../graphql/mutations/revoke_token.mutation.graphql';
+import getClusterAgentQuery from '../graphql/queries/get_cluster_agent.query.graphql';
+import { removeTokenFromStore } from '../graphql/cache_update';
+
+export default {
+ components: {
+ GlButton,
+ GlTooltip,
+ GlModal,
+ GlFormGroup,
+ GlFormInput,
+ GlSprintf,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ inject: ['agentName', 'projectPath', 'canAdminCluster'],
+ props: {
+ token: {
+ required: true,
+ type: Object,
+ validator: (value) => ['id', 'name'].every((prop) => value[prop]),
+ },
+ cursor: {
+ required: true,
+ type: Object,
+ },
+ },
+ i18n: {
+ revokeButton: s__('ClusterAgents|Revoke token'),
+ dropdownDisabledHint: s__(
+ 'ClusterAgents|Requires a Maintainer or greater role to perform this action',
+ ),
+ modalTitle: s__('ClusterAgents|Revoke access token?'),
+ modalBody: s__(
+ 'ClusterAgents|Are you sure you want to revoke this token? You cannot undo this action.',
+ ),
+ modalInputLabel: s__('ClusterAgents|To revoke the token, type %{name} to confirm:'),
+ modalCancel: __('Cancel'),
+ successMessage: s__('ClusterAgents|%{name} successfully revoked'),
+ defaultError: __('An error occurred. Please try again.'),
+ },
+ data() {
+ return {
+ loading: false,
+ error: null,
+ revokeConfirmText: null,
+ tokenName: null,
+ variables: {
+ agentName: this.agentName,
+ projectPath: this.projectPath,
+ tokenStatus: TOKEN_STATUS_ACTIVE,
+ ...this.cursor,
+ },
+ };
+ },
+ computed: {
+ revokeBtnDisabled() {
+ return this.loading || !this.canAdminCluster;
+ },
+ modalId() {
+ return sprintf(REVOKE_TOKEN_MODAL_ID, {
+ tokenName: this.token.name,
+ });
+ },
+ primaryModalProps() {
+ return {
+ text: this.$options.i18n.revokeButton,
+ attributes: [
+ { disabled: this.loading || this.disableModalSubmit, loading: this.loading },
+ { variant: 'danger' },
+ ],
+ };
+ },
+ cancelModalProps() {
+ return {
+ text: this.$options.i18n.modalCancel,
+ attributes: [],
+ };
+ },
+ disableModalSubmit() {
+ return this.revokeConfirmText !== this.token.name;
+ },
+ },
+ methods: {
+ async revokeToken() {
+ if (this.disableModalSubmit || this.loading) {
+ return;
+ }
+
+ this.loading = true;
+ this.error = null;
+ this.tokenName = this.token.name;
+
+ try {
+ const { errors } = await this.revokeTokenMutation();
+
+ if (errors.length) {
+ throw new Error(errors[0]);
+ }
+ } catch (error) {
+ this.error = error?.message || this.$options.i18n.defaultError;
+ } finally {
+ this.loading = false;
+ const successMessage = sprintf(this.$options.i18n.successMessage, {
+ name: this.tokenName,
+ });
+
+ this.$toast.show(this.error || successMessage);
+
+ this.hideModal();
+ }
+ },
+ revokeTokenMutation() {
+ return this.$apollo
+ .mutate({
+ mutation: revokeAgentToken,
+ variables: {
+ input: {
+ id: this.token.id,
+ },
+ },
+ update: (store) => {
+ removeTokenFromStore(store, this.token, getClusterAgentQuery, this.variables);
+ },
+ })
+
+ .then(({ data: { clusterAgentTokenRevoke } }) => {
+ return clusterAgentTokenRevoke;
+ });
+ },
+ resetModal() {
+ this.loading = false;
+ this.error = null;
+ this.revokeConfirmText = null;
+ },
+ hideModal() {
+ this.resetModal();
+ this.$refs.modal.hide();
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div ref="revokeToken" class="gl-display-inline-block">
+ <gl-button
+ v-gl-modal-directive="modalId"
+ icon="remove"
+ category="secondary"
+ variant="danger"
+ :disabled="revokeBtnDisabled"
+ :title="$options.i18n.revokeButton"
+ :aria-label="$options.i18n.revokeButton"
+ />
+
+ <gl-tooltip
+ v-if="!canAdminCluster"
+ :target="() => $refs.revokeToken"
+ :title="$options.i18n.dropdownDisabledHint"
+ />
+ </div>
+
+ <gl-modal
+ ref="modal"
+ :modal-id="modalId"
+ :title="$options.i18n.modalTitle"
+ :action-primary="primaryModalProps"
+ :action-cancel="cancelModalProps"
+ size="sm"
+ @primary="revokeToken"
+ @hide="hideModal"
+ >
+ <p>{{ $options.i18n.modalBody }}</p>
+
+ <gl-form-group>
+ <template #label>
+ <gl-sprintf :message="$options.i18n.modalInputLabel">
+ <template #name>
+ <code>{{ token.name }}</code>
+ </template>
+ </gl-sprintf>
+ </template>
+ <gl-form-input v-model="revokeConfirmText" @keydown.enter="revokeToken" />
+ </gl-form-group>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters/agents/components/show.vue b/app/assets/javascripts/clusters/agents/components/show.vue
index 5df3e0811a5..e3de8339325 100644
--- a/app/assets/javascripts/clusters/agents/components/show.vue
+++ b/app/assets/javascripts/clusters/agents/components/show.vue
@@ -140,7 +140,7 @@ export default {
</span>
</template>
- <gl-loading-icon v-if="isLoading" size="md" class="gl-m-3" />
+ <gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" />
<div v-else>
<token-table :tokens="tokens" :cluster-agent-id="clusterAgent.id" :cursor="cursor" />
diff --git a/app/assets/javascripts/clusters/agents/components/token_table.vue b/app/assets/javascripts/clusters/agents/components/token_table.vue
index fbb39c28d78..9e64c9da712 100644
--- a/app/assets/javascripts/clusters/agents/components/token_table.vue
+++ b/app/assets/javascripts/clusters/agents/components/token_table.vue
@@ -3,6 +3,7 @@ import { GlEmptyState, GlTable, GlTooltip, GlTruncate } from '@gitlab/ui';
import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import CreateTokenButton from './create_token_button.vue';
+import RevokeTokenButton from './revoke_token_button.vue';
export default {
components: {
@@ -12,6 +13,7 @@ export default {
GlTruncate,
TimeAgoTooltip,
CreateTokenButton,
+ RevokeTokenButton,
},
i18n: {
createdBy: s__('ClusterAgents|Created by'),
@@ -66,6 +68,11 @@ export default {
label: this.$options.i18n.description,
tdAttr: { 'data-testid': 'agent-token-description' },
},
+ {
+ key: 'actions',
+ label: '',
+ tdAttr: { 'data-testid': 'agent-token-revoke' },
+ },
];
},
},
@@ -119,6 +126,10 @@ export default {
</gl-tooltip>
</div>
</template>
+
+ <template #cell(actions)="{ item }">
+ <revoke-token-button :token="item" :cluster-agent-id="clusterAgentId" :cursor="cursor" />
+ </template>
</gl-table>
</div>
diff --git a/app/assets/javascripts/clusters/agents/constants.js b/app/assets/javascripts/clusters/agents/constants.js
index 50d8f5e9e40..962fa243903 100644
--- a/app/assets/javascripts/clusters/agents/constants.js
+++ b/app/assets/javascripts/clusters/agents/constants.js
@@ -44,3 +44,5 @@ export const EVENT_ACTIONS_OPEN = 'open_modal';
export const EVENT_ACTIONS_CLICK = 'click_button';
export const TOKEN_NAME_LIMIT = 255;
+
+export const REVOKE_TOKEN_MODAL_ID = 'revoke-token-%{tokenName}';
diff --git a/app/assets/javascripts/clusters/agents/graphql/cache_update.js b/app/assets/javascripts/clusters/agents/graphql/cache_update.js
index 0219c4150eb..8db79c82708 100644
--- a/app/assets/javascripts/clusters/agents/graphql/cache_update.js
+++ b/app/assets/javascripts/clusters/agents/graphql/cache_update.js
@@ -22,3 +22,25 @@ export function addAgentTokenToStore(store, clusterAgentTokenCreate, query, vari
});
}
}
+
+export function removeTokenFromStore(store, revokeToken, query, variables) {
+ if (!hasErrors(revokeToken)) {
+ const sourceData = store.readQuery({
+ query,
+ variables,
+ });
+
+ const data = produce(sourceData, (draftData) => {
+ draftData.project.clusterAgent.tokens.nodes = draftData.project.clusterAgent.tokens.nodes.filter(
+ ({ id }) => id !== revokeToken.id,
+ );
+ draftData.project.clusterAgent.tokens.count -= 1;
+ });
+
+ store.writeQuery({
+ query,
+ variables,
+ data,
+ });
+ }
+}
diff --git a/app/assets/javascripts/clusters/agents/graphql/mutations/revoke_token.mutation.graphql b/app/assets/javascripts/clusters/agents/graphql/mutations/revoke_token.mutation.graphql
new file mode 100644
index 00000000000..6f1c6a66690
--- /dev/null
+++ b/app/assets/javascripts/clusters/agents/graphql/mutations/revoke_token.mutation.graphql
@@ -0,0 +1,5 @@
+mutation revokeAgentToken($input: ClusterAgentTokenRevokeInput!) {
+ clusterAgentTokenRevoke(input: $input) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/clusters/components/new_cluster.vue b/app/assets/javascripts/clusters/components/new_cluster.vue
index 8f3e2916270..41a33a8459f 100644
--- a/app/assets/javascripts/clusters/components/new_cluster.vue
+++ b/app/assets/javascripts/clusters/components/new_cluster.vue
@@ -1,6 +1,6 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
-import { mapState } from 'vuex';
+import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
export default {
@@ -10,13 +10,11 @@ export default {
'ClusterIntegration|Enter details about your cluster. %{linkStart}How do I use a certificate to connect to my cluster?%{linkEnd}',
),
},
+ clusterConnectHelpPath: helpPagePath('user/project/clusters/add_existing_cluster'),
components: {
GlLink,
GlSprintf,
},
- computed: {
- ...mapState(['clusterConnectHelpPath']),
- },
};
</script>
@@ -26,7 +24,7 @@ export default {
<p>
<gl-sprintf :message="$options.i18n.information">
<template #link="{ content }">
- <gl-link :href="clusterConnectHelpPath" target="_blank">{{ content }}</gl-link>
+ <gl-link :href="$options.clusterConnectHelpPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
index 98db620e3ab..dca89133931 100644
--- a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
+++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
@@ -159,7 +159,7 @@ export default {
)
}}</span>
<template #modal-footer>
- <gl-button variant="secondary" @click="handleCancel">{{ __('Cancel') }}</gl-button>
+ <gl-button @click="handleCancel">{{ __('Cancel') }}</gl-button>
<template v-if="confirmCleanup">
<gl-button
:disabled="!canSubmit"
diff --git a/app/assets/javascripts/clusters/forms/components/integration_form.vue b/app/assets/javascripts/clusters/forms/components/integration_form.vue
index 3f61a1b18a7..b2a8381f937 100644
--- a/app/assets/javascripts/clusters/forms/components/integration_form.vue
+++ b/app/assets/javascripts/clusters/forms/components/integration_form.vue
@@ -140,7 +140,7 @@ export default {
<div v-if="editable" class="form group gl-display-flex gl-justify-content-end">
<gl-button
category="primary"
- variant="success"
+ variant="confirm"
type="submit"
:disabled="!canSubmit"
:aria-disabled="!canSubmit"
diff --git a/app/assets/javascripts/create_cluster/gke_cluster_namespace/index.js b/app/assets/javascripts/clusters/gke_cluster_namespace/index.js
index 2b3dfb99328..2b3dfb99328 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster_namespace/index.js
+++ b/app/assets/javascripts/clusters/gke_cluster_namespace/index.js
diff --git a/app/assets/javascripts/clusters/new_cluster.js b/app/assets/javascripts/clusters/new_cluster.js
index 71f585fd307..4df6872bcc1 100644
--- a/app/assets/javascripts/clusters/new_cluster.js
+++ b/app/assets/javascripts/clusters/new_cluster.js
@@ -1,17 +1,15 @@
import Vue from 'vue';
import NewCluster from './components/new_cluster.vue';
-import { createStore } from './stores/new_cluster';
export default () => {
- const entryPoint = document.querySelector('#js-cluster-new');
+ const el = document.querySelector('#js-cluster-new');
- if (!entryPoint) {
+ if (!el) {
return null;
}
return new Vue({
- el: '#js-cluster-new',
- store: createStore(entryPoint.dataset),
+ el,
render(createElement) {
return createElement(NewCluster);
},
diff --git a/app/assets/javascripts/clusters/stores/new_cluster/index.js b/app/assets/javascripts/clusters/stores/new_cluster/index.js
deleted file mode 100644
index 87f1c05fdf9..00000000000
--- a/app/assets/javascripts/clusters/stores/new_cluster/index.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import state from './state';
-
-Vue.use(Vuex);
-
-export const createStore = (initialState) =>
- new Vuex.Store({
- state: state(initialState),
- });
-
-export default createStore;
diff --git a/app/assets/javascripts/clusters/stores/new_cluster/state.js b/app/assets/javascripts/clusters/stores/new_cluster/state.js
deleted file mode 100644
index 1ca1ac8de18..00000000000
--- a/app/assets/javascripts/clusters/stores/new_cluster/state.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default (initialState = {}) => ({
- clusterConnectHelpPath: initialState.clusterConnectHelpPath,
-});
diff --git a/app/assets/javascripts/clusters_list/components/agent_table.vue b/app/assets/javascripts/clusters_list/components/agent_table.vue
index 2decdb5307b..496baf8cb08 100644
--- a/app/assets/javascripts/clusters_list/components/agent_table.vue
+++ b/app/assets/javascripts/clusters_list/components/agent_table.vue
@@ -39,7 +39,7 @@ export default {
configHelpLink: helpPagePath('user/clusters/agent/install/index', {
anchor: 'create-an-agent-configuration-file',
}),
- inject: ['gitlabVersion'],
+ inject: ['gitlabVersion', 'kasVersion'],
props: {
agents: {
required: true,
@@ -102,6 +102,9 @@ export default {
return { ...agent, versions };
});
},
+ serverVersion() {
+ return this.kasVersion || this.gitlabVersion;
+ },
},
methods: {
getStatusCellId(item) {
@@ -135,12 +138,12 @@ export default {
if (!agent.versions.length) return false;
const [agentMajorVersion, agentMinorVersion] = this.getAgentVersionString(agent).split('.');
- const [gitlabMajorVersion, gitlabMinorVersion] = this.gitlabVersion.split('.');
+ const [serverMajorVersion, serverMinorVersion] = this.serverVersion.split('.');
- const majorVersionMismatch = agentMajorVersion !== gitlabMajorVersion;
+ const majorVersionMismatch = agentMajorVersion !== serverMajorVersion;
// We should warn user if their current GitLab and agent versions are more than 1 minor version apart:
- const minorVersionMismatch = Math.abs(agentMinorVersion - gitlabMinorVersion) > 1;
+ const minorVersionMismatch = Math.abs(agentMinorVersion - serverMinorVersion) > 1;
return majorVersionMismatch || minorVersionMismatch;
},
@@ -165,8 +168,6 @@ export default {
:items="agentsList"
:fields="fields"
stacked="md"
- head-variant="white"
- thead-class="gl-border-b-solid gl-border-b-2 gl-border-b-gray-100"
class="gl-mb-4!"
data-testid="cluster-agent-list-table"
>
@@ -242,7 +243,7 @@ export default {
<p class="gl-mb-0">
<gl-sprintf :message="$options.i18n.versionOutdatedText">
- <template #version>{{ gitlabVersion }}</template>
+ <template #version>{{ serverVersion }}</template>
</gl-sprintf>
<gl-link :href="$options.versionUpdateLink" class="gl-font-sm">
{{ $options.i18n.viewDocsText }}</gl-link
@@ -255,7 +256,7 @@ export default {
<p v-else-if="isVersionOutdated(item)" class="gl-mb-0">
<gl-sprintf :message="$options.i18n.versionOutdatedText">
- <template #version>{{ gitlabVersion }}</template>
+ <template #version>{{ serverVersion }}</template>
</gl-sprintf>
<gl-link :href="$options.versionUpdateLink" class="gl-font-sm">
{{ $options.i18n.viewDocsText }}</gl-link
diff --git a/app/assets/javascripts/clusters_list/components/agents.vue b/app/assets/javascripts/clusters_list/components/agents.vue
index 89b18ed6d06..8a4a81d3e96 100644
--- a/app/assets/javascripts/clusters_list/components/agents.vue
+++ b/app/assets/javascripts/clusters_list/components/agents.vue
@@ -175,7 +175,7 @@ export default {
</script>
<template>
- <gl-loading-icon v-if="isLoading" size="md" />
+ <gl-loading-icon v-if="isLoading" size="lg" />
<section v-else-if="agentList">
<div v-if="agentList.length">
diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue
index 59cfdde731d..fb3c8ff66b0 100644
--- a/app/assets/javascripts/clusters_list/components/clusters.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters.vue
@@ -224,7 +224,7 @@ export default {
</script>
<template>
- <gl-loading-icon v-if="loadingClusters" size="md" />
+ <gl-loading-icon v-if="loadingClusters" size="lg" />
<section v-else>
<ancestor-notice />
@@ -235,8 +235,6 @@ export default {
:fields="fields"
fixed
stacked="md"
- head-variant="white"
- thead-class="gl-border-b-solid gl-border-b-2 gl-border-b-gray-100"
class="qa-clusters-table gl-mb-4!"
data-testid="cluster_list_table"
>
diff --git a/app/assets/javascripts/clusters_list/components/clusters_actions.vue b/app/assets/javascripts/clusters_list/components/clusters_actions.vue
index 8fd759bd3e9..2675d46dd16 100644
--- a/app/assets/javascripts/clusters_list/components/clusters_actions.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters_actions.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown, GlDropdownItem, GlModalDirective, GlTooltip } from '@gitlab/ui';
+import { GlButton, GlDropdown, GlDropdownItem, GlModalDirective, GlTooltip } from '@gitlab/ui';
import { INSTALL_AGENT_MODAL_ID, CLUSTERS_ACTIONS } from '../constants';
@@ -7,6 +7,7 @@ export default {
i18n: CLUSTERS_ACTIONS,
INSTALL_AGENT_MODAL_ID,
components: {
+ GlButton,
GlDropdown,
GlDropdownItem,
GlTooltip,
@@ -15,7 +16,6 @@ export default {
GlModalDirective,
},
inject: [
- 'newClusterPath',
'addClusterPath',
'newClusterDocsPath',
'canAddCluster',
@@ -42,20 +42,59 @@ export default {
}
return this.addClusterPath;
},
+ actionItems() {
+ const createCluster = {
+ href: this.newClusterDocsPath,
+ title: this.$options.i18n.createCluster,
+ testid: 'create-cluster-link',
+ };
+ const connectCluster = {
+ href: this.addClusterPath,
+ title: this.$options.i18n.connectClusterCertificate,
+ testid: 'connect-cluster-link',
+ };
+ const actions = [];
+
+ if (this.displayClusterAgents) {
+ actions.push(createCluster);
+ }
+ if (this.displayClusterAgents && this.certificateBasedClustersEnabled) {
+ actions.push(connectCluster);
+ }
+
+ return actions;
+ },
+ },
+ methods: {
+ getTooltipTarget() {
+ return this.actionItems.length ? this.$refs.actions.$el : this.$refs.actionsContainer;
+ },
},
};
</script>
<template>
- <div class="nav-controls gl-ml-auto">
+ <div ref="actionsContainer" class="nav-controls gl-ml-auto">
<gl-tooltip
v-if="!canAddCluster"
- :target="() => $refs.dropdown.$el"
- :title="$options.i18n.dropdownDisabledHint"
+ :target="() => getTooltipTarget()"
+ :title="$options.i18n.actionsDisabledHint"
/>
+ <gl-button
+ v-if="!actionItems.length"
+ data-qa-selector="clusters_actions_button"
+ category="primary"
+ variant="confirm"
+ :disabled="!canAddCluster"
+ :href="defaultActionUrl"
+ >
+ {{ defaultActionText }}
+ </gl-button>
+
<gl-dropdown
- ref="dropdown"
+ v-else
+ ref="actions"
v-gl-modal-directive="shouldTriggerModal && $options.INSTALL_AGENT_MODAL_ID"
data-qa-selector="clusters_actions_button"
category="primary"
@@ -67,31 +106,13 @@ export default {
right
>
<gl-dropdown-item
- v-if="displayClusterAgents"
- :href="newClusterDocsPath"
- data-testid="create-cluster-link"
- @click.stop
- >
- {{ $options.i18n.createCluster }}
- </gl-dropdown-item>
-
- <template v-if="displayClusterAgents && certificateBasedClustersEnabled">
- <gl-dropdown-item :href="newClusterPath" data-testid="new-cluster-link" @click.stop>
- {{ $options.i18n.createClusterCertificate }}
- </gl-dropdown-item>
-
- <gl-dropdown-item :href="addClusterPath" data-testid="connect-cluster-link" @click.stop>
- {{ $options.i18n.connectClusterCertificate }}
- </gl-dropdown-item>
- </template>
-
- <gl-dropdown-item
- v-if="certificateBasedClustersEnabled && !displayClusterAgents"
- :href="newClusterPath"
- data-testid="new-cluster-link"
+ v-for="action in actionItems"
+ :key="action.title"
+ :href="action.href"
+ :data-testid="action.testid"
@click.stop
>
- {{ $options.i18n.createClusterDeprecated }}
+ {{ action.title }}
</gl-dropdown-item>
</gl-dropdown>
</div>
diff --git a/app/assets/javascripts/clusters_list/components/clusters_view_all.vue b/app/assets/javascripts/clusters_list/components/clusters_view_all.vue
index 73ca804e111..d831d79b994 100644
--- a/app/assets/javascripts/clusters_list/components/clusters_view_all.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters_view_all.vue
@@ -89,7 +89,7 @@ export default {
</script>
<template>
<div>
- <gl-loading-icon v-if="isLoading" size="md" />
+ <gl-loading-icon v-if="isLoading" size="lg" />
<div v-show="!isLoading" data-testid="clusters-cards-container">
<gl-card
header-class="gl-bg-white gl-display-flex gl-align-items-center gl-justify-content-space-between gl-py-4"
diff --git a/app/assets/javascripts/clusters_list/constants.js b/app/assets/javascripts/clusters_list/constants.js
index 4a168e811aa..10e71513065 100644
--- a/app/assets/javascripts/clusters_list/constants.js
+++ b/app/assets/javascripts/clusters_list/constants.js
@@ -234,11 +234,9 @@ export const CLUSTERS_ACTIONS = {
connectCluster: s__('ClusterAgents|Connect a cluster'),
connectWithAgent: s__('ClusterAgents|Connect a cluster (agent)'),
connectClusterDeprecated: s__('ClusterAgents|Connect a cluster (deprecated)'),
- createClusterDeprecated: s__('ClusterAgents|Create a cluster (deprecated)'),
createCluster: s__('ClusterAgents|Create a cluster'),
- createClusterCertificate: s__('ClusterAgents|Create a cluster (certificate - deprecated)'),
connectClusterCertificate: s__('ClusterAgents|Connect a cluster (certificate - deprecated)'),
- dropdownDisabledHint: s__(
+ actionsDisabledHint: s__(
'ClusterAgents|Requires a Maintainer or greater role to perform these actions',
),
};
diff --git a/app/assets/javascripts/clusters_list/index.js b/app/assets/javascripts/clusters_list/index.js
index f6dfb96ffd9..cd334d80e9c 100644
--- a/app/assets/javascripts/clusters_list/index.js
+++ b/app/assets/javascripts/clusters_list/index.js
@@ -23,7 +23,6 @@ export default () => {
defaultBranchName,
projectPath,
kasAddress,
- newClusterPath,
addClusterPath,
newClusterDocsPath,
emptyStateHelpText,
@@ -31,6 +30,7 @@ export default () => {
canAddCluster,
canAdminCluster,
gitlabVersion,
+ kasVersion,
displayClusterAgents,
certificateBasedClustersEnabled,
} = el.dataset;
@@ -42,7 +42,6 @@ export default () => {
emptyStateImage,
projectPath,
kasAddress,
- newClusterPath,
addClusterPath,
newClusterDocsPath,
emptyStateHelpText,
@@ -50,6 +49,7 @@ export default () => {
canAddCluster: parseBoolean(canAddCluster),
canAdminCluster: parseBoolean(canAdminCluster),
gitlabVersion,
+ kasVersion,
displayClusterAgents: parseBoolean(displayClusterAgents),
certificateBasedClustersEnabled: parseBoolean(certificateBasedClustersEnabled),
},
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index b92f3d5a97b..29530ddb7a2 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -212,7 +212,6 @@ export default {
<template #table-header-actions>
<div v-if="canRenderPipelineButton" class="gl-text-right">
<gl-button
- variant="confirm"
data-testid="run_pipeline_button"
:loading="state.isRunningMergeRequestPipeline"
@click="tryRunPipeline"
diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
index 32d9159ee34..af049738016 100644
--- a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
+++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
@@ -58,6 +58,7 @@ export default {
}
} else if (this.createBtn) {
this.createBtn.setAttribute('disabled', 'disabled');
+ this.createBtn.classList.add('disabled');
}
},
normalizeProjectData(data) {
@@ -89,11 +90,6 @@ export default {
if (this.warningText) {
this.warningText.classList.remove('gl-display-none');
}
-
- if (this.createBtn) {
- this.createBtn.classList.add('btn-warning');
- this.createBtn.classList.remove('btn-success');
- }
},
},
i18n: {
diff --git a/app/assets/javascripts/content_editor/components/code_block_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue
index 87f22a27856..518ddd7a09c 100644
--- a/app/assets/javascripts/content_editor/components/code_block_bubble_menu.vue
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue
@@ -8,11 +8,12 @@ import {
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
import { BubbleMenu } from '@tiptap/vue-2';
-import codeBlockLanguageLoader from '../services/code_block_language_loader';
-import CodeBlockHighlight from '../extensions/code_block_highlight';
-import Diagram from '../extensions/diagram';
-import Frontmatter from '../extensions/frontmatter';
-import EditorStateObserver from './editor_state_observer.vue';
+import { getParentByTagName } from '~/lib/utils/dom_utils';
+import codeBlockLanguageLoader from '../../services/code_block_language_loader';
+import CodeBlockHighlight from '../../extensions/code_block_highlight';
+import Diagram from '../../extensions/diagram';
+import Frontmatter from '../../extensions/frontmatter';
+import EditorStateObserver from '../editor_state_observer.vue';
const CODE_BLOCK_NODE_TYPES = [CodeBlockHighlight.name, Diagram.name, Frontmatter.name];
@@ -32,6 +33,7 @@ export default {
inject: ['tiptapEditor'],
data() {
return {
+ codeBlockType: undefined,
selectedLanguage: {},
filterTerm: '',
filteredLanguages: [],
@@ -50,47 +52,40 @@ export default {
return CODE_BLOCK_NODE_TYPES.some((type) => editor.isActive(type));
},
- getSelectedLanguage() {
- const { language } = this.tiptapEditor.getAttributes(this.getCodeBlockType());
+ updateSelectedLanguage() {
+ this.codeBlockType = CODE_BLOCK_NODE_TYPES.find((type) => this.tiptapEditor.isActive(type));
- this.selectedLanguage = codeBlockLanguageLoader.findLanguageBySyntax(language);
+ if (this.codeBlockType) {
+ const { language } = this.tiptapEditor.getAttributes(this.codeBlockType);
+ this.selectedLanguage = codeBlockLanguageLoader.findLanguageBySyntax(language);
+ }
},
- async setSelectedLanguage(language) {
- this.selectedLanguage = language;
-
- await codeBlockLanguageLoader.loadLanguages([language.syntax]);
+ copyCodeBlockText() {
+ const { view } = this.tiptapEditor;
+ const { from } = this.tiptapEditor.state.selection;
+ const node = getParentByTagName(view.domAtPos(from).node, 'pre');
- this.tiptapEditor.commands.setCodeBlock({ language: this.selectedLanguage.syntax });
+ navigator.clipboard.writeText(node?.textContent || '');
},
- tippyOnBeforeUpdate(tippy, props) {
- if (props.getReferenceClientRect) {
- // eslint-disable-next-line no-param-reassign
- props.getReferenceClientRect = () => {
- const { view } = this.tiptapEditor;
- const { from } = this.tiptapEditor.state.selection;
+ async applySelectedLanguage(language) {
+ this.selectedLanguage = language;
- for (let { node } = view.domAtPos(from); node; node = node.parentElement) {
- if (node.nodeName?.toLowerCase() === 'pre') {
- return node.getBoundingClientRect();
- }
- }
+ await codeBlockLanguageLoader.loadLanguage(language.syntax);
- return new DOMRect(-1000, -1000, 0, 0);
- };
- }
+ this.tiptapEditor.commands.setCodeBlock({ language: this.selectedLanguage.syntax });
},
- deleteCodeBlock() {
- this.tiptapEditor.chain().focus().deleteNode(this.getCodeBlockType()).run();
+ getReferenceClientRect() {
+ const { view } = this.tiptapEditor;
+ const { from } = this.tiptapEditor.state.selection;
+ const node = getParentByTagName(view.domAtPos(from).node, 'pre');
+ return node?.getBoundingClientRect() || new DOMRect(-1000, -1000, 0, 0);
},
- getCodeBlockType() {
- return (
- CODE_BLOCK_NODE_TYPES.find((type) => this.tiptapEditor.isActive(type)) ||
- CodeBlockHighlight.name
- );
+ deleteCodeBlock() {
+ this.tiptapEditor.chain().focus().deleteNode(this.codeBlockType).run();
},
},
};
@@ -98,15 +93,22 @@ export default {
<template>
<bubble-menu
data-testid="code-block-bubble-menu"
- class="gl-shadow gl-rounded-base"
+ class="gl-shadow gl-rounded-base gl-bg-white"
:editor="tiptapEditor"
plugin-key="bubbleMenuCodeBlock"
:should-show="shouldShow"
- :tippy-options="{ onBeforeUpdate: tippyOnBeforeUpdate }"
+ :tippy-options="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ getReferenceClientRect,
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
>
- <editor-state-observer @transaction="getSelectedLanguage">
+ <editor-state-observer @transaction="updateSelectedLanguage">
<gl-button-group>
- <gl-dropdown contenteditable="false" boundary="viewport" :text="selectedLanguage.label">
+ <gl-dropdown
+ category="tertiary"
+ contenteditable="false"
+ boundary="viewport"
+ :text="selectedLanguage.label"
+ >
<template #header>
<gl-search-box-by-type
v-model="filterTerm"
@@ -125,7 +127,7 @@ export default {
v-for="language in filteredLanguages"
v-show="selectedLanguage.syntax !== language.syntax"
:key="language.syntax"
- @click="setSelectedLanguage(language)"
+ @click="applySelectedLanguage(language)"
>
{{ language.label }}
</gl-dropdown-item>
@@ -133,8 +135,20 @@ export default {
<gl-button
v-gl-tooltip
variant="default"
- category="primary"
+ category="tertiary"
+ size="medium"
+ data-testid="copy-code-block"
+ :aria-label="__('Copy code')"
+ :title="__('Copy code')"
+ icon="copy-to-clipboard"
+ @click="copyCodeBlockText"
+ />
+ <gl-button
+ v-gl-tooltip
+ variant="default"
+ category="tertiary"
size="medium"
+ data-testid="delete-code-block"
:aria-label="__('Delete code block')"
:title="__('Delete code block')"
icon="remove"
diff --git a/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/formatting.vue
index 103079534bc..e35fbf14de5 100644
--- a/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/formatting.vue
@@ -1,13 +1,16 @@
<script>
import { GlButtonGroup } from '@gitlab/ui';
import { BubbleMenu } from '@tiptap/vue-2';
-import { BUBBLE_MENU_TRACKING_ACTION } from '../constants';
-import trackUIControl from '../services/track_ui_control';
-import Code from '../extensions/code';
-import CodeBlockHighlight from '../extensions/code_block_highlight';
-import Diagram from '../extensions/diagram';
-import Frontmatter from '../extensions/frontmatter';
-import ToolbarButton from './toolbar_button.vue';
+import { BUBBLE_MENU_TRACKING_ACTION } from '../../constants';
+import trackUIControl from '../../services/track_ui_control';
+import Image from '../../extensions/image';
+import Audio from '../../extensions/audio';
+import Video from '../../extensions/video';
+import Code from '../../extensions/code';
+import CodeBlockHighlight from '../../extensions/code_block_highlight';
+import Diagram from '../../extensions/diagram';
+import Frontmatter from '../../extensions/frontmatter';
+import ToolbarButton from '../toolbar_button.vue';
export default {
components: {
@@ -24,7 +27,15 @@ export default {
shouldShow: ({ editor, from, to }) => {
if (from === to) return false;
- const exclude = [Code.name, CodeBlockHighlight.name, Diagram.name, Frontmatter.name];
+ const exclude = [
+ Code.name,
+ CodeBlockHighlight.name,
+ Diagram.name,
+ Frontmatter.name,
+ Image.name,
+ Audio.name,
+ Video.name,
+ ];
return !exclude.some((type) => editor.isActive(type));
},
@@ -34,7 +45,7 @@ export default {
<template>
<bubble-menu
data-testid="formatting-bubble-menu"
- class="gl-shadow gl-rounded-base"
+ class="gl-shadow gl-rounded-base gl-bg-white"
:editor="tiptapEditor"
:should-show="shouldShow"
>
@@ -44,7 +55,7 @@ export default {
content-type="bold"
icon-name="bold"
editor-command="toggleBold"
- category="primary"
+ category="tertiary"
size="medium"
:label="__('Bold text')"
@execute="trackToolbarControlExecution"
@@ -54,7 +65,7 @@ export default {
content-type="italic"
icon-name="italic"
editor-command="toggleItalic"
- category="primary"
+ category="tertiary"
size="medium"
:label="__('Italic text')"
@execute="trackToolbarControlExecution"
@@ -64,7 +75,7 @@ export default {
content-type="strike"
icon-name="strikethrough"
editor-command="toggleStrike"
- category="primary"
+ category="tertiary"
size="medium"
:label="__('Strikethrough')"
@execute="trackToolbarControlExecution"
@@ -74,11 +85,24 @@ export default {
content-type="code"
icon-name="code"
editor-command="toggleCode"
- category="primary"
+ category="tertiary"
size="medium"
:label="__('Code')"
@execute="trackToolbarControlExecution"
/>
+ <toolbar-button
+ data-testid="link"
+ content-type="link"
+ icon-name="link"
+ editor-command="toggleLink"
+ :editor-command-params="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ href: '',
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ category="tertiary"
+ size="medium"
+ :label="__('Insert link')"
+ @execute="trackToolbarControlExecution"
+ />
</gl-button-group>
</bubble-menu>
</template>
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/link.vue b/app/assets/javascripts/content_editor/components/bubble_menus/link.vue
new file mode 100644
index 00000000000..dae0bc63b5a
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/link.vue
@@ -0,0 +1,189 @@
+<script>
+import {
+ GlLink,
+ GlForm,
+ GlFormGroup,
+ GlFormInput,
+ GlButton,
+ GlButtonGroup,
+ GlTooltipDirective as GlTooltip,
+} from '@gitlab/ui';
+import { BubbleMenu } from '@tiptap/vue-2';
+import Link from '../../extensions/link';
+import EditorStateObserver from '../editor_state_observer.vue';
+
+export default {
+ components: {
+ BubbleMenu,
+ GlForm,
+ GlFormGroup,
+ GlFormInput,
+ GlLink,
+ GlButton,
+ GlButtonGroup,
+ EditorStateObserver,
+ },
+ directives: {
+ GlTooltip,
+ },
+ inject: ['tiptapEditor', 'contentEditor'],
+ data() {
+ return {
+ linkHref: undefined,
+ linkCanonicalSrc: undefined,
+ linkTitle: undefined,
+
+ isEditing: false,
+ };
+ },
+ watch: {
+ linkCanonicalSrc(value) {
+ if (!value) this.isEditing = true;
+ },
+ },
+ methods: {
+ shouldShow() {
+ const shouldShow = this.tiptapEditor.isActive(Link.name);
+
+ if (!shouldShow) this.isEditing = false;
+
+ return shouldShow;
+ },
+
+ startEditingLink() {
+ // select the entire link
+ this.tiptapEditor.chain().focus().extendMarkRange(Link.name).run();
+
+ this.isEditing = true;
+ },
+
+ async endEditingLink() {
+ this.isEditing = false;
+
+ this.linkHref = await this.contentEditor.resolveUrl(this.linkCanonicalSrc);
+
+ if (!this.linkCanonicalSrc && !this.linkHref) {
+ this.removeLink();
+ }
+ },
+
+ cancelEditingLink() {
+ this.endEditingLink();
+ this.updateLinkToState();
+ },
+
+ async saveEditedLink() {
+ if (!this.linkCanonicalSrc) {
+ this.removeLink();
+ } else {
+ this.tiptapEditor
+ .chain()
+ .focus()
+ .extendMarkRange(Link.name)
+ .updateAttributes(Link.name, {
+ href: this.linkCanonicalSrc,
+ canonicalSrc: this.linkCanonicalSrc,
+ title: this.linkTitle,
+ })
+ .run();
+ }
+
+ this.endEditingLink();
+ },
+
+ updateLinkToState() {
+ if (!this.tiptapEditor.isActive(Link.name)) return;
+
+ const { href, title, canonicalSrc } = this.tiptapEditor.getAttributes(Link.name);
+
+ this.linkTitle = title;
+ this.linkHref = href;
+ this.linkCanonicalSrc = canonicalSrc || href;
+ },
+
+ copyLinkHref() {
+ navigator.clipboard.writeText(this.linkCanonicalSrc);
+ },
+
+ removeLink() {
+ this.tiptapEditor.chain().focus().extendMarkRange(Link.name).unsetLink().run();
+ },
+ },
+};
+</script>
+<template>
+ <bubble-menu
+ data-testid="link-bubble-menu"
+ class="gl-shadow gl-rounded-base gl-bg-white"
+ :editor="tiptapEditor"
+ plugin-key="bubbleMenuLink"
+ :should-show="() => shouldShow()"
+ :tippy-options="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ placement: 'bottom',
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ >
+ <editor-state-observer @transaction="updateLinkToState">
+ <gl-button-group v-if="!isEditing" class="gl-display-flex gl-align-items-center">
+ <gl-link
+ v-gl-tooltip
+ :href="linkHref"
+ :aria-label="linkCanonicalSrc"
+ :title="linkCanonicalSrc"
+ target="_blank"
+ class="gl-px-3 gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis"
+ >
+ {{ linkCanonicalSrc }}
+ </gl-link>
+ <gl-button
+ v-gl-tooltip
+ variant="default"
+ category="tertiary"
+ size="medium"
+ data-testid="copy-link-url"
+ :aria-label="__('Copy link URL')"
+ :title="__('Copy link URL')"
+ icon="copy-to-clipboard"
+ @click="copyLinkHref"
+ />
+ <gl-button
+ v-gl-tooltip
+ variant="default"
+ category="tertiary"
+ size="medium"
+ data-testid="edit-link"
+ :aria-label="__('Edit link')"
+ :title="__('Edit link')"
+ icon="pencil"
+ @click="startEditingLink"
+ />
+ <gl-button
+ v-gl-tooltip
+ variant="default"
+ category="tertiary"
+ size="medium"
+ data-testid="remove-link"
+ :aria-label="__('Remove link')"
+ :title="__('Remove link')"
+ icon="unlink"
+ @click="removeLink"
+ />
+ </gl-button-group>
+ <gl-form v-else class="bubble-menu-form gl-p-4 gl-w-100" @submit.prevent="saveEditedLink">
+ <gl-form-group :label="__('URL')" label-for="link-href">
+ <gl-form-input id="link-href" v-model="linkCanonicalSrc" data-testid="link-href" />
+ </gl-form-group>
+ <gl-form-group :label="__('Title')" label-for="link-title">
+ <gl-form-input id="link-title" v-model="linkTitle" data-testid="link-title" />
+ </gl-form-group>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button class="gl-mr-3" data-testid="cancel-link" @click="cancelEditingLink">
+ {{ __('Cancel') }}
+ </gl-button>
+ <gl-button variant="confirm" type="submit">
+ {{ __('Apply') }}
+ </gl-button>
+ </div>
+ </gl-form>
+ </editor-state-observer>
+ </bubble-menu>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/media.vue b/app/assets/javascripts/content_editor/components/bubble_menus/media.vue
new file mode 100644
index 00000000000..a36a860c440
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/media.vue
@@ -0,0 +1,288 @@
+<script>
+import {
+ GlLink,
+ GlForm,
+ GlFormGroup,
+ GlFormInput,
+ GlLoadingIcon,
+ GlButton,
+ GlButtonGroup,
+ GlTooltipDirective as GlTooltip,
+} from '@gitlab/ui';
+import { BubbleMenu } from '@tiptap/vue-2';
+import { __ } from '~/locale';
+import Audio from '../../extensions/audio';
+import Image from '../../extensions/image';
+import Video from '../../extensions/video';
+import EditorStateObserver from '../editor_state_observer.vue';
+import { acceptedMimes } from '../../services/upload_helpers';
+
+const MEDIA_TYPES = [Audio.name, Image.name, Video.name];
+
+export default {
+ i18n: {
+ copySourceLabels: {
+ [Audio.name]: __('Copy audio URL'),
+ [Image.name]: __('Copy image URL'),
+ [Video.name]: __('Copy video URL'),
+ },
+ editLabels: {
+ [Audio.name]: __('Edit audio description'),
+ [Image.name]: __('Edit image description'),
+ [Video.name]: __('Edit video description'),
+ },
+ replaceLabels: {
+ [Audio.name]: __('Replace audio'),
+ [Image.name]: __('Replace image'),
+ [Video.name]: __('Replace video'),
+ },
+ deleteLabels: {
+ [Audio.name]: __('Delete audio'),
+ [Image.name]: __('Delete image'),
+ [Video.name]: __('Delete video'),
+ },
+ },
+ components: {
+ BubbleMenu,
+ GlForm,
+ GlFormGroup,
+ GlFormInput,
+ GlLink,
+ GlLoadingIcon,
+ GlButton,
+ GlButtonGroup,
+ EditorStateObserver,
+ },
+ directives: {
+ GlTooltip,
+ },
+ inject: ['tiptapEditor', 'contentEditor'],
+ data() {
+ return {
+ mediaType: undefined,
+ mediaSrc: undefined,
+ mediaCanonicalSrc: undefined,
+ mediaAlt: undefined,
+ mediaTitle: undefined,
+
+ isEditing: false,
+ isUpdating: false,
+ isUploading: false,
+ };
+ },
+ computed: {
+ copySourceLabel() {
+ return this.$options.i18n.copySourceLabels[this.mediaType];
+ },
+ editLabel() {
+ return this.$options.i18n.editLabels[this.mediaType];
+ },
+ replaceLabel() {
+ return this.$options.i18n.replaceLabels[this.mediaType];
+ },
+ deleteLabel() {
+ return this.$options.i18n.deleteLabels[this.mediaType];
+ },
+ showProgressIndicator() {
+ return this.isUploading || this.isUpdating;
+ },
+ },
+ methods: {
+ shouldShow() {
+ const shouldShow = MEDIA_TYPES.some((type) => this.tiptapEditor.isActive(type));
+
+ if (!shouldShow) this.isEditing = false;
+
+ return shouldShow;
+ },
+
+ startEditingMedia() {
+ this.isEditing = true;
+ },
+
+ endEditingMedia() {
+ this.isEditing = false;
+
+ this.updateMediaInfoToState();
+ },
+
+ cancelEditingMedia() {
+ this.endEditingMedia();
+ this.updateMediaInfoToState();
+ },
+
+ async saveEditedMedia() {
+ this.isUpdating = true;
+
+ this.mediaSrc = await this.contentEditor.resolveUrl(this.mediaCanonicalSrc);
+
+ const position = this.tiptapEditor.state.selection.from;
+
+ this.tiptapEditor
+ .chain()
+ .focus()
+ .updateAttributes(this.mediaType, {
+ src: this.mediaSrc,
+ alt: this.mediaAlt,
+ canonicalSrc: this.mediaCanonicalSrc,
+ title: this.mediaTitle,
+ })
+ .run();
+
+ this.tiptapEditor.commands.setNodeSelection(position);
+
+ this.endEditingMedia();
+
+ this.isUpdating = false;
+ },
+
+ async updateMediaInfoToState() {
+ this.mediaType = MEDIA_TYPES.find((type) => this.tiptapEditor.isActive(type));
+
+ if (!this.mediaType) return;
+
+ this.isUpdating = true;
+
+ const { src, title, alt, canonicalSrc, uploading } = this.tiptapEditor.getAttributes(
+ this.mediaType,
+ );
+
+ this.mediaTitle = title;
+ this.mediaAlt = alt;
+ this.mediaCanonicalSrc = canonicalSrc || src;
+ this.isUploading = uploading;
+ this.mediaSrc = await this.contentEditor.resolveUrl(this.mediaCanonicalSrc);
+
+ this.isUpdating = false;
+ },
+
+ replaceMedia() {
+ this.$refs.fileSelector.click();
+ },
+
+ onFileSelect(e) {
+ this.tiptapEditor
+ .chain()
+ .focus()
+ .deleteSelection()
+ .uploadAttachment({
+ file: e.target.files[0],
+ })
+ .run();
+
+ this.$refs.fileSelector.value = '';
+ },
+
+ copyMediaSrc() {
+ navigator.clipboard.writeText(this.mediaCanonicalSrc);
+ },
+
+ deleteMedia() {
+ this.tiptapEditor.chain().focus().deleteSelection().run();
+ },
+ },
+
+ acceptedMimes,
+};
+</script>
+<template>
+ <bubble-menu
+ data-testid="media-bubble-menu"
+ class="gl-shadow gl-rounded-base gl-bg-white"
+ :editor="tiptapEditor"
+ plugin-key="bubbleMenuMedia"
+ :should-show="() => shouldShow()"
+ >
+ <editor-state-observer @transaction="updateMediaInfoToState">
+ <gl-button-group v-if="!isEditing" class="gl-display-flex gl-align-items-center">
+ <gl-loading-icon v-if="showProgressIndicator" class="gl-pl-4 gl-pr-3" />
+ <input
+ ref="fileSelector"
+ type="file"
+ name="content_editor_image"
+ :accept="$options.acceptedMimes[mediaType]"
+ class="gl-display-none"
+ data-qa-selector="file_upload_field"
+ @change="onFileSelect"
+ />
+
+ <gl-link
+ v-if="!showProgressIndicator"
+ v-gl-tooltip
+ :href="mediaSrc"
+ :aria-label="mediaCanonicalSrc"
+ :title="mediaCanonicalSrc"
+ target="_blank"
+ class="gl-px-3 gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis"
+ >
+ {{ mediaCanonicalSrc }}
+ </gl-link>
+ <gl-button
+ v-gl-tooltip
+ variant="default"
+ category="tertiary"
+ size="medium"
+ data-testid="copy-media-src"
+ :aria-label="copySourceLabel"
+ :title="copySourceLabel"
+ icon="copy-to-clipboard"
+ @click="copyMediaSrc"
+ />
+ <gl-button
+ v-if="!showProgressIndicator"
+ v-gl-tooltip
+ variant="default"
+ category="tertiary"
+ size="medium"
+ data-testid="edit-media"
+ :aria-label="editLabel"
+ :title="editLabel"
+ icon="pencil"
+ @click="startEditingMedia"
+ />
+ <gl-button
+ v-gl-tooltip
+ variant="default"
+ category="tertiary"
+ size="medium"
+ data-testid="replace-media"
+ :aria-label="replaceLabel"
+ :title="replaceLabel"
+ icon="upload"
+ @click="replaceMedia"
+ />
+ <gl-button
+ v-gl-tooltip
+ variant="default"
+ category="tertiary"
+ size="medium"
+ data-testid="delete-media"
+ :aria-label="deleteLabel"
+ :title="deleteLabel"
+ icon="remove"
+ @click="deleteMedia"
+ />
+ </gl-button-group>
+ <gl-form v-else class="bubble-menu-form gl-p-4 gl-w-100" @submit.prevent="saveEditedMedia">
+ <gl-form-group :label="__('URL')" label-for="media-src">
+ <gl-form-input id="media-src" v-model="mediaCanonicalSrc" data-testid="media-src" />
+ </gl-form-group>
+ <gl-form-group :label="__('Description (alt text)')" label-for="media-alt">
+ <gl-form-input id="media-alt" v-model="mediaAlt" data-testid="media-alt" />
+ </gl-form-group>
+ <gl-form-group :label="__('Title')" label-for="media-title">
+ <gl-form-input id="media-title" v-model="mediaTitle" data-testid="media-title" />
+ </gl-form-group>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button
+ class="gl-mr-3"
+ data-testid="cancel-editing-media"
+ @click="cancelEditingMedia"
+ >{{ __('Cancel') }}</gl-button
+ >
+ <gl-button variant="confirm" type="submit">{{ __('Apply') }}</gl-button>
+ </div>
+ </gl-form>
+ </editor-state-observer>
+ </bubble-menu>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 5b3f4f4ddf2..74ae37b6d06 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -4,8 +4,10 @@ import { createContentEditor } from '../services/create_content_editor';
import ContentEditorAlert from './content_editor_alert.vue';
import ContentEditorProvider from './content_editor_provider.vue';
import EditorStateObserver from './editor_state_observer.vue';
-import FormattingBubbleMenu from './formatting_bubble_menu.vue';
-import CodeBlockBubbleMenu from './code_block_bubble_menu.vue';
+import FormattingBubbleMenu from './bubble_menus/formatting.vue';
+import CodeBlockBubbleMenu from './bubble_menus/code_block.vue';
+import LinkBubbleMenu from './bubble_menus/link.vue';
+import MediaBubbleMenu from './bubble_menus/media.vue';
import TopToolbar from './top_toolbar.vue';
import LoadingIndicator from './loading_indicator.vue';
@@ -18,6 +20,8 @@ export default {
TopToolbar,
FormattingBubbleMenu,
CodeBlockBubbleMenu,
+ LinkBubbleMenu,
+ MediaBubbleMenu,
EditorStateObserver,
},
props: {
@@ -92,6 +96,8 @@ export default {
<div class="gl-relative">
<formatting-bubble-menu />
<code-block-bubble-menu />
+ <link-bubble-menu />
+ <media-bubble-menu />
<tiptap-editor-content class="md" :editor="contentEditor.tiptapEditor" />
<loading-indicator />
</div>
diff --git a/app/assets/javascripts/content_editor/components/divider.vue b/app/assets/javascripts/content_editor/components/divider.vue
deleted file mode 100644
index b77bd7b7cf3..00000000000
--- a/app/assets/javascripts/content_editor/components/divider.vue
+++ /dev/null
@@ -1,3 +0,0 @@
-<template>
- <span class="gl-mx-3 gl-border-r-solid gl-border-r-1 gl-border-gray-200"></span>
-</template>
diff --git a/app/assets/javascripts/content_editor/components/loading_indicator.vue b/app/assets/javascripts/content_editor/components/loading_indicator.vue
index 620324adb06..7bc953e0dc3 100644
--- a/app/assets/javascripts/content_editor/components/loading_indicator.vue
+++ b/app/assets/javascripts/content_editor/components/loading_indicator.vue
@@ -34,7 +34,7 @@ export default {
class="gl-w-full gl-display-flex gl-justify-content-center gl-align-items-center gl-absolute gl-top-0 gl-bottom-0"
>
<div class="gl-bg-white gl-absolute gl-w-full gl-h-full gl-opacity-3"></div>
- <gl-loading-icon size="md" />
+ <gl-loading-icon size="lg" />
</div>
</editor-state-observer>
</template>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_button.vue b/app/assets/javascripts/content_editor/components/toolbar_button.vue
index cdb877152d4..c16dc34e36f 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_button.vue
@@ -29,6 +29,11 @@ export default {
required: false,
default: '',
},
+ editorCommandParams: {
+ type: Object,
+ required: false,
+ default: null,
+ },
variant: {
type: String,
required: false,
@@ -42,7 +47,7 @@ export default {
size: {
type: String,
required: false,
- default: 'small',
+ default: 'medium',
},
},
data() {
@@ -58,7 +63,7 @@ export default {
const { contentType } = this;
if (this.editorCommand) {
- this.tiptapEditor.chain()[this.editorCommand]().focus().run();
+ this.tiptapEditor.chain()[this.editorCommand](this.editorCommandParams).focus().run();
}
this.$emit('execute', { contentType });
diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/top_toolbar.vue
index 89182b3a09f..19e150a4da9 100644
--- a/app/assets/javascripts/content_editor/components/top_toolbar.vue
+++ b/app/assets/javascripts/content_editor/components/top_toolbar.vue
@@ -1,6 +1,5 @@
<script>
import trackUIControl from '../services/track_ui_control';
-import Divider from './divider.vue';
import ToolbarButton from './toolbar_button.vue';
import ToolbarImageButton from './toolbar_image_button.vue';
import ToolbarLinkButton from './toolbar_link_button.vue';
@@ -14,7 +13,6 @@ export default {
ToolbarLinkButton,
ToolbarTableButton,
ToolbarImageButton,
- Divider,
},
methods: {
trackToolbarControlExecution({ contentType, value }) {
@@ -25,13 +23,13 @@ export default {
</script>
<template>
<div
- class="gl-display-flex gl-justify-content-end gl-pb-3 gl-pt-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-200"
+ class="gl-display-flex gl-flex-wrap gl-pb-3 gl-pt-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-200"
>
<toolbar-text-style-dropdown
data-testid="text-styles"
+ class="gl-mr-3"
@execute="trackToolbarControlExecution"
/>
- <divider />
<toolbar-button
data-testid="bold"
content-type="bold"
@@ -69,7 +67,6 @@ export default {
@execute="trackToolbarControlExecution"
/>
<toolbar-link-button data-testid="link" @execute="trackToolbarControlExecution" />
- <divider />
<toolbar-image-button
ref="imageButton"
data-testid="image"
diff --git a/app/assets/javascripts/content_editor/components/wrappers/frontmatter.vue b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
index e8829d00986..1390b9b2daf 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/frontmatter.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
@@ -1,9 +1,10 @@
<script>
import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
import { __ } from '~/locale';
+import codeBlockLanguageLoader from '../../services/code_block_language_loader';
export default {
- name: 'FrontMatter',
+ name: 'CodeBlock',
components: {
NodeViewWrapper,
NodeViewContent,
@@ -13,6 +14,16 @@ export default {
type: Object,
required: true,
},
+ updateAttributes: {
+ type: Function,
+ required: true,
+ },
+ },
+ async mounted() {
+ const lang = codeBlockLanguageLoader.findLanguageBySyntax(this.node.attrs.language);
+ await codeBlockLanguageLoader.loadLanguage(lang.syntax);
+
+ this.updateAttributes({ language: this.node.attrs.language });
},
i18n: {
frontmatter: __('frontmatter'),
@@ -22,6 +33,7 @@ export default {
<template>
<node-view-wrapper class="content-editor-code-block gl-relative code highlight" as="pre">
<span
+ v-if="node.attrs.isFrontmatter"
data-testid="frontmatter-label"
class="gl-absolute gl-top-0 gl-right-3"
contenteditable="false"
diff --git a/app/assets/javascripts/content_editor/components/wrappers/media.vue b/app/assets/javascripts/content_editor/components/wrappers/media.vue
deleted file mode 100644
index 37119bdd066..00000000000
--- a/app/assets/javascripts/content_editor/components/wrappers/media.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { NodeViewWrapper } from '@tiptap/vue-2';
-
-const tagNameMap = {
- image: 'img',
- video: 'video',
- audio: 'audio',
-};
-
-export default {
- name: 'MediaWrapper',
- components: {
- NodeViewWrapper,
- GlLoadingIcon,
- },
- props: {
- node: {
- type: Object,
- required: true,
- },
- },
- computed: {
- tagName() {
- return tagNameMap[this.node.type.name] || 'img';
- },
- },
-};
-</script>
-<template>
- <node-view-wrapper class="gl-display-inline-block">
- <span class="gl-relative" :class="{ [`media-container ${tagName}-container`]: true }">
- <gl-loading-icon v-if="node.attrs.uploading" class="gl-absolute gl-left-50p gl-top-half" />
- <component
- :is="tagName"
- data-testid="media"
- :class="{
- 'gl-max-w-full gl-h-auto': tagName !== 'audio',
- 'gl-opacity-5': node.attrs.uploading,
- }"
- :title="node.attrs.title || node.attrs.alt"
- :alt="node.attrs.alt"
- :src="node.attrs.src"
- controls="true"
- />
- <a v-if="tagName !== 'img'" :href="node.attrs.canonicalSrc || node.attrs.src" @click.prevent>
- {{ node.attrs.title || node.attrs.alt }}
- </a>
- </span>
- </node-view-wrapper>
-</template>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
index 41c083111c5..209e4629830 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
@@ -124,7 +124,9 @@ export default {
no-caret
text-sr-only
:text="$options.i18n.editTableActions"
- :popper-opts="{ positionFixed: true }"
+ :popper-opts="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ positionFixed: true,
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@hide="handleHide($event)"
>
<gl-dropdown-item @click="runCommand('addColumnBefore')">
diff --git a/app/assets/javascripts/content_editor/constants/code_block_languages.js b/app/assets/javascripts/content_editor/constants/code_block_languages.js
new file mode 100644
index 00000000000..1a4dbe4fa22
--- /dev/null
+++ b/app/assets/javascripts/content_editor/constants/code_block_languages.js
@@ -0,0 +1,210 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+// List of languages referenced from https://github.com/wooorm/lowlight#data
+const CODE_BLOCK_LANGUAGES = [
+ { syntax: '1c', label: '1C:Enterprise' },
+ { syntax: 'abnf', label: 'Augmented Backus-Naur Form' },
+ { syntax: 'accesslog', label: 'Apache Access Log' },
+ { syntax: 'actionscript', variants: 'as', label: 'ActionScript' },
+ { syntax: 'ada', label: 'Ada' },
+ { syntax: 'angelscript', variants: 'asc', label: 'AngelScript' },
+ { syntax: 'apache', variants: 'apacheconf', label: 'Apache config' },
+ { syntax: 'applescript', variants: 'osascript', label: 'AppleScript' },
+ { syntax: 'arcade', label: 'ArcGIS Arcade' },
+ { syntax: 'arduino', variants: 'ino', label: 'Arduino' },
+ { syntax: 'armasm', variants: 'arm', label: 'ARM Assembly' },
+ { syntax: 'asciidoc', variants: 'adoc', label: 'AsciiDoc' },
+ { syntax: 'aspectj', label: 'AspectJ' },
+ { syntax: 'autohotkey', variants: 'ahk', label: 'AutoHotkey' },
+ { syntax: 'autoit', label: 'AutoIt' },
+ { syntax: 'avrasm', label: 'AVR Assembly' },
+ { syntax: 'awk', label: 'Awk' },
+ { syntax: 'axapta', variants: 'x++', label: 'X++' },
+ { syntax: 'bash', variants: 'sh', label: 'Bash' },
+ { syntax: 'basic', label: 'BASIC' },
+ { syntax: 'bnf', label: 'Backus-Naur Form' },
+ { syntax: 'brainfuck', variants: 'bf', label: 'Brainfuck' },
+ { syntax: 'c', variants: 'h', label: 'C' },
+ { syntax: 'cal', label: 'C/AL' },
+ { syntax: 'capnproto', variants: 'capnp', label: "Cap'n Proto" },
+ { syntax: 'ceylon', label: 'Ceylon' },
+ { syntax: 'clean', variants: 'icl, dcl', label: 'Clean' },
+ { syntax: 'clojure', variants: 'clj, edn', label: 'Clojure' },
+ { syntax: 'clojure-repl', label: 'Clojure REPL' },
+ { syntax: 'cmake', variants: 'cmake.in', label: 'CMake' },
+ { syntax: 'coffeescript', variants: 'coffee, cson, iced', label: 'CoffeeScript' },
+ { syntax: 'coq', label: 'Coq' },
+ { syntax: 'cos', variants: 'cls', label: 'Caché Object Script' },
+ { syntax: 'cpp', variants: 'cc, c++, h++, hpp, hh, hxx, cxx', label: 'C++' },
+ { syntax: 'crmsh', variants: 'crm, pcmk', label: 'crmsh' },
+ { syntax: 'crystal', variants: 'cr', label: 'Crystal' },
+ { syntax: 'csharp', variants: 'cs, c#', label: 'C#' },
+ { syntax: 'csp', label: 'CSP' },
+ { syntax: 'css', label: 'CSS' },
+ { syntax: 'd', label: 'D' },
+ { syntax: 'dart', label: 'Dart' },
+ { syntax: 'delphi', variants: 'dpr, dfm, pas, pascal', label: 'Delphi' },
+ { syntax: 'diff', variants: 'patch', label: 'Diff' },
+ { syntax: 'django', variants: 'jinja', label: 'Django' },
+ { syntax: 'dns', variants: 'bind, zone', label: 'DNS Zone' },
+ { syntax: 'dockerfile', variants: 'docker', label: 'Dockerfile' },
+ { syntax: 'dos', variants: 'bat, cmd', label: 'Batch file (DOS)' },
+ { syntax: 'dsconfig', label: 'DSConfig' },
+ { syntax: 'dts', label: 'Device Tree' },
+ { syntax: 'dust', variants: 'dst', label: 'Dust' },
+ { syntax: 'ebnf', label: 'Extended Backus-Naur Form' },
+ { syntax: 'elixir', variants: 'ex, exs', label: 'Elixir' },
+ { syntax: 'elm', label: 'Elm' },
+ { syntax: 'erb', label: 'ERB' },
+ { syntax: 'erlang', variants: 'erl', label: 'Erlang' },
+ { syntax: 'erlang-repl', label: 'Erlang REPL' },
+ { syntax: 'excel', variants: 'xlsx, xls', label: 'Excel formulae' },
+ { syntax: 'fix', label: 'FIX' },
+ { syntax: 'flix', label: 'Flix' },
+ { syntax: 'fortran', variants: 'f90, f95', label: 'Fortran' },
+ { syntax: 'fsharp', variants: 'fs, f#', label: 'F#' },
+ { syntax: 'gams', variants: 'gms', label: 'GAMS' },
+ { syntax: 'gauss', variants: 'gss', label: 'GAUSS' },
+ { syntax: 'gcode', variants: 'nc', label: 'G-code (ISO 6983)' },
+ { syntax: 'gherkin', variants: 'feature', label: 'Gherkin' },
+ { syntax: 'glsl', label: 'GLSL' },
+ { syntax: 'gml', label: 'GML' },
+ { syntax: 'go', variants: 'golang', label: 'Go' },
+ { syntax: 'golo', label: 'Golo' },
+ { syntax: 'gradle', label: 'Gradle' },
+ { syntax: 'graphql', variants: 'gql', label: 'GraphQL' },
+ { syntax: 'groovy', label: 'Groovy' },
+ { syntax: 'haml', label: 'HAML' },
+ {
+ syntax: 'handlebars',
+ variants: 'hbs, html.hbs, html.handlebars, htmlbars',
+ label: 'Handlebars',
+ },
+ { syntax: 'haskell', variants: 'hs', label: 'Haskell' },
+ { syntax: 'haxe', variants: 'hx', label: 'Haxe' },
+ { syntax: 'hsp', label: 'HSP' },
+ { syntax: 'http', variants: 'https', label: 'HTTP' },
+ { syntax: 'hy', variants: 'hylang', label: 'Hy' },
+ { syntax: 'inform7', variants: 'i7', label: 'Inform 7' },
+ { syntax: 'ini', variants: 'toml', label: 'TOML, also INI' },
+ { syntax: 'irpf90', label: 'IRPF90' },
+ { syntax: 'isbl', label: 'ISBL' },
+ { syntax: 'java', variants: 'jsp', label: 'Java' },
+ { syntax: 'javascript', variants: 'js, jsx, mjs, cjs', label: 'Javascript' },
+ { syntax: 'jboss-cli', variants: 'wildfly-cli', label: 'JBoss CLI' },
+ { syntax: 'json', label: 'JSON' },
+ { syntax: 'julia', label: 'Julia' },
+ { syntax: 'julia-repl', variants: 'jldoctest', label: 'Julia REPL' },
+ { syntax: 'kotlin', variants: 'kt, kts', label: 'Kotlin' },
+ { syntax: 'lasso', variants: 'ls, lassoscript', label: 'Lasso' },
+ { syntax: 'latex', variants: 'tex', label: 'LaTeX' },
+ { syntax: 'ldif', label: 'LDIF' },
+ { syntax: 'leaf', label: 'Leaf' },
+ { syntax: 'less', label: 'Less' },
+ { syntax: 'lisp', label: 'Lisp' },
+ { syntax: 'livecodeserver', label: 'LiveCode' },
+ { syntax: 'livescript', variants: 'ls', label: 'LiveScript' },
+ { syntax: 'llvm', label: 'LLVM IR' },
+ { syntax: 'lsl', label: 'LSL (Linden Scripting Language)' },
+ { syntax: 'lua', label: 'Lua' },
+ { syntax: 'makefile', variants: 'mk, mak, make', label: 'Makefile' },
+ { syntax: 'markdown', variants: 'md, mkdown, mkd', label: 'Markdown' },
+ { syntax: 'mathematica', variants: 'mma, wl', label: 'Mathematica' },
+ { syntax: 'matlab', label: 'Matlab' },
+ { syntax: 'maxima', label: 'Maxima' },
+ { syntax: 'mel', label: 'MEL' },
+ { syntax: 'mercury', variants: 'm, moo', label: 'Mercury' },
+ { syntax: 'mipsasm', variants: 'mips', label: 'MIPS Assembly' },
+ { syntax: 'mizar', label: 'Mizar' },
+ { syntax: 'mojolicious', label: 'Mojolicious' },
+ { syntax: 'monkey', label: 'Monkey' },
+ { syntax: 'moonscript', variants: 'moon', label: 'MoonScript' },
+ { syntax: 'n1ql', label: 'N1QL' },
+ { syntax: 'nestedtext', variants: 'nt', label: 'Nested Text' },
+ { syntax: 'nginx', variants: 'nginxconf', label: 'Nginx config' },
+ { syntax: 'nim', label: 'Nim' },
+ { syntax: 'nix', variants: 'nixos', label: 'Nix' },
+ { syntax: 'node-repl', label: 'Node REPL' },
+ { syntax: 'nsis', label: 'NSIS' },
+ {
+ syntax: 'objectivec',
+ variants: 'mm, objc, obj-c, obj-c++, objective-c++',
+ label: 'Objective-C',
+ },
+ { syntax: 'ocaml', variants: 'ml', label: 'OCaml' },
+ { syntax: 'openscad', variants: 'scad', label: 'OpenSCAD' },
+ { syntax: 'oxygene', label: 'Oxygene' },
+ { syntax: 'parser3', label: 'Parser3' },
+ { syntax: 'perl', variants: 'pl, pm', label: 'Perl' },
+ { syntax: 'pf', variants: 'pf.conf', label: 'Packet Filter config' },
+ { syntax: 'pgsql', variants: 'postgres, postgresql', label: 'PostgreSQL' },
+ { syntax: 'php', label: 'PHP' },
+ { syntax: 'php-template', label: 'PHP template' },
+ { syntax: 'plaintext', variants: 'text, txt', label: 'Plain text' },
+ { syntax: 'pony', label: 'Pony' },
+ { syntax: 'powershell', variants: 'pwsh, ps, ps1', label: 'PowerShell' },
+ { syntax: 'processing', variants: 'pde', label: 'Processing' },
+ { syntax: 'profile', label: 'Python profiler' },
+ { syntax: 'prolog', label: 'Prolog' },
+ { syntax: 'properties', label: '.properties' },
+ { syntax: 'protobuf', label: 'Protocol Buffers' },
+ { syntax: 'puppet', variants: 'pp', label: 'Puppet' },
+ { syntax: 'purebasic', variants: 'pb, pbi', label: 'PureBASIC' },
+ { syntax: 'python', variants: 'py, gyp, ipython', label: 'Python' },
+ { syntax: 'python-repl', variants: 'pycon', label: 'Python REPL' },
+ { syntax: 'q', variants: 'k, kdb', label: 'Q' },
+ { syntax: 'qml', variants: 'qt', label: 'QML' },
+ { syntax: 'r', label: 'R' },
+ { syntax: 'reasonml', variants: 're', label: 'ReasonML' },
+ { syntax: 'rib', label: 'RenderMan RIB' },
+ { syntax: 'roboconf', variants: 'graph, instances', label: 'Roboconf' },
+ { syntax: 'routeros', variants: 'mikrotik', label: 'Microtik RouterOS script' },
+ { syntax: 'rsl', label: 'RenderMan RSL' },
+ { syntax: 'ruby', variants: 'rb, gemspec, podspec, thor, irb', label: 'Ruby' },
+ { syntax: 'ruleslanguage', label: 'Oracle Rules Language' },
+ { syntax: 'rust', variants: 'rs', label: 'Rust' },
+ { syntax: 'sas', label: 'SAS' },
+ { syntax: 'scala', label: 'Scala' },
+ { syntax: 'scheme', label: 'Scheme' },
+ { syntax: 'scilab', variants: 'sci', label: 'Scilab' },
+ { syntax: 'scss', label: 'SCSS' },
+ { syntax: 'shell', variants: 'console, shellsession', label: 'Shell Session' },
+ { syntax: 'smali', label: 'Smali' },
+ { syntax: 'smalltalk', variants: 'st', label: 'Smalltalk' },
+ { syntax: 'sml', variants: 'ml', label: 'SML (Standard ML)' },
+ { syntax: 'sqf', label: 'SQF' },
+ { syntax: 'sql', label: 'SQL' },
+ { syntax: 'stan', variants: 'stanfuncs', label: 'Stan' },
+ { syntax: 'stata', variants: 'do, ado', label: 'Stata' },
+ { syntax: 'step21', variants: 'p21, step, stp', label: 'STEP Part 21' },
+ { syntax: 'stylus', variants: 'styl', label: 'Stylus' },
+ { syntax: 'subunit', label: 'SubUnit' },
+ { syntax: 'swift', label: 'Swift' },
+ { syntax: 'taggerscript', label: 'Tagger Script' },
+ { syntax: 'tap', label: 'Test Anything Protocol' },
+ { syntax: 'tcl', variants: 'tk', label: 'Tcl' },
+ { syntax: 'thrift', label: 'Thrift' },
+ { syntax: 'tp', label: 'TP' },
+ { syntax: 'twig', variants: 'craftcms', label: 'Twig' },
+ { syntax: 'typescript', variants: 'ts, tsx', label: 'TypeScript' },
+ { syntax: 'vala', label: 'Vala' },
+ { syntax: 'vbnet', variants: 'vb', label: 'Visual Basic .NET' },
+ { syntax: 'vbscript', variants: 'vbs', label: 'VBScript' },
+ { syntax: 'vbscript-html', label: 'VBScript in HTML' },
+ { syntax: 'verilog', variants: 'v, sv, svh', label: 'Verilog' },
+ { syntax: 'vhdl', label: 'VHDL' },
+ { syntax: 'vim', label: 'Vim Script' },
+ { syntax: 'wasm', label: 'WebAssembly' },
+ { syntax: 'wren', label: 'Wren' },
+ { syntax: 'x86asm', label: 'Intel x86 Assembly' },
+ { syntax: 'xl', variants: 'tao', label: 'XL' },
+ {
+ syntax: 'xml',
+ variants: 'html, xhtml, rss, atom, xjb, xsd, xsl, plist, wsf, svg',
+ label: 'HTML, XML',
+ },
+ { syntax: 'xquery', variants: 'xpath, xq', label: 'XQuery' },
+ { syntax: 'yaml', variants: 'yml', label: 'YAML' },
+ { syntax: 'zephir', variants: 'zep', label: 'Zephir' },
+];
+
+export default CODE_BLOCK_LANGUAGES;
diff --git a/app/assets/javascripts/content_editor/constants.js b/app/assets/javascripts/content_editor/constants/index.js
index a39a243ec6b..a39a243ec6b 100644
--- a/app/assets/javascripts/content_editor/constants.js
+++ b/app/assets/javascripts/content_editor/constants/index.js
diff --git a/app/assets/javascripts/content_editor/extensions/blockquote.js b/app/assets/javascripts/content_editor/extensions/blockquote.js
index 5632bc28592..9b424ac8367 100644
--- a/app/assets/javascripts/content_editor/extensions/blockquote.js
+++ b/app/assets/javascripts/content_editor/extensions/blockquote.js
@@ -26,7 +26,7 @@ export default Blockquote.extend({
const multilineInputRegex = /^\s*>>>\s$/gm;
return [
- ...this.parent?.(),
+ ...this.parent(),
wrappingInputRule({
find: multilineInputRegex,
type: this.type,
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 61f379fc0a2..cc4ba84a29d 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -1,6 +1,8 @@
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
import { textblockTypeInputRule } from '@tiptap/core';
-import codeBlockLanguageLoader from '../services/code_block_language_loader';
+import { VueNodeViewRenderer } from '@tiptap/vue-2';
+import languageLoader from '../services/code_block_language_loader';
+import CodeBlockWrapper from '../components/wrappers/code_block.vue';
const extractLanguage = (element) => element.getAttribute('lang');
export const backtickInputRegex = /^```([a-z]+)?[\s\n]$/;
@@ -9,14 +11,6 @@ export const tildeInputRegex = /^~~~([a-z]+)?[\s\n]$/;
export default CodeBlockLowlight.extend({
isolating: true,
exitOnArrowDown: false,
-
- addOptions() {
- return {
- ...this.parent?.(),
- languageLoader: codeBlockLanguageLoader,
- };
- },
-
addAttributes() {
return {
language: {
@@ -30,7 +24,6 @@ export default CodeBlockLowlight.extend({
};
},
addInputRules() {
- const { languageLoader } = this.options;
const getAttributes = (match) => languageLoader?.loadLanguageFromInputRule(match) || {};
return [
@@ -65,4 +58,8 @@ export default CodeBlockLowlight.extend({
['code', {}, 0],
];
},
+
+ addNodeView() {
+ return new VueNodeViewRenderer(CodeBlockWrapper);
+ },
});
diff --git a/app/assets/javascripts/content_editor/extensions/diagram.js b/app/assets/javascripts/content_editor/extensions/diagram.js
index d192b815092..f9dfeb92e9a 100644
--- a/app/assets/javascripts/content_editor/extensions/diagram.js
+++ b/app/assets/javascripts/content_editor/extensions/diagram.js
@@ -14,6 +14,9 @@ export default CodeBlockHighlight.extend({
return element.dataset.diagram;
},
},
+ isDiagram: {
+ default: true,
+ },
};
},
diff --git a/app/assets/javascripts/content_editor/extensions/frontmatter.js b/app/assets/javascripts/content_editor/extensions/frontmatter.js
index 9842027e192..2ec22158106 100644
--- a/app/assets/javascripts/content_editor/extensions/frontmatter.js
+++ b/app/assets/javascripts/content_editor/extensions/frontmatter.js
@@ -1,10 +1,18 @@
-import { VueNodeViewRenderer } from '@tiptap/vue-2';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
-import FrontmatterWrapper from '../components/wrappers/frontmatter.vue';
import CodeBlockHighlight from './code_block_highlight';
export default CodeBlockHighlight.extend({
name: 'frontmatter',
+
+ addAttributes() {
+ return {
+ ...this.parent?.(),
+ isFrontmatter: {
+ default: true,
+ },
+ };
+ },
+
parseHTML() {
return [
{
@@ -24,9 +32,6 @@ export default CodeBlockHighlight.extend({
},
};
},
- addNodeView() {
- return new VueNodeViewRenderer(FrontmatterWrapper);
- },
addInputRules() {
return [];
diff --git a/app/assets/javascripts/content_editor/extensions/image.js b/app/assets/javascripts/content_editor/extensions/image.js
index 311db8151cb..25f976f524f 100644
--- a/app/assets/javascripts/content_editor/extensions/image.js
+++ b/app/assets/javascripts/content_editor/extensions/image.js
@@ -1,6 +1,4 @@
import { Image } from '@tiptap/extension-image';
-import { VueNodeViewRenderer } from '@tiptap/vue-2';
-import MediaWrapper from '../components/wrappers/media.vue';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
const resolveImageEl = (element) =>
@@ -77,7 +75,4 @@ export default Image.extend({
},
];
},
- addNodeView() {
- return VueNodeViewRenderer(MediaWrapper);
- },
});
diff --git a/app/assets/javascripts/content_editor/extensions/paste_markdown.js b/app/assets/javascripts/content_editor/extensions/paste_markdown.js
index c349aa42a62..f87e4d8d1dd 100644
--- a/app/assets/javascripts/content_editor/extensions/paste_markdown.js
+++ b/app/assets/javascripts/content_editor/extensions/paste_markdown.js
@@ -2,7 +2,7 @@ import { Extension } from '@tiptap/core';
import { Plugin, PluginKey } from 'prosemirror-state';
import { __ } from '~/locale';
import { VARIANT_DANGER } from '~/flash';
-import createMarkdownDeserializer from '../services/markdown_deserializer';
+import createMarkdownDeserializer from '../services/gl_api_markdown_deserializer';
import {
ALERT_EVENT,
LOADING_CONTENT_EVENT,
@@ -10,10 +10,14 @@ import {
LOADING_ERROR_EVENT,
EXTENSION_PRIORITY_HIGHEST,
} from '../constants';
+import CodeBlockHighlight from './code_block_highlight';
+import Diagram from './diagram';
+import Frontmatter from './frontmatter';
const TEXT_FORMAT = 'text/plain';
const HTML_FORMAT = 'text/html';
const VS_CODE_FORMAT = 'vscode-editor-data';
+const CODE_BLOCK_NODE_TYPES = [CodeBlockHighlight.name, Diagram.name, Frontmatter.name];
export default Extension.create({
name: 'pasteMarkdown',
@@ -75,6 +79,11 @@ export default Extension.create({
return false;
}
+ // if a code block is active, paste as plain text
+ if (CODE_BLOCK_NODE_TYPES.some((type) => this.editor.isActive(type))) {
+ return false;
+ }
+
this.editor.commands.pasteMarkdown(content);
return true;
diff --git a/app/assets/javascripts/content_editor/extensions/playable.js b/app/assets/javascripts/content_editor/extensions/playable.js
index 2c5269377c5..ed343d8acf8 100644
--- a/app/assets/javascripts/content_editor/extensions/playable.js
+++ b/app/assets/javascripts/content_editor/extensions/playable.js
@@ -1,8 +1,6 @@
/* eslint-disable @gitlab/require-i18n-strings */
import { Node } from '@tiptap/core';
-import { VueNodeViewRenderer } from '@tiptap/vue-2';
-import MediaWrapper from '../components/wrappers/media.vue';
const queryPlayableElement = (element, mediaType) => element.querySelector(mediaType);
@@ -68,8 +66,4 @@ export default Node.create({
['a', { href: node.attrs.src }, node.attrs.title || node.attrs.alt || ''],
];
},
-
- addNodeView() {
- return VueNodeViewRenderer(MediaWrapper);
- },
});
diff --git a/app/assets/javascripts/content_editor/extensions/sourcemap.js b/app/assets/javascripts/content_editor/extensions/sourcemap.js
new file mode 100644
index 00000000000..94236e2e70e
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/sourcemap.js
@@ -0,0 +1,48 @@
+import { Extension } from '@tiptap/core';
+import Blockquote from './blockquote';
+import Bold from './bold';
+import BulletList from './bullet_list';
+import Code from './code';
+import CodeBlockHighlight from './code_block_highlight';
+import Heading from './heading';
+import HardBreak from './hard_break';
+import HorizontalRule from './horizontal_rule';
+import Image from './image';
+import Italic from './italic';
+import Link from './link';
+import ListItem from './list_item';
+import OrderedList from './ordered_list';
+import Paragraph from './paragraph';
+
+export default Extension.create({
+ addGlobalAttributes() {
+ return [
+ {
+ types: [
+ Bold.name,
+ Blockquote.name,
+ BulletList.name,
+ Code.name,
+ CodeBlockHighlight.name,
+ HardBreak.name,
+ Heading.name,
+ HorizontalRule.name,
+ Image.name,
+ Italic.name,
+ Link.name,
+ ListItem.name,
+ OrderedList.name,
+ Paragraph.name,
+ ],
+ attributes: {
+ sourceMarkdown: {
+ default: null,
+ },
+ sourceMapKey: {
+ default: null,
+ },
+ },
+ },
+ ];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/services/asset_resolver.js b/app/assets/javascripts/content_editor/services/asset_resolver.js
new file mode 100644
index 00000000000..942457b9664
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/asset_resolver.js
@@ -0,0 +1,13 @@
+import { memoize } from 'lodash';
+
+export default ({ renderMarkdown }) => ({
+ resolveUrl: memoize(async (canonicalSrc) => {
+ const html = await renderMarkdown(`[link](${canonicalSrc})`);
+ if (!html) return canonicalSrc;
+
+ const parser = new DOMParser();
+ const { body } = parser.parseFromString(html, 'text/html');
+
+ return body.querySelector('a').getAttribute('href');
+ }),
+});
diff --git a/app/assets/javascripts/content_editor/services/code_block_language_loader.js b/app/assets/javascripts/content_editor/services/code_block_language_loader.js
index 081400cfd9a..1afaf4bfef6 100644
--- a/app/assets/javascripts/content_editor/services/code_block_language_loader.js
+++ b/app/assets/javascripts/content_editor/services/code_block_language_loader.js
@@ -1,215 +1,7 @@
import { lowlight } from 'lowlight/lib/core';
import { __, sprintf } from '~/locale';
-
-/* eslint-disable @gitlab/require-i18n-strings */
-// List of languages referenced from https://github.com/wooorm/lowlight#data
-const CODE_BLOCK_LANGUAGES = [
- { syntax: '1c', label: '1C:Enterprise' },
- { syntax: 'abnf', label: 'Augmented Backus-Naur Form' },
- { syntax: 'accesslog', label: 'Apache Access Log' },
- { syntax: 'actionscript', variants: 'as', label: 'ActionScript' },
- { syntax: 'ada', label: 'Ada' },
- { syntax: 'angelscript', variants: 'asc', label: 'AngelScript' },
- { syntax: 'apache', variants: 'apacheconf', label: 'Apache config' },
- { syntax: 'applescript', variants: 'osascript', label: 'AppleScript' },
- { syntax: 'arcade', label: 'ArcGIS Arcade' },
- { syntax: 'arduino', variants: 'ino', label: 'Arduino' },
- { syntax: 'armasm', variants: 'arm', label: 'ARM Assembly' },
- { syntax: 'asciidoc', variants: 'adoc', label: 'AsciiDoc' },
- { syntax: 'aspectj', label: 'AspectJ' },
- { syntax: 'autohotkey', variants: 'ahk', label: 'AutoHotkey' },
- { syntax: 'autoit', label: 'AutoIt' },
- { syntax: 'avrasm', label: 'AVR Assembly' },
- { syntax: 'awk', label: 'Awk' },
- { syntax: 'axapta', variants: 'x++', label: 'X++' },
- { syntax: 'bash', variants: 'sh', label: 'Bash' },
- { syntax: 'basic', label: 'BASIC' },
- { syntax: 'bnf', label: 'Backus-Naur Form' },
- { syntax: 'brainfuck', variants: 'bf', label: 'Brainfuck' },
- { syntax: 'c', variants: 'h', label: 'C' },
- { syntax: 'cal', label: 'C/AL' },
- { syntax: 'capnproto', variants: 'capnp', label: "Cap'n Proto" },
- { syntax: 'ceylon', label: 'Ceylon' },
- { syntax: 'clean', variants: 'icl, dcl', label: 'Clean' },
- { syntax: 'clojure', variants: 'clj, edn', label: 'Clojure' },
- { syntax: 'clojure-repl', label: 'Clojure REPL' },
- { syntax: 'cmake', variants: 'cmake.in', label: 'CMake' },
- { syntax: 'coffeescript', variants: 'coffee, cson, iced', label: 'CoffeeScript' },
- { syntax: 'coq', label: 'Coq' },
- { syntax: 'cos', variants: 'cls', label: 'Caché Object Script' },
- { syntax: 'cpp', variants: 'cc, c++, h++, hpp, hh, hxx, cxx', label: 'C++' },
- { syntax: 'crmsh', variants: 'crm, pcmk', label: 'crmsh' },
- { syntax: 'crystal', variants: 'cr', label: 'Crystal' },
- { syntax: 'csharp', variants: 'cs, c#', label: 'C#' },
- { syntax: 'csp', label: 'CSP' },
- { syntax: 'css', label: 'CSS' },
- { syntax: 'd', label: 'D' },
- { syntax: 'dart', label: 'Dart' },
- { syntax: 'delphi', variants: 'dpr, dfm, pas, pascal', label: 'Delphi' },
- { syntax: 'diff', variants: 'patch', label: 'Diff' },
- { syntax: 'django', variants: 'jinja', label: 'Django' },
- { syntax: 'dns', variants: 'bind, zone', label: 'DNS Zone' },
- { syntax: 'dockerfile', variants: 'docker', label: 'Dockerfile' },
- { syntax: 'dos', variants: 'bat, cmd', label: 'Batch file (DOS)' },
- { syntax: 'dsconfig', label: 'DSConfig' },
- { syntax: 'dts', label: 'Device Tree' },
- { syntax: 'dust', variants: 'dst', label: 'Dust' },
- { syntax: 'ebnf', label: 'Extended Backus-Naur Form' },
- { syntax: 'elixir', variants: 'ex, exs', label: 'Elixir' },
- { syntax: 'elm', label: 'Elm' },
- { syntax: 'erb', label: 'ERB' },
- { syntax: 'erlang', variants: 'erl', label: 'Erlang' },
- { syntax: 'erlang-repl', label: 'Erlang REPL' },
- { syntax: 'excel', variants: 'xlsx, xls', label: 'Excel formulae' },
- { syntax: 'fix', label: 'FIX' },
- { syntax: 'flix', label: 'Flix' },
- { syntax: 'fortran', variants: 'f90, f95', label: 'Fortran' },
- { syntax: 'fsharp', variants: 'fs, f#', label: 'F#' },
- { syntax: 'gams', variants: 'gms', label: 'GAMS' },
- { syntax: 'gauss', variants: 'gss', label: 'GAUSS' },
- { syntax: 'gcode', variants: 'nc', label: 'G-code (ISO 6983)' },
- { syntax: 'gherkin', variants: 'feature', label: 'Gherkin' },
- { syntax: 'glsl', label: 'GLSL' },
- { syntax: 'gml', label: 'GML' },
- { syntax: 'go', variants: 'golang', label: 'Go' },
- { syntax: 'golo', label: 'Golo' },
- { syntax: 'gradle', label: 'Gradle' },
- { syntax: 'graphql', variants: 'gql', label: 'GraphQL' },
- { syntax: 'groovy', label: 'Groovy' },
- { syntax: 'haml', label: 'HAML' },
- {
- syntax: 'handlebars',
- variants: 'hbs, html.hbs, html.handlebars, htmlbars',
- label: 'Handlebars',
- },
- { syntax: 'haskell', variants: 'hs', label: 'Haskell' },
- { syntax: 'haxe', variants: 'hx', label: 'Haxe' },
- { syntax: 'hsp', label: 'HSP' },
- { syntax: 'http', variants: 'https', label: 'HTTP' },
- { syntax: 'hy', variants: 'hylang', label: 'Hy' },
- { syntax: 'inform7', variants: 'i7', label: 'Inform 7' },
- { syntax: 'ini', variants: 'toml', label: 'TOML, also INI' },
- { syntax: 'irpf90', label: 'IRPF90' },
- { syntax: 'isbl', label: 'ISBL' },
- { syntax: 'java', variants: 'jsp', label: 'Java' },
- { syntax: 'javascript', variants: 'js, jsx, mjs, cjs', label: 'Javascript' },
- { syntax: 'jboss-cli', variants: 'wildfly-cli', label: 'JBoss CLI' },
- { syntax: 'json', label: 'JSON' },
- { syntax: 'julia', label: 'Julia' },
- { syntax: 'julia-repl', variants: 'jldoctest', label: 'Julia REPL' },
- { syntax: 'kotlin', variants: 'kt, kts', label: 'Kotlin' },
- { syntax: 'lasso', variants: 'ls, lassoscript', label: 'Lasso' },
- { syntax: 'latex', variants: 'tex', label: 'LaTeX' },
- { syntax: 'ldif', label: 'LDIF' },
- { syntax: 'leaf', label: 'Leaf' },
- { syntax: 'less', label: 'Less' },
- { syntax: 'lisp', label: 'Lisp' },
- { syntax: 'livecodeserver', label: 'LiveCode' },
- { syntax: 'livescript', variants: 'ls', label: 'LiveScript' },
- { syntax: 'llvm', label: 'LLVM IR' },
- { syntax: 'lsl', label: 'LSL (Linden Scripting Language)' },
- { syntax: 'lua', label: 'Lua' },
- { syntax: 'makefile', variants: 'mk, mak, make', label: 'Makefile' },
- { syntax: 'markdown', variants: 'md, mkdown, mkd', label: 'Markdown' },
- { syntax: 'mathematica', variants: 'mma, wl', label: 'Mathematica' },
- { syntax: 'matlab', label: 'Matlab' },
- { syntax: 'maxima', label: 'Maxima' },
- { syntax: 'mel', label: 'MEL' },
- { syntax: 'mercury', variants: 'm, moo', label: 'Mercury' },
- { syntax: 'mipsasm', variants: 'mips', label: 'MIPS Assembly' },
- { syntax: 'mizar', label: 'Mizar' },
- { syntax: 'mojolicious', label: 'Mojolicious' },
- { syntax: 'monkey', label: 'Monkey' },
- { syntax: 'moonscript', variants: 'moon', label: 'MoonScript' },
- { syntax: 'n1ql', label: 'N1QL' },
- { syntax: 'nestedtext', variants: 'nt', label: 'Nested Text' },
- { syntax: 'nginx', variants: 'nginxconf', label: 'Nginx config' },
- { syntax: 'nim', label: 'Nim' },
- { syntax: 'nix', variants: 'nixos', label: 'Nix' },
- { syntax: 'node-repl', label: 'Node REPL' },
- { syntax: 'nsis', label: 'NSIS' },
- {
- syntax: 'objectivec',
- variants: 'mm, objc, obj-c, obj-c++, objective-c++',
- label: 'Objective-C',
- },
- { syntax: 'ocaml', variants: 'ml', label: 'OCaml' },
- { syntax: 'openscad', variants: 'scad', label: 'OpenSCAD' },
- { syntax: 'oxygene', label: 'Oxygene' },
- { syntax: 'parser3', label: 'Parser3' },
- { syntax: 'perl', variants: 'pl, pm', label: 'Perl' },
- { syntax: 'pf', variants: 'pf.conf', label: 'Packet Filter config' },
- { syntax: 'pgsql', variants: 'postgres, postgresql', label: 'PostgreSQL' },
- { syntax: 'php', label: 'PHP' },
- { syntax: 'php-template', label: 'PHP template' },
- { syntax: 'plaintext', variants: 'text, txt', label: 'Plain text' },
- { syntax: 'pony', label: 'Pony' },
- { syntax: 'powershell', variants: 'pwsh, ps, ps1', label: 'PowerShell' },
- { syntax: 'processing', variants: 'pde', label: 'Processing' },
- { syntax: 'profile', label: 'Python profiler' },
- { syntax: 'prolog', label: 'Prolog' },
- { syntax: 'properties', label: '.properties' },
- { syntax: 'protobuf', label: 'Protocol Buffers' },
- { syntax: 'puppet', variants: 'pp', label: 'Puppet' },
- { syntax: 'purebasic', variants: 'pb, pbi', label: 'PureBASIC' },
- { syntax: 'python', variants: 'py, gyp, ipython', label: 'Python' },
- { syntax: 'python-repl', variants: 'pycon', label: 'Python REPL' },
- { syntax: 'q', variants: 'k, kdb', label: 'Q' },
- { syntax: 'qml', variants: 'qt', label: 'QML' },
- { syntax: 'r', label: 'R' },
- { syntax: 'reasonml', variants: 're', label: 'ReasonML' },
- { syntax: 'rib', label: 'RenderMan RIB' },
- { syntax: 'roboconf', variants: 'graph, instances', label: 'Roboconf' },
- { syntax: 'routeros', variants: 'mikrotik', label: 'Microtik RouterOS script' },
- { syntax: 'rsl', label: 'RenderMan RSL' },
- { syntax: 'ruby', variants: 'rb, gemspec, podspec, thor, irb', label: 'Ruby' },
- { syntax: 'ruleslanguage', label: 'Oracle Rules Language' },
- { syntax: 'rust', variants: 'rs', label: 'Rust' },
- { syntax: 'sas', label: 'SAS' },
- { syntax: 'scala', label: 'Scala' },
- { syntax: 'scheme', label: 'Scheme' },
- { syntax: 'scilab', variants: 'sci', label: 'Scilab' },
- { syntax: 'scss', label: 'SCSS' },
- { syntax: 'shell', variants: 'console, shellsession', label: 'Shell Session' },
- { syntax: 'smali', label: 'Smali' },
- { syntax: 'smalltalk', variants: 'st', label: 'Smalltalk' },
- { syntax: 'sml', variants: 'ml', label: 'SML (Standard ML)' },
- { syntax: 'sqf', label: 'SQF' },
- { syntax: 'sql', label: 'SQL' },
- { syntax: 'stan', variants: 'stanfuncs', label: 'Stan' },
- { syntax: 'stata', variants: 'do, ado', label: 'Stata' },
- { syntax: 'step21', variants: 'p21, step, stp', label: 'STEP Part 21' },
- { syntax: 'stylus', variants: 'styl', label: 'Stylus' },
- { syntax: 'subunit', label: 'SubUnit' },
- { syntax: 'swift', label: 'Swift' },
- { syntax: 'taggerscript', label: 'Tagger Script' },
- { syntax: 'tap', label: 'Test Anything Protocol' },
- { syntax: 'tcl', variants: 'tk', label: 'Tcl' },
- { syntax: 'thrift', label: 'Thrift' },
- { syntax: 'tp', label: 'TP' },
- { syntax: 'twig', variants: 'craftcms', label: 'Twig' },
- { syntax: 'typescript', variants: 'ts, tsx', label: 'TypeScript' },
- { syntax: 'vala', label: 'Vala' },
- { syntax: 'vbnet', variants: 'vb', label: 'Visual Basic .NET' },
- { syntax: 'vbscript', variants: 'vbs', label: 'VBScript' },
- { syntax: 'vbscript-html', label: 'VBScript in HTML' },
- { syntax: 'verilog', variants: 'v, sv, svh', label: 'Verilog' },
- { syntax: 'vhdl', label: 'VHDL' },
- { syntax: 'vim', label: 'Vim Script' },
- { syntax: 'wasm', label: 'WebAssembly' },
- { syntax: 'wren', label: 'Wren' },
- { syntax: 'x86asm', label: 'Intel x86 Assembly' },
- { syntax: 'xl', variants: 'tao', label: 'XL' },
- {
- syntax: 'xml',
- variants: 'html, xhtml, rss, atom, xjb, xsd, xsl, plist, wsf, svg',
- label: 'HTML, XML',
- },
- { syntax: 'xquery', variants: 'xpath, xq', label: 'XQuery' },
- { syntax: 'yaml', variants: 'yml', label: 'YAML' },
- { syntax: 'zephir', variants: 'zep', label: 'Zephir' },
-];
-/* eslint-enable @gitlab/require-i18n-strings */
+import CODE_BLOCK_LANGUAGES from '../constants/code_block_languages';
+import languageLoader from './highlight_js_language_loader';
const codeBlockLanguageLoader = {
lowlight,
@@ -245,38 +37,24 @@ const codeBlockLanguageLoader = {
return this.lowlight.registered(language);
},
- loadLanguagesFromDOM(domTree) {
- const languages = [];
-
- domTree.querySelectorAll('pre').forEach((preElement) => {
- languages.push(preElement.getAttribute('lang'));
- });
-
- return this.loadLanguages(languages);
- },
-
loadLanguageFromInputRule(match) {
const { syntax } = this.findLanguageBySyntax(match[1]);
- this.loadLanguages([syntax]);
+ this.loadLanguage(syntax);
return { language: syntax };
},
- loadLanguages(languageList = []) {
- const loaders = languageList
- .filter((languageName) => !this.isLanguageLoaded(languageName))
- .map((languageName) => {
- return import(
- /* webpackChunkName: 'highlight.language.js' */ `highlight.js/lib/languages/${languageName}`
- )
- .then(({ default: language }) => {
- this.lowlight.registerLanguage(languageName, language);
- })
- .catch(() => false);
- });
+ async loadLanguage(languageName) {
+ if (this.isLanguageLoaded(languageName)) return false;
- return Promise.all(loaders);
+ try {
+ const { default: language } = await languageLoader[languageName]();
+ this.lowlight.registerLanguage(languageName, language);
+ return true;
+ } catch {
+ return false;
+ }
},
};
diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js
index 56badf965ee..52dacb84153 100644
--- a/app/assets/javascripts/content_editor/services/content_editor.js
+++ b/app/assets/javascripts/content_editor/services/content_editor.js
@@ -3,12 +3,13 @@ import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } fro
/* eslint-disable no-underscore-dangle */
export class ContentEditor {
- constructor({ tiptapEditor, serializer, deserializer, eventHub, languageLoader }) {
+ constructor({ tiptapEditor, serializer, deserializer, assetResolver, eventHub }) {
this._tiptapEditor = tiptapEditor;
this._serializer = serializer;
this._deserializer = deserializer;
this._eventHub = eventHub;
- this._languageLoader = languageLoader;
+ this._assetResolver = assetResolver;
+ this._pristineDoc = null;
}
get tiptapEditor() {
@@ -19,6 +20,10 @@ export class ContentEditor {
return this._eventHub;
}
+ get changed() {
+ return this._pristineDoc?.eq(this.tiptapEditor.state.doc);
+ }
+
get empty() {
const doc = this.tiptapEditor?.state.doc;
@@ -34,28 +39,30 @@ export class ContentEditor {
this._eventHub.dispose();
}
+ deserialize(serializedContent) {
+ const { _tiptapEditor: editor, _deserializer: deserializer } = this;
+
+ return deserializer.deserialize({
+ schema: editor.schema,
+ content: serializedContent,
+ });
+ }
+
+ resolveUrl(canonicalSrc) {
+ return this._assetResolver.resolveUrl(canonicalSrc);
+ }
+
async setSerializedContent(serializedContent) {
- const {
- _tiptapEditor: editor,
- _deserializer: deserializer,
- _eventHub: eventHub,
- _languageLoader: languageLoader,
- } = this;
+ const { _tiptapEditor: editor, _eventHub: eventHub } = this;
const { doc, tr } = editor.state;
const selection = TextSelection.create(doc, 0, doc.content.size);
try {
eventHub.$emit(LOADING_CONTENT_EVENT);
- const result = await deserializer.deserialize({
- schema: editor.schema,
- content: serializedContent,
- });
-
- if (Object.keys(result).length !== 0) {
- const { document, dom } = result;
-
- await languageLoader.loadLanguagesFromDOM(dom);
+ const { document } = await this.deserialize(serializedContent);
+ if (document) {
+ this._pristineDoc = document;
tr.setSelection(selection)
.replaceSelectionWith(document, false)
.setMeta('preventUpdate', true);
@@ -70,8 +77,9 @@ export class ContentEditor {
}
getSerializedContent() {
- const { _tiptapEditor: editor, _serializer: serializer } = this;
+ const { _tiptapEditor: editor, _serializer: serializer, _pristineDoc: pristineDoc } = this;
+ const { doc } = editor.state;
- return serializer.serialize({ schema: editor.schema, content: editor.getJSON() });
+ return serializer.serialize({ doc, pristineDoc });
}
}
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index af19a0ab0e4..15aac3d86e5 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -43,6 +43,7 @@ import OrderedList from '../extensions/ordered_list';
import Paragraph from '../extensions/paragraph';
import PasteMarkdown from '../extensions/paste_markdown';
import Reference from '../extensions/reference';
+import Sourcemap from '../extensions/sourcemap';
import Strike from '../extensions/strike';
import Subscript from '../extensions/subscript';
import Superscript from '../extensions/superscript';
@@ -58,9 +59,10 @@ import Video from '../extensions/video';
import WordBreak from '../extensions/word_break';
import { ContentEditor } from './content_editor';
import createMarkdownSerializer from './markdown_serializer';
-import createMarkdownDeserializer from './markdown_deserializer';
+import createGlApiMarkdownDeserializer from './gl_api_markdown_deserializer';
+import createRemarkMarkdownDeserializer from './remark_markdown_deserializer';
+import createAssetResolver from './asset_resolver';
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
-import languageLoader from './code_block_language_loader';
const createTiptapEditor = ({ extensions = [], ...options } = {}) =>
new Editor({
@@ -94,7 +96,7 @@ export const createContentEditor = ({
BulletList,
Code,
ColorChip,
- CodeBlockHighlight.configure({ lowlight, languageLoader }),
+ CodeBlockHighlight.configure({ lowlight }),
DescriptionItem,
DescriptionList,
Details,
@@ -127,6 +129,7 @@ export const createContentEditor = ({
Paragraph,
PasteMarkdown.configure({ renderMarkdown, eventHub }),
Reference,
+ Sourcemap,
Strike,
Subscript,
Superscript,
@@ -146,7 +149,18 @@ export const createContentEditor = ({
const trackedExtensions = allExtensions.map(trackInputRulesAndShortcuts);
const tiptapEditor = createTiptapEditor({ extensions: trackedExtensions, ...tiptapOptions });
const serializer = createMarkdownSerializer({ serializerConfig });
- const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
+ const deserializer = window.gon?.features?.preserveUnchangedMarkdown
+ ? createRemarkMarkdownDeserializer()
+ : createGlApiMarkdownDeserializer({
+ render: renderMarkdown,
+ });
+ const assetResolver = createAssetResolver({ renderMarkdown });
- return new ContentEditor({ tiptapEditor, serializer, eventHub, deserializer, languageLoader });
+ return new ContentEditor({
+ tiptapEditor,
+ serializer,
+ eventHub,
+ deserializer,
+ assetResolver,
+ });
};
diff --git a/app/assets/javascripts/content_editor/services/markdown_deserializer.js b/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js
index cd4863d8eac..dcd56e55268 100644
--- a/app/assets/javascripts/content_editor/services/markdown_deserializer.js
+++ b/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js
@@ -27,7 +27,7 @@ export default ({ render }) => {
// append original source as a comment that nodes can access
body.append(document.createComment(content));
- return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body), dom: body };
+ return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body) };
},
};
};
diff --git a/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js
new file mode 100644
index 00000000000..b6a3e0bc26a
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js
@@ -0,0 +1,475 @@
+/**
+ * This module implements a function that converts a Hast Abstract
+ * Syntax Tree (AST) to a ProseMirror document.
+ *
+ * It is based on the prosemirror-markdown’s from_markdown module
+ * https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/from_markdown.js.
+ *
+ * It deviates significantly from the original because
+ * prosemirror-markdown supports converting an markdown-it AST instead of a
+ * HAST one. It also adds sourcemap attributes automatically to every
+ * ProseMirror node and mark created during the conversion process.
+ *
+ * We recommend becoming familiar with HAST and ProseMirror documents to
+ * facilitate the understanding of the behavior implemented in this module.
+ *
+ * Unist syntax tree documentation: https://github.com/syntax-tree/unist
+ * Hast tree documentation: https://github.com/syntax-tree/hast
+ * ProseMirror document documentation: https://prosemirror.net/docs/ref/#model.Document_Structure
+ * visit-parents documentation: https://github.com/syntax-tree/unist-util-visit-parents
+ */
+
+import { Mark } from 'prosemirror-model';
+import { visitParents } from 'unist-util-visit-parents';
+import { toString } from 'hast-util-to-string';
+import { isFunction } from 'lodash';
+
+/**
+ * Merges two ProseMirror text nodes if both text nodes
+ * have the same set of marks.
+ *
+ * @param {ProseMirror.Node} a first ProseMirror node
+ * @param {ProseMirror.Node} b second ProseMirror node
+ * @returns {model.Node} A new text node that results from combining
+ * the text of the two text node parameters or null.
+ */
+function maybeMerge(a, b) {
+ if (a && a.isText && b && b.isText && Mark.sameSet(a.marks, b.marks)) {
+ return a.withText(a.text + b.text);
+ }
+
+ return null;
+}
+
+/**
+ * Creates an object that contains sourcemap position information
+ * included in a Hast Abstract Syntax Tree. The Content
+ * Editor uses the sourcemap information to restore the
+ * original source of a node when the user doesn’t change it.
+ *
+ * Unist syntax tree documentation: https://github.com/syntax-tree/unist
+ * Hast node documentation: https://github.com/syntax-tree/hast
+ *
+ * @param {HastNode} hastNode A Hast node
+ * @param {String} source Markdown source file
+ *
+ * @returns It returns an object with the following attributes:
+ *
+ * - sourceMapKey: A string that uniquely identifies what is
+ * the position of the hast node in the Markdown source file.
+ * - sourceMarkdown: A node’s original Markdown source extrated
+ * from the Markdown source file.
+ */
+function createSourceMapAttributes(hastNode, source) {
+ const { position } = hastNode;
+
+ return {
+ sourceMapKey: `${position.start.offset}:${position.end.offset}`,
+ sourceMarkdown: source.substring(position.start.offset, position.end.offset),
+ };
+}
+
+/**
+ * Compute ProseMirror node’s attributes from a Hast node.
+ * By default, this function includes sourcemap position
+ * information in the object returned.
+ *
+ * Other attributes are retrieved by invoking a getAttrs
+ * function provided by the ProseMirror node factory spec.
+ *
+ * @param {*} proseMirrorNodeSpec ProseMirror node spec object
+ * @param {HastNode} hastNode A hast node
+ * @param {Array<HastNode>} hastParents All the ancestors of the hastNode
+ * @param {String} source Markdown source file’s content
+ *
+ * @returns An object that contains a ProseMirror node’s attributes
+ */
+function getAttrs(proseMirrorNodeSpec, hastNode, hastParents, source) {
+ const { getAttrs: specGetAttrs } = proseMirrorNodeSpec;
+
+ return {
+ ...createSourceMapAttributes(hastNode, source),
+ ...(isFunction(specGetAttrs) ? specGetAttrs(hastNode, hastParents, source) : {}),
+ };
+}
+
+/**
+ * Keeps track of the Hast -> ProseMirror conversion process.
+ *
+ * When the `openNode` method is invoked, it adds the node to a stack
+ * data structure. When the `closeNode` method is invoked, it removes the
+ * last element from the Stack, creates a ProseMirror node, and adds that
+ * ProseMirror node to the previous node in the Stack.
+ *
+ * For example, given a Hast tree with three levels of nodes:
+ *
+ * - blockquote
+ * - paragraph
+ * - text
+ *
+ * 3. text
+ * 2. paragraph
+ * 1. blockquote
+ *
+ * Calling `closeNode` will fold the text node into paragraph. A 2nd
+ * call to this method will fold "paragraph" into "blockquote".
+ *
+ * Mark state
+ *
+ * When the `openMark` method is invoked, this class adds the Mark to a `MarkSet`
+ * object. When a text node is added, it assigns all the opened marks to that text
+ * node and cleans the marks. It takes care of merging text nodes with the same
+ * set of marks as well.
+ */
+class HastToProseMirrorConverterState {
+ constructor() {
+ this.stack = [];
+ this.marks = Mark.none;
+ }
+
+ /**
+ * Gets the first element of the node stack
+ */
+ get top() {
+ return this.stack[this.stack.length - 1];
+ }
+
+ /**
+ * Detects if the node stack is empty
+ */
+ get empty() {
+ return this.stack.length === 0;
+ }
+
+ /**
+ * Creates a text node and adds it to
+ * the top node in the stack.
+ *
+ * It applies the marks stored temporarily
+ * by calling the `addMark` method. After
+ * the text node is added, it clears the mark
+ * set afterward.
+ *
+ * If the top block node has a text
+ * node with the same set of marks as the
+ * text node created, this method merges
+ * both text nodes
+ *
+ * @param {ProseMirror.Schema} schema ProseMirror schema
+ * @param {String} text Text
+ * @returns
+ */
+ addText(schema, text) {
+ if (!text) return;
+ const nodes = this.top.content;
+ const last = nodes[nodes.length - 1];
+ const node = schema.text(text, this.marks);
+ const merged = maybeMerge(last, node);
+
+ if (last && merged) {
+ nodes[nodes.length - 1] = merged;
+ } else {
+ nodes.push(node);
+ }
+
+ this.closeMarks();
+ }
+
+ /**
+ * Adds a mark to the set of marks stored temporarily
+ * until addText is called.
+ * @param {*} markType
+ * @param {*} attrs
+ */
+ openMark(markType, attrs) {
+ this.marks = markType.create(attrs).addToSet(this.marks);
+ }
+
+ /**
+ * Empties the temporary Mark set.
+ */
+ closeMarks() {
+ this.marks = Mark.none;
+ }
+
+ /**
+ * Adds a node to the stack data structure.
+ *
+ * @param {Schema.NodeType} type ProseMirror Schema for the node
+ * @param {HastNode} hastNode Hast node from which the ProseMirror node will be created
+ * @param {*} attrs Node’s attributes
+ * @param {*} factorySpec The factory spec used to create the node factory
+ */
+ openNode(type, hastNode, attrs, factorySpec) {
+ this.stack.push({ type, attrs, content: [], hastNode, factorySpec });
+ }
+
+ /**
+ * Removes the top ProseMirror node from the
+ * conversion stack and adds the node to the
+ * previous element.
+ * @returns
+ */
+ closeNode() {
+ const { type, attrs, content } = this.stack.pop();
+ const node = type.createAndFill(attrs, content);
+
+ if (!node) return null;
+
+ if (this.marks.length) {
+ this.marks = Mark.none;
+ }
+
+ if (!this.empty) {
+ this.top.content.push(node);
+ }
+
+ return node;
+ }
+
+ closeUntil(hastNode) {
+ while (hastNode !== this.top?.hastNode) {
+ this.closeNode();
+ }
+ }
+}
+
+/**
+ * Create ProseMirror node/mark factories based on one or more
+ * factory specifications.
+ *
+ * Note: Read `createProseMirrorDocFromMdastTree` documentation
+ * for instructions about how to define these specifications.
+ *
+ * @param {model.ProseMirrorSchema} schema A ProseMirror schema used to create the
+ * ProseMirror nodes and marks.
+ * @param {Object} proseMirrorFactorySpecs ProseMirror nodes factory specifications.
+ * @param {String} source Markdown source file’s content
+ *
+ * @returns An object that contains ProseMirror node factories
+ */
+const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source) => {
+ const handlers = {
+ root: (state, hastNode) => state.openNode(schema.topNodeType, hastNode, {}),
+ text: (state, hastNode) => {
+ const { factorySpec } = state.top;
+
+ if (/^\s+$/.test(hastNode.value)) {
+ return;
+ }
+
+ if (factorySpec.wrapTextInParagraph === true) {
+ state.openNode(schema.nodeType('paragraph'));
+ state.addText(schema, hastNode.value);
+ state.closeNode();
+ } else {
+ state.addText(schema, hastNode.value);
+ }
+ },
+ };
+
+ for (const [hastNodeTagName, factorySpec] of Object.entries(proseMirrorFactorySpecs)) {
+ if (factorySpec.block) {
+ handlers[hastNodeTagName] = (state, hastNode, parent, ancestors) => {
+ const nodeType = schema.nodeType(
+ isFunction(factorySpec.block)
+ ? factorySpec.block(hastNode, parent, ancestors)
+ : factorySpec.block,
+ );
+
+ state.closeUntil(parent);
+ state.openNode(
+ nodeType,
+ hastNode,
+ getAttrs(factorySpec, hastNode, parent, source),
+ factorySpec,
+ );
+
+ /**
+ * If a getContent function is provided, we immediately close
+ * the node to delegate content processing to this function.
+ * */
+ if (isFunction(factorySpec.getContent)) {
+ state.addText(
+ schema,
+ factorySpec.getContent({ hastNode, hastNodeText: toString(hastNode) }),
+ );
+ state.closeNode();
+ }
+ };
+ } else if (factorySpec.inline) {
+ const nodeType = schema.nodeType(factorySpec.inline);
+ handlers[hastNodeTagName] = (state, hastNode, parent) => {
+ state.closeUntil(parent);
+ state.openNode(
+ nodeType,
+ hastNode,
+ getAttrs(factorySpec, hastNode, parent, source),
+ factorySpec,
+ );
+ // Inline nodes do not have children therefore they are immediately closed
+ state.closeNode();
+ };
+ } else if (factorySpec.mark) {
+ const markType = schema.marks[factorySpec.mark];
+ handlers[hastNodeTagName] = (state, hastNode, parent) => {
+ state.openMark(markType, getAttrs(factorySpec, hastNode, parent, source));
+
+ if (factorySpec.inlineContent) {
+ state.addText(schema, hastNode.value);
+ }
+ };
+ } else {
+ throw new RangeError(`Unrecognized node factory spec ${JSON.stringify(factorySpec)}`);
+ }
+ }
+
+ return handlers;
+};
+
+/**
+ * Converts a Hast AST to a ProseMirror document based on a series
+ * of specifications that describe how to map all the nodes of the former
+ * to ProseMirror nodes or marks.
+ *
+ * The specification object describes how to map a Hast node to a ProseMirror node or mark.
+ * The converter will trigger an error if it doesn’t find a specification
+ * for a Hast node while traversing the AST.
+ *
+ * The object should have the following shape:
+ *
+ * {
+ * [hastNode.tagName]: {
+ * [block|node|mark]: [ProseMirror.Node.name],
+ * ...configurationOptions
+ * }
+ * }
+ *
+ * Where each property in the object represents a HAST node with a given tag name, for example:
+ *
+ * {
+ * h1: {},
+ * h2: {},
+ * table: {},
+ * strong: {},
+ * // etc
+ * }
+ *
+ * You can specify the type of ProseMirror object adding one the following
+ * properties:
+ *
+ * 1. "block": A ProseMirror node that contains one or more children.
+ * 2. "inline": A ProseMirror node that doesn’t contain any children although
+ * it can have inline content like a code block or a reference.
+ * 3. "mark": A ProseMirror mark.
+ *
+ * The value of that property should be the name of the ProseMirror node or mark, i.e:
+ *
+ * {
+ * h1: {
+ * block: 'heading',
+ * },
+ * h2: {
+ * block: 'heading',
+ * },
+ * img: {
+ * node: 'image',
+ * },
+ * strong: {
+ * mark: 'bold',
+ * }
+ * }
+ *
+ * You can compute a ProseMirror’s node or mark name based on the HAST node
+ * by passing a function instead of a String. The converter invokes the function
+ * and provides a HAST node object:
+ *
+ * {
+ * list: {
+ * block: (hastNode) => {
+ * let type = 'bulletList';
+
+ * if (hastNode.children.some(isTaskItem)) {
+ * type = 'taskList';
+ * } else if (hastNode.ordered) {
+ * type = 'orderedList';
+ * }
+
+ * return type;
+ * }
+ * }
+ * }
+ *
+ * Configuration options
+ * ----------------------
+ *
+ * You can customize the conversion process for every node or mark
+ * setting the following properties in the specification object:
+ *
+ * **getAttrs**
+ *
+ * Computes a ProseMirror node or mark attributes. The converter will invoke
+ * `getAttrs` with the following parameters:
+ *
+ * 1. hastNode: The hast node
+ * 2. hasParents: All the hast node’s ancestors up to the root node
+ * 3. source: Markdown source file’s content
+ *
+ * **wrapTextInParagraph**
+ *
+ * This property only applies to block nodes. If a block node contains text,
+ * it will wrap that text in a paragraph. This is useful for ProseMirror block
+ * nodes that don’t allow text directly such as list items and tables.
+ *
+ * **skipChildren**
+ *
+ * Skips a hast node’s children while traversing the tree.
+ *
+ * **getContent**
+ *
+ * Allows to pass a custom function that returns the content of a block node. The
+ * Content is limited to a single text node therefore the function should return
+ * a String value.
+ *
+ * Use this property along skipChildren to provide custom processing of child nodes
+ * for a block node.
+ *
+ * @param {model.Document_Schema} params.schema A ProseMirror schema that specifies the shape
+ * of the ProseMirror document.
+ * @param {Object} params.factorySpec A factory specification as described above
+ * @param {Hast} params.tree https://github.com/syntax-tree/hast
+ * @param {String} params.source Markdown source from which the MDast tree was generated
+ *
+ * @returns A ProseMirror document
+ */
+export const createProseMirrorDocFromMdastTree = ({ schema, factorySpecs, tree, source }) => {
+ const proseMirrorNodeFactories = createProseMirrorNodeFactories(schema, factorySpecs, source);
+ const state = new HastToProseMirrorConverterState();
+
+ visitParents(tree, (hastNode, ancestors) => {
+ const parent = ancestors[ancestors.length - 1];
+ const skipChildren = factorySpecs[hastNode.tagName]?.skipChildren;
+
+ const handler = proseMirrorNodeFactories[hastNode.tagName || hastNode.type];
+
+ if (!handler) {
+ throw new Error(
+ `Hast node of type "${
+ hastNode.tagName || hastNode.type
+ }" not supported by this converter. Please, provide an specification.`,
+ );
+ }
+
+ handler(state, hastNode, parent, ancestors);
+
+ return skipChildren === true ? 'skip' : true;
+ });
+
+ let doc;
+
+ do {
+ doc = state.closeNode();
+ } while (!state.empty);
+
+ return doc;
+};
diff --git a/app/assets/javascripts/content_editor/services/highlight_js_language_loader.js b/app/assets/javascripts/content_editor/services/highlight_js_language_loader.js
new file mode 100644
index 00000000000..a0ebbebed4e
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/highlight_js_language_loader.js
@@ -0,0 +1,248 @@
+/**
+ * This file is generated based on the contents of highlight.js/lib/languages to avoid
+ * utilizing dynamic expressions within `import()` which were the source of some
+ * confusion when attempting to produce deterministic webpack compilations across
+ * multiple build environments.
+ *
+ * This list of highlight-able languages will need to be updated as new options are
+ * introduced within the highlight.js dependency.
+ */
+
+export default {
+ '1c': () => import(/* webpackChunkName: 'hl-1c' */ 'highlight.js/lib/languages/1c'),
+ abnf: () => import(/* webpackChunkName: 'hl-abnf' */ 'highlight.js/lib/languages/abnf'),
+ accesslog: () =>
+ import(/* webpackChunkName: 'hl-accesslog' */ 'highlight.js/lib/languages/accesslog'),
+ actionscript: () =>
+ import(/* webpackChunkName: 'hl-actionscript' */ 'highlight.js/lib/languages/actionscript'),
+ ada: () => import(/* webpackChunkName: 'hl-ada' */ 'highlight.js/lib/languages/ada'),
+ angelscript: () =>
+ import(/* webpackChunkName: 'hl-angelscript' */ 'highlight.js/lib/languages/angelscript'),
+ apache: () => import(/* webpackChunkName: 'hl-apache' */ 'highlight.js/lib/languages/apache'),
+ applescript: () =>
+ import(/* webpackChunkName: 'hl-applescript' */ 'highlight.js/lib/languages/applescript'),
+ arcade: () => import(/* webpackChunkName: 'hl-arcade' */ 'highlight.js/lib/languages/arcade'),
+ arduino: () => import(/* webpackChunkName: 'hl-arduino' */ 'highlight.js/lib/languages/arduino'),
+ armasm: () => import(/* webpackChunkName: 'hl-armasm' */ 'highlight.js/lib/languages/armasm'),
+ asciidoc: () =>
+ import(/* webpackChunkName: 'hl-asciidoc' */ 'highlight.js/lib/languages/asciidoc'),
+ aspectj: () => import(/* webpackChunkName: 'hl-aspectj' */ 'highlight.js/lib/languages/aspectj'),
+ autohotkey: () =>
+ import(/* webpackChunkName: 'hl-autohotkey' */ 'highlight.js/lib/languages/autohotkey'),
+ autoit: () => import(/* webpackChunkName: 'hl-autoit' */ 'highlight.js/lib/languages/autoit'),
+ avrasm: () => import(/* webpackChunkName: 'hl-avrasm' */ 'highlight.js/lib/languages/avrasm'),
+ awk: () => import(/* webpackChunkName: 'hl-awk' */ 'highlight.js/lib/languages/awk'),
+ axapta: () => import(/* webpackChunkName: 'hl-axapta' */ 'highlight.js/lib/languages/axapta'),
+ bash: () => import(/* webpackChunkName: 'hl-bash' */ 'highlight.js/lib/languages/bash'),
+ basic: () => import(/* webpackChunkName: 'hl-basic' */ 'highlight.js/lib/languages/basic'),
+ bnf: () => import(/* webpackChunkName: 'hl-bnf' */ 'highlight.js/lib/languages/bnf'),
+ brainfuck: () =>
+ import(/* webpackChunkName: 'hl-brainfuck' */ 'highlight.js/lib/languages/brainfuck'),
+ c: () => import(/* webpackChunkName: 'hl-c' */ 'highlight.js/lib/languages/c'),
+ cal: () => import(/* webpackChunkName: 'hl-cal' */ 'highlight.js/lib/languages/cal'),
+ capnproto: () =>
+ import(/* webpackChunkName: 'hl-capnproto' */ 'highlight.js/lib/languages/capnproto'),
+ ceylon: () => import(/* webpackChunkName: 'hl-ceylon' */ 'highlight.js/lib/languages/ceylon'),
+ clean: () => import(/* webpackChunkName: 'hl-clean' */ 'highlight.js/lib/languages/clean'),
+ 'clojure-repl': () =>
+ import(/* webpackChunkName: 'hl-clojure-repl' */ 'highlight.js/lib/languages/clojure-repl'),
+ clojure: () => import(/* webpackChunkName: 'hl-clojure' */ 'highlight.js/lib/languages/clojure'),
+ cmake: () => import(/* webpackChunkName: 'hl-cmake' */ 'highlight.js/lib/languages/cmake'),
+ coffeescript: () =>
+ import(/* webpackChunkName: 'hl-coffeescript' */ 'highlight.js/lib/languages/coffeescript'),
+ coq: () => import(/* webpackChunkName: 'hl-coq' */ 'highlight.js/lib/languages/coq'),
+ cos: () => import(/* webpackChunkName: 'hl-cos' */ 'highlight.js/lib/languages/cos'),
+ cpp: () => import(/* webpackChunkName: 'hl-cpp' */ 'highlight.js/lib/languages/cpp'),
+ crmsh: () => import(/* webpackChunkName: 'hl-crmsh' */ 'highlight.js/lib/languages/crmsh'),
+ crystal: () => import(/* webpackChunkName: 'hl-crystal' */ 'highlight.js/lib/languages/crystal'),
+ csharp: () => import(/* webpackChunkName: 'hl-csharp' */ 'highlight.js/lib/languages/csharp'),
+ csp: () => import(/* webpackChunkName: 'hl-csp' */ 'highlight.js/lib/languages/csp'),
+ css: () => import(/* webpackChunkName: 'hl-css' */ 'highlight.js/lib/languages/css'),
+ d: () => import(/* webpackChunkName: 'hl-d' */ 'highlight.js/lib/languages/d'),
+ dart: () => import(/* webpackChunkName: 'hl-dart' */ 'highlight.js/lib/languages/dart'),
+ delphi: () => import(/* webpackChunkName: 'hl-delphi' */ 'highlight.js/lib/languages/delphi'),
+ diff: () => import(/* webpackChunkName: 'hl-diff' */ 'highlight.js/lib/languages/diff'),
+ django: () => import(/* webpackChunkName: 'hl-django' */ 'highlight.js/lib/languages/django'),
+ dns: () => import(/* webpackChunkName: 'hl-dns' */ 'highlight.js/lib/languages/dns'),
+ dockerfile: () =>
+ import(/* webpackChunkName: 'hl-dockerfile' */ 'highlight.js/lib/languages/dockerfile'),
+ dos: () => import(/* webpackChunkName: 'hl-dos' */ 'highlight.js/lib/languages/dos'),
+ dsconfig: () =>
+ import(/* webpackChunkName: 'hl-dsconfig' */ 'highlight.js/lib/languages/dsconfig'),
+ dts: () => import(/* webpackChunkName: 'hl-dts' */ 'highlight.js/lib/languages/dts'),
+ dust: () => import(/* webpackChunkName: 'hl-dust' */ 'highlight.js/lib/languages/dust'),
+ ebnf: () => import(/* webpackChunkName: 'hl-ebnf' */ 'highlight.js/lib/languages/ebnf'),
+ elixir: () => import(/* webpackChunkName: 'hl-elixir' */ 'highlight.js/lib/languages/elixir'),
+ elm: () => import(/* webpackChunkName: 'hl-elm' */ 'highlight.js/lib/languages/elm'),
+ erb: () => import(/* webpackChunkName: 'hl-erb' */ 'highlight.js/lib/languages/erb'),
+ 'erlang-repl': () =>
+ import(/* webpackChunkName: 'hl-erlang-repl' */ 'highlight.js/lib/languages/erlang-repl'),
+ erlang: () => import(/* webpackChunkName: 'hl-erlang' */ 'highlight.js/lib/languages/erlang'),
+ excel: () => import(/* webpackChunkName: 'hl-excel' */ 'highlight.js/lib/languages/excel'),
+ fix: () => import(/* webpackChunkName: 'hl-fix' */ 'highlight.js/lib/languages/fix'),
+ flix: () => import(/* webpackChunkName: 'hl-flix' */ 'highlight.js/lib/languages/flix'),
+ fortran: () => import(/* webpackChunkName: 'hl-fortran' */ 'highlight.js/lib/languages/fortran'),
+ fsharp: () => import(/* webpackChunkName: 'hl-fsharp' */ 'highlight.js/lib/languages/fsharp'),
+ gams: () => import(/* webpackChunkName: 'hl-gams' */ 'highlight.js/lib/languages/gams'),
+ gauss: () => import(/* webpackChunkName: 'hl-gauss' */ 'highlight.js/lib/languages/gauss'),
+ gcode: () => import(/* webpackChunkName: 'hl-gcode' */ 'highlight.js/lib/languages/gcode'),
+ gherkin: () => import(/* webpackChunkName: 'hl-gherkin' */ 'highlight.js/lib/languages/gherkin'),
+ glsl: () => import(/* webpackChunkName: 'hl-glsl' */ 'highlight.js/lib/languages/glsl'),
+ gml: () => import(/* webpackChunkName: 'hl-gml' */ 'highlight.js/lib/languages/gml'),
+ go: () => import(/* webpackChunkName: 'hl-go' */ 'highlight.js/lib/languages/go'),
+ golo: () => import(/* webpackChunkName: 'hl-golo' */ 'highlight.js/lib/languages/golo'),
+ gradle: () => import(/* webpackChunkName: 'hl-gradle' */ 'highlight.js/lib/languages/gradle'),
+ groovy: () => import(/* webpackChunkName: 'hl-groovy' */ 'highlight.js/lib/languages/groovy'),
+ haml: () => import(/* webpackChunkName: 'hl-haml' */ 'highlight.js/lib/languages/haml'),
+ handlebars: () =>
+ import(/* webpackChunkName: 'hl-handlebars' */ 'highlight.js/lib/languages/handlebars'),
+ haskell: () => import(/* webpackChunkName: 'hl-haskell' */ 'highlight.js/lib/languages/haskell'),
+ haxe: () => import(/* webpackChunkName: 'hl-haxe' */ 'highlight.js/lib/languages/haxe'),
+ hsp: () => import(/* webpackChunkName: 'hl-hsp' */ 'highlight.js/lib/languages/hsp'),
+ http: () => import(/* webpackChunkName: 'hl-http' */ 'highlight.js/lib/languages/http'),
+ hy: () => import(/* webpackChunkName: 'hl-hy' */ 'highlight.js/lib/languages/hy'),
+ inform7: () => import(/* webpackChunkName: 'hl-inform7' */ 'highlight.js/lib/languages/inform7'),
+ ini: () => import(/* webpackChunkName: 'hl-ini' */ 'highlight.js/lib/languages/ini'),
+ irpf90: () => import(/* webpackChunkName: 'hl-irpf90' */ 'highlight.js/lib/languages/irpf90'),
+ isbl: () => import(/* webpackChunkName: 'hl-isbl' */ 'highlight.js/lib/languages/isbl'),
+ java: () => import(/* webpackChunkName: 'hl-java' */ 'highlight.js/lib/languages/java'),
+ javascript: () =>
+ import(/* webpackChunkName: 'hl-javascript' */ 'highlight.js/lib/languages/javascript'),
+ 'jboss-cli': () =>
+ import(/* webpackChunkName: 'hl-jboss-cli' */ 'highlight.js/lib/languages/jboss-cli'),
+ json: () => import(/* webpackChunkName: 'hl-json' */ 'highlight.js/lib/languages/json'),
+ 'julia-repl': () =>
+ import(/* webpackChunkName: 'hl-julia-repl' */ 'highlight.js/lib/languages/julia-repl'),
+ julia: () => import(/* webpackChunkName: 'hl-julia' */ 'highlight.js/lib/languages/julia'),
+ kotlin: () => import(/* webpackChunkName: 'hl-kotlin' */ 'highlight.js/lib/languages/kotlin'),
+ lasso: () => import(/* webpackChunkName: 'hl-lasso' */ 'highlight.js/lib/languages/lasso'),
+ latex: () => import(/* webpackChunkName: 'hl-latex' */ 'highlight.js/lib/languages/latex'),
+ ldif: () => import(/* webpackChunkName: 'hl-ldif' */ 'highlight.js/lib/languages/ldif'),
+ leaf: () => import(/* webpackChunkName: 'hl-leaf' */ 'highlight.js/lib/languages/leaf'),
+ less: () => import(/* webpackChunkName: 'hl-less' */ 'highlight.js/lib/languages/less'),
+ lisp: () => import(/* webpackChunkName: 'hl-lisp' */ 'highlight.js/lib/languages/lisp'),
+ livecodeserver: () =>
+ import(/* webpackChunkName: 'hl-livecodeserver' */ 'highlight.js/lib/languages/livecodeserver'),
+ livescript: () =>
+ import(/* webpackChunkName: 'hl-livescript' */ 'highlight.js/lib/languages/livescript'),
+ llvm: () => import(/* webpackChunkName: 'hl-llvm' */ 'highlight.js/lib/languages/llvm'),
+ lsl: () => import(/* webpackChunkName: 'hl-lsl' */ 'highlight.js/lib/languages/lsl'),
+ lua: () => import(/* webpackChunkName: 'hl-lua' */ 'highlight.js/lib/languages/lua'),
+ makefile: () =>
+ import(/* webpackChunkName: 'hl-makefile' */ 'highlight.js/lib/languages/makefile'),
+ markdown: () =>
+ import(/* webpackChunkName: 'hl-markdown' */ 'highlight.js/lib/languages/markdown'),
+ mathematica: () =>
+ import(/* webpackChunkName: 'hl-mathematica' */ 'highlight.js/lib/languages/mathematica'),
+ matlab: () => import(/* webpackChunkName: 'hl-matlab' */ 'highlight.js/lib/languages/matlab'),
+ maxima: () => import(/* webpackChunkName: 'hl-maxima' */ 'highlight.js/lib/languages/maxima'),
+ mel: () => import(/* webpackChunkName: 'hl-mel' */ 'highlight.js/lib/languages/mel'),
+ mercury: () => import(/* webpackChunkName: 'hl-mercury' */ 'highlight.js/lib/languages/mercury'),
+ mipsasm: () => import(/* webpackChunkName: 'hl-mipsasm' */ 'highlight.js/lib/languages/mipsasm'),
+ mizar: () => import(/* webpackChunkName: 'hl-mizar' */ 'highlight.js/lib/languages/mizar'),
+ mojolicious: () =>
+ import(/* webpackChunkName: 'hl-mojolicious' */ 'highlight.js/lib/languages/mojolicious'),
+ monkey: () => import(/* webpackChunkName: 'hl-monkey' */ 'highlight.js/lib/languages/monkey'),
+ moonscript: () =>
+ import(/* webpackChunkName: 'hl-moonscript' */ 'highlight.js/lib/languages/moonscript'),
+ n1ql: () => import(/* webpackChunkName: 'hl-n1ql' */ 'highlight.js/lib/languages/n1ql'),
+ nestedtext: () =>
+ import(/* webpackChunkName: 'hl-nestedtext' */ 'highlight.js/lib/languages/nestedtext'),
+ nginx: () => import(/* webpackChunkName: 'hl-nginx' */ 'highlight.js/lib/languages/nginx'),
+ nim: () => import(/* webpackChunkName: 'hl-nim' */ 'highlight.js/lib/languages/nim'),
+ nix: () => import(/* webpackChunkName: 'hl-nix' */ 'highlight.js/lib/languages/nix'),
+ 'node-repl': () =>
+ import(/* webpackChunkName: 'hl-node-repl' */ 'highlight.js/lib/languages/node-repl'),
+ nsis: () => import(/* webpackChunkName: 'hl-nsis' */ 'highlight.js/lib/languages/nsis'),
+ objectivec: () =>
+ import(/* webpackChunkName: 'hl-objectivec' */ 'highlight.js/lib/languages/objectivec'),
+ ocaml: () => import(/* webpackChunkName: 'hl-ocaml' */ 'highlight.js/lib/languages/ocaml'),
+ openscad: () =>
+ import(/* webpackChunkName: 'hl-openscad' */ 'highlight.js/lib/languages/openscad'),
+ oxygene: () => import(/* webpackChunkName: 'hl-oxygene' */ 'highlight.js/lib/languages/oxygene'),
+ parser3: () => import(/* webpackChunkName: 'hl-parser3' */ 'highlight.js/lib/languages/parser3'),
+ perl: () => import(/* webpackChunkName: 'hl-perl' */ 'highlight.js/lib/languages/perl'),
+ pf: () => import(/* webpackChunkName: 'hl-pf' */ 'highlight.js/lib/languages/pf'),
+ pgsql: () => import(/* webpackChunkName: 'hl-pgsql' */ 'highlight.js/lib/languages/pgsql'),
+ 'php-template': () =>
+ import(/* webpackChunkName: 'hl-php-template' */ 'highlight.js/lib/languages/php-template'),
+ php: () => import(/* webpackChunkName: 'hl-php' */ 'highlight.js/lib/languages/php'),
+ plaintext: () =>
+ import(/* webpackChunkName: 'hl-plaintext' */ 'highlight.js/lib/languages/plaintext'),
+ pony: () => import(/* webpackChunkName: 'hl-pony' */ 'highlight.js/lib/languages/pony'),
+ powershell: () =>
+ import(/* webpackChunkName: 'hl-powershell' */ 'highlight.js/lib/languages/powershell'),
+ processing: () =>
+ import(/* webpackChunkName: 'hl-processing' */ 'highlight.js/lib/languages/processing'),
+ profile: () => import(/* webpackChunkName: 'hl-profile' */ 'highlight.js/lib/languages/profile'),
+ prolog: () => import(/* webpackChunkName: 'hl-prolog' */ 'highlight.js/lib/languages/prolog'),
+ properties: () =>
+ import(/* webpackChunkName: 'hl-properties' */ 'highlight.js/lib/languages/properties'),
+ protobuf: () =>
+ import(/* webpackChunkName: 'hl-protobuf' */ 'highlight.js/lib/languages/protobuf'),
+ puppet: () => import(/* webpackChunkName: 'hl-puppet' */ 'highlight.js/lib/languages/puppet'),
+ purebasic: () =>
+ import(/* webpackChunkName: 'hl-purebasic' */ 'highlight.js/lib/languages/purebasic'),
+ 'python-repl': () =>
+ import(/* webpackChunkName: 'hl-python-repl' */ 'highlight.js/lib/languages/python-repl'),
+ python: () => import(/* webpackChunkName: 'hl-python' */ 'highlight.js/lib/languages/python'),
+ q: () => import(/* webpackChunkName: 'hl-q' */ 'highlight.js/lib/languages/q'),
+ qml: () => import(/* webpackChunkName: 'hl-qml' */ 'highlight.js/lib/languages/qml'),
+ r: () => import(/* webpackChunkName: 'hl-r' */ 'highlight.js/lib/languages/r'),
+ reasonml: () =>
+ import(/* webpackChunkName: 'hl-reasonml' */ 'highlight.js/lib/languages/reasonml'),
+ rib: () => import(/* webpackChunkName: 'hl-rib' */ 'highlight.js/lib/languages/rib'),
+ roboconf: () =>
+ import(/* webpackChunkName: 'hl-roboconf' */ 'highlight.js/lib/languages/roboconf'),
+ routeros: () =>
+ import(/* webpackChunkName: 'hl-routeros' */ 'highlight.js/lib/languages/routeros'),
+ rsl: () => import(/* webpackChunkName: 'hl-rsl' */ 'highlight.js/lib/languages/rsl'),
+ ruby: () => import(/* webpackChunkName: 'hl-ruby' */ 'highlight.js/lib/languages/ruby'),
+ ruleslanguage: () =>
+ import(/* webpackChunkName: 'hl-ruleslanguage' */ 'highlight.js/lib/languages/ruleslanguage'),
+ rust: () => import(/* webpackChunkName: 'hl-rust' */ 'highlight.js/lib/languages/rust'),
+ sas: () => import(/* webpackChunkName: 'hl-sas' */ 'highlight.js/lib/languages/sas'),
+ scala: () => import(/* webpackChunkName: 'hl-scala' */ 'highlight.js/lib/languages/scala'),
+ scheme: () => import(/* webpackChunkName: 'hl-scheme' */ 'highlight.js/lib/languages/scheme'),
+ scilab: () => import(/* webpackChunkName: 'hl-scilab' */ 'highlight.js/lib/languages/scilab'),
+ scss: () => import(/* webpackChunkName: 'hl-scss' */ 'highlight.js/lib/languages/scss'),
+ shell: () => import(/* webpackChunkName: 'hl-shell' */ 'highlight.js/lib/languages/shell'),
+ smali: () => import(/* webpackChunkName: 'hl-smali' */ 'highlight.js/lib/languages/smali'),
+ smalltalk: () =>
+ import(/* webpackChunkName: 'hl-smalltalk' */ 'highlight.js/lib/languages/smalltalk'),
+ sml: () => import(/* webpackChunkName: 'hl-sml' */ 'highlight.js/lib/languages/sml'),
+ sqf: () => import(/* webpackChunkName: 'hl-sqf' */ 'highlight.js/lib/languages/sqf'),
+ sql: () => import(/* webpackChunkName: 'hl-sql' */ 'highlight.js/lib/languages/sql'),
+ stan: () => import(/* webpackChunkName: 'hl-stan' */ 'highlight.js/lib/languages/stan'),
+ stata: () => import(/* webpackChunkName: 'hl-stata' */ 'highlight.js/lib/languages/stata'),
+ step21: () => import(/* webpackChunkName: 'hl-step21' */ 'highlight.js/lib/languages/step21'),
+ stylus: () => import(/* webpackChunkName: 'hl-stylus' */ 'highlight.js/lib/languages/stylus'),
+ subunit: () => import(/* webpackChunkName: 'hl-subunit' */ 'highlight.js/lib/languages/subunit'),
+ swift: () => import(/* webpackChunkName: 'hl-swift' */ 'highlight.js/lib/languages/swift'),
+ taggerscript: () =>
+ import(/* webpackChunkName: 'hl-taggerscript' */ 'highlight.js/lib/languages/taggerscript'),
+ tap: () => import(/* webpackChunkName: 'hl-tap' */ 'highlight.js/lib/languages/tap'),
+ tcl: () => import(/* webpackChunkName: 'hl-tcl' */ 'highlight.js/lib/languages/tcl'),
+ thrift: () => import(/* webpackChunkName: 'hl-thrift' */ 'highlight.js/lib/languages/thrift'),
+ tp: () => import(/* webpackChunkName: 'hl-tp' */ 'highlight.js/lib/languages/tp'),
+ twig: () => import(/* webpackChunkName: 'hl-twig' */ 'highlight.js/lib/languages/twig'),
+ typescript: () =>
+ import(/* webpackChunkName: 'hl-typescript' */ 'highlight.js/lib/languages/typescript'),
+ vala: () => import(/* webpackChunkName: 'hl-vala' */ 'highlight.js/lib/languages/vala'),
+ vbnet: () => import(/* webpackChunkName: 'hl-vbnet' */ 'highlight.js/lib/languages/vbnet'),
+ 'vbscript-html': () =>
+ import(/* webpackChunkName: 'hl-vbscript-html' */ 'highlight.js/lib/languages/vbscript-html'),
+ vbscript: () =>
+ import(/* webpackChunkName: 'hl-vbscript' */ 'highlight.js/lib/languages/vbscript'),
+ verilog: () => import(/* webpackChunkName: 'hl-verilog' */ 'highlight.js/lib/languages/verilog'),
+ vhdl: () => import(/* webpackChunkName: 'hl-vhdl' */ 'highlight.js/lib/languages/vhdl'),
+ vim: () => import(/* webpackChunkName: 'hl-vim' */ 'highlight.js/lib/languages/vim'),
+ wasm: () => import(/* webpackChunkName: 'hl-wasm' */ 'highlight.js/lib/languages/wasm'),
+ wren: () => import(/* webpackChunkName: 'hl-wren' */ 'highlight.js/lib/languages/wren'),
+ x86asm: () => import(/* webpackChunkName: 'hl-x86asm' */ 'highlight.js/lib/languages/x86asm'),
+ xl: () => import(/* webpackChunkName: 'hl-xl' */ 'highlight.js/lib/languages/xl'),
+ xml: () => import(/* webpackChunkName: 'hl-xml' */ 'highlight.js/lib/languages/xml'),
+ xquery: () => import(/* webpackChunkName: 'hl-xquery' */ 'highlight.js/lib/languages/xquery'),
+ yaml: () => import(/* webpackChunkName: 'hl-yaml' */ 'highlight.js/lib/languages/yaml'),
+ zephir: () => import(/* webpackChunkName: 'hl-zephir' */ 'highlight.js/lib/languages/zephir'),
+};
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index c2be7bc9195..d665f24bba1 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -48,7 +48,6 @@ import Text from '../extensions/text';
import Video from '../extensions/video';
import WordBreak from '../extensions/word_break';
import {
- isPlainURL,
renderCodeBlock,
renderHardBreak,
renderTable,
@@ -61,36 +60,30 @@ import {
renderPlayable,
renderHTMLNode,
renderContent,
+ preserveUnchanged,
+ bold,
+ italic,
+ link,
+ code,
} from './serialization_helpers';
const defaultSerializerConfig = {
marks: {
- [Bold.name]: defaultMarkdownSerializer.marks.strong,
- [Italic.name]: { open: '_', close: '_', mixable: true, expelEnclosingWhitespace: true },
- [Code.name]: defaultMarkdownSerializer.marks.code,
+ [Bold.name]: bold,
+ [Italic.name]: italic,
+ [Code.name]: code,
[Subscript.name]: { open: '<sub>', close: '</sub>', mixable: true },
[Superscript.name]: { open: '<sup>', close: '</sup>', mixable: true },
[InlineDiff.name]: {
mixable: true,
- open(state, mark) {
+ open(_, mark) {
return mark.attrs.type === 'addition' ? '{+' : '{-';
},
- close(state, mark) {
+ close(_, mark) {
return mark.attrs.type === 'addition' ? '+}' : '-}';
},
},
- [Link.name]: {
- open(state, mark, parent, index) {
- return isPlainURL(mark, parent, index, 1) ? '<' : '[';
- },
- close(state, mark, parent, index) {
- const href = mark.attrs.canonicalSrc || mark.attrs.href;
-
- return isPlainURL(mark, parent, index, -1)
- ? '>'
- : `](${state.esc(href)}${mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''})`;
- },
- },
+ [Link.name]: link,
[MathInline.name]: {
open: (...args) => `$${defaultMarkdownSerializer.marks.code.open(...args)}`,
close: (...args) => `${defaultMarkdownSerializer.marks.code.close(...args)}$`,
@@ -119,7 +112,7 @@ const defaultSerializerConfig = {
nodes: {
[Audio.name]: renderPlayable,
- [Blockquote.name]: (state, node) => {
+ [Blockquote.name]: preserveUnchanged((state, node) => {
if (node.attrs.multiline) {
state.write('>>>');
state.ensureNewLine();
@@ -130,9 +123,9 @@ const defaultSerializerConfig = {
} else {
state.wrapBlock('> ', null, node, () => state.renderContent(node));
}
- },
- [BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list,
- [CodeBlockHighlight.name]: renderCodeBlock,
+ }),
+ [BulletList.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.bullet_list),
+ [CodeBlockHighlight.name]: preserveUnchanged(renderCodeBlock),
[Diagram.name]: renderCodeBlock,
[Division.name]: (state, node) => {
if (node.attrs.className?.includes('js-markdown-code')) {
@@ -189,13 +182,13 @@ const defaultSerializerConfig = {
},
[Figure.name]: renderHTMLNode('figure'),
[FigureCaption.name]: renderHTMLNode('figcaption'),
- [HardBreak.name]: renderHardBreak,
- [Heading.name]: defaultMarkdownSerializer.nodes.heading,
- [HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule,
- [Image.name]: renderImage,
- [ListItem.name]: defaultMarkdownSerializer.nodes.list_item,
- [OrderedList.name]: renderOrderedList,
- [Paragraph.name]: defaultMarkdownSerializer.nodes.paragraph,
+ [HardBreak.name]: preserveUnchanged(renderHardBreak),
+ [Heading.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.heading),
+ [HorizontalRule.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.horizontal_rule),
+ [Image.name]: preserveUnchanged(renderImage),
+ [ListItem.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.list_item),
+ [OrderedList.name]: preserveUnchanged(renderOrderedList),
+ [Paragraph.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.paragraph),
[Reference.name]: (state, node) => {
state.write(node.attrs.originalText || node.attrs.text);
},
@@ -221,29 +214,60 @@ const defaultSerializerConfig = {
},
};
+const createChangeTracker = (doc, pristineDoc) => {
+ const changeTracker = new WeakMap();
+ const pristineSourceMarkdownMap = new Map();
+
+ if (doc && pristineDoc) {
+ pristineDoc.descendants((node) => {
+ if (node.attrs.sourceMapKey) {
+ pristineSourceMarkdownMap.set(`${node.attrs.sourceMapKey}${node.type.name}`, node);
+ }
+ });
+ doc.descendants((node) => {
+ const pristineNode = pristineSourceMarkdownMap.get(
+ `${node.attrs.sourceMapKey}${node.type.name}`,
+ );
+
+ if (pristineNode) {
+ changeTracker.set(node, node.eq(pristineNode));
+ }
+ });
+ }
+
+ return changeTracker;
+};
+
/**
- * A markdown serializer converts arbitrary Markdown content
- * into a ProseMirror document and viceversa. To convert Markdown
- * into a ProseMirror document, the Markdown should be rendered.
+ * Converts a ProseMirror document to Markdown. See the
+ * following documentation to learn how to implement
+ * custom node and mark serializer functions.
+ *
+ * https://github.com/prosemirror/prosemirror-markdown
*
- * The client should provide a render function to allow flexibility
- * on the desired rendering approach.
+ * @param {Object} params.nodes ProseMirror node serializer functions
+ * @param {Object} params.marks ProseMirror marks serializer config
*
- * @param {Function} params.render Render function
- * that parses the Markdown and converts it into HTML.
* @returns a markdown serializer
*/
export default ({ serializerConfig = {} } = {}) => ({
/**
- * Converts a ProseMirror JSONDocument based
- * on a ProseMirror schema into Markdown
- * @param {ProseMirror.Schema} params.schema A ProseMirror schema that defines
- * the types of content supported in the document
- * @param {String} params.content A ProseMirror JSONDocument
- * @returns A Markdown string
+ * Serializes a ProseMirror document as Markdown. If a node contains
+ * sourcemap metadata, the serializer is capable of restoring the
+ * Markdown from which the node was generated using a Markdown
+ * deserializer.
+ *
+ * See the Sourcemap metadata extension and the remark_markdown_deserializer
+ * service for more information.
+ *
+ * @param {ProseMirror.Node} params.doc ProseMirror document to convert into Markdown
+ * @param {ProseMirror.Node} params.pristineDoc Pristine version of the document that
+ * should be converted into Markdown. This is used to detect which nodes in the document
+ * changed.
+ * @returns A String that represents the serialized document as Markdown
*/
- serialize: ({ schema, content }) => {
- const proseMirrorDocument = schema.nodeFromJSON(content);
+ serialize: ({ doc, pristineDoc }) => {
+ const changeTracker = createChangeTracker(doc, pristineDoc);
const serializer = new ProseMirrorMarkdownSerializer(
{
...defaultSerializerConfig.nodes,
@@ -255,8 +279,9 @@ export default ({ serializerConfig = {} } = {}) => ({
},
);
- return serializer.serialize(proseMirrorDocument, {
+ return serializer.serialize(doc, {
tightLists: true,
+ changeTracker,
});
},
});
diff --git a/app/assets/javascripts/content_editor/services/markdown_sourcemap.js b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js
index 4285e04bbab..fe1b32c5b0a 100644
--- a/app/assets/javascripts/content_editor/services/markdown_sourcemap.js
+++ b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js
@@ -30,7 +30,7 @@ export const getMarkdownSource = (element) => {
for (let i = range.start.row; i <= range.end.row; i += 1) {
if (i === range.start.row) {
- elSource += source[i]?.substring(range.start.col);
+ elSource += source[i].substring(range.start.col);
} else if (i === range.end.row) {
elSource += `\n${source[i]?.substring(0, range.start.col)}`;
} else {
diff --git a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js
new file mode 100644
index 00000000000..770de1df0d0
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js
@@ -0,0 +1,87 @@
+import { isString } from 'lodash';
+import { render } from '~/lib/gfm';
+import { createProseMirrorDocFromMdastTree } from './hast_to_prosemirror_converter';
+
+const factorySpecs = {
+ blockquote: { block: 'blockquote' },
+ p: { block: 'paragraph' },
+ li: { block: 'listItem', wrapTextInParagraph: true },
+ ul: { block: 'bulletList' },
+ ol: { block: 'orderedList' },
+ h1: {
+ block: 'heading',
+ getAttrs: () => ({ level: 1 }),
+ },
+ h2: {
+ block: 'heading',
+ getAttrs: () => ({ level: 2 }),
+ },
+ h3: {
+ block: 'heading',
+ getAttrs: () => ({ level: 3 }),
+ },
+ h4: {
+ block: 'heading',
+ getAttrs: () => ({ level: 4 }),
+ },
+ h5: {
+ block: 'heading',
+ getAttrs: () => ({ level: 5 }),
+ },
+ h6: {
+ block: 'heading',
+ getAttrs: () => ({ level: 6 }),
+ },
+ pre: {
+ block: 'codeBlock',
+ skipChildren: true,
+ getContent: ({ hastNodeText }) => hastNodeText.replace(/\n$/, ''),
+ getAttrs: (hastNode) => {
+ const languageClass = hastNode.children[0]?.properties.className?.[0];
+ const language = isString(languageClass) ? languageClass.replace('language-', '') : null;
+
+ return { language };
+ },
+ },
+ hr: { inline: 'horizontalRule' },
+ img: {
+ inline: 'image',
+ getAttrs: (hastNode) => ({
+ src: hastNode.properties.src,
+ title: hastNode.properties.title,
+ alt: hastNode.properties.alt,
+ }),
+ },
+ br: { inline: 'hardBreak' },
+ code: { mark: 'code' },
+ em: { mark: 'italic' },
+ i: { mark: 'italic' },
+ strong: { mark: 'bold' },
+ b: { mark: 'bold' },
+ a: {
+ mark: 'link',
+ getAttrs: (hastNode) => ({
+ href: hastNode.properties.href,
+ title: hastNode.properties.title,
+ }),
+ },
+};
+
+export default () => {
+ return {
+ deserialize: async ({ schema, content: markdown }) => {
+ const document = await render({
+ markdown,
+ renderer: (tree) =>
+ createProseMirrorDocFromMdastTree({
+ schema,
+ factorySpecs,
+ tree,
+ source: markdown,
+ }),
+ });
+
+ return { document };
+ },
+ };
+};
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index 3e48434c6f9..089d30edec7 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -349,3 +349,134 @@ export function renderCodeBlock(state, node) {
state.write('```');
state.closeBlock(node);
}
+
+export function preserveUnchanged(render) {
+ return (state, node, parent, index) => {
+ const { sourceMarkdown } = node.attrs;
+ const same = state.options.changeTracker.get(node);
+
+ if (same) {
+ state.write(sourceMarkdown);
+ state.closeBlock(node);
+ } else {
+ render(state, node, parent, index);
+ }
+ };
+}
+
+const generateBoldTags = (open = true) => {
+ return (_, mark) => {
+ const type = /^(\*\*|__|<strong|<b).*/.exec(mark.attrs.sourceMarkdown)?.[1];
+
+ switch (type) {
+ case '**':
+ case '__':
+ return type;
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ case '<strong':
+ case '<b':
+ return (open ? openTag : closeTag)(type.substring(1));
+ default:
+ return '**';
+ }
+ };
+};
+
+export const bold = {
+ open: generateBoldTags(),
+ close: generateBoldTags(false),
+ mixable: true,
+ expelEnclosingWhitespace: true,
+};
+
+const generateItalicTag = (open = true) => {
+ return (_, mark) => {
+ const type = /^(\*|_|<em|<i).*/.exec(mark.attrs.sourceMarkdown)?.[1];
+
+ switch (type) {
+ case '*':
+ case '_':
+ return type;
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ case '<em':
+ case '<i':
+ return (open ? openTag : closeTag)(type.substring(1));
+ default:
+ return '_';
+ }
+ };
+};
+
+export const italic = {
+ open: generateItalicTag(),
+ close: generateItalicTag(false),
+ mixable: true,
+ expelEnclosingWhitespace: true,
+};
+
+const generateCodeTag = (open = true) => {
+ return (_, mark) => {
+ const type = /^(`|<code).*/.exec(mark.attrs.sourceMarkdown)?.[1];
+
+ if (type === '<code') {
+ return (open ? openTag : closeTag)(type.substring(1));
+ }
+
+ return '`';
+ };
+};
+
+export const code = {
+ open: generateCodeTag(),
+ close: generateCodeTag(false),
+ mixable: true,
+ expelEnclosingWhitespace: true,
+};
+
+const LINK_HTML = 'linkHtml';
+const LINK_MARKDOWN = 'linkMarkdown';
+
+const linkType = (sourceMarkdown) => {
+ const expression = /^(\[|<a).*/.exec(sourceMarkdown)?.[1];
+
+ if (!expression || expression === '[') {
+ return LINK_MARKDOWN;
+ }
+
+ return LINK_HTML;
+};
+
+export const link = {
+ open(state, mark, parent, index) {
+ if (isPlainURL(mark, parent, index, 1)) {
+ return '<';
+ }
+
+ const { canonicalSrc, href, title, sourceMarkdown } = mark.attrs;
+
+ if (linkType(sourceMarkdown) === LINK_MARKDOWN) {
+ return '[';
+ }
+
+ const attrs = { href: state.esc(href || canonicalSrc) };
+
+ if (title) {
+ attrs.title = title;
+ }
+
+ return openTag('a', attrs);
+ },
+ close(state, mark, parent, index) {
+ if (isPlainURL(mark, parent, index, -1)) {
+ return '>';
+ }
+
+ const { canonicalSrc, href, title, sourceMarkdown } = mark.attrs;
+
+ if (linkType(sourceMarkdown) === LINK_HTML) {
+ return closeTag('a');
+ }
+
+ return `](${state.esc(canonicalSrc || href)}${title ? ` ${state.quote(title)}` : ''})`;
+ },
+};
diff --git a/app/assets/javascripts/content_editor/services/upload_helpers.js b/app/assets/javascripts/content_editor/services/upload_helpers.js
index ed2c4b39131..09f0738b51b 100644
--- a/app/assets/javascripts/content_editor/services/upload_helpers.js
+++ b/app/assets/javascripts/content_editor/services/upload_helpers.js
@@ -70,6 +70,8 @@ const uploadContent = async ({ type, editor, file, uploadsPath, renderMarkdown,
const position = state.selection.from - 1;
const { tr } = state;
+ editor.commands.setNodeSelection(position);
+
try {
const { src, canonicalSrc } = await uploadFile({ file, uploadsPath, renderMarkdown });
@@ -81,6 +83,8 @@ const uploadContent = async ({ type, editor, file, uploadsPath, renderMarkdown,
canonicalSrc,
}),
);
+
+ editor.commands.setNodeSelection(position);
} catch (e) {
editor.commands.deleteRange({ from: position, to: position + 1 });
eventHub.$emit('alert', {
diff --git a/app/assets/javascripts/content_editor/services/utils.js b/app/assets/javascripts/content_editor/services/utils.js
index b3856b0dd74..e352fa8a9db 100644
--- a/app/assets/javascripts/content_editor/services/utils.js
+++ b/app/assets/javascripts/content_editor/services/utils.js
@@ -15,7 +15,7 @@ export const hasSelection = (tiptapEditor) => {
* @returns {string}
*/
export const extractFilename = (src) => {
- return src.replace(/^.*\/|\..+?$/g, '');
+ return src.replace(/^.*\/|\.[^.]+?$/g, '');
};
export const readFileAsDataURL = (file) => {
diff --git a/app/assets/javascripts/create_cluster/components/cluster_form_dropdown.vue b/app/assets/javascripts/create_cluster/components/cluster_form_dropdown.vue
deleted file mode 100644
index fa0a17f3643..00000000000
--- a/app/assets/javascripts/create_cluster/components/cluster_form_dropdown.vue
+++ /dev/null
@@ -1,246 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-import $ from 'jquery';
-import { isNil } from 'lodash';
-import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
-import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
-import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
-
-const toArray = (value) => (isNil(value) ? [] : [].concat(value));
-const itemsProp = (items, prop) => items.map((item) => item[prop]);
-const defaultSearchFn = (searchQuery, labelProp) => (item) =>
- item[labelProp].toLowerCase().indexOf(searchQuery) > -1;
-
-export default {
- components: {
- DropdownButton,
- DropdownSearchInput,
- DropdownHiddenInput,
- GlIcon,
- },
- props: {
- fieldName: {
- type: String,
- required: false,
- default: '',
- },
- placeholder: {
- type: String,
- required: false,
- default: '',
- },
- defaultValue: {
- type: String,
- required: false,
- default: '',
- },
- value: {
- type: [Object, Array, String],
- required: false,
- default: () => null,
- },
- labelProperty: {
- type: String,
- required: false,
- default: 'name',
- },
- valueProperty: {
- type: String,
- required: false,
- default: 'value',
- },
- items: {
- type: Array,
- required: false,
- default: () => [],
- },
- loading: {
- type: Boolean,
- required: false,
- default: false,
- },
- disabled: {
- type: Boolean,
- required: false,
- default: false,
- },
- loadingText: {
- type: String,
- required: false,
- default: '',
- },
- disabledText: {
- type: String,
- required: false,
- default: '',
- },
- hasErrors: {
- type: Boolean,
- required: false,
- default: false,
- },
- multiple: {
- type: Boolean,
- required: false,
- default: false,
- },
- errorMessage: {
- type: String,
- required: false,
- default: '',
- },
- searchFieldPlaceholder: {
- type: String,
- required: false,
- default: '',
- },
- emptyText: {
- type: String,
- required: false,
- default: '',
- },
- searchFn: {
- type: Function,
- required: false,
- default: defaultSearchFn,
- },
- },
- data() {
- return {
- searchQuery: '',
- focusOnSearch: false,
- };
- },
- computed: {
- toggleText() {
- if (this.loading && this.loadingText) {
- return this.loadingText;
- }
-
- if (this.disabled && this.disabledText) {
- return this.disabledText;
- }
-
- if (!this.selectedItems.length) {
- return this.placeholder;
- }
-
- return this.selectedItemsLabels;
- },
- results() {
- return this.getItemsOrEmptyList().filter(this.searchFn(this.searchQuery, this.labelProperty));
- },
- selectedItems() {
- const valueProp = this.valueProperty;
- const valueList = toArray(this.value);
- const items = this.getItemsOrEmptyList();
-
- return items.filter((item) => valueList.some((value) => item[valueProp] === value));
- },
- selectedItemsLabels() {
- return itemsProp(this.selectedItems, this.labelProperty).join(', ');
- },
- selectedItemsValues() {
- return itemsProp(this.selectedItems, this.valueProperty).join(', ');
- },
- },
- mounted() {
- $(this.$refs.dropdown)
- .on('shown.bs.dropdown', () => {
- this.focusOnSearch = true;
- })
- .on('hidden.bs.dropdown', () => {
- this.focusOnSearch = false;
- });
- },
- beforeDestroy() {
- // eslint-disable-next-line @gitlab/no-global-event-off
- $(this.$refs.dropdown).off();
- },
- methods: {
- getItemsOrEmptyList() {
- return this.items || [];
- },
- selectSingle(item) {
- this.$emit('input', item[this.valueProperty]);
- },
- selectMultiple(item) {
- const value = toArray(this.value);
- const itemValue = item[this.valueProperty];
- const itemValueIndex = value.indexOf(itemValue);
-
- if (itemValueIndex > -1) {
- value.splice(itemValueIndex, 1);
- } else {
- value.push(itemValue);
- }
-
- this.$emit('input', value);
- },
- isSelected(item) {
- return this.selectedItems.includes(item);
- },
- },
-};
-</script>
-
-<template>
- <div>
- <div ref="dropdown" class="dropdown">
- <dropdown-hidden-input :name="fieldName" :value="selectedItemsValues" />
- <dropdown-button
- :class="{ 'border-danger': hasErrors }"
- :is-disabled="disabled"
- :is-loading="loading"
- :toggle-text="toggleText"
- />
- <div class="dropdown-menu dropdown-select">
- <dropdown-search-input
- v-model="searchQuery"
- :focused="focusOnSearch"
- :placeholder-text="searchFieldPlaceholder"
- />
- <div class="dropdown-content">
- <ul>
- <li v-if="!results.length">
- <span class="js-empty-text menu-item">{{ emptyText }}</span>
- </li>
- <li v-for="item in results" :key="item.id">
- <button
- v-if="multiple"
- class="js-dropdown-item d-flex align-items-center"
- type="button"
- @click.stop.prevent="selectMultiple(item)"
- >
- <gl-icon
- :class="[{ invisible: !isSelected(item) }, 'mr-1']"
- name="mobile-issue-close"
- />
- <slot name="item" :item="item">{{ item.name }}</slot>
- </button>
- <button
- v-else
- class="js-dropdown-item"
- type="button"
- @click.prevent="selectSingle(item)"
- >
- <slot name="item" :item="item">{{ item.name }}</slot>
- </button>
- </li>
- </ul>
- </div>
- </div>
- </div>
- <span
- v-if="hasErrors && errorMessage"
- :class="[
- 'form-text js-eks-dropdown-error-message',
- {
- 'text-danger': hasErrors,
- 'text-muted': !hasErrors,
- },
- ]"
- >{{ errorMessage }}</span
- >
- </div>
-</template>
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue
deleted file mode 100644
index ba170dc0e19..00000000000
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-<script>
-import { mapState } from 'vuex';
-import EksClusterConfigurationForm from './eks_cluster_configuration_form.vue';
-import ServiceCredentialsForm from './service_credentials_form.vue';
-
-export default {
- components: {
- ServiceCredentialsForm,
- EksClusterConfigurationForm,
- },
- props: {
- gitlabManagedClusterHelpPath: {
- type: String,
- required: true,
- },
- namespacePerEnvironmentHelpPath: {
- type: String,
- required: true,
- },
- kubernetesIntegrationHelpPath: {
- type: String,
- required: true,
- },
- accountAndExternalIdsHelpPath: {
- type: String,
- required: true,
- },
- createRoleArnHelpPath: {
- type: String,
- required: true,
- },
- externalLinkIcon: {
- type: String,
- required: true,
- },
- },
- computed: {
- ...mapState(['hasCredentials']),
- },
-};
-</script>
-<template>
- <div class="js-create-eks-cluster">
- <eks-cluster-configuration-form
- v-if="hasCredentials"
- :gitlab-managed-cluster-help-path="gitlabManagedClusterHelpPath"
- :namespace-per-environment-help-path="namespacePerEnvironmentHelpPath"
- :kubernetes-integration-help-path="kubernetesIntegrationHelpPath"
- :external-link-icon="externalLinkIcon"
- />
- <service-credentials-form
- v-else
- :create-role-arn-help-path="createRoleArnHelpPath"
- :account-and-external-ids-help-path="accountAndExternalIdsHelpPath"
- :external-link-icon="externalLinkIcon"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
deleted file mode 100644
index 73458a463f2..00000000000
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
+++ /dev/null
@@ -1,530 +0,0 @@
-<script>
-import {
- GlFormGroup,
- GlFormInput,
- GlFormCheckbox,
- GlIcon,
- GlLink,
- GlSprintf,
- GlButton,
-} from '@gitlab/ui';
-import { createNamespacedHelpers, mapState, mapActions, mapGetters } from 'vuex';
-import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
-import { s__ } from '~/locale';
-import { KUBERNETES_VERSIONS } from '../constants';
-
-const { mapState: mapRolesState, mapActions: mapRolesActions } = createNamespacedHelpers('roles');
-const { mapState: mapKeyPairsState, mapActions: mapKeyPairsActions } = createNamespacedHelpers(
- 'keyPairs',
-);
-const { mapState: mapVpcsState, mapActions: mapVpcActions } = createNamespacedHelpers('vpcs');
-const { mapState: mapSubnetsState, mapActions: mapSubnetActions } = createNamespacedHelpers(
- 'subnets',
-);
-const {
- mapState: mapSecurityGroupsState,
- mapActions: mapSecurityGroupsActions,
-} = createNamespacedHelpers('securityGroups');
-const { mapState: mapInstanceTypesState } = createNamespacedHelpers('instanceTypes');
-
-export default {
- components: {
- ClusterFormDropdown,
- GlFormCheckbox,
- GlFormGroup,
- GlFormInput,
- GlIcon,
- GlLink,
- GlSprintf,
- GlButton,
- },
- props: {
- gitlabManagedClusterHelpPath: {
- type: String,
- required: true,
- },
- namespacePerEnvironmentHelpPath: {
- type: String,
- required: true,
- },
- kubernetesIntegrationHelpPath: {
- type: String,
- required: true,
- },
- externalLinkIcon: {
- type: String,
- required: true,
- },
- },
- i18n: {
- kubernetesIntegrationHelpText: s__(
- 'ClusterIntegration|Read our %{linkStart}help page%{linkEnd} on Kubernetes cluster integration.',
- ),
- roleDropdownHelpText: s__(
- 'ClusterIntegration|Your service role is distinct from the provision role used when authenticating. It will allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. To use a new role, first create one on %{linkStart}Amazon Web Services%{linkEnd}.',
- ),
- roleDropdownHelpPath:
- 'https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html#create-service-role',
- regionInputLabel: s__('ClusterIntegration|Cluster Region'),
- regionHelpText: s__(
- 'ClusterIntegration|The region the new cluster will be created in. You must reauthenticate to change regions.',
- ),
- keyPairDropdownHelpText: s__(
- 'ClusterIntegration|Select the key pair name that will be used to create EC2 nodes. To use a new key pair name, first create one on %{linkStart}Amazon Web Services%{linkEnd}.',
- ),
- keyPairDropdownHelpPath:
- 'https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html#having-ec2-create-your-key-pair',
- vpcDropdownHelpText: s__(
- 'ClusterIntegration|Select a VPC to use for your EKS Cluster resources. To use a new VPC, first create one on %{linkStart}Amazon Web Services %{linkEnd}.',
- ),
- vpcDropdownHelpPath:
- 'https://docs.aws.amazon.com/eks/latest/userguide/getting-started-console.html#vpc-create',
- subnetDropdownHelpText: s__(
- 'ClusterIntegration|Choose the %{linkStart}subnets %{linkEnd} in your VPC where your worker nodes will run.',
- ),
- subnetDropdownHelpPath: 'https://console.aws.amazon.com/vpc/home?#subnets',
- securityGroupDropdownHelpText: s__(
- 'ClusterIntegration|Choose the %{linkStart}security group%{linkEnd} to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets.',
- ),
- securityGroupDropdownHelpPath: 'https://console.aws.amazon.com/vpc/home?#securityGroups',
- instanceTypesDropdownHelpText: s__(
- 'ClusterIntegration|Choose the worker node %{linkStart}instance type%{linkEnd}.',
- ),
- instanceTypesDropdownHelpPath: 'https://aws.amazon.com/ec2/instance-types',
- gitlabManagedClusterHelpText: s__(
- 'ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster. %{linkStart}More information%{linkEnd}',
- ),
- namespacePerEnvironmentHelpText: 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. %{linkStart}More information%{linkEnd}',
- ),
- },
- computed: {
- ...mapState([
- 'clusterName',
- 'environmentScope',
- 'kubernetesVersion',
- 'selectedRegion',
- 'selectedKeyPair',
- 'selectedVpc',
- 'selectedSubnet',
- 'selectedRole',
- 'selectedSecurityGroup',
- 'selectedInstanceType',
- 'nodeCount',
- 'gitlabManagedCluster',
- 'namespacePerEnvironment',
- 'isCreatingCluster',
- ]),
- ...mapGetters(['subnetValid']),
- ...mapRolesState({
- roles: 'items',
- isLoadingRoles: 'isLoadingItems',
- loadingRolesError: 'loadingItemsError',
- }),
- ...mapKeyPairsState({
- keyPairs: 'items',
- isLoadingKeyPairs: 'isLoadingItems',
- loadingKeyPairsError: 'loadingItemsError',
- }),
- ...mapVpcsState({
- vpcs: 'items',
- isLoadingVpcs: 'isLoadingItems',
- loadingVpcsError: 'loadingItemsError',
- }),
- ...mapSubnetsState({
- subnets: 'items',
- isLoadingSubnets: 'isLoadingItems',
- loadingSubnetsError: 'loadingItemsError',
- }),
- ...mapSecurityGroupsState({
- securityGroups: 'items',
- isLoadingSecurityGroups: 'isLoadingItems',
- loadingSecurityGroupsError: 'loadingItemsError',
- }),
- ...mapInstanceTypesState({
- instanceTypes: 'items',
- isLoadingInstanceTypes: 'isLoadingItems',
- loadingInstanceTypesError: 'loadingItemsError',
- }),
- kubernetesVersions() {
- return KUBERNETES_VERSIONS;
- },
- vpcDropdownDisabled() {
- return !this.selectedRegion;
- },
- keyPairDropdownDisabled() {
- return !this.selectedRegion;
- },
- subnetDropdownDisabled() {
- return !this.selectedVpc;
- },
- securityGroupDropdownDisabled() {
- return !this.selectedVpc;
- },
- createClusterButtonDisabled() {
- return (
- !this.clusterName ||
- !this.environmentScope ||
- !this.kubernetesVersion ||
- !this.selectedRegion ||
- !this.selectedKeyPair ||
- !this.selectedVpc ||
- !this.subnetValid ||
- !this.selectedRole ||
- !this.selectedSecurityGroup ||
- !this.selectedInstanceType ||
- !this.nodeCount ||
- this.isCreatingCluster
- );
- },
- displaySubnetError() {
- return Boolean(this.loadingSubnetsError) || this.selectedSubnet?.length === 1;
- },
- createClusterButtonLabel() {
- return this.isCreatingCluster
- ? s__('ClusterIntegration|Creating Kubernetes cluster')
- : s__('ClusterIntegration|Create Kubernetes cluster');
- },
- subnetValidationErrorText() {
- if (this.loadingSubnetsError) {
- return s__('ClusterIntegration|Could not load subnets for the selected VPC');
- }
-
- return s__('ClusterIntegration|You should select at least two subnets');
- },
- },
- mounted() {
- this.fetchRoles();
- this.setRegionAndFetchVpcsAndKeyPairs();
- },
- methods: {
- ...mapActions([
- 'createCluster',
- 'setClusterName',
- 'setEnvironmentScope',
- 'setKubernetesVersion',
- 'setRegion',
- 'setVpc',
- 'setSubnet',
- 'setRole',
- 'setKeyPair',
- 'setSecurityGroup',
- 'setInstanceType',
- 'setNodeCount',
- 'setGitlabManagedCluster',
- 'setNamespacePerEnvironment',
- ]),
- ...mapVpcActions({ fetchVpcs: 'fetchItems' }),
- ...mapSubnetActions({ fetchSubnets: 'fetchItems' }),
- ...mapRolesActions({ fetchRoles: 'fetchItems' }),
- ...mapKeyPairsActions({ fetchKeyPairs: 'fetchItems' }),
- ...mapSecurityGroupsActions({ fetchSecurityGroups: 'fetchItems' }),
- setRegionAndFetchVpcsAndKeyPairs() {
- this.setVpc({ vpc: null });
- this.setKeyPair({ keyPair: null });
- this.setSubnet({ subnet: [] });
- this.setSecurityGroup({ securityGroup: null });
- this.fetchVpcs({ region: this.selectedRegion });
- this.fetchKeyPairs({ region: this.selectedRegion });
- },
- setVpcAndFetchSubnets(vpc) {
- this.setVpc({ vpc });
- this.setSubnet({ subnet: [] });
- this.setSecurityGroup({ securityGroup: null });
- this.fetchSubnets({ vpc, region: this.selectedRegion });
- this.fetchSecurityGroups({ vpc, region: this.selectedRegion });
- },
- },
-};
-</script>
-<template>
- <form name="eks-cluster-configuration-form">
- <h4>
- {{ s__('ClusterIntegration|Enter the details for your Amazon EKS Kubernetes cluster') }}
- </h4>
- <div class="mb-3">
- <gl-sprintf :message="$options.i18n.kubernetesIntegrationHelpText">
- <template #link="{ content }">
- <gl-link :href="kubernetesIntegrationHelpPath">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </div>
- <div class="form-group">
- <label class="label-bold" for="eks-cluster-name">{{
- s__('ClusterIntegration|Kubernetes cluster name')
- }}</label>
- <gl-form-input
- id="eks-cluster-name"
- :value="clusterName"
- @input="setClusterName({ clusterName: $event })"
- />
- </div>
- <div class="form-group">
- <label class="label-bold" for="eks-environment-scope">{{
- s__('ClusterIntegration|Environment scope')
- }}</label>
- <gl-form-input
- id="eks-environment-scope"
- :value="environmentScope"
- @input="setEnvironmentScope({ environmentScope: $event })"
- />
- </div>
- <div class="form-group">
- <label class="label-bold" for="eks-kubernetes-version">{{
- s__('ClusterIntegration|Kubernetes version')
- }}</label>
- <cluster-form-dropdown
- field-id="eks-kubernetes-version"
- field-name="eks-kubernetes-version"
- :value="kubernetesVersion"
- :items="kubernetesVersions"
- :empty-text="s__('ClusterIntegration|Kubernetes version not found')"
- @input="setKubernetesVersion({ kubernetesVersion: $event })"
- />
- </div>
- <div class="form-group">
- <label class="label-bold" for="eks-role">{{ s__('ClusterIntegration|Service role') }}</label>
- <cluster-form-dropdown
- field-id="eks-role"
- field-name="eks-role"
- :value="selectedRole"
- :items="roles"
- :loading="isLoadingRoles"
- :loading-text="s__('ClusterIntegration|Loading IAM Roles')"
- :placeholder="s__('ClusterIntegration|Select service role')"
- :search-field-placeholder="s__('ClusterIntegration|Search IAM Roles')"
- :empty-text="s__('ClusterIntegration|No IAM Roles found')"
- :has-errors="Boolean(loadingRolesError)"
- :error-message="s__('ClusterIntegration|Could not load IAM roles')"
- @input="setRole({ role: $event })"
- />
- <p class="form-text text-muted">
- <gl-sprintf :message="$options.i18n.roleDropdownHelpText">
- <template #link="{ content }">
- <gl-link :href="$options.i18n.roleDropdownHelpPath" target="_blank">
- {{ content }}
- <gl-icon name="external-link" class="gl-vertical-align-middle" />
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
- <gl-form-group
- :label="$options.i18n.regionInputLabel"
- :description="$options.i18n.regionHelpText"
- >
- <gl-form-input id="eks-region" :value="selectedRegion" type="text" readonly />
- </gl-form-group>
- <div class="form-group">
- <label class="label-bold" for="eks-key-pair">{{
- s__('ClusterIntegration|Key pair name')
- }}</label>
- <cluster-form-dropdown
- field-id="eks-key-pair"
- field-name="eks-key-pair"
- :value="selectedKeyPair"
- :items="keyPairs"
- :disabled="keyPairDropdownDisabled"
- :disabled-text="s__('ClusterIntegration|Select a region to choose a Key Pair')"
- :loading="isLoadingKeyPairs"
- :loading-text="s__('ClusterIntegration|Loading Key Pairs')"
- :placeholder="s__('ClusterIntegration|Select key pair')"
- :search-field-placeholder="s__('ClusterIntegration|Search Key Pairs')"
- :empty-text="s__('ClusterIntegration|No Key Pairs found')"
- :has-errors="Boolean(loadingKeyPairsError)"
- :error-message="s__('ClusterIntegration|Could not load Key Pairs')"
- @input="setKeyPair({ keyPair: $event })"
- />
- <p class="form-text text-muted">
- <gl-sprintf :message="$options.i18n.keyPairDropdownHelpText">
- <template #link="{ content }">
- <gl-link :href="$options.i18n.keyPairDropdownHelpPath" target="_blank">
- {{ content }}
- <gl-icon name="external-link" class="gl-vertical-align-middle" />
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
- <div class="form-group">
- <label class="label-bold" for="eks-vpc">{{ s__('ClusterIntegration|VPC') }}</label>
- <cluster-form-dropdown
- field-id="eks-vpc"
- field-name="eks-vpc"
- :value="selectedVpc"
- :items="vpcs"
- :loading="isLoadingVpcs"
- :disabled="vpcDropdownDisabled"
- :disabled-text="s__('ClusterIntegration|Select a region to choose a VPC')"
- :loading-text="s__('ClusterIntegration|Loading VPCs')"
- :placeholder="s__('ClusterIntegration|Select a VPC')"
- :search-field-placeholder="s__('ClusterIntegration|Search VPCs')"
- :empty-text="s__('ClusterIntegration|No VPCs found')"
- :has-errors="Boolean(loadingVpcsError)"
- :error-message="s__('ClusterIntegration|Could not load VPCs for the selected region')"
- @input="setVpcAndFetchSubnets($event)"
- />
- <p class="form-text text-muted">
- <gl-sprintf :message="$options.i18n.vpcDropdownHelpText">
- <template #link="{ content }">
- <gl-link :href="$options.i18n.vpcDropdownHelpPath" target="_blank">
- {{ content }}
- <gl-icon name="external-link" class="gl-vertical-align-middle" />
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
- <div class="form-group">
- <label class="label-bold" for="eks-role">{{ s__('ClusterIntegration|Subnets') }}</label>
- <cluster-form-dropdown
- field-id="eks-subnet"
- field-name="eks-subnet"
- multiple
- :value="selectedSubnet"
- :items="subnets"
- :loading="isLoadingSubnets"
- :disabled="subnetDropdownDisabled"
- :disabled-text="s__('ClusterIntegration|Select a VPC to choose a subnet')"
- :loading-text="s__('ClusterIntegration|Loading subnets')"
- :placeholder="s__('ClusterIntegration|Select a subnet')"
- :search-field-placeholder="s__('ClusterIntegration|Search subnets')"
- :empty-text="s__('ClusterIntegration|No subnet found')"
- :has-errors="displaySubnetError"
- :error-message="subnetValidationErrorText"
- @input="setSubnet({ subnet: $event })"
- />
- <p class="form-text text-muted">
- <gl-sprintf :message="$options.i18n.subnetDropdownHelpText">
- <template #link="{ content }">
- <gl-link :href="$options.i18n.subnetDropdownHelpPath" target="_blank">
- {{ content }}
- <gl-icon name="external-link" class="gl-vertical-align-middle" />
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
- <div class="form-group">
- <label class="label-bold" for="eks-security-group">{{
- s__('ClusterIntegration|Security group')
- }}</label>
- <cluster-form-dropdown
- field-id="eks-security-group"
- field-name="eks-security-group"
- :value="selectedSecurityGroup"
- :items="securityGroups"
- :loading="isLoadingSecurityGroups"
- :disabled="securityGroupDropdownDisabled"
- :disabled-text="s__('ClusterIntegration|Select a VPC to choose a security group')"
- :loading-text="s__('ClusterIntegration|Loading security groups')"
- :placeholder="s__('ClusterIntegration|Select a security group')"
- :search-field-placeholder="s__('ClusterIntegration|Search security groups')"
- :empty-text="s__('ClusterIntegration|No security group found')"
- :has-errors="Boolean(loadingSecurityGroupsError)"
- :error-message="
- s__('ClusterIntegration|Could not load security groups for the selected VPC')
- "
- @input="setSecurityGroup({ securityGroup: $event })"
- />
- <p class="form-text text-muted">
- <gl-sprintf :message="$options.i18n.securityGroupDropdownHelpText">
- <template #link="{ content }">
- <gl-link :href="$options.i18n.securityGroupDropdownHelpPath" target="_blank">
- {{ content }}
- <gl-icon name="external-link" class="gl-vertical-align-middle" />
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
- <div class="form-group">
- <label class="label-bold" for="eks-instance-type">{{
- s__('ClusterIntegration|Instance type')
- }}</label>
- <cluster-form-dropdown
- field-id="eks-instance-type"
- field-name="eks-instance-type"
- :value="selectedInstanceType"
- :items="instanceTypes"
- :loading="isLoadingInstanceTypes"
- :loading-text="s__('ClusterIntegration|Loading instance types')"
- :placeholder="s__('ClusterIntegration|Select an instance type')"
- :search-field-placeholder="s__('ClusterIntegration|Search instance types')"
- :empty-text="s__('ClusterIntegration|No instance type found')"
- :has-errors="Boolean(loadingInstanceTypesError)"
- :error-message="s__('ClusterIntegration|Could not load instance types')"
- @input="setInstanceType({ instanceType: $event })"
- />
- <p class="form-text text-muted">
- <gl-sprintf :message="$options.i18n.instanceTypesDropdownHelpText">
- <template #link="{ content }">
- <gl-link :href="$options.i18n.instanceTypesDropdownHelpPath" target="_blank">
- {{ content }}
- <gl-icon name="external-link" class="gl-vertical-align-middle" />
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
- <div class="form-group">
- <label class="label-bold" for="eks-node-count">{{
- s__('ClusterIntegration|Number of nodes')
- }}</label>
- <gl-form-input
- id="eks-node-count"
- type="number"
- min="1"
- step="1"
- :value="nodeCount"
- @input="setNodeCount({ nodeCount: $event })"
- />
- </div>
- <div class="form-group">
- <gl-form-checkbox
- :checked="gitlabManagedCluster"
- @input="setGitlabManagedCluster({ gitlabManagedCluster: $event })"
- >{{ s__('ClusterIntegration|GitLab-managed cluster') }}</gl-form-checkbox
- >
- <p class="form text text-muted">
- <gl-sprintf :message="$options.i18n.gitlabManagedClusterHelpText">
- <template #link="{ content }">
- <gl-link :href="gitlabManagedClusterHelpPath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
- <div class="form-group">
- <gl-form-checkbox
- :checked="namespacePerEnvironment"
- @input="setNamespacePerEnvironment({ namespacePerEnvironment: $event })"
- >{{ s__('ClusterIntegration|Namespace per environment') }}</gl-form-checkbox
- >
- <p class="form text text-muted">
- <gl-sprintf :message="$options.i18n.namespacePerEnvironmentHelpText">
- <template #link="{ content }">
- <gl-link :href="namespacePerEnvironmentHelpPath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
- <div class="form-group">
- <gl-button
- variant="success"
- category="primary"
- class="js-create-cluster"
- :disabled="createClusterButtonDisabled"
- :loading="isCreatingCluster"
- @click="createCluster()"
- >
- {{ createClusterButtonLabel }}
- </gl-button>
- </div>
- </form>
-</template>
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
deleted file mode 100644
index 004c2e26c4e..00000000000
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
+++ /dev/null
@@ -1,182 +0,0 @@
-<script>
-import { GlButton, GlFormGroup, GlFormInput, GlIcon, GlLink, GlSprintf, GlAlert } from '@gitlab/ui';
-import { mapState, mapActions } from 'vuex';
-import { s__, __ } from '~/locale';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import { DEFAULT_REGION } from '../constants';
-
-export default {
- components: {
- GlButton,
- GlFormGroup,
- GlFormInput,
- GlIcon,
- GlLink,
- GlSprintf,
- ClipboardButton,
- GlAlert,
- },
- props: {
- accountAndExternalIdsHelpPath: {
- type: String,
- required: true,
- },
- createRoleArnHelpPath: {
- type: String,
- required: true,
- },
- externalLinkIcon: {
- type: String,
- required: true,
- },
- },
- i18n: {
- regionInputLabel: s__('ClusterIntegration|Cluster Region'),
- regionHelpPath: 'https://aws.amazon.com/about-aws/global-infrastructure/regions_az/',
- regionHelpText: s__(
- 'ClusterIntegration|Select the region you want to create the new cluster in. Make sure you have access to this region for your role to be able to authenticate. If no region is selected, we will use %{codeStart}DEFAULT_REGION%{codeEnd}. Learn more about %{linkStart}Regions%{linkEnd}.',
- ),
- accountAndExternalIdsHelpText: s__(
- 'ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provisioned role, first create one on %{awsLinkStart}Amazon Web Services %{awsLinkEnd} using the above account and external IDs. %{moreInfoStart}More information%{moreInfoEnd}',
- ),
- regionHelpTextDefaultRegion: DEFAULT_REGION,
- },
- data() {
- return {
- roleArn: this.$store.state.roleArn,
- selectedRegion: this.$store.state.selectedRegion,
- };
- },
- computed: {
- ...mapState(['accountId', 'externalId', 'isCreatingRole', 'createRoleError']),
- submitButtonDisabled() {
- return this.isCreatingRole || !this.roleArn;
- },
- submitButtonLabel() {
- return this.isCreatingRole
- ? __('Authenticating')
- : s__('ClusterIntegration|Authenticate with AWS');
- },
- awsHelpLink() {
- return 'https://console.aws.amazon.com/iam/home?#roles';
- },
- },
- methods: {
- ...mapActions(['createRole']),
- },
-};
-</script>
-<template>
- <form name="service-credentials-form">
- <h4>{{ s__('ClusterIntegration|Authenticate with Amazon Web Services') }}</h4>
- <p>
- {{
- s__(
- 'ClusterIntegration|You must grant access to your organization’s AWS resources in order to create a new EKS cluster. To grant access, create a provision role using the account and external ID below and provide us the ARN.',
- )
- }}
- </p>
- <gl-alert
- v-if="createRoleError"
- class="js-invalid-credentials gl-mb-5"
- variant="danger"
- :dismissible="false"
- >
- {{ createRoleError }}
- </gl-alert>
- <div class="form-row">
- <div class="form-group col-md-6">
- <label for="gitlab-account-id">{{ __('Account ID') }}</label>
- <div class="input-group">
- <gl-form-input id="gitlab-account-id" type="text" readonly :value="accountId" />
- <div class="input-group-append">
- <clipboard-button
- :text="accountId"
- :title="__('Copy Account ID to clipboard')"
- class="input-group-text js-copy-account-id-button"
- />
- </div>
- </div>
- </div>
- <div class="form-group col-md-6">
- <label for="eks-external-id">{{ __('External ID') }}</label>
- <div class="input-group">
- <gl-form-input id="eks-external-id" type="text" readonly :value="externalId" />
- <div class="input-group-append">
- <clipboard-button
- :text="externalId"
- :title="__('Copy External ID to clipboard')"
- class="input-group-text js-copy-external-id-button"
- />
- </div>
- </div>
- </div>
- <div class="col-12 mb-3 mt-n3">
- <p class="form-text text-muted">
- <gl-sprintf :message="$options.i18n.accountAndExternalIdsHelpText">
- <template #awsLink="{ content }">
- <gl-link :href="awsHelpLink" target="_blank">
- {{ content }}
- <gl-icon name="external-link" class="gl-vertical-align-middle" />
- </gl-link>
- </template>
- <template #moreInfo="{ content }">
- <gl-link :href="accountAndExternalIdsHelpPath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
- </div>
- <div class="form-group">
- <label for="eks-provision-role-arn">{{ s__('ClusterIntegration|Provision Role ARN') }}</label>
- <gl-form-input id="eks-provision-role-arn" v-model="roleArn" />
- <p class="form-text text-muted">
- <gl-sprintf :message="$options.i18n.accountAndExternalIdsHelpText">
- <template #awsLink="{ content }">
- <gl-link :href="awsHelpLink" target="_blank">
- {{ content }}
- <gl-icon name="external-link" class="gl-vertical-align-middle" />
- </gl-link>
- </template>
- <template #moreInfo="{ content }">
- <gl-link :href="accountAndExternalIdsHelpPath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
-
- <gl-form-group :label="$options.i18n.regionInputLabel">
- <gl-form-input id="eks-region" v-model="selectedRegion" type="text" />
-
- <template #description>
- <gl-sprintf :message="$options.i18n.regionHelpText">
- <template #code>
- <code>{{ $options.i18n.regionHelpTextDefaultRegion }}</code>
- </template>
-
- <template #link="{ content }">
- <gl-link :href="$options.i18n.regionHelpPath" target="_blank">
- {{ content }}
- <gl-icon name="external-link" />
- </gl-link>
- </template>
- </gl-sprintf>
- </template>
- </gl-form-group>
-
- <gl-button
- variant="success"
- category="primary"
- type="submit"
- :disabled="submitButtonDisabled"
- :loading="isCreatingRole"
- @click.prevent="createRole({ roleArn, selectedRegion, externalId })"
- >
- {{ submitButtonLabel }}
- </gl-button>
- </form>
-</template>
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/constants.js b/app/assets/javascripts/create_cluster/eks_cluster/constants.js
deleted file mode 100644
index 3ed0f050301..00000000000
--- a/app/assets/javascripts/create_cluster/eks_cluster/constants.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export const DEFAULT_REGION = 'us-east-2';
-
-export const KUBERNETES_VERSIONS = [
- { name: '1.16', value: '1.16' },
- { name: '1.17', value: '1.17' },
- { name: '1.18', value: '1.18' },
- { name: '1.19', value: '1.19' },
- { name: '1.20', value: '1.20', default: true },
-];
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/index.js b/app/assets/javascripts/create_cluster/eks_cluster/index.js
deleted file mode 100644
index 38b7eefd15b..00000000000
--- a/app/assets/javascripts/create_cluster/eks_cluster/index.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import CreateEksCluster from './components/create_eks_cluster.vue';
-import createStore from './store';
-
-Vue.use(Vuex);
-
-export default (el) => {
- const {
- gitlabManagedClusterHelpPath,
- namespacePerEnvironmentHelpPath,
- kubernetesIntegrationHelpPath,
- accountAndExternalIdsHelpPath,
- createRoleArnHelpPath,
- externalId,
- accountId,
- instanceTypes,
- hasCredentials,
- createRolePath,
- createClusterPath,
- externalLinkIcon,
- roleArn,
- } = el.dataset;
-
- return new Vue({
- el,
- store: createStore({
- initialState: {
- hasCredentials: parseBoolean(hasCredentials),
- externalId,
- accountId,
- instanceTypes: JSON.parse(instanceTypes),
- createRolePath,
- createClusterPath,
- roleArn,
- },
- }),
- components: {
- CreateEksCluster,
- },
- render(createElement) {
- return createElement('create-eks-cluster', {
- props: {
- gitlabManagedClusterHelpPath,
- namespacePerEnvironmentHelpPath,
- kubernetesIntegrationHelpPath,
- accountAndExternalIdsHelpPath,
- createRoleArnHelpPath,
- externalLinkIcon,
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js b/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js
deleted file mode 100644
index bd9554521b8..00000000000
--- a/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import EC2 from 'aws-sdk/clients/ec2';
-import IAM from 'aws-sdk/clients/iam';
-import AWS from 'aws-sdk/global';
-
-const lookupVpcName = ({ Tags: tags, VpcId: id }) => {
- const nameTag = tags.find(({ Key: key }) => key === 'Name');
-
- return nameTag ? nameTag.Value : id;
-};
-
-export const setAWSConfig = ({ awsCredentials }) => {
- AWS.config = awsCredentials;
-};
-
-export const fetchRoles = () => {
- const iam = new IAM();
-
- return iam
- .listRoles()
- .promise()
- .then(({ Roles: roles }) => roles.map(({ RoleName: name, Arn: value }) => ({ name, value })));
-};
-
-export const fetchKeyPairs = ({ region }) => {
- const ec2 = new EC2({ region });
-
- return ec2
- .describeKeyPairs()
- .promise()
- .then(({ KeyPairs: keyPairs }) => keyPairs.map(({ KeyName: name }) => ({ name, value: name })));
-};
-
-export const fetchVpcs = ({ region }) => {
- const ec2 = new EC2({ region });
-
- return ec2
- .describeVpcs()
- .promise()
- .then(({ Vpcs: vpcs }) =>
- vpcs.map((vpc) => ({
- value: vpc.VpcId,
- name: lookupVpcName(vpc),
- })),
- );
-};
-
-export const fetchSubnets = ({ vpc, region }) => {
- const ec2 = new EC2({ region });
-
- return ec2
- .describeSubnets({
- Filters: [
- {
- Name: 'vpc-id',
- Values: [vpc],
- },
- ],
- })
- .promise()
- .then(({ Subnets: subnets }) => subnets.map(({ SubnetId: id }) => ({ value: id, name: id })));
-};
-
-export const fetchSecurityGroups = ({ region, vpc }) => {
- const ec2 = new EC2({ region });
-
- return ec2
- .describeSecurityGroups({
- Filters: [
- {
- Name: 'vpc-id',
- Values: [vpc],
- },
- ],
- })
- .promise()
- .then(({ SecurityGroups: securityGroups }) =>
- securityGroups.map(({ GroupName: name, GroupId: value }) => ({ name, value })),
- );
-};
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
deleted file mode 100644
index cd8212a40f9..00000000000
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
+++ /dev/null
@@ -1,148 +0,0 @@
-import createFlash from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { DEFAULT_REGION } from '../constants';
-import { setAWSConfig } from '../services/aws_services_facade';
-import * as types from './mutation_types';
-
-const getErrorMessage = (data) => {
- const errorKey = Object.keys(data)[0];
-
- return data[errorKey][0];
-};
-
-export const setClusterName = ({ commit }, payload) => {
- commit(types.SET_CLUSTER_NAME, payload);
-};
-
-export const setEnvironmentScope = ({ commit }, payload) => {
- commit(types.SET_ENVIRONMENT_SCOPE, payload);
-};
-
-export const setKubernetesVersion = ({ commit }, payload) => {
- commit(types.SET_KUBERNETES_VERSION, payload);
-};
-
-export const createRole = ({ dispatch, state: { createRolePath } }, payload) => {
- dispatch('requestCreateRole');
-
- const region = payload.selectedRegion || DEFAULT_REGION;
-
- return axios
- .post(createRolePath, {
- role_arn: payload.roleArn,
- role_external_id: payload.externalId,
- region,
- })
- .then(({ data }) => {
- const awsData = {
- ...convertObjectPropsToCamelCase(data),
- region,
- };
-
- dispatch('createRoleSuccess', awsData);
- })
- .catch((error) => {
- let message = error;
- if (error?.response?.data?.message) {
- message = error.response.data.message;
- }
- dispatch('createRoleError', { error: message });
- });
-};
-
-export const requestCreateRole = ({ commit }) => {
- commit(types.REQUEST_CREATE_ROLE);
-};
-
-export const createRoleSuccess = ({ dispatch, commit }, awsCredentials) => {
- dispatch('setRegion', { region: awsCredentials.region });
- setAWSConfig({ awsCredentials });
- commit(types.CREATE_ROLE_SUCCESS);
-};
-
-export const createRoleError = ({ commit }, payload) => {
- commit(types.CREATE_ROLE_ERROR, payload);
-};
-
-export const createCluster = ({ dispatch, state }) => {
- dispatch('requestCreateCluster');
-
- return axios
- .post(state.createClusterPath, {
- name: state.clusterName,
- environment_scope: state.environmentScope,
- managed: state.gitlabManagedCluster,
- namespace_per_environment: state.namespacePerEnvironment,
- provider_aws_attributes: {
- kubernetes_version: state.kubernetesVersion,
- region: state.selectedRegion,
- vpc_id: state.selectedVpc,
- subnet_ids: state.selectedSubnet,
- role_arn: state.selectedRole,
- key_name: state.selectedKeyPair,
- security_group_id: state.selectedSecurityGroup,
- instance_type: state.selectedInstanceType,
- num_nodes: state.nodeCount,
- },
- })
- .then(({ headers: { location } }) => dispatch('createClusterSuccess', location))
- .catch(({ response: { data } }) => {
- dispatch('createClusterError', data);
- });
-};
-
-export const requestCreateCluster = ({ commit }) => {
- commit(types.REQUEST_CREATE_CLUSTER);
-};
-
-export const createClusterSuccess = (_, location) => {
- window.location.assign(location);
-};
-
-export const createClusterError = ({ commit }, error) => {
- commit(types.CREATE_CLUSTER_ERROR, error);
- createFlash({
- message: getErrorMessage(error),
- });
-};
-
-export const setRegion = ({ commit }, payload) => {
- commit(types.SET_REGION, payload);
-};
-
-export const setKeyPair = ({ commit }, payload) => {
- commit(types.SET_KEY_PAIR, payload);
-};
-
-export const setVpc = ({ commit }, payload) => {
- commit(types.SET_VPC, payload);
-};
-
-export const setSubnet = ({ commit }, payload) => {
- commit(types.SET_SUBNET, payload);
-};
-
-export const setRole = ({ commit }, payload) => {
- commit(types.SET_ROLE, payload);
-};
-
-export const setSecurityGroup = ({ commit }, payload) => {
- commit(types.SET_SECURITY_GROUP, payload);
-};
-
-export const setGitlabManagedCluster = ({ commit }, payload) => {
- commit(types.SET_GITLAB_MANAGED_CLUSTER, payload);
-};
-
-export const setNamespacePerEnvironment = ({ commit }, payload) => {
- commit(types.SET_NAMESPACE_PER_ENVIRONMENT, payload);
-};
-
-export const setInstanceType = ({ commit }, payload) => {
- commit(types.SET_INSTANCE_TYPE, payload);
-};
-
-export const setNodeCount = ({ commit }, payload) => {
- commit(types.SET_NODE_COUNT, payload);
-};
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/getters.js b/app/assets/javascripts/create_cluster/eks_cluster/store/getters.js
deleted file mode 100644
index d8489ca31cf..00000000000
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/getters.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export const subnetValid = ({ selectedSubnet }) =>
- Array.isArray(selectedSubnet) && selectedSubnet.length >= 2;
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/index.js b/app/assets/javascripts/create_cluster/eks_cluster/store/index.js
deleted file mode 100644
index ed054989771..00000000000
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/index.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import Vuex from 'vuex';
-import clusterDropdownStore from '~/create_cluster/store/cluster_dropdown';
-import {
- fetchRoles,
- fetchKeyPairs,
- fetchVpcs,
- fetchSubnets,
- fetchSecurityGroups,
-} from '../services/aws_services_facade';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import state from './state';
-
-const createStore = ({ initialState }) =>
- new Vuex.Store({
- actions,
- getters,
- mutations,
- state: Object.assign(state(), initialState),
- modules: {
- roles: {
- namespaced: true,
- ...clusterDropdownStore({ fetchFn: fetchRoles }),
- },
- keyPairs: {
- namespaced: true,
- ...clusterDropdownStore({ fetchFn: fetchKeyPairs }),
- },
- vpcs: {
- namespaced: true,
- ...clusterDropdownStore({ fetchFn: fetchVpcs }),
- },
- subnets: {
- namespaced: true,
- ...clusterDropdownStore({ fetchFn: fetchSubnets }),
- },
- securityGroups: {
- namespaced: true,
- ...clusterDropdownStore({ fetchFn: fetchSecurityGroups }),
- },
- instanceTypes: {
- namespaced: true,
- ...clusterDropdownStore({ initialState: { items: initialState.instanceTypes } }),
- },
- },
- });
-
-export default createStore;
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js b/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js
deleted file mode 100644
index 4a48195a27b..00000000000
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js
+++ /dev/null
@@ -1,19 +0,0 @@
-export const SET_CLUSTER_NAME = 'SET_CLUSTER_NAME';
-export const SET_ENVIRONMENT_SCOPE = 'SET_ENVIRONMENT_SCOPE';
-export const SET_KUBERNETES_VERSION = 'SET_KUBERNETES_VERSION';
-export const SET_REGION = 'SET_REGION';
-export const SET_VPC = 'SET_VPC';
-export const SET_KEY_PAIR = 'SET_KEY_PAIR';
-export const SET_SUBNET = 'SET_SUBNET';
-export const SET_ROLE = 'SET_ROLE';
-export const SET_SECURITY_GROUP = 'SET_SECURITY_GROUP';
-export const SET_INSTANCE_TYPE = 'SET_INSTANCE_TYPE';
-export const SET_NODE_COUNT = 'SET_NODE_COUNT';
-export const SET_GITLAB_MANAGED_CLUSTER = 'SET_GITLAB_MANAGED_CLUSTER';
-export const SET_NAMESPACE_PER_ENVIRONMENT = 'SET_NAMESPACE_PER_ENVIRONMENT';
-export const REQUEST_CREATE_ROLE = 'REQUEST_CREATE_ROLE';
-export const CREATE_ROLE_SUCCESS = 'CREATE_ROLE_SUCCESS';
-export const CREATE_ROLE_ERROR = 'CREATE_ROLE_ERROR';
-export const REQUEST_CREATE_CLUSTER = 'REQUEST_CREATE_CLUSTER';
-export const CREATE_CLUSTER_SUCCESS = 'CREATE_CLUSTER_SUCCESS';
-export const CREATE_CLUSTER_ERROR = 'CREATE_CLUSTER_ERROR';
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js b/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js
deleted file mode 100644
index f57236e0e31..00000000000
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import * as types from './mutation_types';
-
-export default {
- [types.SET_CLUSTER_NAME](state, { clusterName }) {
- state.clusterName = clusterName;
- },
- [types.SET_ENVIRONMENT_SCOPE](state, { environmentScope }) {
- state.environmentScope = environmentScope;
- },
- [types.SET_KUBERNETES_VERSION](state, { kubernetesVersion }) {
- state.kubernetesVersion = kubernetesVersion;
- },
- [types.SET_REGION](state, { region }) {
- state.selectedRegion = region;
- },
- [types.SET_KEY_PAIR](state, { keyPair }) {
- state.selectedKeyPair = keyPair;
- },
- [types.SET_VPC](state, { vpc }) {
- state.selectedVpc = vpc;
- },
- [types.SET_SUBNET](state, { subnet }) {
- state.selectedSubnet = subnet;
- },
- [types.SET_ROLE](state, { role }) {
- state.selectedRole = role;
- },
- [types.SET_SECURITY_GROUP](state, { securityGroup }) {
- state.selectedSecurityGroup = securityGroup;
- },
- [types.SET_INSTANCE_TYPE](state, { instanceType }) {
- state.selectedInstanceType = instanceType;
- },
- [types.SET_NODE_COUNT](state, { nodeCount }) {
- state.nodeCount = nodeCount;
- },
- [types.SET_GITLAB_MANAGED_CLUSTER](state, { gitlabManagedCluster }) {
- state.gitlabManagedCluster = gitlabManagedCluster;
- },
- [types.SET_NAMESPACE_PER_ENVIRONMENT](state, { namespacePerEnvironment }) {
- state.namespacePerEnvironment = namespacePerEnvironment;
- },
- [types.REQUEST_CREATE_ROLE](state) {
- state.isCreatingRole = true;
- state.createRoleError = null;
- state.hasCredentials = false;
- },
- [types.CREATE_ROLE_SUCCESS](state) {
- state.isCreatingRole = false;
- state.createRoleError = null;
- state.hasCredentials = true;
- },
- [types.CREATE_ROLE_ERROR](state, { error }) {
- state.isCreatingRole = false;
- state.createRoleError = error;
- state.hasCredentials = false;
- },
- [types.REQUEST_CREATE_CLUSTER](state) {
- state.isCreatingCluster = true;
- state.createClusterError = null;
- },
- [types.CREATE_CLUSTER_ERROR](state, { error }) {
- state.isCreatingCluster = false;
- state.createClusterError = error;
- },
-};
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
deleted file mode 100644
index c906ddf9011..00000000000
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { KUBERNETES_VERSIONS } from '../constants';
-
-const kubernetesVersion = KUBERNETES_VERSIONS.find((version) => version.default).value;
-
-export default () => ({
- createRolePath: null,
-
- isCreatingRole: false,
- roleCreated: false,
- createRoleError: false,
-
- accountId: '',
- externalId: '',
-
- roleArn: '',
-
- clusterName: '',
- environmentScope: '*',
- kubernetesVersion,
- selectedRegion: '',
- selectedRole: '',
- selectedKeyPair: '',
- selectedVpc: '',
- selectedSubnet: [],
- selectedSecurityGroup: '',
- selectedInstanceType: 'm5.large',
- nodeCount: '3',
-
- isCreatingCluster: false,
- createClusterError: false,
-
- gitlabManagedCluster: true,
- namespacePerEnvironment: true,
-});
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js
deleted file mode 100644
index 1246fdb19d7..00000000000
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
-import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
-import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
-
-import store from '../store';
-
-export default {
- store,
- components: {
- DropdownButton,
- DropdownSearchInput,
- DropdownHiddenInput,
- GlLoadingIcon,
- },
- props: {
- fieldId: {
- type: String,
- required: true,
- },
- fieldName: {
- type: String,
- required: true,
- },
- defaultValue: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- return {
- isLoading: false,
- hasErrors: false,
- searchQuery: '',
- gapiError: '',
- };
- },
- computed: {
- results() {
- if (!this.items) {
- return [];
- }
-
- return this.items.filter((item) => item.name.toLowerCase().indexOf(this.searchQuery) > -1);
- },
- },
- methods: {
- fetchSuccessHandler() {
- if (this.defaultValue) {
- const itemToSelect = this.items.find((item) => item.name === this.defaultValue);
-
- if (itemToSelect) {
- this.setItem(itemToSelect.name);
- }
- }
-
- this.isLoading = false;
- this.hasErrors = false;
- },
- fetchFailureHandler(resp) {
- this.isLoading = false;
- this.hasErrors = true;
-
- if (resp.result && resp.result.error) {
- this.gapiError = resp.result.error.message;
- }
- },
- },
-};
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue
deleted file mode 100644
index 23c477bfbfd..00000000000
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue
+++ /dev/null
@@ -1,112 +0,0 @@
-<script>
-import { mapState, mapGetters, mapActions } from 'vuex';
-import { sprintf, s__ } from '~/locale';
-
-import gkeDropdownMixin from './gke_dropdown_mixin';
-
-export default {
- name: 'GkeMachineTypeDropdown',
- mixins: [gkeDropdownMixin],
- computed: {
- ...mapState([
- 'isValidatingProjectBilling',
- 'projectHasBillingEnabled',
- 'selectedZone',
- 'selectedMachineType',
- ]),
- ...mapState({ items: 'machineTypes' }),
- ...mapGetters(['hasZone', 'hasMachineType']),
- isDisabled() {
- return (
- this.isLoading ||
- this.isValidatingProjectBilling ||
- !this.projectHasBillingEnabled ||
- !this.hasZone
- );
- },
- toggleText() {
- if (this.isLoading) {
- return s__('ClusterIntegration|Fetching machine types');
- }
-
- if (this.selectedMachineType) {
- return this.selectedMachineType;
- }
-
- if (!this.projectHasBillingEnabled && !this.hasZone) {
- return s__('ClusterIntegration|Select project and zone to choose machine type');
- }
-
- return !this.hasZone
- ? s__('ClusterIntegration|Select zone to choose machine type')
- : s__('ClusterIntegration|Select machine type');
- },
- errorMessage() {
- return sprintf(
- s__(
- 'ClusterIntegration|An error occurred while trying to fetch zone machine types: %{error}',
- ),
- { error: this.gapiError },
- );
- },
- },
- watch: {
- selectedZone() {
- this.hasErrors = false;
-
- if (this.hasZone) {
- this.isLoading = true;
-
- this.fetchMachineTypes().then(this.fetchSuccessHandler).catch(this.fetchFailureHandler);
- }
- },
- },
- methods: {
- ...mapActions(['fetchMachineTypes']),
- ...mapActions({ setItem: 'setMachineType' }),
- },
-};
-</script>
-
-<template>
- <div>
- <div class="js-gcp-machine-type-dropdown dropdown">
- <dropdown-hidden-input :name="fieldName" :value="selectedMachineType" />
- <dropdown-button
- :class="{ 'border-danger': hasErrors }"
- :is-disabled="isDisabled"
- :is-loading="isLoading"
- :toggle-text="toggleText"
- />
- <div class="dropdown-menu dropdown-select">
- <dropdown-search-input
- v-model="searchQuery"
- :placeholder-text="s__('ClusterIntegration|Search machine types')"
- />
- <div class="dropdown-content">
- <ul>
- <li v-show="!results.length">
- <span class="menu-item">
- {{ s__('ClusterIntegration|No machine types matched your search') }}
- </span>
- </li>
- <li v-for="result in results" :key="result.id">
- <button type="button" @click.prevent="setItem(result.name)">{{ result.name }}</button>
- </li>
- </ul>
- </div>
- <div class="dropdown-loading"><gl-loading-icon size="sm" /></div>
- </div>
- </div>
- <span
- v-if="hasErrors"
- :class="{
- 'text-danger': hasErrors,
- 'text-muted': !hasErrors,
- }"
- class="form-text"
- >
- {{ errorMessage }}
- </span>
- </div>
-</template>
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue
deleted file mode 100644
index 8f18ac29c0f..00000000000
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<script>
-import { createNamespacedHelpers, mapState, mapGetters, mapActions } from 'vuex';
-
-import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
-
-const { mapState: mapDropdownState } = createNamespacedHelpers('networks');
-const { mapActions: mapSubnetworkActions } = createNamespacedHelpers('subnetworks');
-
-export default {
- components: {
- ClusterFormDropdown,
- },
- props: {
- fieldName: {
- type: String,
- required: true,
- },
- },
- computed: {
- ...mapState(['selectedNetwork']),
- ...mapDropdownState(['items', 'isLoadingItems', 'loadingItemsError']),
- ...mapGetters(['hasZone', 'projectId', 'region']),
- },
- methods: {
- ...mapActions(['setNetwork', 'setSubnetwork']),
- ...mapSubnetworkActions({ fetchSubnetworks: 'fetchItems' }),
- setNetworkAndFetchSubnetworks(network) {
- const { projectId: project, region } = this;
-
- this.setSubnetwork('');
- this.setNetwork(network);
- this.fetchSubnetworks({ project, region, network: network.selfLink });
- },
- },
-};
-</script>
-<template>
- <cluster-form-dropdown
- :field-name="fieldName"
- :value="selectedNetwork"
- :items="items"
- :disabled="!hasZone"
- :loading="isLoadingItems"
- :has-errors="Boolean(loadingItemsError)"
- :loading-text="s__('ClusterIntegration|Loading networks')"
- :placeholder="s__('ClusterIntegration|Select a network')"
- :search-field-placeholder="s__('ClusterIntegration|Search networks')"
- :empty-text="s__('ClusterIntegration|No networks found')"
- :error-message="s__('ClusterIntegration|Could not load networks')"
- :disabled-text="s__('ClusterIntegration|Select a zone to choose a network')"
- @input="setNetworkAndFetchSubnetworks"
- />
-</template>
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue
deleted file mode 100644
index aba6dd4b493..00000000000
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue
+++ /dev/null
@@ -1,194 +0,0 @@
-<script>
-import { GlSprintf, GlLink, GlIcon } from '@gitlab/ui';
-import { mapState, mapGetters, mapActions } from 'vuex';
-import { s__ } from '~/locale';
-
-import gkeDropdownMixin from './gke_dropdown_mixin';
-
-export default {
- name: 'GkeProjectIdDropdown',
- components: {
- GlSprintf,
- GlLink,
- GlIcon,
- },
- mixins: [gkeDropdownMixin],
- props: {
- docsUrl: {
- type: String,
- required: true,
- },
- },
- computed: {
- ...mapState(['selectedProject', 'isValidatingProjectBilling', 'projectHasBillingEnabled']),
- ...mapState({ items: 'projects' }),
- ...mapGetters(['hasProject']),
- hasOneProject() {
- return this.items && this.items.length === 1;
- },
- isDisabled() {
- return (
- this.isLoading || this.isValidatingProjectBilling || (this.items && this.items.length < 2)
- );
- },
- toggleText() {
- if (this.isValidatingProjectBilling) {
- return s__('ClusterIntegration|Validating project billing status');
- }
-
- if (this.isLoading) {
- return s__('ClusterIntegration|Fetching projects');
- }
-
- if (this.hasProject) {
- return this.selectedProject.name;
- }
-
- if (!this.items) {
- return s__('ClusterIntegration|No projects found');
- }
-
- return s__('ClusterIntegration|Select project');
- },
- helpText() {
- if (this.hasErrors) {
- return this.errorMessage;
- }
-
- if (!this.items) {
- return s__(
- 'ClusterIntegration|We were unable to fetch any projects. Ensure that you have a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.',
- );
- }
-
- return this.items.length
- ? s__(
- 'ClusterIntegration|To use a new project, first create one on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.',
- )
- : s__(
- 'ClusterIntegration|To create a cluster, first create a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.',
- );
- },
- errorMessage() {
- if (!this.projectHasBillingEnabled) {
- if (this.gapiError) {
- return s__(
- 'ClusterIntegration|We could not verify that one of your projects on GCP has billing enabled. Please try again.',
- );
- }
-
- return s__(
- 'ClusterIntegration|This project does not have billing enabled. To create a cluster, %{linkToBillingStart}enable billing%{linkToBillingEnd} and try again.',
- );
- }
-
- return s__(
- 'ClusterIntegration|An error occurred while trying to fetch your projects: %{error}',
- );
- },
- },
- watch: {
- selectedProject() {
- this.setIsValidatingProjectBilling(true);
-
- this.validateProjectBilling()
- .then(this.validateProjectBillingSuccessHandler)
- .catch(this.validateProjectBillingFailureHandler);
- },
- },
- created() {
- this.isLoading = true;
-
- this.fetchProjects().then(this.fetchSuccessHandler).catch(this.fetchFailureHandler);
- },
- methods: {
- ...mapActions(['fetchProjects', 'setIsValidatingProjectBilling', 'validateProjectBilling']),
- ...mapActions({ setItem: 'setProject' }),
- fetchSuccessHandler() {
- if (this.defaultValue) {
- const projectToSelect = this.items.find((item) => item.projectId === this.defaultValue);
-
- if (projectToSelect) {
- this.setItem(projectToSelect);
- }
- } else if (this.items.length === 1) {
- this.setItem(this.items[0]);
- }
-
- this.isLoading = false;
- this.hasErrors = false;
- },
- validateProjectBillingSuccessHandler() {
- this.hasErrors = !this.projectHasBillingEnabled;
- },
- validateProjectBillingFailureHandler(resp) {
- this.hasErrors = true;
-
- this.gapiError = resp.result ? resp.result.error.message : resp;
- },
- },
-};
-</script>
-
-<template>
- <div>
- <div class="js-gcp-project-id-dropdown dropdown">
- <dropdown-hidden-input :name="fieldName" :value="selectedProject.projectId" />
- <dropdown-button
- :class="{
- 'border-danger': hasErrors,
- 'read-only': hasOneProject,
- }"
- :is-disabled="isDisabled"
- :is-loading="isLoading"
- :toggle-text="toggleText"
- />
- <div class="dropdown-menu dropdown-select">
- <dropdown-search-input
- v-model="searchQuery"
- :placeholder-text="s__('ClusterIntegration|Search projects')"
- />
- <div class="dropdown-content">
- <ul>
- <li v-show="!results.length">
- <span class="menu-item">
- {{ s__('ClusterIntegration|No projects matched your search') }}
- </span>
- </li>
- <li v-for="result in results" :key="result.project_number">
- <button type="button" @click.prevent="setItem(result)">{{ result.name }}</button>
- </li>
- </ul>
- </div>
- <div class="dropdown-loading"><gl-loading-icon size="sm" /></div>
- </div>
- </div>
- <span
- :class="{
- 'text-danger': hasErrors,
- 'text-muted': !hasErrors,
- }"
- class="form-text"
- >
- <gl-sprintf :message="helpText">
- <template #linkToBilling="{ content }">
- <gl-link
- :href="'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral'"
- target="_blank"
- >{{ content }} <gl-icon name="external-link"
- /></gl-link>
- </template>
-
- <template #docsLink="{ content }">
- <gl-link :href="docsUrl" target="_blank"
- >{{ content }} <gl-icon name="external-link"
- /></gl-link>
- </template>
-
- <template #error>
- {{ gapiError }}
- </template>
- </gl-sprintf>
- </span>
- </div>
-</template>
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_submit_button.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_submit_button.vue
deleted file mode 100644
index a7e08a5e97f..00000000000
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_submit_button.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-<script>
-import { mapGetters } from 'vuex';
-
-export default {
- computed: {
- ...mapGetters(['hasValidData']),
- },
-};
-</script>
-<template>
- <button
- type="submit"
- :disabled="!hasValidData"
- class="js-gke-cluster-creation-submit btn btn-success"
- >
- {{ s__('ClusterIntegration|Create Kubernetes cluster') }}
- </button>
-</template>
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue
deleted file mode 100644
index dab4adc3789..00000000000
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<script>
-import { createNamespacedHelpers, mapState, mapGetters, mapActions } from 'vuex';
-
-import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
-
-const { mapState: mapDropdownState } = createNamespacedHelpers('subnetworks');
-
-export default {
- components: {
- ClusterFormDropdown,
- },
- props: {
- fieldName: {
- type: String,
- required: true,
- },
- },
- computed: {
- ...mapState(['selectedSubnetwork']),
- ...mapDropdownState(['items', 'isLoadingItems', 'loadingItemsError']),
- ...mapGetters(['hasNetwork']),
- },
- methods: {
- ...mapActions(['setSubnetwork']),
- },
-};
-</script>
-<template>
- <cluster-form-dropdown
- :field-name="fieldName"
- :value="selectedSubnetwork"
- :items="items"
- :disabled="!hasNetwork"
- :loading="isLoadingItems"
- :has-errors="Boolean(loadingItemsError)"
- :loading-text="s__('ClusterIntegration|Loading subnetworks')"
- :placeholder="s__('ClusterIntegration|Select a subnetwork')"
- :search-field-placeholder="s__('ClusterIntegration|Search subnetworks')"
- :empty-text="s__('ClusterIntegration|No subnetworks found')"
- :error-message="s__('ClusterIntegration|Could not load subnetworks')"
- :disabled-text="s__('ClusterIntegration|Select a network to choose a subnetwork')"
- @input="setSubnetwork"
- />
-</template>
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue
deleted file mode 100644
index 027ce74753e..00000000000
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<script>
-import { mapState, mapActions } from 'vuex';
-import { sprintf, s__ } from '~/locale';
-
-import gkeDropdownMixin from './gke_dropdown_mixin';
-
-export default {
- name: 'GkeZoneDropdown',
- mixins: [gkeDropdownMixin],
- computed: {
- ...mapState([
- 'selectedProject',
- 'selectedZone',
- 'projects',
- 'isValidatingProjectBilling',
- 'projectHasBillingEnabled',
- ]),
- ...mapState({ items: 'zones' }),
- isDisabled() {
- return this.isLoading || this.isValidatingProjectBilling || !this.projectHasBillingEnabled;
- },
- toggleText() {
- if (this.isLoading) {
- return s__('ClusterIntegration|Fetching zones');
- }
-
- if (this.selectedZone) {
- return this.selectedZone;
- }
-
- return !this.projectHasBillingEnabled
- ? s__('ClusterIntegration|Select project to choose zone')
- : s__('ClusterIntegration|Select zone');
- },
- errorMessage() {
- return sprintf(
- s__('ClusterIntegration|An error occurred while trying to fetch project zones: %{error}'),
- { error: this.gapiError },
- );
- },
- },
- watch: {
- isValidatingProjectBilling(isValidating) {
- this.hasErrors = false;
-
- if (!isValidating && this.projectHasBillingEnabled) {
- this.isLoading = true;
-
- this.fetchZones().then(this.fetchSuccessHandler).catch(this.fetchFailureHandler);
- }
- },
- },
- methods: {
- ...mapActions(['fetchZones']),
- ...mapActions({ setItem: 'setZone' }),
- },
-};
-</script>
-
-<template>
- <div>
- <div class="js-gcp-zone-dropdown dropdown">
- <dropdown-hidden-input :name="fieldName" :value="selectedZone" />
- <dropdown-button
- :class="{ 'border-danger': hasErrors }"
- :is-disabled="isDisabled"
- :is-loading="isLoading"
- :toggle-text="toggleText"
- />
- <div class="dropdown-menu dropdown-select">
- <dropdown-search-input
- v-model="searchQuery"
- :placeholder-text="s__('ClusterIntegration|Search zones')"
- />
- <div class="dropdown-content">
- <ul>
- <li v-show="!results.length">
- <span class="menu-item">
- {{ s__('ClusterIntegration|No zones matched your search') }}
- </span>
- </li>
- <li v-for="result in results" :key="result.id">
- <button type="button" @click.prevent="setItem(result.name)">{{ result.name }}</button>
- </li>
- </ul>
- </div>
- <div class="dropdown-loading"><gl-loading-icon size="sm" /></div>
- </div>
- </div>
- <span
- v-if="hasErrors"
- :class="{
- 'text-danger': hasErrors,
- 'text-muted': !hasErrors,
- }"
- class="form-text"
- >
- {{ errorMessage }}
- </span>
- </div>
-</template>
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/constants.js b/app/assets/javascripts/create_cluster/gke_cluster/constants.js
deleted file mode 100644
index 2a1c0819916..00000000000
--- a/app/assets/javascripts/create_cluster/gke_cluster/constants.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { s__ } from '~/locale';
-
-export const GCP_API_ERROR = s__(
- 'ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later.',
-);
-export const GCP_API_CLOUD_BILLING_ENDPOINT =
- 'https://www.googleapis.com/discovery/v1/apis/cloudbilling/v1/rest';
-export const GCP_API_CLOUD_RESOURCE_MANAGER_ENDPOINT =
- 'https://www.googleapis.com/discovery/v1/apis/cloudresourcemanager/v1/rest';
-export const GCP_API_COMPUTE_ENDPOINT =
- 'https://www.googleapis.com/discovery/v1/apis/compute/v1/rest';
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/gapi_loader.js b/app/assets/javascripts/create_cluster/gke_cluster/gapi_loader.js
deleted file mode 100644
index b5f92fed8eb..00000000000
--- a/app/assets/javascripts/create_cluster/gke_cluster/gapi_loader.js
+++ /dev/null
@@ -1,24 +0,0 @@
-// This is a helper module to lazily import the google APIs for the GKE cluster
-// integration without introducing an indirect global dependency on an
-// initialized window.gapi object.
-export default () => {
- if (window.gapiPromise === undefined) {
- // first time loading the module
- window.gapiPromise = new Promise((resolve, reject) => {
- // this callback is set as a query param to script.src URL
- window.onGapiLoad = () => {
- resolve(window.gapi);
- };
-
- const script = document.createElement('script');
- // do not use script.onload, because gapi continues to load after the initial script load
- script.type = 'text/javascript';
- script.async = true;
- script.src = 'https://apis.google.com/js/api.js?onload=onGapiLoad';
- script.onerror = reject;
- document.head.appendChild(script);
- });
- }
-
- return window.gapiPromise;
-};
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/index.js b/app/assets/javascripts/create_cluster/gke_cluster/index.js
deleted file mode 100644
index 3a42b460e1c..00000000000
--- a/app/assets/javascripts/create_cluster/gke_cluster/index.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import Vue from 'vue';
-import createFlash from '~/flash';
-import GkeMachineTypeDropdown from './components/gke_machine_type_dropdown.vue';
-import GkeProjectIdDropdown from './components/gke_project_id_dropdown.vue';
-import GkeSubmitButton from './components/gke_submit_button.vue';
-import GkeZoneDropdown from './components/gke_zone_dropdown.vue';
-import * as CONSTANTS from './constants';
-import gapiLoader from './gapi_loader';
-
-import store from './store';
-
-const mountComponent = (entryPoint, component, componentName, extraProps = {}) => {
- const el = document.querySelector(entryPoint);
- if (!el) return false;
-
- const hiddenInput = el.querySelector('input');
-
- return new Vue({
- el,
- store,
- components: {
- [componentName]: component,
- },
- render: (createElement) =>
- createElement(componentName, {
- props: {
- fieldName: hiddenInput.getAttribute('name'),
- fieldId: hiddenInput.getAttribute('id'),
- defaultValue: hiddenInput.value,
- ...extraProps,
- },
- }),
- });
-};
-
-const mountGkeProjectIdDropdown = () => {
- const entryPoint = '.js-gcp-project-id-dropdown-entry-point';
- const el = document.querySelector(entryPoint);
-
- mountComponent(entryPoint, GkeProjectIdDropdown, 'gke-project-id-dropdown', {
- docsUrl: el.dataset.docsurl,
- });
-};
-
-const mountGkeZoneDropdown = () => {
- mountComponent('.js-gcp-zone-dropdown-entry-point', GkeZoneDropdown, 'gke-zone-dropdown');
-};
-
-const mountGkeMachineTypeDropdown = () => {
- mountComponent(
- '.js-gcp-machine-type-dropdown-entry-point',
- GkeMachineTypeDropdown,
- 'gke-machine-type-dropdown',
- );
-};
-
-const mountGkeSubmitButton = () => {
- mountComponent('.js-gke-cluster-creation-submit-container', GkeSubmitButton, 'gke-submit-button');
-};
-
-const gkeDropdownErrorHandler = () => {
- createFlash({
- message: CONSTANTS.GCP_API_ERROR,
- });
-};
-
-const initializeGapiClient = (gapi) => () => {
- const el = document.querySelector('.js-gke-cluster-creation');
- if (!el) return false;
-
- return gapi.client
- .init({
- discoveryDocs: [
- CONSTANTS.GCP_API_CLOUD_BILLING_ENDPOINT,
- CONSTANTS.GCP_API_CLOUD_RESOURCE_MANAGER_ENDPOINT,
- CONSTANTS.GCP_API_COMPUTE_ENDPOINT,
- ],
- })
- .then(() => {
- gapi.client.setToken({ access_token: el.dataset.token });
-
- mountGkeProjectIdDropdown();
- mountGkeZoneDropdown();
- mountGkeMachineTypeDropdown();
- mountGkeSubmitButton();
- })
- .catch(gkeDropdownErrorHandler);
-};
-
-const initGkeDropdowns = () =>
- gapiLoader()
- .then((gapi) => gapi.load('client', initializeGapiClient(gapi)))
- .catch(gkeDropdownErrorHandler);
-
-export default initGkeDropdowns;
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js b/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js
deleted file mode 100644
index f4c35dafc22..00000000000
--- a/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import gapiLoader from '../gapi_loader';
-import * as types from './mutation_types';
-
-const gapiResourceListRequest = ({ resource, params, commit, mutation, payloadKey }) =>
- new Promise((resolve, reject) => {
- const request = resource.list(params);
-
- return request.then(
- (resp) => {
- const { result } = resp;
-
- commit(mutation, result[payloadKey]);
-
- resolve();
- },
- (resp) => {
- reject(resp);
- },
- );
- });
-
-export const setProject = ({ commit }, selectedProject) => {
- commit(types.SET_PROJECT, selectedProject);
-};
-
-export const setZone = ({ commit }, selectedZone) => {
- commit(types.SET_ZONE, selectedZone);
-};
-
-export const setMachineType = ({ commit }, selectedMachineType) => {
- commit(types.SET_MACHINE_TYPE, selectedMachineType);
-};
-
-export const setIsValidatingProjectBilling = ({ commit }, isValidatingProjectBilling) => {
- commit(types.SET_IS_VALIDATING_PROJECT_BILLING, isValidatingProjectBilling);
-};
-
-export const fetchProjects = ({ commit }) =>
- gapiLoader().then((gapi) =>
- gapiResourceListRequest({
- resource: gapi.client.cloudresourcemanager.projects,
- params: {},
- commit,
- mutation: types.SET_PROJECTS,
- payloadKey: 'projects',
- }),
- );
-
-export const validateProjectBilling = ({ dispatch, commit, state }) =>
- gapiLoader()
- .then((gapi) => {
- const request = gapi.client.cloudbilling.projects.getBillingInfo({
- name: `projects/${state.selectedProject.projectId}`,
- });
-
- commit(types.SET_ZONE, '');
- commit(types.SET_MACHINE_TYPE, '');
-
- return request;
- })
- .then(
- (resp) => {
- const { billingEnabled } = resp.result;
-
- commit(types.SET_PROJECT_BILLING_STATUS, Boolean(billingEnabled));
- dispatch('setIsValidatingProjectBilling', false);
- },
- (errorResp) => {
- dispatch('setIsValidatingProjectBilling', false);
- return errorResp;
- },
- );
-
-export const fetchZones = ({ commit, state }) =>
- gapiLoader().then((gapi) =>
- gapiResourceListRequest({
- resource: gapi.client.compute.zones,
- params: {
- project: state.selectedProject.projectId,
- },
- commit,
- mutation: types.SET_ZONES,
- payloadKey: 'items',
- }),
- );
-
-export const fetchMachineTypes = ({ commit, state }) =>
- gapiLoader().then((gapi) =>
- gapiResourceListRequest({
- resource: gapi.client.compute.machineTypes,
- params: {
- project: state.selectedProject.projectId,
- zone: state.selectedZone,
- },
- commit,
- mutation: types.SET_MACHINE_TYPES,
- payloadKey: 'items',
- }),
- );
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/store/getters.js b/app/assets/javascripts/create_cluster/gke_cluster/store/getters.js
deleted file mode 100644
index 99f8393ffdb..00000000000
--- a/app/assets/javascripts/create_cluster/gke_cluster/store/getters.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export const hasProject = (state) => Boolean(state.selectedProject.projectId);
-export const hasZone = (state) => Boolean(state.selectedZone);
-export const hasMachineType = (state) => Boolean(state.selectedMachineType);
-export const hasValidData = (state, getters) =>
- Boolean(state.projectHasBillingEnabled) && getters.hasZone && getters.hasMachineType;
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/store/index.js b/app/assets/javascripts/create_cluster/gke_cluster/store/index.js
deleted file mode 100644
index 5f72060633e..00000000000
--- a/app/assets/javascripts/create_cluster/gke_cluster/store/index.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import createState from './state';
-
-Vue.use(Vuex);
-
-export const createStore = () =>
- new Vuex.Store({
- actions,
- getters,
- mutations,
- state: createState(),
- });
-
-export default createStore();
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/store/mutation_types.js b/app/assets/javascripts/create_cluster/gke_cluster/store/mutation_types.js
deleted file mode 100644
index 45a91efc2d9..00000000000
--- a/app/assets/javascripts/create_cluster/gke_cluster/store/mutation_types.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export const SET_PROJECT = 'SET_PROJECT';
-export const SET_PROJECT_BILLING_STATUS = 'SET_PROJECT_BILLING_STATUS';
-export const SET_IS_VALIDATING_PROJECT_BILLING = 'SET_IS_VALIDATING_PROJECT_BILLING';
-export const SET_ZONE = 'SET_ZONE';
-export const SET_MACHINE_TYPE = 'SET_MACHINE_TYPE';
-export const SET_PROJECTS = 'SET_PROJECTS';
-export const SET_ZONES = 'SET_ZONES';
-export const SET_MACHINE_TYPES = 'SET_MACHINE_TYPES';
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/store/mutations.js b/app/assets/javascripts/create_cluster/gke_cluster/store/mutations.js
deleted file mode 100644
index 88a2c1b630d..00000000000
--- a/app/assets/javascripts/create_cluster/gke_cluster/store/mutations.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import * as types from './mutation_types';
-
-export default {
- [types.SET_PROJECT](state, selectedProject) {
- Object.assign(state, { selectedProject });
- },
- [types.SET_IS_VALIDATING_PROJECT_BILLING](state, isValidatingProjectBilling) {
- Object.assign(state, { isValidatingProjectBilling });
- },
- [types.SET_PROJECT_BILLING_STATUS](state, projectHasBillingEnabled) {
- Object.assign(state, { projectHasBillingEnabled });
- },
- [types.SET_ZONE](state, selectedZone) {
- Object.assign(state, { selectedZone });
- },
- [types.SET_MACHINE_TYPE](state, selectedMachineType) {
- Object.assign(state, { selectedMachineType });
- },
- [types.SET_PROJECTS](state, projects) {
- Object.assign(state, { projects });
- },
- [types.SET_ZONES](state, zones) {
- Object.assign(state, { zones });
- },
- [types.SET_MACHINE_TYPES](state, machineTypes) {
- Object.assign(state, { machineTypes });
- },
-};
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/store/state.js b/app/assets/javascripts/create_cluster/gke_cluster/store/state.js
deleted file mode 100644
index 9f3c473d4bc..00000000000
--- a/app/assets/javascripts/create_cluster/gke_cluster/store/state.js
+++ /dev/null
@@ -1,13 +0,0 @@
-export default () => ({
- selectedProject: {
- projectId: '',
- name: '',
- },
- selectedZone: '',
- selectedMachineType: '',
- isValidatingProjectBilling: null,
- projectHasBillingEnabled: null,
- projects: [],
- zones: [],
- machineTypes: [],
-});
diff --git a/app/assets/javascripts/create_cluster/init_create_cluster.js b/app/assets/javascripts/create_cluster/init_create_cluster.js
deleted file mode 100644
index d367d7ec333..00000000000
--- a/app/assets/javascripts/create_cluster/init_create_cluster.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import PersistentUserCallout from '~/persistent_user_callout';
-import initGkeDropdowns from './gke_cluster';
-import initGkeNamespace from './gke_cluster_namespace';
-
-const newClusterViews = [':clusters:new', ':clusters:create_gcp', ':clusters:create_user'];
-
-const isProjectLevelCluster = (page) => page.startsWith('project:clusters');
-
-export default (document) => {
- const { page } = document.body.dataset;
- const isNewClusterView = newClusterViews.some((view) => page.endsWith(view));
-
- if (!isNewClusterView) {
- return;
- }
-
- const callout = document.querySelector('.gcp-signup-offer');
- PersistentUserCallout.factory(callout);
-
- initGkeDropdowns();
-
- if (isProjectLevelCluster(page)) {
- initGkeNamespace();
- }
-
- import(/* webpackChunkName: 'eks_cluster' */ '~/create_cluster/eks_cluster')
- .then(({ default: initCreateEKSCluster }) => {
- const el = document.querySelector('.js-create-eks-cluster-form-container');
-
- if (el) {
- initCreateEKSCluster(el);
- }
- })
- .catch(() => {});
-};
diff --git a/app/assets/javascripts/create_cluster/store/cluster_dropdown/actions.js b/app/assets/javascripts/create_cluster/store/cluster_dropdown/actions.js
deleted file mode 100644
index 669b0dcc732..00000000000
--- a/app/assets/javascripts/create_cluster/store/cluster_dropdown/actions.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import * as types from './mutation_types';
-
-export default (fetchItems) => ({
- requestItems: ({ commit }) => commit(types.REQUEST_ITEMS),
- receiveItemsSuccess: ({ commit }, payload) => commit(types.RECEIVE_ITEMS_SUCCESS, payload),
- receiveItemsError: ({ commit }, payload) => commit(types.RECEIVE_ITEMS_ERROR, payload),
- fetchItems: ({ dispatch }, payload) => {
- dispatch('requestItems');
-
- return fetchItems(payload)
- .then((items) => dispatch('receiveItemsSuccess', { items }))
- .catch((error) => dispatch('receiveItemsError', { error }));
- },
-});
diff --git a/app/assets/javascripts/create_cluster/store/cluster_dropdown/getters.js b/app/assets/javascripts/create_cluster/store/cluster_dropdown/getters.js
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/app/assets/javascripts/create_cluster/store/cluster_dropdown/getters.js
+++ /dev/null
diff --git a/app/assets/javascripts/create_cluster/store/cluster_dropdown/index.js b/app/assets/javascripts/create_cluster/store/cluster_dropdown/index.js
deleted file mode 100644
index de8cc44fa7c..00000000000
--- a/app/assets/javascripts/create_cluster/store/cluster_dropdown/index.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import state from './state';
-
-const createStore = ({ fetchFn, initialState }) => ({
- actions: actions(fetchFn),
- getters,
- mutations,
- state: Object.assign(state(), initialState || {}),
-});
-
-export default createStore;
diff --git a/app/assets/javascripts/create_cluster/store/cluster_dropdown/mutation_types.js b/app/assets/javascripts/create_cluster/store/cluster_dropdown/mutation_types.js
deleted file mode 100644
index 48959a73924..00000000000
--- a/app/assets/javascripts/create_cluster/store/cluster_dropdown/mutation_types.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export const REQUEST_ITEMS = 'REQUEST_ITEMS';
-export const RECEIVE_ITEMS_SUCCESS = 'REQUEST_ITEMS_SUCCESS';
-export const RECEIVE_ITEMS_ERROR = 'RECEIVE_ITEMS_ERROR';
diff --git a/app/assets/javascripts/create_cluster/store/cluster_dropdown/mutations.js b/app/assets/javascripts/create_cluster/store/cluster_dropdown/mutations.js
deleted file mode 100644
index d09689f1f6c..00000000000
--- a/app/assets/javascripts/create_cluster/store/cluster_dropdown/mutations.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import * as types from './mutation_types';
-
-export default {
- [types.REQUEST_ITEMS](state) {
- state.isLoadingItems = true;
- state.loadingItemsError = null;
- },
- [types.RECEIVE_ITEMS_SUCCESS](state, { items }) {
- state.isLoadingItems = false;
- state.items = items;
- },
- [types.RECEIVE_ITEMS_ERROR](state, { error }) {
- state.isLoadingItems = false;
- state.loadingItemsError = error;
- },
-};
diff --git a/app/assets/javascripts/create_cluster/store/cluster_dropdown/state.js b/app/assets/javascripts/create_cluster/store/cluster_dropdown/state.js
deleted file mode 100644
index b949a24216e..00000000000
--- a/app/assets/javascripts/create_cluster/store/cluster_dropdown/state.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export default () => ({
- isLoadingItems: false,
- items: [],
- loadingItemsError: null,
-});
diff --git a/app/assets/javascripts/crm/components/form.vue b/app/assets/javascripts/crm/components/form.vue
index 4f94898ff63..72def54aedf 100644
--- a/app/assets/javascripts/crm/components/form.vue
+++ b/app/assets/javascripts/crm/components/form.vue
@@ -1,5 +1,5 @@
<script>
-import { GlAlert, GlButton, GlDrawer, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { GlAlert, GlButton, GlDrawer, GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui';
import { get as getPropValueByPath, isEmpty } from 'lodash';
import { produce } from 'immer';
import { MountingPortal } from 'portal-vue';
@@ -28,6 +28,7 @@ export default {
GlDrawer,
GlFormGroup,
GlFormInput,
+ GlFormSelect,
MountingPortal,
},
props: {
@@ -136,15 +137,25 @@ export default {
methods: {
setInitialModel() {
const existingModel = this.records.find(({ id }) => id === this.existingId);
+ const noModel = !this.isEditMode || !existingModel;
this.model = this.fields.reduce(
(map, field) =>
Object.assign(map, {
- [field.name]: !this.isEditMode || !existingModel ? null : existingModel[field.name],
+ [field.name]: noModel ? null : this.extractValue(existingModel, field.name),
}),
{},
);
},
+ extractValue(existingModel, fieldName) {
+ const value = existingModel[fieldName];
+ if (value != null) return value;
+
+ /* eslint-disable-next-line @gitlab/require-i18n-strings */
+ if (!fieldName.endsWith('Id')) return null;
+
+ return existingModel[fieldName.slice(0, -2)]?.id;
+ },
formatValue(model, field) {
if (!isEmpty(model[field.name]) && field.input?.type === 'number') {
return parseFloat(model[field.name]);
@@ -216,6 +227,15 @@ export default {
return data[keys[0]];
},
+ getDrawerHeaderHeight() {
+ const wrapperEl = document.querySelector('.content-wrapper');
+
+ if (wrapperEl) {
+ return `${wrapperEl.offsetTop}px`;
+ }
+
+ return '';
+ },
},
MSG_CANCEL,
INDEX_ROUTE_NAME,
@@ -224,7 +244,12 @@ export default {
<template>
<mounting-portal v-if="!loading" mount-to="#js-crm-form-portal" append>
- <gl-drawer class="gl-drawer-responsive gl-absolute" :open="drawerOpen" @close="close(false)">
+ <gl-drawer
+ :header-height="getDrawerHeaderHeight()"
+ class="gl-drawer-responsive"
+ :open="drawerOpen"
+ @close="close(false)"
+ >
<template #title>
<h3>{{ title }}</h3>
</template>
@@ -242,7 +267,13 @@ export default {
:label="getFieldLabel(field)"
:label-for="field.name"
>
- <gl-form-input :id="field.name" v-bind="field.input" v-model="model[field.name]" />
+ <gl-form-select
+ v-if="field.values"
+ :id="field.name"
+ v-model="model[field.name]"
+ :options="field.values"
+ />
+ <gl-form-input v-else :id="field.name" v-bind="field.input" v-model="model[field.name]" />
</gl-form-group>
<span class="gl-float-right">
<gl-button data-testid="cancel-button" @click="close(false)">
diff --git a/app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue b/app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue
index 58eaabfbb7f..f114ffedfe6 100644
--- a/app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue
+++ b/app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue
@@ -3,6 +3,7 @@ import { s__, __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_CRM_CONTACT, TYPE_GROUP } from '~/graphql_shared/constants';
import ContactForm from '../../components/form.vue';
+import getGroupOrganizationsQuery from '../../organizations/components/graphql/get_group_organizations.query.graphql';
import getGroupContactsQuery from './graphql/get_group_contacts.query.graphql';
import createContactMutation from './graphql/create_contact.mutation.graphql';
import updateContactMutation from './graphql/update_contact.mutation.graphql';
@@ -19,6 +20,26 @@ export default {
default: false,
},
},
+ data() {
+ return {
+ organizations: [],
+ };
+ },
+ apollo: {
+ organizations: {
+ query() {
+ return getGroupOrganizationsQuery;
+ },
+ variables() {
+ return {
+ groupFullPath: this.groupFullPath,
+ };
+ },
+ update(data) {
+ return this.extractOrganizations(data);
+ },
+ },
+ },
computed: {
contactGraphQLId() {
if (!this.isEditMode) return null;
@@ -52,14 +73,35 @@ export default {
additionalCreateParams() {
return { groupId: this.groupGraphQLId };
},
+ fields() {
+ return [
+ { name: 'firstName', label: __('First name'), required: true },
+ { name: 'lastName', label: __('Last name'), required: true },
+ { name: 'email', label: __('Email'), required: true },
+ { name: 'phone', label: __('Phone') },
+ {
+ name: 'organizationId',
+ label: s__('Crm|Organization'),
+ values: this.organizationSelectValues,
+ },
+ { name: 'description', label: __('Description') },
+ ];
+ },
+ organizationSelectValues() {
+ const values = this.organizations.map((o) => {
+ return { value: o.id, text: o.name };
+ });
+
+ values.unshift({ value: null, text: s__('Crm|No organization') });
+ return values;
+ },
+ },
+ methods: {
+ extractOrganizations(data) {
+ const organizations = data?.group?.organizations?.nodes || [];
+ return organizations.slice().sort((a, b) => a.name.localeCompare(b.name));
+ },
},
- fields: [
- { name: 'firstName', label: __('First name'), required: true },
- { name: 'lastName', label: __('Last name'), required: true },
- { name: 'email', label: __('Email'), required: true },
- { name: 'phone', label: __('Phone') },
- { name: 'description', label: __('Description') },
- ],
};
</script>
@@ -71,7 +113,7 @@ export default {
:mutation="mutation"
:additional-create-params="additionalCreateParams"
:existing-id="contactGraphQLId"
- :fields="$options.fields"
+ :fields="fields"
:title="title"
:success-message="successMessage"
/>
diff --git a/app/assets/javascripts/crm/contacts/components/contacts_root.vue b/app/assets/javascripts/crm/contacts/components/contacts_root.vue
index 17be3800256..9d6f34c73b7 100644
--- a/app/assets/javascripts/crm/contacts/components/contacts_root.vue
+++ b/app/assets/javascripts/crm/contacts/components/contacts_root.vue
@@ -55,7 +55,7 @@ export default {
return contacts.slice().sort((a, b) => a.firstName.localeCompare(b.firstName));
},
getIssuesPath(path, value) {
- return `${path}?scope=all&state=opened&crm_contact_id=${value}`;
+ return `${path}?crm_contact_id=${value}`;
},
getEditRoute(id) {
return { name: this.$options.EDIT_ROUTE_NAME, params: { id } };
diff --git a/app/assets/javascripts/crm/organizations/components/organizations_root.vue b/app/assets/javascripts/crm/organizations/components/organizations_root.vue
index 522e29eb2af..a165dd68603 100644
--- a/app/assets/javascripts/crm/organizations/components/organizations_root.vue
+++ b/app/assets/javascripts/crm/organizations/components/organizations_root.vue
@@ -55,7 +55,7 @@ export default {
return organizations.slice().sort((a, b) => a.name.localeCompare(b.name));
},
getIssuesPath(path, value) {
- return `${path}?scope=all&state=opened&crm_organization_id=${value}`;
+ return `${path}?crm_organization_id=${value}`;
},
getEditRoute(id) {
return { name: this.$options.EDIT_ROUTE_NAME, params: { id } };
diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/cycle_analytics/components/base.vue
index 3d7a34581b3..1883030e51f 100644
--- a/app/assets/javascripts/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/cycle_analytics/components/base.vue
@@ -3,6 +3,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
import { getCookie, setCookie } from '~/lib/utils/common_utils';
import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
+import { VSA_METRICS_GROUPS } from '~/analytics/shared/constants';
import { toYmd } from '~/analytics/shared/utils';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
@@ -150,34 +151,36 @@ export default {
pageTitle: __('Value Stream Analytics'),
recentActivity: __('Recent Project Activity'),
},
+ VSA_METRICS_GROUPS,
};
</script>
<template>
<div>
<h3>{{ $options.i18n.pageTitle }}</h3>
+ <value-stream-filters
+ :group-id="endpoints.groupId"
+ :group-path="endpoints.groupPath"
+ :has-project-filter="false"
+ :start-date="createdAfter"
+ :end-date="createdBefore"
+ @setDateRange="onSetDateRange"
+ />
<div class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row">
<path-navigation
v-if="displayPathNavigation"
data-testid="vsa-path-navigation"
- class="gl-w-full gl-pb-2"
+ class="gl-w-full gl-mt-4"
:loading="isLoading || isLoadingStage"
:stages="pathNavigationData"
:selected-stage="selectedStage"
@selected="onSelectStage"
/>
</div>
- <value-stream-filters
- :group-id="endpoints.groupId"
- :group-path="endpoints.groupPath"
- :has-project-filter="false"
- :start-date="createdAfter"
- :end-date="createdBefore"
- @setDateRange="onSetDateRange"
- />
<value-stream-metrics
:request-path="endpoints.fullPath"
:request-params="filterParams"
:requests="metricsRequests"
+ :group-by="$options.VSA_METRICS_GROUPS"
/>
<gl-loading-icon v-if="isLoading" size="lg" />
<stage-table
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_table.vue b/app/assets/javascripts/cycle_analytics/components/stage_table.vue
index 6a45969fd1a..e4236968efc 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_table.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_table.vue
@@ -191,9 +191,7 @@ export default {
/>
<gl-table
v-else
- head-variant="white"
stacked="lg"
- thead-class="border-bottom"
show-empty
:sort-by.sync="sort"
:sort-direction.sync="direction"
diff --git a/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue b/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue
index 66bccf19496..f686cd0db95 100644
--- a/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue
+++ b/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue
@@ -1,21 +1,13 @@
<script>
-import { GlIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { GlTooltipDirective } from '@gitlab/ui';
import DateRange from '~/analytics/shared/components/daterange.vue';
import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
import { DATE_RANGE_LIMIT, PROJECTS_PER_PAGE } from '~/analytics/shared/constants';
import FilterBar from './filter_bar.vue';
-export const AGGREGATION_TOGGLE_LABEL = s__('CycleAnalytics|Filter by stop date');
-export const AGGREGATION_DESCRIPTION = s__(
- 'CycleAnalytics|When enabled, the results show items with a stop event within the date range. When disabled, the results show items with a start event within the date range.',
-);
-
export default {
name: 'ValueStreamFilters',
components: {
- GlIcon,
- GlToggle,
DateRange,
ProjectsDropdownFilter,
FilterBar,
@@ -57,21 +49,6 @@ export default {
required: false,
default: null,
},
- canToggleAggregation: {
- type: Boolean,
- required: false,
- default: false,
- },
- isAggregationEnabled: {
- type: Boolean,
- required: false,
- default: false,
- },
- isUpdatingAggregationData: {
- type: Boolean,
- required: false,
- default: false,
- },
},
computed: {
projectsQueryParams() {
@@ -81,19 +58,8 @@ export default {
};
},
},
- methods: {
- onUpdateAggregation(ev) {
- if (!this.isUpdatingAggregationData) {
- this.$emit('toggleAggregation', ev);
- }
- },
- },
multiProjectSelect: true,
maxDateRange: DATE_RANGE_LIMIT,
- i18n: {
- AGGREGATION_TOGGLE_LABEL,
- AGGREGATION_DESCRIPTION,
- },
};
</script>
<template>
@@ -123,27 +89,6 @@ export default {
/>
</div>
<div class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row">
- <div
- v-if="canToggleAggregation"
- class="gl-display-flex gl-text-align-center gl-my-2 gl-lg-mt-0 gl-lg-mb-0 gl-mr-5"
- >
- <gl-toggle
- class="gl-flex-direction-row"
- :value="isAggregationEnabled"
- :label="$options.i18n.AGGREGATION_TOGGLE_LABEL"
- :disabled="isUpdatingAggregationData"
- label-position="left"
- @change="onUpdateAggregation"
- >
- <template #label>
- {{ $options.i18n.AGGREGATION_TOGGLE_LABEL }}&nbsp;<gl-icon
- v-gl-tooltip.hover
- :title="$options.i18n.AGGREGATION_DESCRIPTION"
- name="information-o"
- />
- </template>
- </gl-toggle>
- </div>
<date-range
v-if="hasDateRangeFilter"
:start-date="startDate"
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 7acb5549273..814a4b672a2 100644
--- a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
+++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
@@ -137,7 +137,7 @@ export default {
v-model="freezeStartCron"
class="gl-font-monospace!"
data-qa-selector="deploy_freeze_start_field"
- :placeholder="this.$options.translations.cronPlaceholder"
+ :placeholder="$options.translations.cronPlaceholder"
:state="freezeStartCronState"
trim
/>
@@ -154,7 +154,7 @@ export default {
v-model="freezeEndCron"
class="gl-font-monospace!"
data-qa-selector="deploy_freeze_end_field"
- :placeholder="this.$options.translations.cronPlaceholder"
+ :placeholder="$options.translations.cronPlaceholder"
:state="freezeEndCronState"
trim
/>
diff --git a/app/assets/javascripts/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql
index 124f12ef018..c235e7fbf3d 100644
--- a/app/assets/javascripts/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql
+++ b/app/assets/javascripts/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql
@@ -2,7 +2,7 @@
#import "../fragments/discussion_resolved_status.fragment.graphql"
#import "../fragments/design_todo_item.fragment.graphql"
-mutation toggleResolveDiscussion($id: ID!, $resolve: Boolean!) {
+mutation toggleResolveDiscussion($id: DiscussionID!, $resolve: Boolean!) {
discussionToggleResolve(input: { id: $id, resolve: $resolve }) {
discussion {
id
diff --git a/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
index 34d683ac1ee..3200327e03d 100644
--- a/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
+++ b/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
@@ -3,7 +3,6 @@
mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) {
designManagementUpload(input: { projectPath: $projectPath, iid: $iid, files: $files }) {
- # eslint-disable-next-line @graphql-eslint/require-id-when-available
designs {
...DesignItem
versions {
diff --git a/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql b/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql
index a5394457f73..730467c33f6 100644
--- a/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql
+++ b/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql
@@ -13,7 +13,6 @@ query getDesign(
id
designCollection {
designs(atVersion: $atVersion, filenames: $filenames) {
- # eslint-disable-next-line @graphql-eslint/require-id-when-available
nodes {
...DesignItem
issue {
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index 837320b9423..2b395921ee1 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -349,7 +349,9 @@ export default {
class="gl-display-flex gl-overflow-hidden gl-flex-grow-1 gl-flex-direction-column gl-relative"
>
<design-destroyer
- :filenames="[design.filename]"
+ :filenames="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
+ design.filename,
+ ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:project-path="projectPath"
:iid="issueIid"
@done="$router.push({ name: $options.DESIGNS_ROUTE_NAME })"
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index c86f2c8451c..c3436159cea 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -17,7 +17,6 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import { parseBoolean } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
-import MrWidgetHowToMergeModal from '~/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -75,7 +74,6 @@ export default {
PanelResizer,
GlPagination,
GlSprintf,
- MrWidgetHowToMergeModal,
GlAlert,
},
mixins: [glFeatureFlagsMixin()],
@@ -373,7 +371,7 @@ export default {
events.push(TRACKING_MULTIPLE_FILES_MODE);
}
- queueRedisHllEvents(events);
+ queueRedisHllEvents(events, { verifyCap: true });
this.subscribeToVirtualScrollingEvents();
},
@@ -738,15 +736,6 @@ export default {
/>
</div>
</div>
- <mr-widget-how-to-merge-modal
- :is-fork="isForked"
- :can-merge="canMerge"
- :source-branch="branchName"
- :source-project-path="sourceProjectFullPath"
- :target-branch="targetBranchName"
- :source-project-default-url="sourceProjectDefaultUrl"
- :reviewing-docs-path="$options.howToMergeDocsPath"
- />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
index 6c5973b7c28..fd219a7d00f 100644
--- a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
+++ b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
@@ -24,7 +24,12 @@ export default {
</script>
<template>
- <gl-dropdown :text="selectedVersionName" data-qa-selector="dropdown_content">
+ <gl-dropdown
+ :text="selectedVersionName"
+ data-qa-selector="dropdown_content"
+ size="small"
+ category="tertiary"
+ >
<template v-for="version in versions">
<gl-dropdown-divider v-if="version.addDivider" :key="version.id" />
<gl-dropdown-item
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 4dfd672f99b..8a5325cf218 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -79,7 +79,7 @@ export default {
</script>
<template>
- <div class="mr-version-controls border-top">
+ <div class="mr-version-controls">
<div class="mr-version-menus-container content-block">
<gl-button
v-if="hasChanges"
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index b4bffdcb07f..1eba12a3ae9 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -156,7 +156,7 @@ export default {
:link-href="author.path"
:img-src="author.avatar_url"
:img-alt="author.name"
- :img-size="40"
+ :img-size="48"
class="d-none d-sm-block new-comment"
/>
<diff-discussions
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index 4c7b8e8f667..4e7dc578193 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -1,42 +1,33 @@
<script>
-import { GlIcon } from '@gitlab/ui';
-import { mapState, mapActions } from 'vuex';
+import { GlTooltipDirective, GlSafeHtmlDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
+import { mapActions } from 'vuex';
import createFlash from '~/flash';
import { s__, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { UNFOLD_COUNT, INLINE_DIFF_VIEW_TYPE, INLINE_DIFF_LINES_KEY } from '../constants';
+import { UNFOLD_COUNT, INLINE_DIFF_LINES_KEY } from '../constants';
import * as utils from '../store/utils';
const EXPAND_ALL = 0;
const EXPAND_UP = 1;
const EXPAND_DOWN = 2;
-const lineNumberByViewType = (viewType, diffLine) => {
- const numberGetters = {
- [INLINE_DIFF_VIEW_TYPE]: (line) => line?.new_line,
- };
- const numberGetter = numberGetters[viewType];
- return numberGetter && numberGetter(diffLine);
-};
-
-const i18n = {
- showMore: sprintf(s__('Diffs|Show %{unfoldCount} lines'), { unfoldCount: UNFOLD_COUNT }),
- showAll: s__('Diffs|Show all unchanged lines'),
-};
-
export default {
- i18n,
+ i18n: {
+ showMore: sprintf(s__('Diffs|Show %{unfoldCount} lines'), { unfoldCount: UNFOLD_COUNT }),
+ showAll: s__('Diffs|Show all unchanged lines'),
+ },
components: {
GlIcon,
+ GlLoadingIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ SafeHtml: GlSafeHtmlDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
- fileHash: {
- type: String,
- required: true,
- },
- contextLinesPath: {
- type: String,
+ file: {
+ type: Object,
required: true,
},
line: {
@@ -53,34 +44,45 @@ export default {
required: false,
default: false,
},
+ inline: {
+ type: Boolean,
+ required: true,
+ },
+ lineCountBetween: {
+ type: Number,
+ required: false,
+ default: -1,
+ },
+ },
+ data() {
+ return { loading: { up: false, down: false, all: false } };
},
computed: {
- ...mapState({
- diffFiles: (state) => state.diffs.diffFiles,
- }),
canExpandUp() {
return !this.isBottom;
},
canExpandDown() {
return this.isBottom || !this.isTop;
},
- },
- created() {
- this.EXPAND_DOWN = EXPAND_DOWN;
- this.EXPAND_UP = EXPAND_UP;
+ isLineCountSmall() {
+ return this.lineCountBetween >= 20 || this.lineCountBetween === -1;
+ },
+ showExpandDown() {
+ return this.canExpandDown && this.isLineCountSmall;
+ },
+ showExpandUp() {
+ return this.canExpandUp && this.isLineCountSmall;
+ },
},
methods: {
...mapActions('diffs', ['loadMoreLines']),
getPrevLineNumber(oldLineNumber, newLineNumber) {
- const diffFile = utils.findDiffFile(this.diffFiles, this.fileHash);
- const index = utils.getPreviousLineIndex(INLINE_DIFF_VIEW_TYPE, diffFile, {
+ const index = utils.getPreviousLineIndex(this.file, {
oldLineNumber,
newLineNumber,
});
- return (
- lineNumberByViewType(INLINE_DIFF_VIEW_TYPE, diffFile[INLINE_DIFF_LINES_KEY][index - 2]) || 0
- );
+ return this.file[INLINE_DIFF_LINES_KEY][index - 2]?.new_line || 0;
},
callLoadMoreLines(
endpoint,
@@ -99,6 +101,9 @@ export default {
message: s__('Diffs|Something went wrong while fetching diff lines.'),
});
this.isRequesting = false;
+ })
+ .finally(() => {
+ this.loading = { up: false, down: false, all: false };
});
},
handleExpandLines(type = EXPAND_ALL) {
@@ -107,25 +112,26 @@ export default {
}
this.isRequesting = true;
- const endpoint = this.contextLinesPath;
- const { fileHash } = this;
- const view = INLINE_DIFF_VIEW_TYPE;
+ const endpoint = this.file.context_lines_path;
const oldLineNumber = this.line.meta_data.old_pos || 0;
const newLineNumber = this.line.meta_data.new_pos || 0;
const offset = newLineNumber - oldLineNumber;
- const expandOptions = { endpoint, fileHash, view, oldLineNumber, newLineNumber, offset };
+ const expandOptions = { endpoint, oldLineNumber, newLineNumber, offset };
if (type === EXPAND_UP) {
+ this.loading.up = true;
this.handleExpandUpLines(expandOptions);
} else if (type === EXPAND_DOWN) {
+ this.loading.down = true;
this.handleExpandDownLines(expandOptions);
} else {
+ this.loading.all = true;
this.handleExpandAllLines(expandOptions);
}
},
handleExpandUpLines(expandOptions) {
- const { endpoint, fileHash, view, oldLineNumber, newLineNumber, offset } = expandOptions;
+ const { endpoint, oldLineNumber, newLineNumber, offset } = expandOptions;
const bottom = this.isBottom;
const lineNumber = newLineNumber - 1;
@@ -139,15 +145,13 @@ export default {
unfold = false;
}
- const params = { since, to, bottom, offset, unfold, view };
+ const params = { since, to, bottom, offset, unfold };
const lineNumbers = { oldLineNumber, newLineNumber };
- this.callLoadMoreLines(endpoint, params, lineNumbers, fileHash);
+ this.callLoadMoreLines(endpoint, params, lineNumbers, this.file.file_hash);
},
handleExpandDownLines(expandOptions) {
const {
endpoint,
- fileHash,
- view,
oldLineNumber: metaOldPos,
newLineNumber: metaNewPos,
offset,
@@ -183,19 +187,19 @@ export default {
}
}
- const params = { since, to, bottom, offset, unfold, view };
+ const params = { since, to, bottom, offset, unfold };
const lineNumbers = { oldLineNumber, newLineNumber };
this.callLoadMoreLines(
endpoint,
params,
lineNumbers,
- fileHash,
+ this.file.file_hash,
isExpandDown,
nextLineNumbers,
);
},
handleExpandAllLines(expandOptions) {
- const { endpoint, fileHash, view, oldLineNumber, newLineNumber, offset } = expandOptions;
+ const { endpoint, oldLineNumber, newLineNumber, offset } = expandOptions;
const bottom = this.isBottom;
const unfold = false;
let since;
@@ -213,21 +217,71 @@ export default {
to = newLineNumber - 1;
}
- const params = { since, to, bottom, offset, unfold, view };
+ const params = { since, to, bottom, offset, unfold };
const lineNumbers = { oldLineNumber, newLineNumber };
- this.callLoadMoreLines(endpoint, params, lineNumbers, fileHash);
+ this.callLoadMoreLines(endpoint, params, lineNumbers, this.file.file_hash);
},
},
+ EXPAND_DOWN,
+ EXPAND_UP,
};
</script>
<template>
- <div class="content js-line-expansion-content">
+ <div
+ v-if="glFeatures.updatedDiffExpansionButtons"
+ class="diff-grid-row diff-grid-row-full diff-tr line_holder match expansion"
+ >
+ <div :class="{ parallel: !inline }" class="diff-grid-left diff-grid-2-col left-side">
+ <div
+ class="diff-td diff-line-num gl-text-center! gl-p-0! gl-w-full! gl-display-flex gl-flex-direction-column"
+ >
+ <button
+ v-if="showExpandDown"
+ v-gl-tooltip.left
+ :title="s__('Diffs|Next 20 lines')"
+ type="button"
+ class="js-unfold-down gl-rounded-0 gl-border-0 diff-line-expand-button"
+ @click="handleExpandLines($options.EXPAND_DOWN)"
+ >
+ <gl-loading-icon v-if="loading.down" size="sm" color="dark" inline />
+ <gl-icon v-else name="expand-down" />
+ </button>
+ <button
+ v-if="lineCountBetween !== -1 && lineCountBetween < 20"
+ v-gl-tooltip.left
+ :title="s__('Diffs|Expand all lines')"
+ type="button"
+ class="js-unfold-all gl-rounded-0 gl-border-0 diff-line-expand-button"
+ @click="handleExpandLines()"
+ >
+ <gl-loading-icon v-if="loading.all" size="sm" color="dark" inline />
+ <gl-icon v-else name="expand" />
+ </button>
+ <button
+ v-if="showExpandUp"
+ v-gl-tooltip.left
+ :title="s__('Diffs|Previous 20 lines')"
+ type="button"
+ class="js-unfold gl-rounded-0 gl-border-0 diff-line-expand-button"
+ @click="handleExpandLines($options.EXPAND_UP)"
+ >
+ <gl-loading-icon v-if="loading.up" size="sm" color="dark" inline />
+ <gl-icon v-else name="expand-up" />
+ </button>
+ </div>
+ <div
+ v-safe-html="line.rich_text"
+ class="gl-display-flex! gl-flex-direction-column gl-justify-content-center diff-td line_content left-side gl-white-space-normal!"
+ ></div>
+ </div>
+ </div>
+ <div v-else class="content js-line-expansion-content">
<button
type="button"
:disabled="!canExpandDown"
class="js-unfold-down gl-mx-2 gl-py-4 gl-cursor-pointer"
- @click="handleExpandLines(EXPAND_DOWN)"
+ @click="handleExpandLines($options.EXPAND_DOWN)"
>
<gl-icon :size="12" name="expand-down" />
<span>{{ $options.i18n.showMore }}</span>
@@ -244,7 +298,7 @@ export default {
type="button"
:disabled="!canExpandUp"
class="js-unfold gl-mx-2 gl-py-4 gl-cursor-pointer"
- @click="handleExpandLines(EXPAND_UP)"
+ @click="handleExpandLines($options.EXPAND_UP)"
>
<gl-icon :size="12" name="expand-up" />
<span>{{ $options.i18n.showMore }}</span>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index d8f27a967df..0b82be7140c 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -5,7 +5,6 @@ import {
GlSafeHtmlDirective as SafeHtml,
GlSprintf,
GlAlert,
- GlModalDirective,
} from '@gitlab/ui';
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
@@ -42,7 +41,6 @@ export default {
},
directives: {
SafeHtml,
- GlModalDirective,
},
mixins: [glFeatureFlagsMixin(), IdState({ idProp: (vm) => vm.file.file_hash })],
props: {
@@ -360,10 +358,12 @@ export default {
class="js-file-fork-suggestion-section file-fork-suggestion"
>
<span v-safe-html="forkMessage" class="file-fork-suggestion-note"></span>
- <a
+ <gl-button
:href="file.fork_path"
- class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success"
- >{{ $options.i18n.fork }}</a
+ class="js-fork-suggestion-button"
+ category="secondary"
+ variant="confirm"
+ >{{ $options.i18n.fork }}</gl-button
>
<button
class="js-cancel-fork-suggestion-button btn btn-grouped"
@@ -379,6 +379,53 @@ export default {
:class="hasBodyClasses.contentByHash"
data-testid="content-area"
>
+ <gl-alert
+ v-if="!showLoadingIcon && file.conflict_type"
+ variant="danger"
+ :dismissible="false"
+ data-testid="conflictsAlert"
+ >
+ {{ $options.CONFLICT_TEXT[file.conflict_type] }}
+ <template v-if="!canMerge">
+ {{ __('Ask someone with write access to resolve it.') }}
+ </template>
+ <gl-sprintf
+ v-else-if="conflictResolutionPath"
+ :message="
+ __(
+ 'You can %{gitlabLinkStart}resolve conflicts on GitLab%{gitlabLinkEnd} or %{resolveLocallyStart}resolve it locally%{resolveLocallyEnd}.',
+ )
+ "
+ >
+ <template #gitlabLink="{ content }">
+ <gl-button
+ :href="conflictResolutionPath"
+ variant="link"
+ class="gl-vertical-align-text-bottom"
+ >{{ content }}</gl-button
+ >
+ </template>
+ <template #resolveLocally="{ content }">
+ <gl-button
+ variant="link"
+ class="gl-vertical-align-text-bottom js-check-out-modal-trigger"
+ >{{ content }}</gl-button
+ >
+ </template>
+ </gl-sprintf>
+ <gl-sprintf
+ v-else
+ :message="__('You can %{resolveLocallyStart}resolve it locally%{resolveLocallyEnd}.')"
+ >
+ <template #resolveLocally="{ content }">
+ <gl-button
+ variant="link"
+ class="gl-vertical-align-text-bottom js-check-out-modal-trigger"
+ >{{ content }}</gl-button
+ >
+ </template>
+ </gl-sprintf>
+ </gl-alert>
<gl-loading-icon
v-if="showLoadingIcon"
size="sm"
@@ -402,55 +449,6 @@ export default {
<div v-else v-safe-html="errorMessage" class="nothing-here-block"></div>
</div>
<template v-else>
- <gl-alert
- v-if="file.conflict_type"
- variant="danger"
- :dismissible="false"
- data-testid="conflictsAlert"
- >
- {{ $options.CONFLICT_TEXT[file.conflict_type] }}
- <template v-if="!canMerge">
- {{ __('Ask someone with write access to resolve it.') }}
- </template>
- <gl-sprintf
- v-else-if="conflictResolutionPath"
- :message="
- __(
- 'You can %{gitlabLinkStart}resolve conflicts on GitLab%{gitlabLinkEnd} or %{resolveLocallyStart}resolve it locally%{resolveLocallyEnd}.',
- )
- "
- >
- <template #gitlabLink="{ content }">
- <gl-button
- :href="conflictResolutionPath"
- variant="link"
- class="gl-vertical-align-text-bottom"
- >{{ content }}</gl-button
- >
- </template>
- <template #resolveLocally="{ content }">
- <gl-button
- v-gl-modal-directive="'modal-merge-info'"
- variant="link"
- class="gl-vertical-align-text-bottom"
- >{{ content }}</gl-button
- >
- </template>
- </gl-sprintf>
- <gl-sprintf
- v-else
- :message="__('You can %{resolveLocallyStart}resolve it locally%{resolveLocallyEnd}.')"
- >
- <template #resolveLocally="{ content }">
- <gl-button
- v-gl-modal-directive="'modal-merge-info'"
- variant="link"
- class="gl-vertical-align-text-bottom"
- >{{ content }}</gl-button
- >
- </template>
- </gl-sprintf>
- </gl-alert>
<div
v-if="showWarning"
class="collapsed-file-warning gl-p-7 gl-bg-orange-50 gl-text-center gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 8cdbd2b7dbc..a75262ee303 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -302,6 +302,7 @@ export default {
@click="handleFileNameClick"
>
<file-icon
+ v-if="!glFeatures.removeDiffHeaderIcons"
:file-name="filePath"
:size="16"
aria-hidden="true"
@@ -394,6 +395,7 @@ export default {
<gl-dropdown
v-gl-tooltip.hover.focus="$options.i18n.optionsDropdownTitle"
size="small"
+ category="tertiary"
right
toggle-class="btn-icon js-diff-more-actions"
class="gl-pt-0!"
@@ -402,7 +404,7 @@ export default {
@hidden="setMoreActionsShown(false)"
>
<template #button-content>
- <gl-icon name="ellipsis_v" class="mr-0" :size="12" />
+ <gl-icon name="ellipsis_v" class="mr-0" />
<span class="sr-only">{{ $options.i18n.optionsDropdownTitle }}</span>
</template>
<gl-dropdown-item
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index 7a30740e31b..a2f0e2c2653 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -179,7 +179,10 @@ export default {
if (shouldConfirm && isDirty) {
const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
- const confirmed = await confirmAction(msg);
+ const confirmed = await confirmAction(msg, {
+ primaryBtnText: __('Discard changes'),
+ cancelBtnText: __('Continue editing'),
+ });
if (!confirmed) {
return;
diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue
index 4893803a3b6..1b07b00d725 100644
--- a/app/assets/javascripts/diffs/components/diff_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_row.vue
@@ -160,7 +160,13 @@ export default {
<!-- eslint-disable-next-line vue/no-deprecated-functional-template -->
<template functional>
- <div :class="$options.classNameMap(props)" class="diff-grid-row diff-tr line_holder">
+ <div
+ :class="[
+ $options.classNameMap(props),
+ { expansion: props.line.left && props.line.left.type === 'expanded' },
+ ]"
+ class="diff-grid-row diff-tr line_holder"
+ >
<div
:id="props.line.left && props.line.left.line_code"
data-testid="left-side"
diff --git a/app/assets/javascripts/diffs/components/diff_row_utils.js b/app/assets/javascripts/diffs/components/diff_row_utils.js
index 99999445c43..f610ac979ca 100644
--- a/app/assets/javascripts/diffs/components/diff_row_utils.js
+++ b/app/assets/javascripts/diffs/components/diff_row_utils.js
@@ -10,6 +10,7 @@ import {
CONFLICT_MARKER_THEIR,
CONFLICT_THEIR,
CONFLICT_OUR,
+ EXPANDED_LINE_TYPE,
} from '../constants';
export const isHighlighted = (highlightedRow, line, isCommented) => {
@@ -118,10 +119,12 @@ export const mapParallel = (content) => (line) => {
if (right) {
right = {
...right,
- renderDiscussion: Boolean(hasExpandedDiscussionOnRight && right.type),
+ renderDiscussion: Boolean(
+ hasExpandedDiscussionOnRight && right.type && right.type !== EXPANDED_LINE_TYPE,
+ ),
hasDraft: content.hasParallelDraftRight(content.diffFile.file_hash, line),
lineDraft: content.draftForLine(content.diffFile.file_hash, line, 'right'),
- hasCommentForm: Boolean(right.hasForm && right.type),
+ hasCommentForm: Boolean(right.hasForm && right.type && right.type !== EXPANDED_LINE_TYPE),
emptyCellClassMap: { conflict_their: line.left?.type === CONFLICT_OUR },
addCommentTooltip: addCommentTooltip(line.right),
};
diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue
index f46b0a538f1..529f8e0a2f9 100644
--- a/app/assets/javascripts/diffs/components/diff_view.vue
+++ b/app/assets/javascripts/diffs/components/diff_view.vue
@@ -141,6 +141,18 @@ export default {
table.classList.add(`${lineClass}-selected`);
}
},
+ getCountBetweenIndex(index) {
+ if (index === 0) {
+ return -1;
+ } else if (!this.diffLines[index + 1]) {
+ return -1;
+ }
+
+ return (
+ Number(this.diffLines[index + 1].left.new_line) -
+ Number(this.diffLines[index - 1].left.new_line)
+ );
+ },
},
userColorScheme: window.gon.user_color_scheme,
};
@@ -158,38 +170,51 @@ export default {
>
<template v-for="(line, index) in diffLines">
<template v-if="line.isMatchLineLeft || line.isMatchLineRight">
- <div :key="`expand-${index}`" class="diff-tr line_expansion match">
- <div class="diff-td text-center gl-font-regular">
- <diff-expansion-cell
- :file-hash="diffFile.file_hash"
- :context-lines-path="diffFile.context_lines_path"
- :line="line.left"
- :is-top="index === 0"
- :is-bottom="index + 1 === diffLinesLength"
- />
+ <diff-expansion-cell
+ v-if="glFeatures.updatedDiffExpansionButtons"
+ :key="`expand-${index}`"
+ :file="diffFile"
+ :line="line.left"
+ :is-top="index === 0"
+ :is-bottom="index + 1 === diffLinesLength"
+ :inline="inline"
+ :line-count-between="getCountBetweenIndex(index)"
+ />
+ <template v-else>
+ <div :key="`expand-${index}`" class="diff-tr line_expansion old-line_expansion match">
+ <div class="diff-td text-center gl-font-regular">
+ <diff-expansion-cell
+ :file="diffFile"
+ :context-lines-path="diffFile.context_lines_path"
+ :line="line.left"
+ :is-top="index === 0"
+ :is-bottom="index + 1 === diffLinesLength"
+ :inline="inline"
+ />
+ </div>
</div>
- </div>
- <div
- v-if="line.left.rich_text"
- :key="`expand-definition-${index}`"
- class="diff-grid-row diff-tr line_holder match"
- >
- <div class="diff-grid-left diff-grid-3-col left-side">
- <div class="diff-td diff-line-num"></div>
- <div v-if="inline" class="diff-td diff-line-num"></div>
- <div
- v-safe-html="line.left.rich_text"
- class="diff-td line_content left-side gl-white-space-normal!"
- ></div>
+ <div
+ v-if="line.left.rich_text"
+ :key="`expand-definition-${index}`"
+ class="diff-grid-row diff-tr line_holder match"
+ >
+ <div class="diff-grid-left diff-grid-3-col left-side">
+ <div class="diff-td diff-line-num"></div>
+ <div v-if="inline" class="diff-td diff-line-num"></div>
+ <div
+ v-safe-html="line.left.rich_text"
+ class="diff-td line_content left-side gl-white-space-normal!"
+ ></div>
+ </div>
+ <div v-if="!inline" class="diff-grid-right diff-grid-3-col right-side">
+ <div class="diff-td diff-line-num"></div>
+ <div
+ v-safe-html="line.left.rich_text"
+ class="diff-td line_content right-side gl-white-space-normal!"
+ ></div>
+ </div>
</div>
- <div v-if="!inline" class="diff-grid-right diff-grid-3-col right-side">
- <div class="diff-td diff-line-num"></div>
- <div
- v-safe-html="line.left.rich_text"
- class="diff-td line_content right-side gl-white-space-normal!"
- ></div>
- </div>
- </div>
+ </template>
</template>
<diff-row
v-if="!line.isMatchLineLeft && !line.isMatchLineRight"
diff --git a/app/assets/javascripts/diffs/components/hidden_files_warning.vue b/app/assets/javascripts/diffs/components/hidden_files_warning.vue
index b9962682848..f6a8c679f3b 100644
--- a/app/assets/javascripts/diffs/components/hidden_files_warning.vue
+++ b/app/assets/javascripts/diffs/components/hidden_files_warning.vue
@@ -51,7 +51,7 @@ export default {
__(
'To preserve performance only %{strongStart}%{visible} of %{total}%{strongEnd} files are displayed.',
),
- { visible, total },
+ { visible, total } /* eslint-disable-line @gitlab/vue-no-new-non-primitive-in-template */,
)
"
>
diff --git a/app/assets/javascripts/diffs/components/image_diff_overlay.vue b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
index 8871be1f9af..bd040cd1ba1 100644
--- a/app/assets/javascripts/diffs/components/image_diff_overlay.vue
+++ b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
@@ -132,10 +132,10 @@ export default {
<design-note-pin
v-if="canComment && currentCommentForm"
- :position="{
+ :position="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
left: `${currentCommentForm.xPercent}%`,
top: `${currentCommentForm.yPercent}%`,
- }"
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
/>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index bbe27c0dbd6..6c0c9c4e1d0 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -1,6 +1,7 @@
export const INLINE_DIFF_VIEW_TYPE = 'inline';
export const PARALLEL_DIFF_VIEW_TYPE = 'parallel';
export const MATCH_LINE_TYPE = 'match';
+export const EXPANDED_LINE_TYPE = 'expanded';
export const OLD_NO_NEW_LINE_TYPE = 'old-nonewline';
export const NEW_NO_NEW_LINE_TYPE = 'new-nonewline';
export const CONTEXT_LINE_TYPE = 'context';
@@ -101,6 +102,8 @@ export const CONFLICT_MARKER_THEIR = 'conflict_marker_their';
// Tracking events
export const DEFER_DURATION = 750;
+export const TRACKING_CAP_KEY = 'code_review_events_dispatched';
+export const TRACKING_CAP_LENGTH = 86400000; // 24 hours
export const TRACKING_CLICK_DIFF_VIEW_SETTING = 'i_code_review_click_diff_view_setting';
export const TRACKING_DIFF_VIEW_INLINE = 'i_code_review_diff_view_inline';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index fb35114c0a9..d2b798245fc 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -4,6 +4,7 @@ import {
DIFF_FILE_MANUAL_COLLAPSE,
DIFF_FILE_AUTOMATIC_COLLAPSE,
INLINE_DIFF_LINES_KEY,
+ EXPANDED_LINE_TYPE,
} from '../constants';
import * as types from './mutation_types';
import {
@@ -131,6 +132,7 @@ export default {
: line.line_code || `${fileHash}_${line.old_line}_${line.new_line}`;
return {
...line,
+ type: line.type || EXPANDED_LINE_TYPE,
line_code: lineCode,
discussions: line.discussions || [],
hasForm: false,
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index f2028892a5f..92f3cf83740 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -15,13 +15,15 @@ import {
CONFLICT_MARKER,
CONFLICT_MARKER_OUR,
CONFLICT_MARKER_THEIR,
+ EXPANDED_LINE_TYPE,
} from '../constants';
import { prepareRawDiffFile } from '../utils/diff_file';
export const isAdded = (line) => ['new', 'new-nonewline'].includes(line.type);
export const isRemoved = (line) => ['old', 'old-nonewline'].includes(line.type);
export const isUnchanged = (line) => !line.type;
-export const isMeta = (line) => ['match', 'new-nonewline', 'old-nonewline'].includes(line.type);
+export const isMeta = (line) =>
+ ['match', EXPANDED_LINE_TYPE, 'new-nonewline', 'old-nonewline'].includes(line.type);
export const isConflictMarker = (line) =>
[CONFLICT_MARKER_OUR, CONFLICT_MARKER_THEIR].includes(line.type);
export const isConflictSeperator = (line) => line.type === CONFLICT_MARKER;
@@ -60,7 +62,7 @@ export const parallelizeDiffLines = (diffLines, inline) => {
const line = diffLines[i];
line.chunk = chunk;
- if (isMeta(line)) chunk += 1;
+ if (isMeta(line) && line.type !== EXPANDED_LINE_TYPE) chunk += 1;
if (isRemoved(line) || isConflictOur(line) || inline) {
lines.push({
@@ -205,7 +207,7 @@ export const findIndexInInlineLines = (lines, lineNumbers) => {
);
};
-export const getPreviousLineIndex = (diffViewType, file, lineNumbers) => {
+export const getPreviousLineIndex = (file, lineNumbers) => {
return findIndexInInlineLines(file[INLINE_DIFF_LINES_KEY], lineNumbers);
};
@@ -406,7 +408,7 @@ function deduplicateFilesList(files) {
export function prepareDiffData({ diff, priorFiles = [], meta = false }) {
const cleanedFiles = (diff.diff_files || [])
- .map((file, index, allFiles) => prepareRawDiffFile({ file, allFiles, meta }))
+ .map((file, index, allFiles) => prepareRawDiffFile({ file, allFiles, meta, index }))
.map(ensureBasicDiffFileLines)
.map(prepareDiffFileLines)
.map((file) => finalizeDiffFile(file));
diff --git a/app/assets/javascripts/diffs/utils/diff_file.js b/app/assets/javascripts/diffs/utils/diff_file.js
index 54dcf70c491..bcd9fa01278 100644
--- a/app/assets/javascripts/diffs/utils/diff_file.js
+++ b/app/assets/javascripts/diffs/utils/diff_file.js
@@ -50,7 +50,7 @@ function identifier(file) {
export const isNotDiffable = (file) => file?.viewer?.name === viewerModes.not_diffable;
-export function prepareRawDiffFile({ file, allFiles, meta = false }) {
+export function prepareRawDiffFile({ file, allFiles, meta = false, index = -1 }) {
const additionalProperties = {
brokenSymlink: fileSymlinkInformation(file, allFiles),
viewer: {
@@ -66,6 +66,10 @@ export function prepareRawDiffFile({ file, allFiles, meta = false }) {
additionalProperties.id = identifier(file);
}
+ if (index >= 0 && Number(index) === index) {
+ additionalProperties.order = index;
+ }
+
return Object.assign(file, additionalProperties);
}
@@ -89,6 +93,27 @@ export function getShortShaFromFile(file) {
return file.content_sha ? truncateSha(String(file.content_sha)) : null;
}
+export function match({ fileA, fileB, mode = 'universal' } = {}) {
+ const matching = {
+ universal: (a, b) => (a?.id && b?.id ? a.id === b.id : false),
+ /*
+ * MR mode can be wildly incorrect if there is ever the possibility of files from multiple MRs
+ * (e.g. a browser-local merge request/file cache).
+ * That's why the default here is "universal" mode: UUIDs can't conflict, but you can opt into
+ * the dangerous one.
+ *
+ * For reference:
+ * file_identifier_hash === sha1( `${filePath}-${Boolean(isNew)}-${Boolean(isDeleted)}-${Boolean(isRenamed)}` )
+ */
+ mr: (a, b) =>
+ a?.file_identifier_hash && b?.file_identifier_hash
+ ? a.file_identifier_hash === b.file_identifier_hash
+ : false,
+ };
+
+ return (matching[mode] || (() => false))(fileA, fileB);
+}
+
export function stats(file) {
let valid = false;
let classes = '';
diff --git a/app/assets/javascripts/diffs/utils/queue_events.js b/app/assets/javascripts/diffs/utils/queue_events.js
index 08fcc98d45f..cc7141df622 100644
--- a/app/assets/javascripts/diffs/utils/queue_events.js
+++ b/app/assets/javascripts/diffs/utils/queue_events.js
@@ -1,13 +1,31 @@
import { delay } from 'lodash';
import api from '~/api';
-import { DEFER_DURATION } from '../constants';
+import { DEFER_DURATION, TRACKING_CAP_KEY, TRACKING_CAP_LENGTH } from '../constants';
-function trackRedisHllUserEvent(event, deferDuration = 0) {
+function shouldDispatchEvent() {
+ const timestamp = parseInt(localStorage.getItem(TRACKING_CAP_KEY), 10);
+
+ if (Number.isNaN(timestamp)) {
+ return true;
+ }
+
+ return timestamp + TRACKING_CAP_LENGTH < Date.now();
+}
+
+export function dispatchRedisHllUserEvent(event, deferDuration = 0) {
delay(() => api.trackRedisHllUserEvent(event), deferDuration);
}
-export function queueRedisHllEvents(events) {
+export function queueRedisHllEvents(events, { verifyCap = false } = {}) {
+ if (verifyCap) {
+ if (!shouldDispatchEvent()) {
+ return;
+ }
+
+ localStorage.setItem(TRACKING_CAP_KEY, Date.now());
+ }
+
events.forEach((event, index) => {
- trackRedisHllUserEvent(event, DEFER_DURATION * (index + 1));
+ dispatchRedisHllUserEvent(event, DEFER_DURATION * (index + 1));
});
}
diff --git a/app/assets/javascripts/editor/components/source_editor_toolbar.vue b/app/assets/javascripts/editor/components/source_editor_toolbar.vue
index 1427f2df461..2c177634bbe 100644
--- a/app/assets/javascripts/editor/components/source_editor_toolbar.vue
+++ b/app/assets/javascripts/editor/components/source_editor_toolbar.vue
@@ -55,8 +55,8 @@ export default {
id="se-toolbar"
class="gl-py-3 gl-px-5 gl-bg-white gl-border-t gl-border-b gl-display-flex gl-justify-content-space-between gl-align-items-center"
>
- <template v-for="group in $options.groups">
- <gl-button-group v-if="hasGroupItems(group)" :key="group">
+ <div v-for="group in $options.groups" :key="group">
+ <gl-button-group v-if="hasGroupItems(group)">
<template v-for="item in getGroupItems(group)">
<source-editor-toolbar-button
:key="item.id"
@@ -65,6 +65,6 @@ export default {
/>
</template>
</gl-button-group>
- </template>
+ </div>
</section>
</template>
diff --git a/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue b/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue
index 2595d67af34..194b482c12e 100644
--- a/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue
+++ b/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue
@@ -1,7 +1,5 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
-import updateToolbarItemMutation from '~/editor/graphql/update_item.mutation.graphql';
-import getToolbarItemQuery from '~/editor/graphql/get_item.query.graphql';
export default {
name: 'SourceEditorToolbarButton',
@@ -20,70 +18,40 @@ export default {
},
},
},
- data() {
- return {
- buttonItem: this.button,
- };
- },
- apollo: {
- buttonItem: {
- query: getToolbarItemQuery,
- variables() {
- return {
- id: this.button.id,
- };
- },
- update({ item }) {
- return item;
- },
- skip() {
- return !this.button.id;
- },
- },
- },
computed: {
icon() {
- return this.buttonItem.selected
- ? this.buttonItem.selectedIcon || this.buttonItem.icon
- : this.buttonItem.icon;
+ return this.button.selected ? this.button.selectedIcon || this.button.icon : this.button.icon;
},
label() {
- return this.buttonItem.selected
- ? this.buttonItem.selectedLabel || this.buttonItem.label
- : this.buttonItem.label;
+ return this.button.selected
+ ? this.button.selectedLabel || this.button.label
+ : this.button.label;
+ },
+ showButton() {
+ return Object.entries(this.button).length > 0;
},
},
methods: {
clickHandler() {
- if (this.buttonItem.onClick) {
- this.buttonItem.onClick();
+ if (this.button.onClick) {
+ this.button.onClick();
}
- this.$apollo.mutate({
- mutation: updateToolbarItemMutation,
- variables: {
- id: this.buttonItem.id,
- propsToUpdate: {
- selected: !this.buttonItem.selected,
- },
- },
- });
this.$emit('click');
},
},
};
</script>
<template>
- <div>
- <gl-button
- v-gl-tooltip.hover
- :category="buttonItem.category"
- :variant="buttonItem.variant"
- type="button"
- :selected="buttonItem.selected"
- :icon="icon"
- :title="label"
- :aria-label="label"
- @click="clickHandler"
- />
- </div>
+ <gl-button
+ v-if="showButton"
+ v-gl-tooltip.hover
+ :category="button.category"
+ :variant="button.variant"
+ type="button"
+ :selected="button.selected"
+ :icon="icon"
+ :title="label"
+ :aria-label="label"
+ @click="clickHandler"
+ />
</template>
diff --git a/app/assets/javascripts/editor/components/source_editor_toolbar_graphql.js b/app/assets/javascripts/editor/components/source_editor_toolbar_graphql.js
new file mode 100644
index 00000000000..603ba26f22e
--- /dev/null
+++ b/app/assets/javascripts/editor/components/source_editor_toolbar_graphql.js
@@ -0,0 +1,53 @@
+import produce from 'immer';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import typeDefs from '~/editor/graphql/typedefs.graphql';
+import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql';
+import createDefaultClient from '~/lib/graphql';
+
+Vue.use(VueApollo);
+
+const resolvers = {
+ Mutation: {
+ addToolbarItems: (_, { items = [] }, { cache }) => {
+ const itemsSourceData = cache.readQuery({ query: getToolbarItemsQuery });
+ const data = produce(itemsSourceData, (draftData) => {
+ const existingNodes = draftData?.items?.nodes || [];
+ draftData.items = {
+ nodes: Array.isArray(items) ? [...existingNodes, ...items] : [...existingNodes, items],
+ };
+ });
+ cache.writeQuery({ query: getToolbarItemsQuery, data });
+ },
+
+ removeToolbarItems: (_, { ids }, { cache }) => {
+ const sourceData = cache.readQuery({ query: getToolbarItemsQuery });
+ const {
+ items: { nodes },
+ } = sourceData;
+ const data = produce(sourceData, (draftData) => {
+ draftData.items.nodes = nodes.filter((item) => !ids.includes(item.id));
+ });
+ cache.writeQuery({ query: getToolbarItemsQuery, data });
+ },
+
+ updateToolbarItem: (_, { id, propsToUpdate }, { cache }) => {
+ const itemSourceData = cache.readQuery({ query: getToolbarItemsQuery });
+ const data = produce(itemSourceData, (draftData) => {
+ const existingNodes = draftData?.items?.nodes || [];
+ draftData.items = {
+ nodes: existingNodes.map((item) => {
+ return item.id === id ? { ...item, ...propsToUpdate } : item;
+ }),
+ };
+ });
+ cache.writeQuery({ query: getToolbarItemsQuery, data });
+ },
+ },
+};
+
+const defaultClient = createDefaultClient(resolvers, { typeDefs });
+
+export const apolloProvider = new VueApollo({
+ defaultClient,
+});
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
index 361122d8890..83cfdd25757 100644
--- a/app/assets/javascripts/editor/constants.js
+++ b/app/assets/javascripts/editor/constants.js
@@ -1,5 +1,5 @@
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
export const URI_PREFIX = 'gitlab';
export const CONTENT_UPDATE_DEBOUNCE = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
@@ -57,5 +57,8 @@ export const EXTENSION_CI_SCHEMA_FILE_NAME_MATCH = '.gitlab-ci.yml';
export const EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS = 'md';
export const EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS = 'source-editor-preview';
export const EXTENSION_MARKDOWN_PREVIEW_ACTION_ID = 'markdown-preview';
+export const EXTENSION_MARKDOWN_PREVIEW_HIDE_ACTION_ID = 'markdown-preview-hide';
export const EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH = 0.5; // 50% of the width
export const EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY = 250; // ms
+export const EXTENSION_MARKDOWN_PREVIEW_LABEL = __('Preview Markdown');
+export const EXTENSION_MARKDOWN_HIDE_PREVIEW_LABEL = __('Hide Live Preview');
diff --git a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
index 3aa19df964c..0590bb7455a 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
@@ -5,7 +5,7 @@ import {
EXTENSION_BASE_LINE_NUMBERS_CLASS,
} from '../constants';
-const hashRegexp = new RegExp('#?L', 'g');
+const hashRegexp = /#?L/g;
const createAnchor = (href) => {
const fragment = new DocumentFragment();
@@ -64,7 +64,7 @@ export class SourceEditorExtension {
const [start, end] =
bounds && Array.isArray(bounds)
? bounds
- : window.location.hash?.replace(hashRegexp, '').split('-');
+ : window.location.hash.replace(hashRegexp, '').split('-');
let startLine = start ? parseInt(start, 10) : null;
let endLine = end ? parseInt(end, 10) : startLine;
if (endLine < startLine) {
diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
index 9d53268c340..11cc85c659d 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
@@ -3,14 +3,17 @@ import { BLOB_PREVIEW_ERROR } from '~/blob_edit/constants';
import createFlash from '~/flash';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
import syntaxHighlight from '~/syntax_highlight';
import {
EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS,
EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
+ EXTENSION_MARKDOWN_PREVIEW_HIDE_ACTION_ID,
EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH,
EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS,
EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY,
+ EXTENSION_MARKDOWN_PREVIEW_LABEL,
+ EXTENSION_MARKDOWN_HIDE_PREVIEW_LABEL,
+ EDITOR_TOOLBAR_RIGHT_GROUP,
} from '../constants';
const fetchPreview = (text, previewMarkdownPath) => {
@@ -41,31 +44,58 @@ export class EditorMarkdownPreviewExtension {
onSetup(instance, setupOptions) {
this.preview = {
el: undefined,
- action: undefined,
+ actions: {
+ preview: undefined,
+ hide: undefined,
+ },
shown: false,
modelChangeListener: undefined,
path: setupOptions.previewMarkdownPath,
+ actionShowPreviewCondition: instance.createContextKey('toggleLivePreview', true),
};
+ this.toolbarButtons = [];
+
this.setupPreviewAction(instance);
+ if (instance.toolbar) {
+ this.setupToolbar(instance);
+ }
+ }
- instance.getModel().onDidChangeLanguage(({ newLanguage, oldLanguage } = {}) => {
- if (newLanguage === 'markdown' && oldLanguage !== newLanguage) {
- instance.setupPreviewAction();
- } else {
- instance.cleanup();
- }
- });
+ onBeforeUnuse(instance) {
+ this.cleanup(instance);
+ const ids = this.toolbarButtons.map((item) => item.id);
+ if (instance.toolbar) {
+ instance.toolbar.removeItems(ids);
+ }
+ }
- instance.onDidChangeModel(() => {
- const model = instance.getModel();
- if (model) {
- const { language } = model.getLanguageIdentifier();
- instance.cleanup();
- if (language === 'markdown') {
- instance.setupPreviewAction();
- }
- }
- });
+ cleanup(instance) {
+ if (this.preview.modelChangeListener) {
+ this.preview.modelChangeListener.dispose();
+ }
+ this.preview.actions.preview.dispose();
+ this.preview.actions.hide.dispose();
+ if (this.preview.shown) {
+ this.togglePreviewPanel(instance);
+ this.togglePreviewLayout(instance);
+ }
+ this.preview.shown = false;
+ }
+
+ setupToolbar(instance) {
+ this.toolbarButtons = [
+ {
+ id: EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
+ label: EXTENSION_MARKDOWN_PREVIEW_LABEL,
+ icon: 'live-preview',
+ selected: false,
+ group: EDITOR_TOOLBAR_RIGHT_GROUP,
+ category: 'primary',
+ selectedLabel: EXTENSION_MARKDOWN_HIDE_PREVIEW_LABEL,
+ onClick: () => instance.togglePreview(),
+ },
+ ];
+ instance.toolbar.addItems(this.toolbarButtons);
}
togglePreviewLayout(instance) {
@@ -103,22 +133,33 @@ export class EditorMarkdownPreviewExtension {
setupPreviewAction(instance) {
if (instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)) return;
-
- this.preview.action = instance.addAction({
- id: EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
- label: __('Preview Markdown'),
+ const actionBasis = {
keybindings: [
// eslint-disable-next-line no-bitwise,no-undef
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KEY_P),
],
contextMenuGroupId: 'navigation',
contextMenuOrder: 1.5,
-
// Method that will be executed when the action is triggered.
// @param ed The editor instance is passed in as a convenience
run(inst) {
inst.togglePreview();
},
+ };
+
+ this.preview.actions.preview = instance.addAction({
+ ...actionBasis,
+ id: EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
+ label: EXTENSION_MARKDOWN_PREVIEW_LABEL,
+
+ precondition: 'toggleLivePreview',
+ });
+ this.preview.actions.hide = instance.addAction({
+ ...actionBasis,
+ id: EXTENSION_MARKDOWN_PREVIEW_HIDE_ACTION_ID,
+ label: EXTENSION_MARKDOWN_HIDE_PREVIEW_LABEL,
+
+ precondition: '!toggleLivePreview',
});
}
@@ -126,18 +167,6 @@ export class EditorMarkdownPreviewExtension {
return {
markdownPreview: this.preview,
- cleanup: (instance) => {
- if (this.preview.modelChangeListener) {
- this.preview.modelChangeListener.dispose();
- }
- this.preview.action.dispose();
- if (this.preview.shown) {
- this.togglePreviewPanel(instance);
- this.togglePreviewLayout(instance);
- }
- this.preview.shown = false;
- },
-
fetchPreview: (instance) => this.fetchPreview(instance),
setupPreviewAction: (instance) => this.setupPreviewAction(instance),
@@ -149,6 +178,8 @@ export class EditorMarkdownPreviewExtension {
this.togglePreviewLayout(instance);
this.togglePreviewPanel(instance);
+ this.preview.actionShowPreviewCondition.set(!this.preview.actionShowPreviewCondition.get());
+
if (!this.preview?.shown) {
this.preview.modelChangeListener = instance.onDidChangeModelContent(
debounce(
@@ -161,6 +192,11 @@ export class EditorMarkdownPreviewExtension {
}
this.preview.shown = !this.preview?.shown;
+ if (instance.toolbar) {
+ instance.toolbar.updateItem(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID, {
+ selected: this.preview.shown,
+ });
+ }
},
};
}
diff --git a/app/assets/javascripts/editor/extensions/source_editor_toolbar_ext.js b/app/assets/javascripts/editor/extensions/source_editor_toolbar_ext.js
new file mode 100644
index 00000000000..9655c8ae76a
--- /dev/null
+++ b/app/assets/javascripts/editor/extensions/source_editor_toolbar_ext.js
@@ -0,0 +1,98 @@
+import Vue from 'vue';
+import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql';
+import removeToolbarItemsMutation from '~/editor/graphql/remove_items.mutation.graphql';
+import updateToolbarItemMutation from '~/editor/graphql/update_item.mutation.graphql';
+import addToolbarItemsMutation from '~/editor/graphql/add_items.mutation.graphql';
+import SourceEditorToolbar from '~/editor/components/source_editor_toolbar.vue';
+import { apolloProvider } from '~/editor/components/source_editor_toolbar_graphql';
+
+const client = apolloProvider.defaultClient;
+
+export class ToolbarExtension {
+ /**
+ * A required getter returning the extension's name
+ * We have to provide it for every extension instead of relying on the built-in
+ * `name` prop because the prop does not survive the webpack's minification
+ * and the name mangling.
+ * @returns {string}
+ */
+ static get extensionName() {
+ return 'ToolbarExtension';
+ }
+ /**
+ * THE LIFE-CYCLE CALLBACKS
+ */
+
+ /**
+ * Is called before the extension gets used by an instance,
+ * Use `onSetup` to setup Monaco directly:
+ * actions, keystrokes, update options, etc.
+ * Is called only once before the extension gets registered
+ *
+ * @param { Object } [instance] The Source Editor instance
+ * @param { Object } [setupOptions] The setupOptions object
+ */
+ // eslint-disable-next-line class-methods-use-this
+ onSetup(instance, setupOptions) {
+ const el = setupOptions?.el || document.getElementById('editor-toolbar');
+ ToolbarExtension.setupVue(el);
+ }
+
+ static setupVue(el) {
+ client.cache.writeQuery({ query: getToolbarItemsQuery, data: { items: { nodes: [] } } });
+ const ToolbarComponent = Vue.extend(SourceEditorToolbar);
+
+ const toolbar = new ToolbarComponent({
+ el,
+ apolloProvider,
+ });
+ toolbar.$mount();
+ }
+
+ /**
+ * The public API of the extension: these are the methods that will be exposed
+ * to the end user
+ * @returns {Object}
+ */
+ // eslint-disable-next-line class-methods-use-this
+ provides() {
+ return {
+ toolbar: {
+ getItem: (id) => {
+ const items = client.readQuery({ query: getToolbarItemsQuery })?.items?.nodes || [];
+ return items.find((item) => item.id === id);
+ },
+ getAllItems: () => {
+ return client.readQuery({ query: getToolbarItemsQuery })?.items?.nodes || [];
+ },
+ addItems: (items = []) => {
+ return client.mutate({
+ mutation: addToolbarItemsMutation,
+ variables: {
+ items,
+ },
+ });
+ },
+ removeItems: (ids = []) => {
+ client.mutate({
+ mutation: removeToolbarItemsMutation,
+ variables: {
+ ids,
+ },
+ });
+ },
+ updateItem: (id = '', propsToUpdate = {}) => {
+ if (id) {
+ client.mutate({
+ mutation: updateToolbarItemMutation,
+ variables: {
+ id,
+ propsToUpdate,
+ },
+ });
+ }
+ },
+ },
+ };
+ }
+}
diff --git a/app/assets/javascripts/editor/graphql/add_items.mutation.graphql b/app/assets/javascripts/editor/graphql/add_items.mutation.graphql
new file mode 100644
index 00000000000..13afcc04a48
--- /dev/null
+++ b/app/assets/javascripts/editor/graphql/add_items.mutation.graphql
@@ -0,0 +1,3 @@
+mutation addItems($items: [Item]) {
+ addToolbarItems(items: $items) @client
+}
diff --git a/app/assets/javascripts/editor/graphql/get_item.query.graphql b/app/assets/javascripts/editor/graphql/get_item.query.graphql
deleted file mode 100644
index 7c8bc09f7b0..00000000000
--- a/app/assets/javascripts/editor/graphql/get_item.query.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-query ToolbarItem($id: String!) {
- item(id: $id) @client {
- id
- label
- icon
- selected
- group
- }
-}
diff --git a/app/assets/javascripts/editor/graphql/remove_items.mutation.graphql b/app/assets/javascripts/editor/graphql/remove_items.mutation.graphql
new file mode 100644
index 00000000000..627f105b0ec
--- /dev/null
+++ b/app/assets/javascripts/editor/graphql/remove_items.mutation.graphql
@@ -0,0 +1,3 @@
+mutation removeToolbarItems($ids: [ID!]) {
+ removeToolbarItems(ids: $ids) @client
+}
diff --git a/app/assets/javascripts/editor/graphql/typedefs.graphql b/app/assets/javascripts/editor/graphql/typedefs.graphql
new file mode 100644
index 00000000000..2433ebf6c66
--- /dev/null
+++ b/app/assets/javascripts/editor/graphql/typedefs.graphql
@@ -0,0 +1,23 @@
+type Item {
+ id: ID!
+ label: String!
+ icon: String
+ selected: Boolean
+ group: Int!
+ category: String
+ selectedLabel: String
+}
+
+type Items {
+ nodes: [Item]!
+}
+
+extend type Query {
+ items: Items
+}
+
+extend type Mutation {
+ updateToolbarItem(id: ID!, propsToUpdate: Item!): LocalErrors
+ removeToolbarItems(ids: [ID!]): LocalErrors
+ addToolbarItems(items: [Item]): LocalErrors
+}
diff --git a/app/assets/javascripts/editor/graphql/update_item.mutation.graphql b/app/assets/javascripts/editor/graphql/update_item.mutation.graphql
index f8424c65181..05c18988c87 100644
--- a/app/assets/javascripts/editor/graphql/update_item.mutation.graphql
+++ b/app/assets/javascripts/editor/graphql/update_item.mutation.graphql
@@ -1,3 +1,3 @@
-mutation updateItem($id: String!, $propsToUpdate: Item!) {
+mutation updateItem($id: ID!, $propsToUpdate: Item!) {
updateToolbarItem(id: $id, propsToUpdate: $propsToUpdate) @client
}
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index fe3229ac91b..1352211b927 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -170,23 +170,6 @@
}
]
},
- "cobertura": {
- "description": "Path for file(s) that should be parsed as Cobertura XML coverage report",
- "oneOf": [
- {
- "type": "string",
- "description": "Path to a single XML file"
- },
- {
- "type": "array",
- "description": "A list of paths to XML files that will automatically be merged into one report",
- "items": {
- "type": "string"
- },
- "minItems": 1
- }
- ]
- },
"coverage_report": {
"type": "object",
"description": "Used to collect coverage reports from the job.",
@@ -1093,8 +1076,8 @@
"description": "The name of a job to execute when the environment is about to be stopped."
},
"action": {
- "enum": ["start", "prepare", "stop"],
- "description": "Specifies what this job will do. 'start' (default) indicates the job will start the deployment. 'prepare' indicates this will not affect the deployment. 'stop' indicates this will stop the deployment.",
+ "enum": ["start", "prepare", "stop", "verify", "access"],
+ "description": "Specifies what this job will do. 'start' (default) indicates the job will start the deployment. 'prepare'/'verify'/'access' indicates this will not affect the deployment. 'stop' indicates this will stop the deployment.",
"default": "start"
},
"auto_stop_in": {
diff --git a/app/assets/javascripts/environments/components/enable_review_app_modal.vue b/app/assets/javascripts/environments/components/enable_review_app_modal.vue
index 4d43ee156fb..6343fe8702a 100644
--- a/app/assets/javascripts/environments/components/enable_review_app_modal.vue
+++ b/app/assets/javascripts/environments/components/enable_review_app_modal.vue
@@ -45,6 +45,8 @@ export default {
copyToClipboardText: s__('EnableReviewApp|Copy snippet text'),
title: s__('ReviewApp|Enable Review App'),
},
+ visualReviewsDocs: helpPagePath('ci/review_apps/index.md', { anchor: 'visual-reviews' }),
+ connectClusterDocs: helpPagePath('user/clusters/agent/index'),
data() {
const modalInfoCopyId = uniqueId('enable-review-app-copy-string-');
@@ -64,9 +66,6 @@ export default {
except:
- ${this.defaultBranchName}`;
},
- visualReviewsDocs() {
- return helpPagePath('ci/review_apps/index.md', { anchor: 'visual-reviews' });
- },
},
};
</script>
@@ -88,11 +87,7 @@ export default {
<strong>{{ content }}</strong>
</template>
<template #link="{ content }">
- <gl-link
- href="https://docs.gitlab.com/ee/user/project/clusters/add_remove_clusters.html"
- target="_blank"
- >{{ content }}</gl-link
- >
+ <gl-link :href="$options.connectClusterDocs" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
@@ -134,7 +129,7 @@ export default {
<strong>{{ content }}</strong>
</template>
<template #link="{ content }">
- <gl-link :href="visualReviewsDocs" target="_blank">{{ content }}</gl-link>
+ <gl-link :href="$options.visualReviewsDocs" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
diff --git a/app/assets/javascripts/environments/components/environment_folder.vue b/app/assets/javascripts/environments/components/environment_folder.vue
index d5c6d26cfd0..788c3ba6fed 100644
--- a/app/assets/javascripts/environments/components/environment_folder.vue
+++ b/app/assets/javascripts/environments/components/environment_folder.vue
@@ -34,9 +34,6 @@ export default {
variables() {
return { environment: this.nestedEnvironment.latest, scope: this.scope };
},
- pollInterval() {
- return this.interval;
- },
},
interval: {
query: pollIntervalQuery,
@@ -73,6 +70,11 @@ export default {
methods: {
toggleCollapse() {
this.visible = !this.visible;
+ if (this.visible) {
+ this.$apollo.queries.folder.startPolling(this.interval);
+ } else {
+ this.$apollo.queries.folder.stopPolling();
+ }
},
isFirstEnvironment(index) {
return index === 0;
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 7b8b756487b..7fcd6e5fff8 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -146,7 +146,7 @@ export default {
{{ tableData.autoStop.title }}
</div>
</div>
- <template v-for="(model, i) in sortedEnvironments" :model="model">
+ <template v-for="(model, i) in sortedEnvironments">
<environment-item
:key="`environment-item-${i}`"
:model="model"
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index 3c608ad0ba9..adb14ce3d6f 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -54,7 +54,9 @@ export default {
:key="`${tab.name}-${i}`"
:active="tab.isActive"
:title-item-class="tab.isActive ? 'gl-outline-none' : ''"
- :title-link-attributes="{ 'data-testid': `environments-tab-${tab.scope}` }"
+ :title-link-attributes="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ 'data-testid': `environments-tab-${tab.scope}`,
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@click="onChangeTab(tab.scope)"
>
<template #title>
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 3d540d46b3c..86102fd54b1 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -254,7 +254,7 @@ export default {
data-testid="integrated-disabled-alert"
@dismiss="isAlertDismissed = true"
>
- <gl-sprintf :message="this.$options.i18n.integratedErrorTrackingDisabledText">
+ <gl-sprintf :message="$options.i18n.integratedErrorTrackingDisabledText">
<template #epicLink="{ content }">
<gl-link :href="$options.epicLink" target="_blank">{{ content }}</gl-link>
</template>
diff --git a/app/assets/javascripts/error_tracking/queries/details.query.graphql b/app/assets/javascripts/error_tracking/queries/details.query.graphql
index f70e09d76f7..dd21b0f9c92 100644
--- a/app/assets/javascripts/error_tracking/queries/details.query.graphql
+++ b/app/assets/javascripts/error_tracking/queries/details.query.graphql
@@ -1,4 +1,4 @@
-query errorDetails($fullPath: ID!, $errorId: ID!) {
+query errorDetails($fullPath: ID!, $errorId: GitlabErrorTrackingDetailedErrorID!) {
project(fullPath: $fullPath) {
id
sentryErrors {
diff --git a/app/assets/javascripts/error_tracking_settings/components/app.vue b/app/assets/javascripts/error_tracking_settings/components/app.vue
index e850d954e0a..70fb1fa9cd7 100644
--- a/app/assets/javascripts/error_tracking_settings/components/app.vue
+++ b/app/assets/javascripts/error_tracking_settings/components/app.vue
@@ -152,7 +152,7 @@ export default {
<template>
<div>
<gl-alert v-if="showIntegratedTrackingDisabledAlert" variant="danger" @dismiss="dismissAlert">
- <gl-sprintf :message="this.$options.i18n.integratedErrorTrackingDisabledText">
+ <gl-sprintf :message="$options.i18n.integratedErrorTrackingDisabledText">
<template #epicLink="{ content }">
<gl-link :href="$options.epicLink" target="_blank">{{ content }}</gl-link>
</template>
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index 09cef74477c..b57db73a86e 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -1,18 +1,15 @@
import { sortMilestonesByDueDate } from '~/milestones/utils';
import { mergeUrlParams } from '../lib/utils/url_utility';
-import DropdownAjaxFilter from './dropdown_ajax_filter';
import DropdownEmoji from './dropdown_emoji';
import DropdownHint from './dropdown_hint';
import DropdownNonUser from './dropdown_non_user';
import DropdownOperator from './dropdown_operator';
import DropdownUser from './dropdown_user';
import DropdownUtils from './dropdown_utils';
-import NullDropdown from './null_dropdown';
export default class AvailableDropdownMappings {
constructor({
container,
- runnerTagsEndpoint,
labelsEndpoint,
milestonesEndpoint,
releasesEndpoint,
@@ -22,7 +19,6 @@ export default class AvailableDropdownMappings {
includeDescendantGroups,
}) {
this.container = container;
- this.runnerTagsEndpoint = runnerTagsEndpoint;
this.labelsEndpoint = labelsEndpoint;
this.milestonesEndpoint = milestonesEndpoint;
this.releasesEndpoint = releasesEndpoint;
@@ -135,25 +131,6 @@ export default class AvailableDropdownMappings {
gl: DropdownNonUser,
element: this.container.querySelector('#js-dropdown-confidential'),
},
- status: {
- reference: null,
- gl: NullDropdown,
- element: this.container.querySelector('#js-dropdown-admin-runner-status'),
- },
- type: {
- reference: null,
- gl: NullDropdown,
- element: this.container.querySelector('#js-dropdown-admin-runner-type'),
- },
- tag: {
- reference: null,
- gl: DropdownAjaxFilter,
- extraArguments: {
- endpoint: this.getRunnerTagsEndpoint(),
- symbol: '~',
- },
- element: this.container.querySelector('#js-dropdown-runner-tag'),
- },
'target-branch': {
reference: null,
gl: DropdownNonUser,
@@ -202,10 +179,6 @@ export default class AvailableDropdownMappings {
return endpoint;
}
- getRunnerTagsEndpoint() {
- return `${this.runnerTagsEndpoint}.json`;
- }
-
getMergeRequestTargetBranchesEndpoint() {
const endpoint = `${
gon.relative_url_root || ''
diff --git a/app/assets/javascripts/filtered_search/constants.js b/app/assets/javascripts/filtered_search/constants.js
index f8b5910de9e..e07dccd11e8 100644
--- a/app/assets/javascripts/filtered_search/constants.js
+++ b/app/assets/javascripts/filtered_search/constants.js
@@ -15,6 +15,4 @@ export const MAX_HISTORY_SIZE = 5;
export const FILTERED_SEARCH = {
MERGE_REQUESTS: 'merge_requests',
ISSUES: 'issues',
- ADMIN_RUNNERS: 'admin/runners',
- GROUP_RUNNERS_ANCHOR: 'runners-settings',
};
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index e467e97dda9..7471d3204d6 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -9,7 +9,6 @@ import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
export default class FilteredSearchDropdownManager {
constructor({
- runnerTagsEndpoint = '',
labelsEndpoint = '',
milestonesEndpoint = '',
iterationsEndpoint = '',
@@ -26,7 +25,6 @@ export default class FilteredSearchDropdownManager {
const removeTrailingSlash = (url) => url.replace(/\/$/, '');
this.container = FilteredSearchContainer.container;
- this.runnerTagsEndpoint = removeTrailingSlash(runnerTagsEndpoint);
this.labelsEndpoint = removeTrailingSlash(labelsEndpoint);
this.milestonesEndpoint = removeTrailingSlash(milestonesEndpoint);
this.iterationsEndpoint = removeTrailingSlash(iterationsEndpoint);
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 5ba69f052c9..07f2c75f00a 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -114,7 +114,6 @@ export default class FilteredSearchManager {
this.tokenizer = FilteredSearchTokenizer;
const {
- runnerTagsEndpoint = '',
labelsEndpoint = '',
milestonesEndpoint = '',
releasesEndpoint = '',
@@ -124,7 +123,6 @@ export default class FilteredSearchManager {
} = this.filteredSearchInput.dataset;
this.dropdownManager = new FilteredSearchDropdownManager({
- runnerTagsEndpoint,
labelsEndpoint,
milestonesEndpoint,
releasesEndpoint,
@@ -583,7 +581,7 @@ export default class FilteredSearchManager {
* Eg. not[foo]=%bar
* key = foo; value = %bar
*/
- const notKeyValueRegex = new RegExp(/not\[(\w+)\]\[?\]?=(.*)/);
+ const notKeyValueRegex = /not\[(\w+)\]\[?\]?=(.*)/;
return params.map((query) => {
// Check if there are matches for `not` operator
diff --git a/app/assets/javascripts/filtered_search/group_runners_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/group_runners_filtered_search_token_keys.js
deleted file mode 100644
index ceeb71c4eec..00000000000
--- a/app/assets/javascripts/filtered_search/group_runners_filtered_search_token_keys.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { __ } from '~/locale';
-import FilteredSearchTokenKeys from './filtered_search_token_keys';
-
-const tokenKeys = [
- {
- formattedKey: __('Status'),
- key: 'status',
- type: 'string',
- param: 'status',
- symbol: '',
- icon: 'messages',
- tag: 'status',
- },
- {
- formattedKey: __('Type'),
- key: 'type',
- type: 'string',
- param: 'type',
- symbol: '',
- icon: 'cube',
- tag: 'type',
- },
-];
-
-const GroupRunnersFilteredSearchTokenKeys = new FilteredSearchTokenKeys(tokenKeys);
-
-export default GroupRunnersFilteredSearchTokenKeys;
diff --git a/app/assets/javascripts/filtered_search/null_dropdown.js b/app/assets/javascripts/filtered_search/null_dropdown.js
deleted file mode 100644
index 4cfce2a5beb..00000000000
--- a/app/assets/javascripts/filtered_search/null_dropdown.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import FilteredSearchDropdown from './filtered_search_dropdown';
-
-export default class NullDropdown extends FilteredSearchDropdown {
- renderContent(forceShowList = false) {
- this.droplab.changeHookList(this.hookId, this.dropdown, [], this.config);
-
- super.renderContent(forceShowList);
- }
-}
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 24ec16bf20e..5a47e76d597 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -86,48 +86,43 @@ const addDismissFlashClickListener = (flashEl, fadeTransition) => {
/**
* Render an alert at the top of the page, or, optionally an
- * arbitrary existing container.
- *
- * This alert is always dismissible.
- *
- * Usage:
- *
- * 1. Render a new alert
+ * arbitrary existing container. This alert is always dismissible.
*
+ * @example
+ * // Render a new alert
* import { createAlert, VARIANT_WARNING } from '~/flash';
*
* createAlert({ message: 'My error message' });
* createAlert({ message: 'My warning message', variant: VARIANT_WARNING });
*
- * 2. Dismiss this alert programmatically
- *
+ * @example
+ * // Dismiss this alert programmatically
* const alert = createAlert({ message: 'Message' });
*
* // ...
*
* alert.dismiss();
*
- * 3. Respond to the alert being dismissed
+ * @example
+ * // Respond to the alert being dismissed
+ * createAlert({ message: 'Message', onDismiss: () => {} });
*
- * createAlert({ message: 'Message', onDismiss: () => { ... }});
- *
- * @param {Object} options Options to control the flash message
- * @param {String} options.message Alert message text
- * @param {String?} options.variant Which GlAlert variant to use, should be VARIANT_SUCCESS, VARIANT_WARNING, VARIANT_DANGER, VARIANT_INFO or VARIANT_TIP. Defaults to VARIANT_DANGER.
- * @param {Object?} options.parent Reference to parent element under which alert needs to appear. Defaults to `document`.
- * @param {Function?} options.onDismiss Handler to call when this alert is dismissed.
- * @param {Object?} options.containerSelector Selector for the container of the alert
- * @param {Object?} options.primaryButton Object describing primary button of alert
- * @param {String?} link Href of primary button
- * @param {String?} text Text of primary button
- * @param {Function?} clickHandler Handler to call when primary button is clicked on. The click event is sent as an argument.
- * @param {Object?} options.secondaryButton Object describing secondary button of alert
- * @param {String?} link Href of secondary button
- * @param {String?} text Text of secondary button
- * @param {Function?} clickHandler Handler to call when secondary button is clicked on. The click event is sent as an argument.
- * @param {Boolean?} options.captureError Whether to send error to Sentry
- * @param {Object} options.error Error to be captured in Sentry
- * @returns
+ * @param {object} options - Options to control the flash message
+ * @param {string} options.message - Alert message text
+ * @param {VARIANT_SUCCESS|VARIANT_WARNING|VARIANT_DANGER|VARIANT_INFO|VARIANT_TIP} [options.variant] - Which GlAlert variant to use; it defaults to VARIANT_DANGER.
+ * @param {object} [options.parent] - Reference to parent element under which alert needs to appear. Defaults to `document`.
+ * @param {Function} [options.onDismiss] - Handler to call when this alert is dismissed.
+ * @param {string} [options.containerSelector] - Selector for the container of the alert
+ * @param {object} [options.primaryButton] - Object describing primary button of alert
+ * @param {string} [options.primaryButton.link] - Href of primary button
+ * @param {string} [options.primaryButton.text] - Text of primary button
+ * @param {Function} [options.primaryButton.clickHandler] - Handler to call when primary button is clicked on. The click event is sent as an argument.
+ * @param {object} [options.secondaryButton] - Object describing secondary button of alert
+ * @param {string} [options.secondaryButton.link] - Href of secondary button
+ * @param {string} [options.secondaryButton.text] - Text of secondary button
+ * @param {Function} [options.secondaryButton.clickHandler] - Handler to call when secondary button is clicked on. The click event is sent as an argument.
+ * @param {boolean} [options.captureError] - Whether to send error to Sentry
+ * @param {object} [options.error] - Error to be captured in Sentry
*/
const createAlert = function createAlert({
message,
@@ -207,22 +202,25 @@ const createAlert = function createAlert({
});
};
-/*
- * Flash banner supports different types of Flash configurations
- * along with ability to provide actionConfig which can be used to show
- * additional action or link on banner next to message
+/**
+ * @deprecated use `createAlert` instead
+ *
+ * Flash banner supports different types of Flash configurations
+ * along with ability to provide actionConfig which can be used to show
+ * additional action or link on banner next to message
*
- * @param {Object} options Options to control the flash message
- * @param {String} options.message Flash message text
- * @param {String} options.type Type of Flash, it can be `notice`, `success`, `warning` or `alert` (default)
- * @param {Object} options.parent Reference to parent element under which Flash needs to appear
- * @param {Object} options.actionConfig Map of config to show action on banner
- * @param {String} href URL to which action config should point to (default: '#')
- * @param {String} title Title of action
- * @param {Function} clickHandler Method to call when action is clicked on
- * @param {Boolean} options.fadeTransition Boolean to determine whether to fade the alert out
- * @param {Boolean} options.captureError Boolean to determine whether to send error to Sentry
- * @param {Object} options.error Error to be captured in Sentry
+ * @param {object} options - Options to control the flash message
+ * @param {string} options.message - Flash message text
+ * @param {'alert'|'notice'|'success'|'warning'} [options.type] - Type of Flash; it defaults to 'alert'
+ * @param {Element|Document} [options.parent] - Reference to parent element under which Flash needs to appear
+ * @param {object} [options.actionConfig] - Map of config to show action on banner
+ * @param {string} [options.actionConfig.href] - URL to which action config should point to (default: '#')
+ * @param {string} [options.actionConfig.title] - Title of action
+ * @param {Function} [options.actionConfig.clickHandler] - Method to call when action is clicked on
+ * @param {boolean} [options.fadeTransition] - Boolean to determine whether to fade the alert out
+ * @param {boolean} [options.addBodyClass] - Adds `flash-shown` class to the `body` element
+ * @param {boolean} [options.captureError] - Boolean to determine whether to send error to Sentry
+ * @param {object} [options.error] - Error to be captured in Sentry
*/
const createFlash = function createFlash({
message,
diff --git a/app/assets/javascripts/frequent_items/constants.js b/app/assets/javascripts/frequent_items/constants.js
index 9e1dcf70aa5..cb5d21161a9 100644
--- a/app/assets/javascripts/frequent_items/constants.js
+++ b/app/assets/javascripts/frequent_items/constants.js
@@ -7,7 +7,7 @@ export const FREQUENT_ITEMS = {
ELIGIBLE_FREQUENCY: 3,
};
-export const HOUR_IN_MS = 3600000;
+export const FIFTEEN_MINUTES_IN_MS = 900000;
export const STORAGE_KEY = {
projects: 'frequent-projects',
diff --git a/app/assets/javascripts/frequent_items/utils.js b/app/assets/javascripts/frequent_items/utils.js
index 27ef47df8c8..1c33c8b1084 100644
--- a/app/assets/javascripts/frequent_items/utils.js
+++ b/app/assets/javascripts/frequent_items/utils.js
@@ -1,7 +1,7 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { take } from 'lodash';
import { sanitize } from '~/lib/dompurify';
-import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
+import { FREQUENT_ITEMS, FIFTEEN_MINUTES_IN_MS } from './constants';
export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize());
@@ -38,7 +38,8 @@ export const updateExistingFrequentItem = (frequentItem, item) => {
// `frequentItem` comes from localStorage and it's possible it doesn't have a `lastAccessedOn`
const neverAccessed = !frequentItem.lastAccessedOn;
const shouldUpdate =
- neverAccessed || Math.abs(item.lastAccessedOn - frequentItem.lastAccessedOn) / HOUR_IN_MS > 1;
+ neverAccessed ||
+ Math.abs(item.lastAccessedOn - frequentItem.lastAccessedOn) / FIFTEEN_MINUTES_IN_MS > 1;
return {
...item,
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 8cb2e9e249b..b1af1ad797b 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -844,7 +844,7 @@ class GfmAutoComplete {
}
}
-GfmAutoComplete.regexSubtext = new RegExp(/\s+/g);
+GfmAutoComplete.regexSubtext = /\s+/g;
GfmAutoComplete.defaultLoadingData = ['loading'];
diff --git a/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql
index 78b2cd34a5c..824997f8e33 100644
--- a/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql
@@ -1,4 +1,5 @@
fragment TimelogFragment on Timelog {
+ id
timeSpent
user {
id
diff --git a/app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql
index 429993b37bf..0b451262b5a 100644
--- a/app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql
@@ -1,4 +1,3 @@
-# eslint-disable-next-line @graphql-eslint/require-id-when-available
fragment UserAvailability on User {
status {
availability
diff --git a/app/assets/javascripts/graphql_shared/mutations/todo_mark_done.mutation.graphql b/app/assets/javascripts/graphql_shared/mutations/todo_mark_done.mutation.graphql
index 4d59b4d94cd..e8ad919f4a8 100644
--- a/app/assets/javascripts/graphql_shared/mutations/todo_mark_done.mutation.graphql
+++ b/app/assets/javascripts/graphql_shared/mutations/todo_mark_done.mutation.graphql
@@ -1,4 +1,4 @@
-mutation todoMarkDone($id: ID!) {
+mutation todoMarkDone($id: TodoID!) {
todoMarkDone(input: { id: $id }) {
errors
todo {
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 3d6360fc4f8..7ca3f20ec1c 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -76,6 +76,10 @@
"Discussion",
"Note"
],
+ "SecurityPolicySource": [
+ "GroupSecurityPolicySource",
+ "ProjectSecurityPolicySource"
+ ],
"Service": [
"BaseService",
"JiraService"
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js
index b6a6720e7a1..49e7dd28ff6 100644
--- a/app/assets/javascripts/group.js
+++ b/app/assets/javascripts/group.js
@@ -1,8 +1,13 @@
+import { debounce } from 'lodash';
+
import createFlash from '~/flash';
import { __ } from '~/locale';
import { getGroupPathAvailability } from '~/rest_api';
+import axios from '~/lib/utils/axios_utils';
import { slugify } from './lib/utils/text_utility';
+const DEBOUNCE_TIMEOUT_DURATION = 1000;
+
export default class Group {
constructor() {
this.groupPaths = Array.from(document.querySelectorAll('.js-autofill-group-path'));
@@ -10,7 +15,11 @@ export default class Group {
this.parentId = document.getElementById('group_parent_id');
this.updateHandler = this.update.bind(this);
this.resetHandler = this.reset.bind(this);
- this.updateGroupPathSlugHandler = this.updateGroupPathSlug.bind(this);
+ this.updateGroupPathSlugHandler = debounce(
+ this.updateGroupPathSlug.bind(this),
+ DEBOUNCE_TIMEOUT_DURATION,
+ );
+ this.currentApiRequestController = null;
this.groupNames.forEach((groupName) => {
groupName.addEventListener('keyup', this.updateHandler);
@@ -44,13 +53,23 @@ export default class Group {
});
}
- updateGroupPathSlug({ currentTarget: { value } = '' } = {}) {
- const slug = this.groupPaths[0]?.value || slugify(value);
+ updateGroupPathSlug({ target: { value } = '' } = {}) {
+ if (this.currentApiRequestController !== null) {
+ this.currentApiRequestController.abort();
+ }
+
+ this.currentApiRequestController = new AbortController();
+
+ const slug = slugify(value);
if (!slug) return;
- getGroupPathAvailability(slug, this.parentId?.value)
+ getGroupPathAvailability(slug, this.parentId?.value, {
+ signal: this.currentApiRequestController.signal,
+ })
.then(({ data }) => data)
.then(({ exists, suggests }) => {
+ this.currentApiRequestController = null;
+
if (exists && suggests.length) {
const [suggestedSlug] = suggests;
@@ -63,10 +82,14 @@ export default class Group {
});
}
})
- .catch(() =>
+ .catch((error) => {
+ if (axios.isCancel(error)) {
+ return;
+ }
+
createFlash({
message: __('An error occurred while checking group path. Please refresh and try again.'),
- }),
- );
+ });
+ });
}
}
diff --git a/app/assets/javascripts/group_settings/components/shared_runners_form.vue b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
index dcac337c6ef..3365f4aa76c 100644
--- a/app/assets/javascripts/group_settings/components/shared_runners_form.vue
+++ b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
@@ -1,73 +1,64 @@
<script>
-import { GlToggle, GlLoadingIcon, GlTooltip, GlAlert } from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { GlToggle, GlAlert } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
-import { DEBOUNCE_TOGGLE_DELAY, ERROR_MESSAGE } from '../constants';
+import { ERROR_MESSAGE } from '../constants';
export default {
components: {
GlToggle,
- GlLoadingIcon,
- GlTooltip,
GlAlert,
},
inject: [
'updatePath',
- 'sharedRunnersAvailability',
- 'parentSharedRunnersAvailability',
- 'runnerEnabled',
- 'runnerDisabled',
- 'runnerAllowOverride',
+ 'sharedRunnersSetting',
+ 'parentSharedRunnersSetting',
+ 'runnerEnabledValue',
+ 'runnerDisabledValue',
+ 'runnerAllowOverrideValue',
],
data() {
return {
isLoading: false,
- enabled: true,
- allowOverride: false,
+ value: this.sharedRunnersSetting,
error: null,
};
},
computed: {
- toggleDisabled() {
- return this.parentSharedRunnersAvailability === this.runnerDisabled || this.isLoading;
+ isSharedRunnersToggleDisabled() {
+ return this.parentSharedRunnersSetting === this.runnerDisabledValue;
},
- enabledOrDisabledSetting() {
- return this.enabled ? this.runnerEnabled : this.runnerDisabled;
+ sharedRunnersToggleValue() {
+ return this.value === this.runnerEnabledValue;
},
- disabledWithOverrideSetting() {
- return this.allowOverride ? this.runnerAllowOverride : this.runnerDisabled;
+ isOverrideToggleDisabled() {
+ // cannot override when sharing is enabled
+ return this.isSharedRunnersToggleDisabled || this.value === this.runnerEnabledValue;
+ },
+ overrideToggleValue() {
+ return this.value === this.runnerAllowOverrideValue;
},
- },
- created() {
- if (this.sharedRunnersAvailability !== this.runnerEnabled) {
- this.enabled = false;
- }
-
- if (this.sharedRunnersAvailability === this.runnerAllowOverride) {
- this.allowOverride = true;
- }
},
methods: {
- generatePayload(data) {
- return { shared_runners_setting: data };
- },
- enableOrDisable() {
- this.updateRunnerSettings(this.generatePayload(this.enabledOrDisabledSetting));
-
- // reset override toggle to false if shared runners are enabled
- this.allowOverride = false;
+ onSharedRunnersToggle(value) {
+ const newSetting = value ? this.runnerEnabledValue : this.runnerDisabledValue;
+ this.updateSetting(newSetting);
},
- override() {
- this.updateRunnerSettings(this.generatePayload(this.disabledWithOverrideSetting));
+ onOverrideToggle(value) {
+ const newSetting = value ? this.runnerAllowOverrideValue : this.runnerDisabledValue;
+ this.updateSetting(newSetting);
},
- updateRunnerSettings: debounce(function debouncedUpdateRunnerSettings(setting) {
+ updateSetting(setting) {
+ if (this.isLoading) {
+ return;
+ }
+
this.isLoading = true;
axios
- .put(this.updatePath, setting)
+ .put(this.updatePath, { shared_runners_setting: setting })
.then(() => {
- this.isLoading = false;
+ this.value = setting;
})
.catch((error) => {
const message = [
@@ -76,51 +67,52 @@ export default {
].join(' ');
this.error = message;
+ })
+ .finally(() => {
+ this.isLoading = false;
});
- }, DEBOUNCE_TOGGLE_DELAY),
+ },
},
};
</script>
<template>
- <div ref="sharedRunnersForm">
- <gl-alert v-if="error" variant="danger" :dismissible="false">{{ error }}</gl-alert>
+ <div>
+ <gl-alert v-if="error" variant="danger" :dismissible="false" class="gl-mb-5">
+ {{ error }}
+ </gl-alert>
- <h4 class="gl-display-flex gl-align-items-center">
- {{ __('Set up shared runner availability') }}
- <gl-loading-icon v-if="isLoading" class="gl-ml-3" size="sm" inline />
- </h4>
+ <gl-alert
+ v-if="isSharedRunnersToggleDisabled"
+ variant="warning"
+ :dismissible="false"
+ class="gl-mb-5"
+ >
+ {{ __('Shared runners are disabled for the parent group') }}
+ </gl-alert>
- <section class="gl-mt-5">
+ <section class="gl-mb-5">
<gl-toggle
- v-model="enabled"
- :disabled="toggleDisabled"
+ :value="sharedRunnersToggleValue"
+ :is-loading="isLoading"
+ :disabled="isSharedRunnersToggleDisabled"
:label="__('Enable shared runners for this group')"
- data-testid="enable-runners-toggle"
- @change="enableOrDisable"
+ :help="__('Enable shared runners for all projects and subgroups in this group.')"
+ data-testid="shared-runners-toggle"
+ @change="onSharedRunnersToggle"
/>
-
- <span class="gl-text-gray-600">
- {{ __('Enable shared runners for all projects and subgroups in this group.') }}
- </span>
</section>
- <section v-if="!enabled" class="gl-mt-5">
+ <section class="gl-mb-5">
<gl-toggle
- v-model="allowOverride"
- :disabled="toggleDisabled"
+ :value="overrideToggleValue"
+ :is-loading="isLoading"
+ :disabled="isOverrideToggleDisabled"
:label="__('Allow projects and subgroups to override the group setting')"
+ :help="__('Allows projects or subgroups in this group to override the global setting.')"
data-testid="override-runners-toggle"
- @change="override"
+ @change="onOverrideToggle"
/>
-
- <span class="gl-text-gray-600">
- {{ __('Allows projects or subgroups in this group to override the global setting.') }}
- </span>
</section>
-
- <gl-tooltip v-if="toggleDisabled" :target="() => $refs.sharedRunnersForm">
- {{ __('Shared runners are disabled for the parent group') }}
- </gl-tooltip>
</div>
</template>
diff --git a/app/assets/javascripts/group_settings/constants.js b/app/assets/javascripts/group_settings/constants.js
index 4067b6b52a3..ab5c0db45ba 100644
--- a/app/assets/javascripts/group_settings/constants.js
+++ b/app/assets/javascripts/group_settings/constants.js
@@ -1,6 +1,3 @@
import { __ } from '~/locale';
-// Debounce delay in milliseconds
-export const DEBOUNCE_TOGGLE_DELAY = 1000;
-
export const ERROR_MESSAGE = __('Refresh the page and try again.');
diff --git a/app/assets/javascripts/group_settings/mount_shared_runners.js b/app/assets/javascripts/group_settings/mount_shared_runners.js
index 21a2373e2b1..aeb6d57a11a 100644
--- a/app/assets/javascripts/group_settings/mount_shared_runners.js
+++ b/app/assets/javascripts/group_settings/mount_shared_runners.js
@@ -6,22 +6,22 @@ export default (containerId = 'update-shared-runners-form') => {
const {
updatePath,
- sharedRunnersAvailability,
- parentSharedRunnersAvailability,
- runnerEnabled,
- runnerDisabled,
- runnerAllowOverride,
+ sharedRunnersSetting,
+ parentSharedRunnersSetting,
+ runnerEnabledValue,
+ runnerDisabledValue,
+ runnerAllowOverrideValue,
} = containerEl.dataset;
return new Vue({
el: containerEl,
provide: {
updatePath,
- sharedRunnersAvailability,
- parentSharedRunnersAvailability,
- runnerEnabled,
- runnerDisabled,
- runnerAllowOverride,
+ sharedRunnersSetting,
+ parentSharedRunnersSetting,
+ runnerEnabledValue,
+ runnerDisabledValue,
+ runnerAllowOverrideValue,
},
render(createElement) {
return createElement(UpdateSharedRunnersForm);
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue
index 4406cacdf3f..adf304aebc7 100644
--- a/app/assets/javascripts/header_search/components/app.vue
+++ b/app/assets/javascripts/header_search/components/app.vue
@@ -120,9 +120,11 @@ export default {
...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']),
openDropdown() {
this.showDropdown = true;
+ this.$emit('toggleDropdown', this.showDropdown);
},
closeDropdown() {
this.showDropdown = false;
+ this.$emit('toggleDropdown', this.showDropdown);
},
submitSearch() {
return visitUrl(this.currentFocusedOption?.url || this.searchQuery);
@@ -146,7 +148,7 @@ export default {
v-outside="closeDropdown"
role="search"
:aria-label="$options.i18n.searchGitlab"
- class="header-search gl-relative gl-rounded-base"
+ class="header-search gl-relative gl-rounded-base gl-w-full"
:class="headerSearchActivityDescriptor"
>
<gl-search-box-by-type
diff --git a/app/assets/javascripts/header_search/index.js b/app/assets/javascripts/header_search/index.js
index 4af8513ecdb..b2c505d569f 100644
--- a/app/assets/javascripts/header_search/index.js
+++ b/app/assets/javascripts/header_search/index.js
@@ -7,6 +7,7 @@ Vue.use(Translate);
export const initHeaderSearchApp = (search = '') => {
const el = document.getElementById('js-header-search');
+ let navBarEl = null;
if (!el) {
return false;
@@ -19,8 +20,21 @@ export const initHeaderSearchApp = (search = '') => {
return new Vue({
el,
store: createStore({ searchPath, issuesPath, mrPath, autocompletePath, searchContext, search }),
+ mounted() {
+ navBarEl = document.querySelector('.header-content');
+ },
render(createElement) {
- return createElement(HeaderSearchApp);
+ return createElement(HeaderSearchApp, {
+ on: {
+ toggleDropdown: (isVisible = false) => {
+ if (isVisible) {
+ navBarEl?.classList.add('header-search-is-active');
+ } else {
+ navBarEl?.classList.remove('header-search-is-active');
+ }
+ },
+ },
+ });
},
});
};
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 38f3b094b7c..cb906374fe1 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -216,7 +216,9 @@ export default {
modal-id="ide-commit-error-modal"
:title="lastCommitError.title"
:action-primary="commitErrorPrimaryAction.button"
- :action-cancel="{ text: __('Cancel') }"
+ :action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ text: __('Cancel'),
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@ok="commitErrorPrimaryAction.callback"
>
<div v-safe-html="lastCommitError.messageHTML"></div>
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 32f87cb0a92..93ff7e8566f 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -108,7 +108,7 @@ export default {
by
<user-avatar-image
css-classes="ide-status-avatar"
- :size="18"
+ :size="16"
:img-src="latestPipeline && latestPipeline.commit.author_gravatar_url"
:img-alt="lastCommit.author_name"
:tooltip-text="lastCommit.author_name"
diff --git a/app/assets/javascripts/import_entities/components/import_status.vue b/app/assets/javascripts/import_entities/components/import_status.vue
index 9262a4e1e95..5455a034106 100644
--- a/app/assets/javascripts/import_entities/components/import_status.vue
+++ b/app/assets/javascripts/import_entities/components/import_status.vue
@@ -44,6 +44,11 @@ const STATUS_MAP = {
text: __('Failed'),
variant: 'danger',
},
+ [STATUSES.TIMEOUT]: {
+ icon: 'status-failed',
+ text: __('Timeout'),
+ variant: 'danger',
+ },
[STATUSES.CANCELLED]: {
icon: 'status-stopped',
text: __('Cancelled'),
diff --git a/app/assets/javascripts/import_entities/constants.js b/app/assets/javascripts/import_entities/constants.js
index 20a4d2d84b4..c470da21765 100644
--- a/app/assets/javascripts/import_entities/constants.js
+++ b/app/assets/javascripts/import_entities/constants.js
@@ -10,4 +10,5 @@ export const STATUSES = {
NONE: 'none',
SCHEDULING: 'scheduling',
CANCELLED: 'cancelled',
+ TIMEOUT: 'timeout',
};
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 028197ec9b1..ce401862cc1 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
@@ -25,7 +25,7 @@ import importGroupsMutation from '../graphql/mutations/import_groups.mutation.gr
import updateImportStatusMutation from '../graphql/mutations/update_import_status.mutation.graphql';
import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql';
import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql';
-import { NEW_NAME_FIELD, i18n } from '../constants';
+import { NEW_NAME_FIELD, ROOT_NAMESPACE, i18n } from '../constants';
import { StatusPoller } from '../services/status_poller';
import { isFinished, isAvailableForImport, isNameValid, isSameTarget } from '../utils';
import ImportActionsCell from './import_actions_cell.vue';
@@ -71,6 +71,10 @@ export default {
type: String,
required: true,
},
+ historyPath: {
+ type: String,
+ required: true,
+ },
},
data() {
@@ -426,10 +430,10 @@ export default {
return this.importTargets[group.id];
}
- const defaultTargetNamespace = this.availableNamespaces[0] ?? { fullPath: '', id: null };
+ const defaultTargetNamespace = this.availableNamespaces[0] ?? ROOT_NAMESPACE;
let importTarget;
if (group.lastImportTarget) {
- const targetNamespace = this.availableNamespaces.find(
+ const targetNamespace = [ROOT_NAMESPACE, ...this.availableNamespaces].find(
(ns) => ns.fullPath === group.lastImportTarget.targetNamespace,
);
@@ -485,12 +489,15 @@ export default {
<template>
<div>
- <h1
- class="gl-my-0 gl-py-4 gl-font-size-h1 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex"
+ <div
+ class="gl-display-flex gl-align-items-center gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1"
>
- <img :src="$options.gitlabLogo" class="gl-w-6 gl-h-6 gl-mb-2 gl-display-inline gl-mr-2" />
- {{ s__('BulkImport|Import groups from GitLab') }}
- </h1>
+ <h1 class="gl-my-0 gl-py-4 gl-font-size-h1gl-display-flex">
+ <img :src="$options.gitlabLogo" class="gl-w-6 gl-h-6 gl-mb-2 gl-display-inline gl-mr-2" />
+ {{ s__('BulkImport|Import groups from GitLab') }}
+ </h1>
+ <gl-link :href="historyPath" class="gl-ml-auto">{{ s__('BulkImport|History') }}</gl-link>
+ </div>
<gl-alert
v-if="unavailableFeatures.length > 0 && unavailableFeaturesAlertVisible"
variant="warning"
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
index 344a6e45370..4fbbd5b239c 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
@@ -57,6 +57,7 @@ export default {
toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
class="gl-h-7 gl-flex-grow-1"
data-qa-selector="target_namespace_selector_dropdown"
+ data-testid="target-namespace-selector"
>
<gl-dropdown-item @click="$emit('update-target-namespace', { fullPath: '', id: null })">{{
s__('BulkImport|No parent')
diff --git a/app/assets/javascripts/import_entities/import_groups/constants.js b/app/assets/javascripts/import_entities/import_groups/constants.js
index ac1466238d0..32137308684 100644
--- a/app/assets/javascripts/import_entities/import_groups/constants.js
+++ b/app/assets/javascripts/import_entities/import_groups/constants.js
@@ -18,3 +18,5 @@ export const i18n = {
};
export const NEW_NAME_FIELD = 'newName';
+
+export const ROOT_NAMESPACE = { fullPath: '', id: null };
diff --git a/app/assets/javascripts/import_entities/import_groups/index.js b/app/assets/javascripts/import_entities/import_groups/index.js
index 67a7258d504..02af0db7f9a 100644
--- a/app/assets/javascripts/import_entities/import_groups/index.js
+++ b/app/assets/javascripts/import_entities/import_groups/index.js
@@ -17,6 +17,7 @@ export function mountImportGroupsApp(mountElement) {
jobsPath,
sourceUrl,
groupPathRegex,
+ historyPath,
} = mountElement.dataset;
const apolloProvider = new VueApollo({
defaultClient: createApolloClient({
@@ -38,6 +39,7 @@ export function mountImportGroupsApp(mountElement) {
sourceUrl,
jobsPath,
groupPathRegex: new RegExp(`^(${groupPathRegex})$`),
+ historyPath,
},
});
},
diff --git a/app/assets/javascripts/import_entities/import_groups/utils.js b/app/assets/javascripts/import_entities/import_groups/utils.js
index 1d0ab75e1cb..f896203ba60 100644
--- a/app/assets/javascripts/import_entities/import_groups/utils.js
+++ b/app/assets/javascripts/import_entities/import_groups/utils.js
@@ -10,7 +10,7 @@ export function getInvalidNameValidationMessage(importTarget) {
}
export function isFinished(group) {
- return [STATUSES.FINISHED, STATUSES.FAILED].includes(group.progress?.status);
+ return [STATUSES.FINISHED, STATUSES.FAILED, STATUSES.TIMEOUT].includes(group.progress?.status);
}
export function isAvailableForImport(group) {
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 e0703a77424..0307607321e 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
@@ -1,5 +1,11 @@
<script>
-import { GlButton, GlLoadingIcon, GlIntersectionObserver, GlModal, GlFormInput } from '@gitlab/ui';
+import {
+ GlButton,
+ GlLoadingIcon,
+ GlIntersectionObserver,
+ GlModal,
+ GlSearchBoxByClick,
+} from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
import { n__, __, sprintf } from '~/locale';
import ProviderRepoTableRow from './provider_repo_table_row.vue';
@@ -12,7 +18,7 @@ export default {
GlButton,
GlModal,
GlIntersectionObserver,
- GlFormInput,
+ GlSearchBoxByClick,
},
props: {
providerTitle: {
@@ -134,13 +140,13 @@ export default {
<slot name="actions"></slot>
<form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent>
- <gl-form-input
+ <gl-search-box-by-click
data-qa-selector="githubish_import_filter_field"
name="filter"
:placeholder="__('Filter by name')"
autofocus
- size="lg"
- @keyup.enter="setFilter($event.target.value)"
+ @submit="setFilter"
+ @clear="setFilter('')"
/>
</form>
</div>
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index bfc5bd823a2..922e870caa7 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -338,7 +338,9 @@ export default {
:show-items="showList"
:show-error-msg="showErrorMsg"
:i18n="$options.i18n"
- :items="incidents.list || []"
+ :items="
+ incidents.list || [] /* eslint-disable-line @gitlab/vue-no-new-non-primitive-in-template */
+ "
:page-info="incidents.pageInfo"
:items-count="incidentsCount"
:status-tabs="$options.statusTabs"
@@ -372,7 +374,10 @@ export default {
<template #table>
<gl-table
- :items="incidents.list || []"
+ :items="
+ incidents.list ||
+ [] /* eslint-disable-line @gitlab/vue-no-new-non-primitive-in-template */
+ "
:fields="availableFields"
:busy="loading"
stacked="md"
diff --git a/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql b/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql
index b72941966c6..2d84b141f32 100644
--- a/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql
+++ b/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql
@@ -1,4 +1,3 @@
-# eslint-disable-next-line @graphql-eslint/require-id-when-available
fragment IncidentFields on Issue {
severity
escalationStatus
diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js
index c5ed5bb08a9..b9975eed716 100644
--- a/app/assets/javascripts/integrations/constants.js
+++ b/app/assets/javascripts/integrations/constants.js
@@ -37,3 +37,13 @@ export const integrationFormSectionComponents = {
[integrationFormSections.JIRA_TRIGGER]: 'IntegrationSectionJiraTrigger',
[integrationFormSections.JIRA_ISSUES]: 'IntegrationSectionJiraIssues',
};
+
+export const billingPlans = {
+ PREMIUM: 'premium',
+ ULTIMATE: 'ultimate',
+};
+
+export const billingPlanNames = {
+ [billingPlans.PREMIUM]: s__('BillingPlans|Premium'),
+ [billingPlans.ULTIMATE]: s__('BillingPlans|Ultimate'),
+};
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index 661299920c7..9f43360fb73 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -1,5 +1,11 @@
<script>
-import { GlButton, GlModalDirective, GlSafeHtmlDirective as SafeHtml, GlForm } from '@gitlab/ui';
+import {
+ GlBadge,
+ GlButton,
+ GlModalDirective,
+ GlSafeHtmlDirective as SafeHtml,
+ GlForm,
+} from '@gitlab/ui';
import axios from 'axios';
import * as Sentry from '@sentry/browser';
import { mapState, mapActions, mapGetters } from 'vuex';
@@ -10,6 +16,7 @@ import {
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
integrationLevels,
integrationFormSectionComponents,
+ billingPlanNames,
} from '~/integrations/constants';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import csrf from '~/lib/utils/csrf';
@@ -42,6 +49,7 @@ export default {
import(
/* webpackChunkName: 'integrationSectionJiraTrigger' */ '~/integrations/edit/components/sections/jira_trigger.vue'
),
+ GlBadge,
GlButton,
GlForm,
},
@@ -177,6 +185,7 @@ export default {
},
csrf,
integrationFormSectionComponents,
+ billingPlanNames,
};
</script>
@@ -214,7 +223,20 @@ export default {
>
<div class="row">
<div class="col-lg-4">
- <h4 class="gl-mt-0">{{ section.title }}</h4>
+ <h4 class="gl-mt-0">
+ {{ section.title
+ }}<gl-badge
+ v-if="section.plan"
+ :href="propsSource.aboutPricingUrl"
+ target="_blank"
+ rel="noopener noreferrer"
+ variant="tier"
+ icon="license"
+ class="gl-ml-3"
+ >
+ {{ $options.billingPlanNames[section.plan] }}
+ </gl-badge>
+ </h4>
<p v-safe-html:[$options.descriptionHtmlConfig]="section.description"></p>
</div>
diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
index f00339c92fa..584d23e17e1 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
@@ -2,7 +2,6 @@
import { GlFormGroup, GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { s__, __ } from '~/locale';
-import JiraUpgradeCta from './jira_upgrade_cta.vue';
export default {
name: 'JiraIssuesFields',
@@ -10,7 +9,6 @@ export default {
GlFormGroup,
GlFormCheckbox,
GlFormInput,
- JiraUpgradeCta,
JiraIssueCreationVulnerabilities: () =>
import('ee_component/integrations/edit/components/jira_issue_creation_vulnerabilities.vue'),
},
@@ -45,11 +43,6 @@ export default {
required: false,
default: null,
},
- upgradePlanPath: {
- type: String,
- required: false,
- default: '',
- },
isValidated: {
type: Boolean,
required: false,
@@ -64,6 +57,9 @@ export default {
},
computed: {
...mapGetters(['isInheriting']),
+ checkboxDisabled() {
+ return !this.showJiraIssuesIntegration || this.isInheriting;
+ },
validProjectKey() {
return !this.enableJiraIssues || Boolean(this.projectKey) || !this.isValidated;
},
@@ -85,64 +81,48 @@ export default {
<template>
<div>
- <template v-if="showJiraIssuesIntegration">
- <input name="service[issues_enabled]" type="hidden" :value="enableJiraIssues || false" />
- <gl-form-checkbox
- v-model="enableJiraIssues"
- :disabled="isInheriting"
- data-qa-selector="service_jira_issues_enabled_checkbox"
- >
- {{ $options.i18n.enableCheckboxLabel }}
- <template #help>
- {{ $options.i18n.enableCheckboxHelp }}
- </template>
- </gl-form-checkbox>
+ <input name="service[issues_enabled]" type="hidden" :value="enableJiraIssues || false" />
+ <gl-form-checkbox
+ v-model="enableJiraIssues"
+ :disabled="checkboxDisabled"
+ data-qa-selector="service_jira_issues_enabled_checkbox"
+ >
+ {{ $options.i18n.enableCheckboxLabel }}
+ <template #help>
+ {{ $options.i18n.enableCheckboxHelp }}
+ </template>
+ </gl-form-checkbox>
- <div v-if="enableJiraIssues" class="gl-pl-6 gl-mt-3">
- <gl-form-group
- :label="$options.i18n.projectKeyLabel"
- label-for="service_project_key"
- :invalid-feedback="$options.i18n.requiredFieldFeedback"
+ <div v-if="enableJiraIssues" class="gl-pl-6 gl-mt-3">
+ <gl-form-group
+ :label="$options.i18n.projectKeyLabel"
+ label-for="service_project_key"
+ :invalid-feedback="$options.i18n.requiredFieldFeedback"
+ :state="validProjectKey"
+ class="gl-max-w-26"
+ data-testid="project-key-form-group"
+ >
+ <gl-form-input
+ id="service_project_key"
+ v-model="projectKey"
+ name="service[project_key]"
+ data-qa-selector="service_jira_project_key_field"
+ :placeholder="$options.i18n.projectKeyPlaceholder"
+ :required="enableJiraIssues"
:state="validProjectKey"
- class="gl-max-w-26"
- data-testid="project-key-form-group"
- >
- <gl-form-input
- id="service_project_key"
- v-model="projectKey"
- name="service[project_key]"
- data-qa-selector="service_jira_project_key_field"
- :placeholder="$options.i18n.projectKeyPlaceholder"
- :required="enableJiraIssues"
- :state="validProjectKey"
- :readonly="isInheriting"
- />
- </gl-form-group>
-
- <jira-issue-creation-vulnerabilities
- :project-key="projectKey"
- :initial-is-enabled="initialEnableJiraVulnerabilities"
- :initial-issue-type-id="initialVulnerabilitiesIssuetype"
- :show-full-feature="showJiraVulnerabilitiesIntegration"
- class="gl-mt-6"
- data-testid="jira-for-vulnerabilities"
- @request-jira-issue-types="$emit('request-jira-issue-types')"
- />
- <jira-upgrade-cta
- v-if="!showJiraVulnerabilitiesIntegration"
- class="gl-mt-2 gl-ml-6"
- data-testid="ultimate-upgrade-cta"
- show-ultimate-message
- :upgrade-plan-path="upgradePlanPath"
+ :readonly="isInheriting"
/>
- </div>
- </template>
+ </gl-form-group>
- <jira-upgrade-cta
- v-else
- data-testid="premium-upgrade-cta"
- show-premium-message
- :upgrade-plan-path="upgradePlanPath"
- />
+ <jira-issue-creation-vulnerabilities
+ :project-key="projectKey"
+ :initial-is-enabled="initialEnableJiraVulnerabilities"
+ :initial-issue-type-id="initialVulnerabilitiesIssuetype"
+ :show-full-feature="showJiraVulnerabilitiesIntegration"
+ class="gl-mt-6"
+ data-testid="jira-for-vulnerabilities"
+ @request-jira-issue-types="$emit('request-jira-issue-types')"
+ />
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js
index 9a9aae36657..92e6ca509c3 100644
--- a/app/assets/javascripts/integrations/edit/index.js
+++ b/app/assets/javascripts/integrations/edit/index.js
@@ -23,6 +23,7 @@ function parseDatasetToProps(data) {
projectKey,
upgradePlanPath,
learnMorePath,
+ aboutPricingUrl,
triggerEvents,
sections,
fields,
@@ -82,6 +83,7 @@ function parseDatasetToProps(data) {
upgradePlanPath,
},
learnMorePath,
+ aboutPricingUrl,
triggerEvents: JSON.parse(triggerEvents),
sections: JSON.parse(sections, { deep: true }),
fields: convertObjectPropsToCamelCase(JSON.parse(fields), { deep: true }),
diff --git a/app/assets/javascripts/invite_members/components/import_a_project_modal.vue b/app/assets/javascripts/invite_members/components/import_a_project_modal.vue
index d71468284ca..fb6c376cfe6 100644
--- a/app/assets/javascripts/invite_members/components/import_a_project_modal.vue
+++ b/app/assets/javascripts/invite_members/components/import_a_project_modal.vue
@@ -145,7 +145,7 @@ export default {
<gl-button
:disabled="importDisabled"
:loading="isLoading"
- variant="success"
+ variant="confirm"
data-testid="import-button"
@click="submitImport"
>{{ $options.i18n.modalPrimaryButton }}</gl-button
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 a9aa0e9b760..7857b9d86d2 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -88,6 +88,11 @@ export default {
type: Array,
required: true,
},
+ usersLimitDataset: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
data() {
return {
@@ -146,6 +151,18 @@ export default {
isOnLearnGitlab() {
return this.source === LEARN_GITLAB;
},
+ reachedLimit() {
+ if (this.usersLimitDataset.freeUsersLimit && this.usersLimitDataset.membersCount) {
+ return this.usersLimitDataset.membersCount >= this.usersLimitDataset.freeUsersLimit;
+ }
+
+ return false;
+ },
+ formGroupDescription() {
+ return this.reachedLimit
+ ? this.$options.labels.placeHolderDisabled
+ : this.$options.labels.placeHolder;
+ },
},
mounted() {
eventHub.$on('openModal', (options) => {
@@ -274,12 +291,14 @@ export default {
:help-link="helpLink"
:label-intro-text="labelIntroText"
:label-search-field="$options.labels.searchField"
- :form-group-description="$options.labels.placeHolder"
+ :form-group-description="formGroupDescription"
:submit-disabled="inviteDisabled"
:invalid-feedback-message="invalidFeedbackMessage"
:is-loading="isLoading"
:new-users-to-invite="newUsersToInvite"
:root-group-id="rootId"
+ :reached-limit="reachedLimit"
+ :users-limit-dataset="usersLimitDataset"
@reset="resetFields"
@submit="sendInvite"
@access-level="onAccessLevelUpdate"
@@ -294,7 +313,10 @@ export default {
</template>
<template #user-limit-notification>
- <user-limit-notification />
+ <user-limit-notification
+ :reached-limit="reachedLimit"
+ :users-limit-dataset="usersLimitDataset"
+ />
</template>
<template #select="{ validationState, labelId }">
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 d9297614a7e..33d37b809c2 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -8,7 +8,9 @@ import {
GlLink,
GlSprintf,
GlFormInput,
+ GlIcon,
} from '@gitlab/ui';
+import Tracking from '~/tracking';
import { sprintf } from '~/locale';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
import {
@@ -16,8 +18,13 @@ import {
ACCESS_EXPIRE_DATE,
READ_MORE_TEXT,
INVITE_BUTTON_TEXT,
+ INVITE_BUTTON_TEXT_DISABLED,
CANCEL_BUTTON_TEXT,
+ CANCEL_BUTTON_TEXT_DISABLED,
HEADER_CLOSE_LABEL,
+ ON_SHOW_TRACK_LABEL,
+ ON_CLOSE_TRACK_LABEL,
+ ON_SUBMIT_TRACK_LABEL,
} from '../constants';
const DEFAULT_SLOT = 'default';
@@ -41,8 +48,10 @@ export default {
GlDropdownItem,
GlSprintf,
GlFormInput,
+ GlIcon,
ContentTransition,
},
+ mixins: [Tracking.mixin()],
inheritAttrs: false,
props: {
modalTitle: {
@@ -122,6 +131,16 @@ export default {
required: false,
default: false,
},
+ reachedLimit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ usersLimitDataset: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
data() {
// Be sure to check out reset!
@@ -151,20 +170,27 @@ export default {
},
actionPrimary() {
return {
- text: this.submitButtonText,
+ text: this.reachedLimit ? INVITE_BUTTON_TEXT_DISABLED : this.submitButtonText,
attributes: {
variant: 'confirm',
- disabled: this.submitDisabled,
- loading: this.isLoading,
+ disabled: this.reachedLimit ? false : this.submitDisabled,
+ loading: this.reachedLimit ? false : this.isLoading,
'data-qa-selector': 'invite_button',
+ ...(this.reachedLimit && { href: this.usersLimitDataset.membersPath }),
},
};
},
actionCancel() {
+ if (this.reachedLimit && this.usersLimitDataset.userNamespace) return undefined;
+
return {
- text: this.cancelButtonText,
+ text: this.reachedLimit ? CANCEL_BUTTON_TEXT_DISABLED : this.cancelButtonText,
+ ...(this.reachedLimit && { attributes: { href: this.usersLimitDataset.purchasePath } }),
};
},
+ selectLabelClass() {
+ return `col-form-label ${this.reachedLimit ? 'gl-text-gray-500' : ''}`;
+ },
},
watch: {
selectedAccessLevel: {
@@ -183,15 +209,24 @@ export default {
this.$emit('reset');
},
+ onShowModal() {
+ if (this.reachedLimit) {
+ this.track('render', { category: 'default', label: ON_SHOW_TRACK_LABEL });
+ }
+ },
onCloseModal(e) {
- if (this.preventCancelDefault) {
+ if (this.preventCancelDefault || this.reachedLimit) {
e.preventDefault();
} else {
this.onReset();
this.$refs.modal.hide();
}
- this.$emit('cancel');
+ if (this.reachedLimit) {
+ this.track('click_button', { category: 'default', label: ON_CLOSE_TRACK_LABEL });
+ } else {
+ this.$emit('cancel');
+ }
},
changeSelectedItem(item) {
this.selectedAccessLevel = item;
@@ -200,10 +235,14 @@ export default {
// We never want to hide when submitting
e.preventDefault();
- this.$emit('submit', {
- accessLevel: this.selectedAccessLevel,
- expiresAt: this.selectedDate,
- });
+ if (this.reachedLimit) {
+ this.track('click_button', { category: 'default', label: ON_SUBMIT_TRACK_LABEL });
+ } else {
+ this.$emit('submit', {
+ accessLevel: this.selectedAccessLevel,
+ expiresAt: this.selectedDate,
+ });
+ }
},
},
HEADER_CLOSE_LABEL,
@@ -227,6 +266,7 @@ export default {
:header-close-label="$options.HEADER_CLOSE_LABEL"
:action-primary="actionPrimary"
:action-cancel="actionCancel"
+ @shown="onShowModal"
@primary="onSubmit"
@cancel="onCloseModal"
@hidden="onReset"
@@ -255,64 +295,73 @@ export default {
<gl-form-group
:invalid-feedback="invalidFeedbackMessage"
:state="validationState"
- :description="formGroupDescription"
data-testid="members-form-group"
>
- <label :id="selectLabelId" class="col-form-label">{{ labelSearchField }}</label>
- <slot name="select" v-bind="{ validationState, labelId: selectLabelId }"></slot>
+ <template #description>
+ <gl-icon v-if="reachedLimit" name="lock" />
+ {{ formGroupDescription }}
+ </template>
+
+ <label :id="selectLabelId" :class="selectLabelClass">{{ labelSearchField }}</label>
+ <gl-form-input v-if="reachedLimit" data-testid="disabled-input" disabled />
+ <slot v-else name="select" v-bind="{ validationState, labelId: selectLabelId }"></slot>
</gl-form-group>
- <label class="gl-font-weight-bold">{{ $options.ACCESS_LEVEL }}</label>
- <div class="gl-mt-2 gl-w-half gl-xs-w-full">
- <gl-dropdown
- class="gl-shadow-none gl-w-full"
- data-qa-selector="access_level_dropdown"
- v-bind="$attrs"
- :text="selectedRoleName"
- >
- <template v-for="(key, item) in accessLevels">
- <gl-dropdown-item
- :key="key"
- active-class="is-active"
- is-check-item
- :is-checked="key === selectedAccessLevel"
- @click="changeSelectedItem(key)"
- >
- <div>{{ item }}</div>
- </gl-dropdown-item>
- </template>
- </gl-dropdown>
- </div>
+ <template v-if="!reachedLimit">
+ <label class="gl-font-weight-bold">{{ $options.ACCESS_LEVEL }}</label>
- <div class="gl-mt-2 gl-w-half gl-xs-w-full">
- <gl-sprintf :message="$options.READ_MORE_TEXT">
- <template #link="{ content }">
- <gl-link :href="helpLink" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </div>
+ <div class="gl-mt-2 gl-w-half gl-xs-w-full">
+ <gl-dropdown
+ class="gl-shadow-none gl-w-full"
+ data-qa-selector="access_level_dropdown"
+ v-bind="$attrs"
+ :text="selectedRoleName"
+ >
+ <template v-for="(key, item) in accessLevels">
+ <gl-dropdown-item
+ :key="key"
+ active-class="is-active"
+ is-check-item
+ :is-checked="key === selectedAccessLevel"
+ @click="changeSelectedItem(key)"
+ >
+ <div>{{ item }}</div>
+ </gl-dropdown-item>
+ </template>
+ </gl-dropdown>
+ </div>
- <label class="gl-mt-5 gl-display-block" for="expires_at">{{
- $options.ACCESS_EXPIRE_DATE
- }}</label>
- <div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block">
- <gl-datepicker
- v-model="selectedDate"
- class="gl-display-inline!"
- :min-date="minDate"
- :target="null"
- >
- <template #default="{ formattedDate }">
- <gl-form-input
- class="gl-w-full"
- :value="formattedDate"
- :placeholder="__(`YYYY-MM-DD`)"
- />
- </template>
- </gl-datepicker>
- </div>
- <slot name="form-after"></slot>
+ <div class="gl-mt-2 gl-w-half gl-xs-w-full">
+ <gl-sprintf :message="$options.READ_MORE_TEXT">
+ <template #link="{ content }">
+ <gl-link :href="helpLink" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+
+ <label class="gl-mt-5 gl-display-block" for="expires_at">{{
+ $options.ACCESS_EXPIRE_DATE
+ }}</label>
+ <div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block">
+ <gl-datepicker
+ v-model="selectedDate"
+ class="gl-display-inline!"
+ :min-date="minDate"
+ :target="null"
+ >
+ <template #default="{ formattedDate }">
+ <gl-form-input
+ class="gl-w-full"
+ :value="formattedDate"
+ :placeholder="__(`YYYY-MM-DD`)"
+ />
+ </template>
+ </gl-datepicker>
+ </div>
+ <slot name="form-after"></slot>
+ </template>
</template>
+
<template v-for="{ key } in extraSlots" #[key]>
<slot :name="key"></slot>
</template>
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 0a191f6d406..30c9294344e 100644
--- a/app/assets/javascripts/invite_members/components/members_token_select.vue
+++ b/app/assets/javascripts/invite_members/components/members_token_select.vue
@@ -134,10 +134,10 @@ export default {
:hide-dropdown-with-no-items="hideDropdownWithNoItems"
:placeholder="placeholderText"
:aria-labelledby="ariaLabelledby"
- :text-input-attrs="{
+ :text-input-attrs="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
'data-testid': 'members-token-select-input',
'data-qa-selector': 'members_token_select_input',
- }"
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@blur="handleBlur"
@text-input="handleTextInput"
@input="handleInput"
diff --git a/app/assets/javascripts/invite_members/components/user_limit_notification.vue b/app/assets/javascripts/invite_members/components/user_limit_notification.vue
index beef1aef8a1..ea5f4317d86 100644
--- a/app/assets/javascripts/invite_members/components/user_limit_notification.vue
+++ b/app/assets/javascripts/invite_members/components/user_limit_notification.vue
@@ -1,35 +1,51 @@
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
-import { s__, n__, sprintf } from '~/locale';
+import { n__, sprintf } from '~/locale';
-const CLOSE_TO_LIMIT_COUNT = 2;
-
-const WARNING_ALERT_TITLE = s__(
- 'InviteMembersModal|You only have space for %{count} more %{members} in %{name}',
-);
-
-const DANGER_ALERT_TITLE = s__(
- "InviteMembersModal|You've reached your %{count} %{members} limit for %{name}",
-);
-
-const CLOSE_TO_LIMIT_MESSAGE = s__(
- 'InviteMembersModal|To get more members an owner of this namespace can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier.',
-);
+import {
+ WARNING_ALERT_TITLE,
+ DANGER_ALERT_TITLE,
+ REACHED_LIMIT_MESSAGE,
+ REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
+ CLOSE_TO_LIMIT_MESSAGE,
+} from '../constants';
-const REACHED_LIMIT_MESSAGE = s__(
- 'InviteMembersModal|New members will be unable to participate. You can manage your members by removing ones you no longer need.',
-).concat(' ', CLOSE_TO_LIMIT_MESSAGE);
+const CLOSE_TO_LIMIT_COUNT = 2;
export default {
name: 'UserLimitNotification',
components: { GlAlert, GlSprintf, GlLink },
- inject: ['name', 'newTrialRegistrationPath', 'purchasePath', 'freeUsersLimit', 'membersCount'],
+ inject: ['name'],
+ props: {
+ reachedLimit: {
+ type: Boolean,
+ required: true,
+ },
+ usersLimitDataset: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
computed: {
- reachedLimit() {
- return this.isLimit();
+ freeUsersLimit() {
+ return this.usersLimitDataset.freeUsersLimit;
+ },
+ membersCount() {
+ return this.usersLimitDataset.membersCount;
+ },
+ newTrialRegistrationPath() {
+ return this.usersLimitDataset.newTrialRegistrationPath;
+ },
+ purchasePath() {
+ return this.usersLimitDataset.purchasePath;
},
closeToLimit() {
- return this.isLimit(CLOSE_TO_LIMIT_COUNT);
+ if (this.freeUsersLimit && this.membersCount) {
+ return this.membersCount >= this.freeUsersLimit - CLOSE_TO_LIMIT_COUNT;
+ }
+
+ return false;
},
warningAlertTitle() {
return sprintf(WARNING_ALERT_TITLE, {
@@ -51,28 +67,29 @@ export default {
title() {
return this.reachedLimit ? this.dangerAlertTitle : this.warningAlertTitle;
},
+ reachedLimitMessage() {
+ if (this.usersLimitDataset.userNamespace) {
+ return this.$options.i18n.reachedLimitMessage;
+ }
+
+ return this.$options.i18n.reachedLimitUpgradeSuggestionMessage;
+ },
message() {
if (this.reachedLimit) {
- return this.$options.i18n.reachedLimitMessage;
+ return this.reachedLimitMessage;
}
return this.$options.i18n.closeToLimitMessage;
},
},
methods: {
- isLimit(deviation = 0) {
- if (this.freeUsersLimit && this.membersCount) {
- return this.membersCount >= this.freeUsersLimit - deviation;
- }
-
- return false;
- },
pluralMembers(count) {
return n__('member', 'members', count);
},
},
i18n: {
reachedLimitMessage: REACHED_LIMIT_MESSAGE,
+ reachedLimitUpgradeSuggestionMessage: REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
closeToLimitMessage: CLOSE_TO_LIMIT_MESSAGE,
},
};
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index 3cd0bfc0181..928f79f1c8d 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -35,8 +35,11 @@ export const MEMBERS_TO_PROJECT_DEFAULT_INTRO_TEXT = s__(
export const MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT = s__(
"InviteMembersModal|Congratulations on creating your project, you're almost there!",
);
-export const MEMBERS_SEARCH_FIELD = s__('InviteMembersModal|GitLab member or email address');
+export const MEMBERS_SEARCH_FIELD = s__('InviteMembersModal|Username or email address');
export const MEMBERS_PLACEHOLDER = s__('InviteMembersModal|Select members or type email addresses');
+export const MEMBERS_PLACEHOLDER_DISABLED = s__(
+ 'InviteMembersModal|This feature is disabled until this group has space for more members.',
+);
export const MEMBERS_TASKS_TO_BE_DONE_TITLE = s__(
'InviteMembersModal|Create issues for your new team member to work on (optional)',
);
@@ -66,7 +69,9 @@ export const READ_MORE_TEXT = s__(
`InviteMembersModal|%{linkStart}Read more%{linkEnd} about role permissions`,
);
export const INVITE_BUTTON_TEXT = s__('InviteMembersModal|Invite');
+export const INVITE_BUTTON_TEXT_DISABLED = s__('InviteMembersModal|Manage members');
export const CANCEL_BUTTON_TEXT = s__('InviteMembersModal|Cancel');
+export const CANCEL_BUTTON_TEXT_DISABLED = s__('InviteMembersModal|Explore paid plans');
export const HEADER_CLOSE_LABEL = s__('InviteMembersModal|Close invite team members');
export const MEMBER_MODAL_LABELS = {
@@ -94,6 +99,7 @@ export const MEMBER_MODAL_LABELS = {
},
searchField: MEMBERS_SEARCH_FIELD,
placeHolder: MEMBERS_PLACEHOLDER,
+ placeHolderDisabled: MEMBERS_PLACEHOLDER_DISABLED,
tasksToBeDone: {
title: MEMBERS_TASKS_TO_BE_DONE_TITLE,
noProjects: MEMBERS_TASKS_TO_BE_DONE_NO_PROJECTS,
@@ -118,3 +124,27 @@ export const GROUP_MODAL_LABELS = {
};
export const LEARN_GITLAB = 'learn_gitlab';
+export const ON_SHOW_TRACK_LABEL = 'locked_modal_viewed';
+export const ON_CLOSE_TRACK_LABEL = 'explore_paid_plans_clicked';
+export const ON_SUBMIT_TRACK_LABEL = 'manage_members_clicked';
+
+export const WARNING_ALERT_TITLE = s__(
+ 'InviteMembersModal|You only have space for %{count} more %{members} in %{name}',
+);
+export const DANGER_ALERT_TITLE = s__(
+ "InviteMembersModal|You've reached your %{count} %{members} limit for %{name}",
+);
+
+export const REACHED_LIMIT_MESSAGE = s__(
+ 'InviteMembersModal|You cannot add more members, but you can remove members who no longer need access.',
+);
+
+export const REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE = REACHED_LIMIT_MESSAGE.concat(
+ s__(
+ 'InviteMembersModal| To get more members and access to additional paid features, an owner of this namespace can start a trial or upgrade to a paid tier.',
+ ),
+);
+
+export const CLOSE_TO_LIMIT_MESSAGE = s__(
+ 'InviteMembersModal|To get more members an owner of this namespace can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier.',
+);
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 958121ad735..a4be3f205a3 100644
--- a/app/assets/javascripts/invite_members/init_invite_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js
@@ -1,7 +1,7 @@
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
+import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
Vue.use(GlToast);
@@ -26,10 +26,6 @@ export default (function initInviteMembersModal() {
provide: {
name: el.dataset.name,
newProjectPath: el.dataset.newProjectPath,
- newTrialRegistrationPath: el.dataset.newTrialRegistrationPath,
- purchasePath: el.dataset.purchasePath,
- freeUsersLimit: el.dataset.freeUsersLimit && parseInt(el.dataset.freeUsersLimit, 10),
- membersCount: el.dataset.membersCount && parseInt(el.dataset.membersCount, 10),
},
render: (createElement) =>
createElement(InviteMembersModal, {
@@ -42,6 +38,9 @@ export default (function initInviteMembersModal() {
projects: JSON.parse(el.dataset.projects || '[]'),
usersFilter: el.dataset.usersFilter,
filterId: parseInt(el.dataset.filterId, 10),
+ usersLimitDataset: convertObjectPropsToCamelCase(
+ JSON.parse(el.dataset.usersLimitDataset || '{}'),
+ ),
},
}),
});
diff --git a/app/assets/javascripts/issuable/components/issuable_header_warnings.vue b/app/assets/javascripts/issuable/components/issuable_header_warnings.vue
index 82223ab9ef4..06d1a2ee233 100644
--- a/app/assets/javascripts/issuable/components/issuable_header_warnings.vue
+++ b/app/assets/javascripts/issuable/components/issuable_header_warnings.vue
@@ -2,14 +2,21 @@
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { __ } from '~/locale';
+import { IssuableType, WorkspaceType } from '~/issues/constants';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
export default {
+ WorkspaceType,
+ IssuableType,
components: {
GlIcon,
+ ConfidentialityBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagMixin()],
inject: ['hidden'],
computed: {
...mapGetters(['getNoteableData']),
@@ -19,6 +26,9 @@ export default {
isConfidential() {
return this.getNoteableData.confidential;
},
+ isMergeRequest() {
+ return this.getNoteableData.targetType === 'merge_request' && this.glFeatures.updatedMrHeader;
+ },
warningIconsMeta() {
return [
{
@@ -27,11 +37,6 @@ export default {
dataTestId: 'locked',
},
{
- iconName: 'eye-slash',
- visible: this.isConfidential,
- dataTestId: 'confidential',
- },
- {
iconName: 'spam',
visible: this.hidden,
dataTestId: 'hidden',
@@ -45,6 +50,12 @@ export default {
<template>
<div class="gl-display-inline-block">
+ <confidentiality-badge
+ v-if="isConfidential"
+ data-testid="confidential"
+ :workspace-type="$options.WorkspaceType.project"
+ :issuable-type="$options.IssuableType.Issue"
+ />
<template v-for="meta in warningIconsMeta">
<div
v-if="meta.visible"
@@ -52,7 +63,11 @@ export default {
v-gl-tooltip
:data-testid="meta.dataTestId"
:title="meta.tooltip || null"
- class="issuable-warning-icon inline"
+ :class="{
+ 'gl-mr-3 gl-mt-2 gl-display-flex gl-justify-content-center gl-align-items-center': isMergeRequest,
+ 'gl-display-inline-block': !isMergeRequest,
+ }"
+ class="issuable-warning-icon"
>
<gl-icon :name="meta.iconName" class="icon" />
</div>
diff --git a/app/assets/javascripts/issuable/components/related_issuable_item.vue b/app/assets/javascripts/issuable/components/related_issuable_item.vue
index 2bb0e3c80f9..dfe18567608 100644
--- a/app/assets/javascripts/issuable/components/related_issuable_item.vue
+++ b/app/assets/javascripts/issuable/components/related_issuable_item.vue
@@ -116,7 +116,7 @@ export default {
<div
class="item-path-area item-path-id d-flex align-items-center mr-2 mt-2 mt-xl-0 ml-xl-2"
>
- <gl-tooltip :target="() => this.$refs.iconElement">
+ <gl-tooltip :target="() => $refs.iconElement">
<span v-safe-html="stateTitle"></span>
</gl-tooltip>
<span v-gl-tooltip :title="itemPath" class="path-id-text d-inline-block">{{
diff --git a/app/assets/javascripts/issuable/components/status_box.vue b/app/assets/javascripts/issuable/components/status_box.vue
index bd6fdc131cb..498dc859186 100644
--- a/app/assets/javascripts/issuable/components/status_box.vue
+++ b/app/assets/javascripts/issuable/components/status_box.vue
@@ -1,32 +1,50 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlBadge, GlIcon } from '@gitlab/ui';
import Vue from 'vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { fetchPolicies } from '~/lib/graphql';
import { __ } from '~/locale';
+import { IssuableType } from '~/issues/constants';
+import { IssuableStates } from '~/vue_shared/issuable/list/constants';
-export const statusBoxState = Vue.observable({
+export const badgeState = Vue.observable({
state: '',
updateStatus: null,
});
const CLASSES = {
- opened: 'status-box-open',
- locked: 'status-box-open',
- closed: 'status-box-mr-closed',
- merged: 'status-box-mr-merged',
+ opened: 'issuable-status-badge-open',
+ locked: 'issuable-status-badge-open',
+ closed: 'issuable-status-badge-closed',
+ merged: 'issuable-status-badge-merged',
+};
+
+const ISSUE_ICONS = {
+ opened: 'issues',
+ locked: 'issues',
+ closed: 'issue-closed',
+};
+
+const MERGE_REQUEST_ICONS = {
+ opened: 'merge-request-open',
+ locked: 'merge-request-open',
+ closed: 'merge-request-close',
+ merged: 'merge',
};
const STATUS = {
- opened: [__('Open'), 'issue-open-m'],
- locked: [__('Open'), 'issue-open-m'],
- closed: [__('Closed'), 'issue-close'],
- merged: [__('Merged'), 'git-merge'],
+ opened: __('Open'),
+ locked: __('Open'),
+ closed: __('Closed'),
+ merged: __('Merged'),
};
export default {
components: {
+ GlBadge,
GlIcon,
},
+ mixins: [glFeatureFlagMixin()],
inject: {
query: { default: null },
projectPath: { default: null },
@@ -46,30 +64,47 @@ export default {
},
data() {
if (this.initialState) {
- statusBoxState.state = this.initialState;
+ badgeState.state = this.initialState;
}
- return statusBoxState;
+ return badgeState;
},
computed: {
- statusBoxClass() {
- return CLASSES[`${this.issuableType}_${this.state}`] || CLASSES[this.state];
+ badgeClass() {
+ return [
+ CLASSES[this.state],
+ {
+ 'gl-vertical-align-bottom':
+ this.issuableType === IssuableType.MergeRequest && this.glFeatures.updatedMrHeader,
+ },
+ ];
+ },
+ badgeVariant() {
+ if (this.state === IssuableStates.Opened) {
+ return 'success';
+ } else if (this.state === IssuableStates.Closed) {
+ return this.issuableType === IssuableType.MergeRequest ? 'danger' : 'info';
+ }
+ return 'info';
},
- statusHumanName() {
- return (STATUS[`${this.issuableType}_${this.state}`] || STATUS[this.state])[0];
+ badgeText() {
+ return STATUS[this.state];
},
- statusIconName() {
- return (STATUS[`${this.issuableType}_${this.state}`] || STATUS[this.state])[1];
+ badgeIcon() {
+ if (this.issuableType === IssuableType.Issue) {
+ return ISSUE_ICONS[this.state];
+ }
+ return MERGE_REQUEST_ICONS[this.state];
},
},
created() {
- if (!statusBoxState.updateStatus) {
- statusBoxState.updateStatus = this.fetchState;
+ if (!badgeState.updateStatus) {
+ badgeState.updateStatus = this.fetchState;
}
},
beforeDestroy() {
- if (statusBoxState.updateStatus && this.query) {
- statusBoxState.updateStatus = null;
+ if (badgeState.updateStatus && this.query) {
+ badgeState.updateStatus = null;
}
},
methods: {
@@ -83,17 +118,15 @@ export default {
fetchPolicy: fetchPolicies.NO_CACHE,
});
- statusBoxState.state = data?.workspace?.issuable?.state;
+ badgeState.state = data?.workspace?.issuable?.state;
},
},
};
</script>
<template>
- <div :class="statusBoxClass" class="issuable-status-box status-box">
- <gl-icon :name="statusIconName" class="gl-display-block gl-sm-display-none!" />
- <span class="gl-display-none gl-sm-display-block">
- {{ statusHumanName }}
- </span>
- </div>
+ <gl-badge class="issuable-status-badge gl-mr-3" :class="badgeClass" :variant="badgeVariant">
+ <gl-icon :name="badgeIcon" />
+ <span class="gl-display-none gl-sm-display-block gl-ml-2">{{ badgeText }}</span>
+ </gl-badge>
</template>
diff --git a/app/assets/javascripts/issuable/issuable_form.js b/app/assets/javascripts/issuable/issuable_form.js
index 018cadad50f..8e76a33c7dd 100644
--- a/app/assets/javascripts/issuable/issuable_form.js
+++ b/app/assets/javascripts/issuable/issuable_form.js
@@ -65,7 +65,8 @@ export default class IssuableForm {
this.gfmAutoComplete = new GfmAutoComplete(
gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources,
).setup();
- this.usersSelect = new UsersSelect();
+ const autoAssignToMe = form.get(0).id === 'new_merge_request';
+ this.usersSelect = new UsersSelect(undefined, undefined, { autoAssignToMe });
this.reviewersSelect = new UsersSelect(undefined, '.js-reviewer-search');
this.zenMode = new ZenMode();
diff --git a/app/assets/javascripts/issues/create_merge_request_dropdown.js b/app/assets/javascripts/issues/create_merge_request_dropdown.js
index c96af6da720..8294c018117 100644
--- a/app/assets/javascripts/issues/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/issues/create_merge_request_dropdown.js
@@ -69,12 +69,12 @@ export default class CreateMergeRequestDropdown {
// with user's inputs.
this.regexps = {
branch: {
- createBranchPath: new RegExp('(branch_name=)(.+?)(?=&issue)'),
- createMrPath: new RegExp('(source_branch%5D=)(.+?)(?=&)'),
+ createBranchPath: /(branch_name=)(.+?)(?=&issue)/,
+ createMrPath: /(source_branch%5D=)(.+?)(?=&)/,
},
ref: {
- createBranchPath: new RegExp('(ref=)(.+?)$'),
- createMrPath: new RegExp('(target_branch%5D=)(.+?)$'),
+ createBranchPath: /(ref=)(.+?)$/,
+ createMrPath: /(target_branch%5D=)(.+?)$/,
},
};
@@ -170,7 +170,6 @@ export default class CreateMergeRequestDropdown {
createMergeRequest() {
return new Promise(() => {
this.isCreatingMergeRequest = true;
-
return this.createBranch().then(() => {
let path = canCreateConfidentialMergeRequest()
? this.createMrPath.replace(
diff --git a/app/assets/javascripts/issues/issue.js b/app/assets/javascripts/issues/issue.js
index 8e27f547b5c..a9321cf200d 100644
--- a/app/assets/javascripts/issues/issue.js
+++ b/app/assets/javascripts/issues/issue.js
@@ -49,8 +49,8 @@ export default class Issue {
issueFailMessage = __('Unable to update this issue at this time.'),
) {
if ('id' in data) {
- const isClosedBadge = $('div.status-box-issue-closed');
- const isOpenBadge = $('div.status-box-open');
+ const isClosedBadge = $('.issuable-status-badge-closed');
+ const isOpenBadge = $('.issuable-status-badge-open');
const projectIssuesCounter = $('.issue_counter');
isClosedBadge.toggleClass('hidden', !isClosed);
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 a43aed6c521..b81ab103271 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -13,6 +13,8 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
+import getIssuesWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_without_crm.query.graphql';
+import getIssuesCountsWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_counts_without_crm.query.graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -45,6 +47,7 @@ import {
PAGE_SIZE,
PARAM_PAGE_AFTER,
PARAM_PAGE_BEFORE,
+ PARAM_SORT,
PARAM_STATE,
RELATIVE_POSITION_ASC,
TOKEN_TYPE_ASSIGNEE,
@@ -122,6 +125,7 @@ export default {
'isAnonymousSearchDisabled',
'isIssueRepositioningDisabled',
'isProject',
+ 'isPublicVisibilityRestricted',
'isSignedIn',
'jiraIntegrationPath',
'newIssuePath',
@@ -138,48 +142,24 @@ export default {
},
},
data() {
- const pageAfter = getParameterByName(PARAM_PAGE_AFTER);
- const pageBefore = getParameterByName(PARAM_PAGE_BEFORE);
- const state = getParameterByName(PARAM_STATE);
- const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
- const dashboardSortKey = getSortKey(this.initialSort);
- const graphQLSortKey =
- isSortKey(this.initialSort?.toUpperCase()) && this.initialSort.toUpperCase();
-
- // The initial sort is an old enum value when it is saved on the dashboard issues page.
- // The initial sort is a GraphQL enum value when it is saved on the Vue issues list page.
- let sortKey = dashboardSortKey || graphQLSortKey || defaultSortKey;
-
- if (this.isIssueRepositioningDisabled && sortKey === RELATIVE_POSITION_ASC) {
- this.showIssueRepositioningMessage();
- sortKey = defaultSortKey;
- }
-
- const isSearchDisabled =
- this.isAnonymousSearchDisabled &&
- !this.isSignedIn &&
- window.location.search.includes('search=');
-
- if (isSearchDisabled) {
- this.showAnonymousSearchingMessage();
- }
-
return {
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
- filterTokens: isSearchDisabled ? [] : getFilterTokens(window.location.search),
+ filterTokens: [],
issues: [],
issuesCounts: {},
issuesError: null,
pageInfo: {},
- pageParams: getInitialPageParams(sortKey, pageAfter, pageBefore),
+ pageParams: {},
showBulkEditSidebar: false,
- sortKey,
- state: state || IssuableStates.Opened,
+ sortKey: CREATED_DESC,
+ state: IssuableStates.Opened,
};
},
apollo: {
issues: {
- query: getIssuesQuery,
+ query() {
+ return this.hasCrmParameter ? getIssuesQuery : getIssuesWithoutCrmQuery;
+ },
variables() {
return this.queryVariables;
},
@@ -203,7 +183,9 @@ export default {
debounce: 200,
},
issuesCounts: {
- query: getIssuesCountsQuery,
+ query() {
+ return this.hasCrmParameter ? getIssuesCountsQuery : getIssuesCountsWithoutCrmQuery;
+ },
variables() {
return this.queryVariables;
},
@@ -228,6 +210,7 @@ export default {
const isIidSearch = ISSUE_REFERENCE.test(this.searchQuery);
return {
fullPath: this.fullPath,
+ hideUsers: this.isPublicVisibilityRestricted && !this.isSignedIn,
iid: isIidSearch ? this.searchQuery.slice(1) : undefined,
isProject: this.isProject,
isSignedIn: this.isSignedIn,
@@ -415,8 +398,22 @@ export default {
...this.urlFilterParams,
};
},
+ hasCrmParameter() {
+ return (
+ window.location.search.includes('crm_contact_id=') ||
+ window.location.search.includes('crm_organization_id=')
+ );
+ },
+ },
+ watch: {
+ $route(newValue, oldValue) {
+ if (newValue.fullPath !== oldValue.fullPath) {
+ this.updateData(getParameterByName(PARAM_SORT));
+ }
+ },
},
created() {
+ this.updateData(this.initialSort);
this.cache = {};
},
mounted() {
@@ -516,6 +513,8 @@ export default {
this.pageParams = getInitialPageParams(this.sortKey);
}
this.state = state;
+
+ this.$router.push({ query: this.urlParams });
},
handleDismissAlert() {
this.issuesError = null;
@@ -525,8 +524,11 @@ export default {
this.showAnonymousSearchingMessage();
return;
}
+
this.pageParams = getInitialPageParams(this.sortKey);
this.filterTokens = filter;
+
+ this.$router.push({ query: this.urlParams });
},
handleNextPage() {
this.pageParams = {
@@ -534,6 +536,8 @@ export default {
firstPageSize: PAGE_SIZE,
};
scrollUp();
+
+ this.$router.push({ query: this.urlParams });
},
handlePreviousPage() {
this.pageParams = {
@@ -541,6 +545,8 @@ export default {
lastPageSize: PAGE_SIZE,
};
scrollUp();
+
+ this.$router.push({ query: this.urlParams });
},
handleReorder({ newIndex, oldIndex }) {
const issueToMove = this.issues[oldIndex];
@@ -592,6 +598,8 @@ export default {
if (this.isSignedIn) {
this.saveSortPreference(sortKey);
}
+
+ this.$router.push({ query: this.urlParams });
},
saveSortPreference(sortKey) {
this.$apollo
@@ -623,6 +631,39 @@ export default {
toggleBulkEditSidebar(showBulkEditSidebar) {
this.showBulkEditSidebar = showBulkEditSidebar;
},
+ updateData(sortValue) {
+ const pageAfter = getParameterByName(PARAM_PAGE_AFTER);
+ const pageBefore = getParameterByName(PARAM_PAGE_BEFORE);
+ const state = getParameterByName(PARAM_STATE);
+
+ const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
+ const dashboardSortKey = getSortKey(sortValue);
+ const graphQLSortKey = isSortKey(sortValue?.toUpperCase()) && sortValue.toUpperCase();
+
+ // The initial sort is an old enum value when it is saved on the dashboard issues page.
+ // The initial sort is a GraphQL enum value when it is saved on the Vue issues list page.
+ let sortKey = dashboardSortKey || graphQLSortKey || defaultSortKey;
+
+ if (this.isIssueRepositioningDisabled && sortKey === RELATIVE_POSITION_ASC) {
+ this.showIssueRepositioningMessage();
+ sortKey = defaultSortKey;
+ }
+
+ const isSearchDisabled =
+ this.isAnonymousSearchDisabled &&
+ !this.isSignedIn &&
+ window.location.search.includes('search=');
+
+ if (isSearchDisabled) {
+ this.showAnonymousSearchingMessage();
+ }
+
+ this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
+ this.filterTokens = isSearchDisabled ? [] : getFilterTokens(window.location.search);
+ this.pageParams = getInitialPageParams(sortKey, pageAfter, pageBefore);
+ this.sortKey = sortKey;
+ this.state = state || IssuableStates.Opened;
+ },
},
};
</script>
@@ -649,10 +690,10 @@ export default {
:is-manual-ordering="isManualOrdering"
:show-bulk-edit-sidebar="showBulkEditSidebar"
:show-pagination-controls="showPaginationControls"
- :use-keyset-pagination="true"
+ sync-filter-and-sort
+ use-keyset-pagination
:has-next-page="pageInfo.hasNextPage"
:has-previous-page="pageInfo.hasPreviousPage"
- :url-params="urlParams"
@click-tab="handleClickTab"
@dismiss-alert="handleDismissAlert"
@filter="handleFilter"
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 4b07a078512..0795df10a7c 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -56,8 +56,10 @@ export const ISSUE_REFERENCE = /^#\d+$/;
export const MAX_LIST_SIZE = 10;
export const PAGE_SIZE = 20;
export const PAGE_SIZE_MANUAL = 100;
+export const PARAM_ASSIGNEE_ID = 'assignee_id';
export const PARAM_PAGE_AFTER = 'page_after';
export const PARAM_PAGE_BEFORE = 'page_before';
+export const PARAM_SORT = 'sort';
export const PARAM_STATE = 'state';
export const RELATIVE_POSITION = 'relative_position';
@@ -112,7 +114,8 @@ export const URL_PARAM = 'urlParam';
export const NORMAL_FILTER = 'normalFilter';
export const SPECIAL_FILTER = 'specialFilter';
export const ALTERNATIVE_FILTER = 'alternativeFilter';
-export const SPECIAL_FILTER_VALUES = [
+
+export const specialFilterValues = [
FILTER_NONE,
FILTER_ANY,
FILTER_CURRENT,
@@ -131,6 +134,8 @@ export const TOKEN_TYPE_CONFIDENTIAL = 'confidential';
export const TOKEN_TYPE_ITERATION = 'iteration';
export const TOKEN_TYPE_EPIC = 'epic_id';
export const TOKEN_TYPE_WEIGHT = 'weight';
+export const TOKEN_TYPE_CONTACT = 'crm_contact';
+export const TOKEN_TYPE_ORGANIZATION = 'crm_organization';
export const filters = {
[TOKEN_TYPE_AUTHOR]: {
@@ -174,6 +179,7 @@ export const filters = {
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[milestone_title]',
+ [SPECIAL_FILTER]: 'not[milestone_title]',
},
},
},
@@ -258,6 +264,7 @@ export const filters = {
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[iteration_id]',
+ [SPECIAL_FILTER]: 'not[iteration_id]',
},
},
},
@@ -291,4 +298,24 @@ export const filters = {
},
},
},
+ [TOKEN_TYPE_CONTACT]: {
+ [API_PARAM]: {
+ [NORMAL_FILTER]: 'crmContactId',
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'crm_contact_id',
+ },
+ },
+ },
+ [TOKEN_TYPE_ORGANIZATION]: {
+ [API_PARAM]: {
+ [NORMAL_FILTER]: 'crmOrganizationId',
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'crm_organization_id',
+ },
+ },
+ },
};
diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js
index 3b2d37eab74..f5cb160e344 100644
--- a/app/assets/javascripts/issues/list/index.js
+++ b/app/assets/javascripts/issues/list/index.js
@@ -1,6 +1,7 @@
import produce from 'immer';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import IssuesListApp from 'ee_else_ce/issues/list/components/issues_list_app.vue';
import createDefaultClient from '~/lib/graphql';
@@ -53,6 +54,7 @@ export function mountIssuesListApp() {
}
Vue.use(VueApollo);
+ Vue.use(VueRouter);
const resolvers = {
Mutation: {
@@ -74,11 +76,6 @@ export function mountIssuesListApp() {
},
};
- const defaultClient = createDefaultClient(resolvers);
- const apolloProvider = new VueApollo({
- defaultClient,
- });
-
const {
autocompleteAwardEmojisPath,
calendarPath,
@@ -104,6 +101,7 @@ export function mountIssuesListApp() {
isAnonymousSearchDisabled,
isIssueRepositioningDisabled,
isProject,
+ isPublicVisibilityRestricted,
isSignedIn,
jiraIntegrationPath,
markdownHelpPath,
@@ -121,7 +119,14 @@ export function mountIssuesListApp() {
return new Vue({
el,
name: 'IssuesListRoot',
- apolloProvider,
+ apolloProvider: new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ }),
+ router: new VueRouter({
+ base: window.location.pathname,
+ mode: 'history',
+ routes: [{ path: '/' }],
+ }),
provide: {
autocompleteAwardEmojisPath,
calendarPath,
@@ -140,6 +145,7 @@ export function mountIssuesListApp() {
isAnonymousSearchDisabled: parseBoolean(isAnonymousSearchDisabled),
isIssueRepositioningDisabled: parseBoolean(isIssueRepositioningDisabled),
isProject: parseBoolean(isProject),
+ isPublicVisibilityRestricted: parseBoolean(isPublicVisibilityRestricted),
isSignedIn: parseBoolean(isSignedIn),
jiraIntegrationPath,
newIssuePath,
diff --git a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
index ec24ea7c56a..df7016aeb74 100644
--- a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
@@ -2,6 +2,7 @@
#import "./issue.fragment.graphql"
query getIssues(
+ $hideUsers: Boolean = false
$isProject: Boolean = false
$isSignedIn: Boolean = false
$fullPath: ID!
@@ -20,6 +21,8 @@ query getIssues(
$releaseTag: [String!]
$releaseTagWildcardId: ReleaseTagWildcardId
$types: [IssueType!]
+ $crmContactId: String
+ $crmOrganizationId: String
$not: NegatedIssueFilterInput
$beforeCursor: String
$afterCursor: String
@@ -43,6 +46,8 @@ query getIssues(
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
types: $types
+ crmContactId: $crmContactId
+ crmOrganizationId: $crmOrganizationId
not: $not
before: $beforeCursor
after: $afterCursor
@@ -76,6 +81,8 @@ query getIssues(
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
+ crmContactId: $crmContactId
+ crmOrganizationId: $crmOrganizationId
not: $not
before: $beforeCursor
after: $afterCursor
diff --git a/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql
index 58e7ce32e7c..c1aee772167 100644
--- a/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql
@@ -14,6 +14,8 @@ query getIssuesCount(
$releaseTag: [String!]
$releaseTagWildcardId: ReleaseTagWildcardId
$types: [IssueType!]
+ $crmContactId: String
+ $crmOrganizationId: String
$not: NegatedIssueFilterInput
) {
group(fullPath: $fullPath) @skip(if: $isProject) {
@@ -32,6 +34,8 @@ query getIssuesCount(
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
types: $types
+ crmContactId: $crmContactId
+ crmOrganizationId: $crmOrganizationId
not: $not
) {
count
@@ -50,6 +54,8 @@ query getIssuesCount(
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
types: $types
+ crmContactId: $crmContactId
+ crmOrganizationId: $crmOrganizationId
not: $not
) {
count
@@ -68,6 +74,8 @@ query getIssuesCount(
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
types: $types
+ crmContactId: $crmContactId
+ crmOrganizationId: $crmOrganizationId
not: $not
) {
count
@@ -90,6 +98,8 @@ query getIssuesCount(
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
+ crmContactId: $crmContactId
+ crmOrganizationId: $crmOrganizationId
not: $not
) {
count
@@ -109,6 +119,8 @@ query getIssuesCount(
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
+ crmContactId: $crmContactId
+ crmOrganizationId: $crmOrganizationId
not: $not
) {
count
@@ -128,6 +140,8 @@ query getIssuesCount(
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
+ crmContactId: $crmContactId
+ crmOrganizationId: $crmOrganizationId
not: $not
) {
count
diff --git a/app/assets/javascripts/issues/list/queries/get_issues_counts_without_crm.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues_counts_without_crm.query.graphql
new file mode 100644
index 00000000000..ab91aab1218
--- /dev/null
+++ b/app/assets/javascripts/issues/list/queries/get_issues_counts_without_crm.query.graphql
@@ -0,0 +1,136 @@
+query getIssuesCountWithoutCrm(
+ $isProject: Boolean = false
+ $fullPath: ID!
+ $iid: String
+ $search: String
+ $assigneeId: String
+ $assigneeUsernames: [String!]
+ $authorUsername: String
+ $confidential: Boolean
+ $labelName: [String]
+ $milestoneTitle: [String]
+ $milestoneWildcardId: MilestoneWildcardId
+ $myReactionEmoji: String
+ $releaseTag: [String!]
+ $releaseTagWildcardId: ReleaseTagWildcardId
+ $types: [IssueType!]
+ $not: NegatedIssueFilterInput
+) {
+ group(fullPath: $fullPath) @skip(if: $isProject) {
+ id
+ openedIssues: issues(
+ includeSubgroups: true
+ state: opened
+ iid: $iid
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ confidential: $confidential
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ types: $types
+ not: $not
+ ) {
+ count
+ }
+ closedIssues: issues(
+ includeSubgroups: true
+ state: closed
+ iid: $iid
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ confidential: $confidential
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ types: $types
+ not: $not
+ ) {
+ count
+ }
+ allIssues: issues(
+ includeSubgroups: true
+ state: all
+ iid: $iid
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ confidential: $confidential
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ types: $types
+ not: $not
+ ) {
+ count
+ }
+ }
+ project(fullPath: $fullPath) @include(if: $isProject) {
+ id
+ openedIssues: issues(
+ state: opened
+ iid: $iid
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ confidential: $confidential
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ releaseTag: $releaseTag
+ releaseTagWildcardId: $releaseTagWildcardId
+ types: $types
+ not: $not
+ ) {
+ count
+ }
+ closedIssues: issues(
+ state: closed
+ iid: $iid
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ confidential: $confidential
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ releaseTag: $releaseTag
+ releaseTagWildcardId: $releaseTagWildcardId
+ types: $types
+ not: $not
+ ) {
+ count
+ }
+ allIssues: issues(
+ state: all
+ iid: $iid
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ confidential: $confidential
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ releaseTag: $releaseTag
+ releaseTagWildcardId: $releaseTagWildcardId
+ types: $types
+ not: $not
+ ) {
+ count
+ }
+ }
+}
diff --git a/app/assets/javascripts/issues/list/queries/get_issues_without_crm.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues_without_crm.query.graphql
new file mode 100644
index 00000000000..4a8b1dfd618
--- /dev/null
+++ b/app/assets/javascripts/issues/list/queries/get_issues_without_crm.query.graphql
@@ -0,0 +1,94 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+#import "./issue.fragment.graphql"
+
+query getIssuesWithoutCrm(
+ $hideUsers: Boolean = false
+ $isProject: Boolean = false
+ $isSignedIn: Boolean = false
+ $fullPath: ID!
+ $iid: String
+ $search: String
+ $sort: IssueSort
+ $state: IssuableState
+ $assigneeId: String
+ $assigneeUsernames: [String!]
+ $authorUsername: String
+ $confidential: Boolean
+ $labelName: [String]
+ $milestoneTitle: [String]
+ $milestoneWildcardId: MilestoneWildcardId
+ $myReactionEmoji: String
+ $releaseTag: [String!]
+ $releaseTagWildcardId: ReleaseTagWildcardId
+ $types: [IssueType!]
+ $not: NegatedIssueFilterInput
+ $beforeCursor: String
+ $afterCursor: String
+ $firstPageSize: Int
+ $lastPageSize: Int
+) {
+ group(fullPath: $fullPath) @skip(if: $isProject) {
+ id
+ issues(
+ includeSubgroups: true
+ iid: $iid
+ search: $search
+ sort: $sort
+ state: $state
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ confidential: $confidential
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ types: $types
+ not: $not
+ before: $beforeCursor
+ after: $afterCursor
+ first: $firstPageSize
+ last: $lastPageSize
+ ) {
+ pageInfo {
+ ...PageInfo
+ }
+ nodes {
+ ...IssueFragment
+ reference(full: true)
+ }
+ }
+ }
+ project(fullPath: $fullPath) @include(if: $isProject) {
+ id
+ issues(
+ iid: $iid
+ search: $search
+ sort: $sort
+ state: $state
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ confidential: $confidential
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ releaseTag: $releaseTag
+ releaseTagWildcardId: $releaseTagWildcardId
+ types: $types
+ not: $not
+ before: $beforeCursor
+ after: $afterCursor
+ first: $firstPageSize
+ last: $lastPageSize
+ ) {
+ pageInfo {
+ ...PageInfo
+ }
+ nodes {
+ ...IssueFragment
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
index d09e4d9df2b..73a13cea94a 100644
--- a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
@@ -17,7 +17,7 @@ fragment IssueFragment on Issue {
userDiscussionsCount @include(if: $isSignedIn)
webPath
webUrl
- assignees {
+ assignees @skip(if: $hideUsers) {
nodes {
__typename
id
@@ -27,7 +27,7 @@ fragment IssueFragment on Issue {
webUrl
}
}
- author {
+ author @skip(if: $hideUsers) {
__typename
id
avatarUrl
diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js
index 4b77bd9bc5f..3ca93069628 100644
--- a/app/assets/javascripts/issues/list/utils.js
+++ b/app/assets/javascripts/issues/list/utils.js
@@ -1,4 +1,6 @@
+import { createTerm } from '@gitlab/ui/src/components/base/filtered_search/filtered_search_utils';
import { isPositiveInteger } from '~/lib/utils/number_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import {
FILTERED_SEARCH_TERM,
@@ -20,13 +22,14 @@ import {
NORMAL_FILTER,
PAGE_SIZE,
PAGE_SIZE_MANUAL,
+ PARAM_ASSIGNEE_ID,
POPULARITY_ASC,
POPULARITY_DESC,
PRIORITY_ASC,
PRIORITY_DESC,
RELATIVE_POSITION_ASC,
SPECIAL_FILTER,
- SPECIAL_FILTER_VALUES,
+ specialFilterValues,
TITLE_ASC,
TITLE_DESC,
TOKEN_TYPE_ASSIGNEE,
@@ -195,23 +198,27 @@ const convertToFilteredSearchTerms = (locationSearch) =>
export const getFilterTokens = (locationSearch) => {
if (!locationSearch) {
- return [];
+ return [createTerm()];
}
const filterTokens = convertToFilteredTokens(locationSearch);
const searchTokens = convertToFilteredSearchTerms(locationSearch);
- return filterTokens.concat(searchTokens);
+ const tokens = filterTokens.concat(searchTokens);
+ return tokens.length ? tokens : [createTerm()];
};
-const getFilterType = (data, tokenType = '') =>
- SPECIAL_FILTER_VALUES.includes(data) ||
- (tokenType === TOKEN_TYPE_ASSIGNEE && isPositiveInteger(data))
- ? SPECIAL_FILTER
- : NORMAL_FILTER;
+const getFilterType = (data, tokenType = '') => {
+ const isAssigneeIdParam =
+ tokenType === TOKEN_TYPE_ASSIGNEE &&
+ isPositiveInteger(data) &&
+ getParameterByName(PARAM_ASSIGNEE_ID) === data;
+
+ return specialFilterValues.includes(data) || isAssigneeIdParam ? SPECIAL_FILTER : NORMAL_FILTER;
+};
const wildcardTokens = [TOKEN_TYPE_ITERATION, TOKEN_TYPE_MILESTONE, TOKEN_TYPE_RELEASE];
const isWildcardValue = (tokenType, value) =>
- wildcardTokens.includes(tokenType) && SPECIAL_FILTER_VALUES.includes(value);
+ wildcardTokens.includes(tokenType) && specialFilterValues.includes(value);
const requiresUpperCaseValue = (tokenType, value) =>
tokenType === TOKEN_TYPE_TYPE || isWildcardValue(tokenType, value);
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index 456a2029703..c664135f30e 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -1,11 +1,17 @@
<script>
-import { GlIcon, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlBadge, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
import Visibility from 'visibilityjs';
import createFlash from '~/flash';
-import { IssuableStatus, IssuableStatusText, IssuableType } from '~/issues/constants';
+import {
+ IssuableStatus,
+ IssuableStatusText,
+ WorkspaceType,
+ IssuableType,
+} from '~/issues/constants';
import Poll from '~/lib/utils/poll';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import { ISSUE_TYPE_PATH, INCIDENT_TYPE_PATH, INCIDENT_TYPE, POLLING_DELAY } from '../constants';
import eventHub from '../event_hub';
import getIssueStateQuery from '../queries/get_issue_state.query.graphql';
@@ -18,13 +24,16 @@ import PinnedLinks from './pinned_links.vue';
import titleComponent from './title.vue';
export default {
+ WorkspaceType,
components: {
GlIcon,
+ GlBadge,
GlIntersectionObserver,
titleComponent,
editedComponent,
formComponent,
PinnedLinks,
+ ConfidentialityBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -156,7 +165,7 @@ export default {
issuableType: {
type: String,
required: false,
- default: 'issue',
+ default: IssuableType.Issue,
},
canAttachFile: {
type: Boolean,
@@ -259,13 +268,19 @@ export default {
: '';
},
statusIcon() {
- return this.isClosed ? 'issue-close' : 'issue-open-m';
+ if (this.issuableType === IssuableType.Issue) {
+ return this.isClosed ? 'issue-closed' : 'issues';
+ }
+ return this.isClosed ? 'epic-closed' : 'epic';
+ },
+ statusVariant() {
+ return this.isClosed ? 'info' : 'success';
},
statusText() {
return IssuableStatusText[this.issuableStatus];
},
shouldShowStickyHeader() {
- return this.issuableType === IssuableType.Issue;
+ return [IssuableType.Issue, IssuableType.Epic].includes(this.issuableType);
},
},
created() {
@@ -327,20 +342,24 @@ export default {
});
},
- updateFormState(state) {
+ setFormState(state) {
this.store.setFormState(state);
},
- updateAndShowForm(templates = {}) {
+ updateFormState(templates = {}) {
+ this.setFormState({
+ title: this.state.titleText,
+ description: this.state.descriptionText,
+ lock_version: this.state.lock_version,
+ lockedWarningVisible: false,
+ updateLoading: false,
+ issuableTemplates: templates,
+ });
+ },
+
+ updateAndShowForm(templates) {
if (!this.showForm) {
- this.store.setFormState({
- title: this.state.titleText,
- description: this.state.descriptionText,
- lock_version: this.state.lock_version,
- lockedWarningVisible: false,
- updateLoading: false,
- issuableTemplates: templates,
- });
+ this.updateFormState(templates);
this.showForm = true;
}
},
@@ -373,9 +392,7 @@ export default {
},
updateIssuable() {
- this.store.setFormState({
- updateLoading: true,
- });
+ this.setFormState({ updateLoading: true });
const {
store: { formState },
@@ -413,10 +430,6 @@ export default {
.catch((error = {}) => {
const { message, response = {} } = error;
- this.store.setFormState({
- updateLoading: false,
- });
-
let errMsg = this.defaultErrorMessage;
if (response.data && response.data.errors) {
@@ -428,6 +441,9 @@ export default {
this.flashContainer = createFlash({
message: errMsg,
});
+ })
+ .finally(() => {
+ this.setFormState({ updateLoading: false });
});
},
@@ -446,6 +462,12 @@ export default {
}
},
+ handleListItemReorder(description) {
+ this.updateFormState();
+ this.setFormState({ description });
+ this.updateIssuable();
+ },
+
taskListUpdateStarted() {
this.poll.stop();
},
@@ -483,7 +505,7 @@ export default {
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
:issuable-type="issuableType"
- @updateForm="updateFormState"
+ @updateForm="setFormState"
/>
</div>
<div v-else>
@@ -509,19 +531,21 @@ export default {
<div
class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-mx-auto gl-px-5"
>
- <p
- class="issuable-status-box status-box gl-my-0"
- :class="[isClosed ? 'status-box-issue-closed' : 'status-box-open']"
+ <gl-badge :variant="statusVariant" class="gl-mr-2">
+ <gl-icon :name="statusIcon" />
+ <span class="gl-display-none gl-sm-display-block gl-ml-2">{{
+ statusText
+ }}</span></gl-badge
>
- <gl-icon :name="statusIcon" class="gl-display-block d-sm-none gl-h-6!" />
- <span class="gl-display-none d-sm-block">{{ statusText }}</span>
- </p>
<span v-if="isLocked" data-testid="locked" class="issuable-warning-icon">
<gl-icon name="lock" :aria-label="__('Locked')" />
</span>
- <span v-if="isConfidential" data-testid="confidential" class="issuable-warning-icon">
- <gl-icon name="eye-slash" :aria-label="__('Confidential')" />
- </span>
+ <confidentiality-badge
+ v-if="isConfidential"
+ data-testid="confidential"
+ :workspace-type="$options.WorkspaceType.project"
+ :issuable-type="issuableType"
+ />
<span
v-if="isHidden"
v-gl-tooltip
@@ -559,6 +583,8 @@ export default {
:issuable-type="issuableType"
:update-url="updateEndpoint"
:lock-version="state.lock_version"
+ :is-updating="formState.updateLoading"
+ @listItemReorder="handleListItemReorder"
@taskListUpdateStarted="taskListUpdateStarted"
@taskListUpdateSucceeded="taskListUpdateSucceeded"
@taskListUpdateFailed="taskListUpdateFailed"
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 0b7e128c47b..4f97458dcd1 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -7,22 +7,31 @@ import {
GlModalDirective,
} from '@gitlab/ui';
import $ from 'jquery';
+import Sortable from 'sortablejs';
import Vue from 'vue';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
import createFlash from '~/flash';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale';
+import { getSortableDefaultOptions, isDragging } from '~/sortable/utils';
import TaskList from '~/task_list';
import Tracking from '~/tracking';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import animateMixin from '../mixins/animate';
+import { convertDescriptionWithNewSort } from '../utils';
Vue.use(GlToast);
+const workItemTypes = {
+ TASK: 'task',
+};
+
export default {
directives: {
SafeHtml,
@@ -74,6 +83,11 @@ export default {
required: false,
default: null,
},
+ isUpdating: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
const workItemId = getParameterByName('work_item_id');
@@ -89,10 +103,20 @@ export default {
: undefined,
};
},
- computed: {
- showWorkItemDetailModal() {
- return Boolean(this.workItemId);
+ apollo: {
+ workItem: {
+ query: workItemQuery,
+ variables() {
+ return {
+ id: this.workItemId,
+ };
+ },
+ skip() {
+ return !this.workItemId || !this.workItemsEnabled;
+ },
},
+ },
+ computed: {
workItemsEnabled() {
return this.glFeatures.workItems;
},
@@ -126,6 +150,16 @@ export default {
if (this.workItemsEnabled) {
this.renderTaskActions();
}
+
+ if (this.workItemId) {
+ const taskLink = this.$el.querySelector(
+ `.gfm-issue[data-issue="${getIdFromGraphQLId(this.workItemId)}"]`,
+ );
+ this.openWorkItemDetailModal(taskLink);
+ }
+ },
+ beforeDestroy() {
+ this.removeAllPointerEventListeners();
},
methods: {
renderGFM() {
@@ -142,9 +176,67 @@ export default {
onSuccess: this.taskListUpdateSuccess.bind(this),
onError: this.taskListUpdateError.bind(this),
});
+
+ this.renderSortableLists();
}
},
+ renderSortableLists() {
+ this.removeAllPointerEventListeners();
+
+ const lists = document.querySelectorAll('.description ul, .description ol');
+ lists.forEach((list) => {
+ Array.from(list.children).forEach((listItem) => {
+ listItem.prepend(this.createDragIconElement());
+ this.addPointerEventListeners(listItem);
+ });
+ Sortable.create(
+ list,
+ getSortableDefaultOptions({
+ handle: '.drag-icon',
+ onUpdate: (event) => {
+ const description = convertDescriptionWithNewSort(this.descriptionText, event.to);
+ this.$emit('listItemReorder', description);
+ },
+ }),
+ );
+ });
+ },
+ createDragIconElement() {
+ const container = document.createElement('div');
+ container.innerHTML = `<svg class="drag-icon s14 gl-icon gl-cursor-grab gl-visibility-hidden" role="img" aria-hidden="true">
+ <use href="${gon.sprite_icons}#drag-vertical"></use>
+ </svg>`;
+ return container.firstChild;
+ },
+ addPointerEventListeners(listItem) {
+ const pointeroverListener = (event) => {
+ if (isDragging() || this.isUpdating) {
+ return;
+ }
+ event.target.closest('li').querySelector('.drag-icon').style.visibility = 'visible'; // eslint-disable-line no-param-reassign
+ };
+ const pointeroutListener = (event) => {
+ event.target.closest('li').querySelector('.drag-icon').style.visibility = 'hidden'; // eslint-disable-line no-param-reassign
+ };
+
+ // We use pointerover/pointerout instead of CSS so that when we hover over a
+ // list item with children, the drag icons of its children do not become visible.
+ listItem.addEventListener('pointerover', pointeroverListener);
+ listItem.addEventListener('pointerout', pointeroutListener);
+
+ this.pointerEventListeners = this.pointerEventListeners || new Map();
+ this.pointerEventListeners.set(listItem, [
+ { type: 'pointerover', listener: pointeroverListener },
+ { type: 'pointerout', listener: pointeroutListener },
+ ]);
+ },
+ removeAllPointerEventListeners() {
+ this.pointerEventListeners?.forEach((events, listItem) => {
+ events.forEach((event) => listItem.removeEventListener(event.type, event.listener));
+ this.pointerEventListeners.delete(listItem);
+ });
+ },
taskListUpdateStarted() {
this.$emit('taskListUpdateStarted');
},
@@ -195,10 +287,16 @@ export default {
taskListFields.forEach((item, index) => {
const taskLink = item.querySelector('.gfm-issue');
if (taskLink) {
- const { issue, referenceType } = taskLink.dataset;
+ const { issue, referenceType, issueType } = taskLink.dataset;
+ if (issueType !== workItemTypes.TASK) {
+ return;
+ }
+ const workItemId = convertToGraphQLId(TYPE_WORK_ITEM, issue);
+ this.addHoverListeners(taskLink, workItemId);
taskLink.addEventListener('click', (e) => {
e.preventDefault();
- this.workItemId = convertToGraphQLId(TYPE_WORK_ITEM, issue);
+ this.openWorkItemDetailModal(taskLink);
+ this.workItemId = workItemId;
this.updateWorkItemIdUrlQuery(issue);
this.track('viewed_work_item_from_modal', {
category: 'workItems:show',
@@ -215,10 +313,9 @@ export default {
'btn-md',
'gl-button',
'btn-default-tertiary',
- 'gl-left-0',
'gl-p-0!',
- 'gl-top-2',
- 'gl-absolute',
+ 'gl-mt-n1',
+ 'gl-ml-3',
'js-add-task',
);
button.id = `js-task-button-${index}`;
@@ -229,24 +326,46 @@ export default {
</svg>
`;
button.setAttribute('aria-label', s__('WorkItem|Convert to work item'));
- button.addEventListener('click', () => this.openCreateTaskModal(button.id));
- item.prepend(button);
+ button.addEventListener('click', () => this.openCreateTaskModal(button));
+ item.append(button);
});
},
- openCreateTaskModal(id) {
- const { parentElement } = this.$el.querySelector(`#${id}`);
+ addHoverListeners(taskLink, id) {
+ let workItemPrefetch;
+ taskLink.addEventListener('mouseover', () => {
+ workItemPrefetch = setTimeout(() => {
+ this.workItemId = id;
+ }, 150);
+ });
+ taskLink.addEventListener('mouseout', () => {
+ if (workItemPrefetch) {
+ clearTimeout(workItemPrefetch);
+ }
+ });
+ },
+ setActiveTask(el) {
+ const { parentElement } = el;
const lineNumbers = parentElement.getAttribute('data-sourcepos').match(/\b\d+(?=:)/g);
this.activeTask = {
- id,
title: parentElement.innerText,
lineNumberStart: lineNumbers[0],
lineNumberEnd: lineNumbers[1],
};
+ },
+ openCreateTaskModal(el) {
+ this.setActiveTask(el);
this.$refs.modal.show();
},
closeCreateTaskModal() {
this.$refs.modal.hide();
},
+ openWorkItemDetailModal(el) {
+ if (!el) {
+ return;
+ }
+ this.setActiveTask(el);
+ this.$refs.detailsModal.show();
+ },
closeWorkItemDetailModal() {
this.workItemId = undefined;
this.updateWorkItemIdUrlQuery(undefined);
@@ -255,7 +374,8 @@ export default {
this.$emit('updateDescription', description);
this.closeCreateTaskModal();
},
- handleDeleteTask() {
+ handleDeleteTask(description) {
+ this.$emit('updateDescription', description);
this.$toast.show(s__('WorkItem|Work item deleted'));
},
updateWorkItemIdUrlQuery(workItemId) {
@@ -318,9 +438,13 @@ export default {
/>
</gl-modal>
<work-item-detail-modal
+ ref="detailsModal"
:can-update="canUpdate"
- :visible="showWorkItemDetailModal"
:work-item-id="workItemId"
+ :issue-gid="issueGid"
+ :lock-version="lockVersion"
+ :line-number-start="activeTask.lineNumberStart"
+ :line-number-end="activeTask.lineNumberEnd"
@workItemDeleted="handleDeleteTask"
@close="closeWorkItemDetailModal"
/>
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index 4a5ebf9615b..6b0b26ef2e3 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -38,6 +38,7 @@ export function initIncidentApp(issueData = {}) {
projectId,
slaFeatureAvailable,
uploadMetricsFeatureAvailable,
+ state,
} = issueData;
const fullPath = `${projectNamespace}/${projectPath}`;
@@ -60,6 +61,7 @@ export function initIncidentApp(issueData = {}) {
return createElement(IssueApp, {
props: {
...issueData,
+ issuableStatus: state,
descriptionComponent: IncidentTabs,
showTitleBorder: false,
},
diff --git a/app/assets/javascripts/issues/show/utils.js b/app/assets/javascripts/issues/show/utils.js
new file mode 100644
index 00000000000..60e66f59f92
--- /dev/null
+++ b/app/assets/javascripts/issues/show/utils.js
@@ -0,0 +1,99 @@
+import { COLON, HYPHEN, NEWLINE } from '~/lib/utils/text_utility';
+
+/**
+ * Get the index from sourcepos that represents the line of
+ * the description when the description is split by newline.
+ *
+ * @param {String} sourcepos Source position in format `23:3-23:14`
+ * @returns {Number} Index of description split by newline
+ */
+const getDescriptionIndex = (sourcepos) => {
+ const [startRange] = sourcepos.split(HYPHEN);
+ const [startRow] = startRange.split(COLON);
+ return startRow - 1;
+};
+
+/**
+ * Given a `ul` or `ol` element containing a new sort order, this function performs
+ * a depth-first search to get the new sort order in the form of sourcepos indices.
+ *
+ * @param {HTMLElement} list A `ul` or `ol` element containing a new sort order
+ * @returns {Array<Number>} An array representing the new order of the list
+ */
+const getNewSourcePositions = (list) => {
+ const newSourcePositions = [];
+
+ function pushPositionOfChildListItems(el) {
+ if (!el) {
+ return;
+ }
+ if (el.tagName === 'LI') {
+ newSourcePositions.push(getDescriptionIndex(el.dataset.sourcepos));
+ }
+ Array.from(el.children).forEach(pushPositionOfChildListItems);
+ }
+
+ pushPositionOfChildListItems(list);
+
+ return newSourcePositions;
+};
+
+/**
+ * Converts a description to one with a new list sort order.
+ *
+ * Given a description like:
+ *
+ * <pre>
+ * 1. I am text
+ * 2.
+ * 3. - Item 1
+ * 4. - Item 2
+ * 5. - Item 3
+ * 6. - Item 4
+ * 7. - Item 5
+ * </pre>
+ *
+ * And a reordered list (due to dragging Item 2 into Item 1's position) like:
+ *
+ * <pre>
+ * <ul data-sourcepos="3:1-8:0">
+ * <li data-sourcepos="4:1-4:8">
+ * Item 2
+ * <ul data-sourcepos="5:1-6:10">
+ * <li data-sourcepos="5:1-5:10">Item 3</li>
+ * <li data-sourcepos="6:1-6:10">Item 4</li>
+ * </ul>
+ * </li>
+ * <li data-sourcepos="3:1-3:8">Item 1</li>
+ * <li data-sourcepos="7:1-8:0">Item 5</li>
+ * <ul>
+ * </pre>
+ *
+ * This function returns:
+ *
+ * <pre>
+ * 1. I am text
+ * 2.
+ * 3. - Item 2
+ * 4. - Item 3
+ * 5. - Item 4
+ * 6. - Item 1
+ * 7. - Item 5
+ * </pre>
+ *
+ * @param {String} description Description in markdown format
+ * @param {HTMLElement} list A `ul` or `ol` element containing a new sort order
+ * @returns {String} Markdown with a new list sort order
+ */
+export const convertDescriptionWithNewSort = (description, list) => {
+ const descriptionLines = description.split(NEWLINE);
+ const startIndexOfList = getDescriptionIndex(list.dataset.sourcepos);
+
+ getNewSourcePositions(list)
+ .map((lineIndex) => descriptionLines[lineIndex])
+ .forEach((line, index) => {
+ descriptionLines[startIndexOfList + index] = line;
+ });
+
+ return descriptionLines.join(NEWLINE);
+};
diff --git a/app/assets/javascripts/jira_connect/subscriptions/api.js b/app/assets/javascripts/jira_connect/subscriptions/api.js
index 14947b6c835..de67703356f 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/api.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/api.js
@@ -29,3 +29,13 @@ export const fetchGroups = async (groupsPath, { page, perPage, search }) => {
},
});
};
+
+export const fetchSubscriptions = async (subscriptionsPath) => {
+ const jwt = await getJwt();
+
+ return axios.get(subscriptionsPath, {
+ params: {
+ jwt,
+ },
+ });
+};
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue
index 1fc40e5c0d6..fa1c2e1912c 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue
@@ -1,17 +1,27 @@
<script>
+import { mapActions } from 'vuex';
import { GlButton } from '@gitlab/ui';
-import { helpPagePath } from '~/helpers/help_page_helper';
import { addSubscription } from '~/jira_connect/subscriptions/api';
import { persistAlert, reloadPage } from '~/jira_connect/subscriptions/utils';
-import { s__ } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import GroupItemName from '../group_item_name.vue';
+import {
+ INTEGRATIONS_DOC_LINK,
+ I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_TITLE,
+ I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_MESSAGE,
+ I18N_ADD_SUBSCRIPTIONS_ERROR_MESSAGE,
+} from '../../constants';
export default {
components: {
GlButton,
GroupItemName,
},
+ mixins: [glFeatureFlagMixin()],
inject: {
+ addSubscriptionsPath: {
+ default: '',
+ },
subscriptionsPath: {
default: '',
},
@@ -32,31 +42,41 @@ export default {
isLoading: false,
};
},
+ computed: {
+ oauthEnabled() {
+ return this.glFeatures.jiraConnectOauth;
+ },
+ },
methods: {
- onClick() {
+ ...mapActions(['addSubscription']),
+ async onClick() {
+ if (this.oauthEnabled) {
+ this.isLoading = true;
+ await this.addSubscription({
+ namespacePath: this.group.full_path,
+ subscriptionsPath: this.subscriptionsPath,
+ });
+ this.isLoading = false;
+ } else {
+ this.deprecatedAddSubscription();
+ }
+ },
+ deprecatedAddSubscription() {
this.isLoading = true;
- addSubscription(this.subscriptionsPath, this.group.full_path)
+ addSubscription(this.addSubscriptionsPath, this.group.full_path)
.then(() => {
persistAlert({
- title: s__('Integrations|Namespace successfully linked'),
- message: s__(
- 'Integrations|You should now see GitLab.com activity inside your Jira Cloud issues. %{linkStart}Learn more%{linkEnd}',
- ),
- linkUrl: helpPagePath('integration/jira_development_panel.html', {
- anchor: 'use-the-integration',
- }),
+ title: I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_TITLE,
+ message: I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_MESSAGE,
+ linkUrl: INTEGRATIONS_DOC_LINK,
variant: 'success',
});
reloadPage();
})
.catch((error) => {
- this.$emit(
- 'error',
- error?.response?.data?.error ||
- s__('Integrations|Failed to link namespace. Please try again.'),
- );
+ this.$emit('error', error?.response?.data?.error || I18N_ADD_SUBSCRIPTIONS_ERROR_MESSAGE);
this.isLoading = false;
});
},
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
index 51db3e784aa..22422872183 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
@@ -1,14 +1,14 @@
<script>
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
-import { mapState, mapMutations } from 'vuex';
+import { mapState, mapMutations, mapActions } from 'vuex';
import { retrieveAlert } from '~/jira_connect/subscriptions/utils';
import AccessorUtilities from '~/lib/utils/accessor';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { I18N_DEFAULT_SIGN_IN_ERROR_MESSAGE } from '../constants';
import { SET_ALERT } from '../store/mutation_types';
-import SignInPage from '../pages/sign_in.vue';
-import SubscriptionsPage from '../pages/subscriptions.vue';
+import SignInPage from '../pages/sign_in/sign_in_page.vue';
+import SubscriptionsPage from '../pages/subscriptions_page.vue';
import UserLink from './user_link.vue';
import CompatibilityAlert from './compatibility_alert.vue';
import BrowserSupportAlert from './browser_support_alert.vue';
@@ -30,17 +30,13 @@ export default {
usersPath: {
default: '',
},
- subscriptions: {
- default: [],
+ subscriptionsPath: {
+ default: '',
},
},
- data() {
- return {
- user: null,
- };
- },
computed: {
- ...mapState(['alert']),
+ ...mapState(['currentUser']),
+ ...mapState(['alert', 'subscriptions']),
shouldShowAlert() {
return Boolean(this.alert?.message);
},
@@ -48,7 +44,11 @@ export default {
return !isEmpty(this.subscriptions);
},
userSignedIn() {
- return Boolean(!this.usersPath || this.user);
+ if (this.isOauthEnabled) {
+ return Boolean(this.currentUser);
+ }
+
+ return Boolean(!this.usersPath);
},
isOauthEnabled() {
return this.glFeatures.jiraConnectOauth;
@@ -64,16 +64,29 @@ export default {
created() {
this.setInitialAlert();
},
+ mounted() {
+ this.fetchSubscriptionsOauth();
+ },
methods: {
...mapMutations({
setAlert: SET_ALERT,
}),
+ ...mapActions(['fetchSubscriptions']),
+ /**
+ * Fetch subscriptions from the REST API,
+ * if the jiraConnectOauth flag is enabled.
+ */
+ fetchSubscriptionsOauth() {
+ if (!this.isOauthEnabled) return;
+
+ this.fetchSubscriptions(this.subscriptionsPath);
+ },
setInitialAlert() {
const { linkUrl, title, message, variant } = retrieveAlert() || {};
this.setAlert({ linkUrl, title, message, variant });
},
- onSignInOauth(user) {
- this.user = user;
+ onSignInOauth() {
+ this.fetchSubscriptionsOauth();
},
onSignInError() {
this.setAlert({
@@ -109,9 +122,12 @@ export default {
</template>
</gl-alert>
- <user-link :user-signed-in="userSignedIn" :has-subscriptions="hasSubscriptions" :user="user" />
+ <user-link
+ :user-signed-in="userSignedIn"
+ :has-subscriptions="hasSubscriptions"
+ :user="currentUser"
+ />
- <h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
<div class="gl-layout-w-limited gl-mx-auto gl-px-5 gl-mb-7">
<sign-in-page
v-if="!userSignedIn"
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue
index dfed57df7d6..b9e8bab019f 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue
@@ -1,4 +1,5 @@
<script>
+import { mapActions, mapMutations } from 'vuex';
import { GlButton } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import {
@@ -8,8 +9,8 @@ import {
} from '~/jira_connect/subscriptions/constants';
import { setUrlParams } from '~/lib/utils/url_utility';
import AccessorUtilities from '~/lib/utils/accessor';
-
import { createCodeVerifier, createCodeChallenge } from '../pkce';
+import { SET_ACCESS_TOKEN } from '../store/mutation_types';
export default {
components: {
@@ -31,6 +32,10 @@ export default {
window.removeEventListener('message', this.handleWindowMessage);
},
methods: {
+ ...mapActions(['loadCurrentUser']),
+ ...mapMutations({
+ setAccessToken: SET_ACCESS_TOKEN,
+ }),
async startOAuthFlow() {
this.loading = true;
@@ -40,6 +45,7 @@ export default {
// Build the initial OAuth authorization URL
const { oauth_authorize_url: oauthAuthorizeURL } = this.oauthMetadata;
+
const oauthAuthorizeURLWithChallenge = setUrlParams(
{
code_challenge: codeChallenge,
@@ -57,7 +63,6 @@ export default {
async handleWindowMessage(event) {
if (window.origin !== event.origin) {
this.loading = false;
- this.handleError();
return;
}
@@ -73,7 +78,10 @@ export default {
const code = event.data?.code;
try {
const accessToken = await this.getOAuthToken(code);
- await this.loadUser(accessToken);
+ await this.loadCurrentUser(accessToken);
+
+ this.setAccessToken(accessToken);
+ this.$emit('sign-in');
} catch (e) {
this.handleError();
} finally {
@@ -96,13 +104,6 @@ export default {
return data.access_token;
},
- async loadUser(accessToken) {
- const { data } = await axios.get('/api/v4/user', {
- headers: { Authorization: `Bearer ${accessToken}` },
- });
-
- this.$emit('sign-in', data);
- },
},
i18n: {
defaultButtonText: I18N_DEFAULT_SIGN_IN_BUTTON_TEXT,
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue b/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue
index 0251728c896..4c039be9ba5 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlTableLite } from '@gitlab/ui';
import { isEmpty } from 'lodash';
-import { mapMutations } from 'vuex';
+import { mapMutations, mapState } from 'vuex';
import { removeSubscription } from '~/jira_connect/subscriptions/api';
import { reloadPage } from '~/jira_connect/subscriptions/utils';
import { __, s__ } from '~/locale';
@@ -16,11 +16,6 @@ export default {
GroupItemName,
TimeagoTooltip,
},
- inject: {
- subscriptions: {
- default: [],
- },
- },
data() {
return {
loadingItem: null,
@@ -45,6 +40,9 @@ export default {
i18n: {
unlinkError: s__('Integrations|Failed to unlink namespace. Please try again.'),
},
+ computed: {
+ ...mapState(['subscriptions']),
+ },
methods: {
...mapMutations({
setAlert: SET_ALERT,
diff --git a/app/assets/javascripts/jira_connect/subscriptions/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js
index d30ebdbb487..8faafb1b0d0 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/constants.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js
@@ -1,4 +1,5 @@
import { s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
export const DEFAULT_GROUPS_PER_PAGE = 10;
export const ALERT_LOCALSTORAGE_KEY = 'gitlab_alert';
@@ -8,6 +9,22 @@ export const ADD_NAMESPACE_MODAL_ID = 'add-namespace-modal';
export const I18N_DEFAULT_SIGN_IN_BUTTON_TEXT = s__('Integrations|Sign in to GitLab');
export const I18N_DEFAULT_SIGN_IN_ERROR_MESSAGE = s__('Integrations|Failed to sign in to GitLab.');
+export const I18N_DEFAULT_SUBSCRIPTIONS_ERROR_MESSAGE = s__(
+ 'Integrations|Failed to load subscriptions.',
+);
+export const I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_TITLE = s__(
+ 'Integrations|Namespace successfully linked',
+);
+export const I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_MESSAGE = s__(
+ 'Integrations|You should now see GitLab.com activity inside your Jira Cloud issues. %{linkStart}Learn more%{linkEnd}',
+);
+export const INTEGRATIONS_DOC_LINK = helpPagePath('integration/jira_development_panel', {
+ anchor: 'use-the-integration',
+});
+
+export const I18N_ADD_SUBSCRIPTIONS_ERROR_MESSAGE = s__(
+ 'Integrations|Failed to link namespace. Please try again.',
+);
const OAUTH_WINDOW_SIZE = 800;
export const OAUTH_WINDOW_OPTIONS = [
diff --git a/app/assets/javascripts/jira_connect/subscriptions/index.js b/app/assets/javascripts/jira_connect/subscriptions/index.js
index 3b584b5fe98..8e9f73538b9 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/index.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/index.js
@@ -9,8 +9,6 @@ import JiraConnectApp from './components/app.vue';
import createStore from './store';
import { sizeToParent } from './utils';
-const store = createStore();
-
export function initJiraConnect() {
const el = document.querySelector('.js-jira-connect-app');
if (!el) {
@@ -24,6 +22,7 @@ export function initJiraConnect() {
const {
groupsPath,
subscriptions,
+ addSubscriptionsPath,
subscriptionsPath,
usersPath,
gitlabUserPath,
@@ -31,12 +30,14 @@ export function initJiraConnect() {
} = el.dataset;
sizeToParent();
+ const store = createStore({ subscriptions: JSON.parse(subscriptions) });
+
return new Vue({
el,
store,
provide: {
groupsPath,
- subscriptions: JSON.parse(subscriptions),
+ addSubscriptionsPath,
subscriptionsPath,
usersPath,
gitlabUserPath,
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in.vue
deleted file mode 100644
index a24ee33b723..00000000000
--- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
-import { s__ } from '~/locale';
-
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import SubscriptionsList from '../components/subscriptions_list.vue';
-
-export default {
- name: 'SignInPage',
- components: {
- SubscriptionsList,
- SignInLegacyButton: () => import('../components/sign_in_legacy_button.vue'),
- SignInOauthButton: () => import('../components/sign_in_oauth_button.vue'),
- },
- mixins: [glFeatureFlagMixin()],
- inject: ['usersPath'],
- props: {
- hasSubscriptions: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- useSignInOauthButton() {
- return this.glFeatures.jiraConnectOauth;
- },
- },
- i18n: {
- signInButtonTextWithSubscriptions: s__('Integrations|Sign in to add namespaces'),
- signInText: s__('JiraService|Sign in to GitLab.com to get started.'),
- },
- methods: {
- onSignInError() {
- this.$emit('error');
- },
- },
-};
-</script>
-
-<template>
- <div v-if="hasSubscriptions">
- <div class="gl-display-flex gl-justify-content-end">
- <sign-in-oauth-button
- v-if="useSignInOauthButton"
- @sign-in="$emit('sign-in-oauth', $event)"
- @error="onSignInError"
- >
- {{ $options.i18n.signInButtonTextWithSubscriptions }}
- </sign-in-oauth-button>
- <sign-in-legacy-button v-else :users-path="usersPath">
- {{ $options.i18n.signInButtonTextWithSubscriptions }}
- </sign-in-legacy-button>
- </div>
-
- <subscriptions-list />
- </div>
- <div v-else class="gl-text-center">
- <p class="gl-mb-7">{{ $options.i18n.signInText }}</p>
- <sign-in-oauth-button
- v-if="useSignInOauthButton"
- @sign-in="$emit('sign-in-oauth', $event)"
- @error="onSignInError"
- />
- <sign-in-legacy-button v-else class="gl-mb-7" :users-path="usersPath" />
- </div>
-</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com.vue
new file mode 100644
index 00000000000..91b66c87694
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com.vue
@@ -0,0 +1,68 @@
+<script>
+import { s__ } from '~/locale';
+
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import SubscriptionsList from '../../components/subscriptions_list.vue';
+
+export default {
+ name: 'SignInGitlabCom',
+ components: {
+ SubscriptionsList,
+ SignInLegacyButton: () => import('../../components/sign_in_legacy_button.vue'),
+ SignInOauthButton: () => import('../../components/sign_in_oauth_button.vue'),
+ },
+ mixins: [glFeatureFlagMixin()],
+ inject: ['usersPath'],
+ props: {
+ hasSubscriptions: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ useSignInOauthButton() {
+ return this.glFeatures.jiraConnectOauth;
+ },
+ },
+ i18n: {
+ signInButtonTextWithSubscriptions: s__('Integrations|Sign in to add namespaces'),
+ signInText: s__('JiraService|Sign in to GitLab.com to get started.'),
+ },
+ methods: {
+ onSignInError() {
+ this.$emit('error');
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
+ <div v-if="hasSubscriptions">
+ <div class="gl-display-flex gl-justify-content-end gl-mb-3">
+ <sign-in-oauth-button
+ v-if="useSignInOauthButton"
+ @sign-in="$emit('sign-in-oauth', $event)"
+ @error="onSignInError"
+ >
+ {{ $options.i18n.signInButtonTextWithSubscriptions }}
+ </sign-in-oauth-button>
+ <sign-in-legacy-button v-else :users-path="usersPath">
+ {{ $options.i18n.signInButtonTextWithSubscriptions }}
+ </sign-in-legacy-button>
+ </div>
+
+ <subscriptions-list />
+ </div>
+ <div v-else class="gl-text-center">
+ <p class="gl-mb-7">{{ $options.i18n.signInText }}</p>
+ <sign-in-oauth-button
+ v-if="useSignInOauthButton"
+ @sign-in="$emit('sign-in-oauth', $event)"
+ @error="onSignInError"
+ />
+ <sign-in-legacy-button v-else class="gl-mb-7" :users-path="usersPath" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue
new file mode 100644
index 00000000000..4f5aa4c255c
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue
@@ -0,0 +1,72 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import SignInOauthButton from '../../../components/sign_in_oauth_button.vue';
+import VersionSelectForm from './version_select_form.vue';
+
+export default {
+ name: 'SignInGitlabMultiversion',
+ components: {
+ GlButton,
+ SignInOauthButton,
+ VersionSelectForm,
+ },
+ data() {
+ return {
+ gitlabBasePath: null,
+ };
+ },
+ computed: {
+ hasSelectedVersion() {
+ return this.gitlabBasePath !== null;
+ },
+ subtitle() {
+ return this.hasSelectedVersion
+ ? this.$options.i18n.signInSubtitle
+ : this.$options.i18n.versionSelectSubtitle;
+ },
+ },
+ methods: {
+ resetGitlabBasePath() {
+ this.gitlabBasePath = null;
+ },
+ onVersionSelect(gitlabBasePath) {
+ this.gitlabBasePath = gitlabBasePath;
+ },
+ onSignInError() {
+ this.$emit('error');
+ },
+ },
+ i18n: {
+ title: s__('JiraService|Welcome to GitLab for Jira'),
+ signInSubtitle: s__('JiraService|Sign in to GitLab to link namespaces.'),
+ versionSelectSubtitle: s__('JiraService|What version of GitLab are you using?'),
+ changeVersionButtonText: s__('JiraService|Change GitLab version'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="gl-text-center">
+ <h2>{{ $options.i18n.title }}</h2>
+ <p data-testid="subtitle">{{ subtitle }}</p>
+ </div>
+
+ <version-select-form v-if="!hasSelectedVersion" class="gl-mt-7" @submit="onVersionSelect" />
+
+ <div v-else class="gl-text-center">
+ <sign-in-oauth-button
+ class="gl-mb-5"
+ @sign-in="$emit('sign-in-oauth', $event)"
+ @error="onSignInError"
+ />
+
+ <div>
+ <gl-button category="tertiary" variant="confirm" @click="resetGitlabBasePath">
+ {{ $options.i18n.changeVersionButtonText }}
+ </gl-button>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue
new file mode 100644
index 00000000000..0fa745ed7e3
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue
@@ -0,0 +1,88 @@
+<script>
+import {
+ GlForm,
+ GlFormGroup,
+ GlFormRadioGroup,
+ GlFormInput,
+ GlFormRadio,
+ GlButton,
+} from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+
+const RADIO_OPTIONS = {
+ saas: 'saas',
+ selfManaged: 'selfManaged',
+};
+
+const DEFAULT_RADIO_OPTION = RADIO_OPTIONS.saas;
+const GITLAB_COM_BASE_PATH = 'https://gitlab.com';
+
+export default {
+ name: 'VersionSelectForm',
+ components: {
+ GlForm,
+ GlFormGroup,
+ GlFormRadioGroup,
+ GlFormInput,
+ GlFormRadio,
+ GlButton,
+ },
+ data() {
+ return {
+ selected: DEFAULT_RADIO_OPTION,
+ selfManagedBasePathInput: '',
+ };
+ },
+ computed: {
+ isSelfManagedSelected() {
+ return this.selected === RADIO_OPTIONS.selfManaged;
+ },
+ },
+ methods: {
+ onSubmit() {
+ const gitlabBasePath =
+ this.selected === RADIO_OPTIONS.saas ? GITLAB_COM_BASE_PATH : this.selfManagedBasePathInput;
+ this.$emit('submit', gitlabBasePath);
+ },
+ },
+ radioOptions: RADIO_OPTIONS,
+ i18n: {
+ title: s__('JiraService|Welcome to GitLab for Jira'),
+ saasRadioLabel: __('GitLab.com (SaaS)'),
+ saasRadioHelp: __('Most common'),
+ selfManagedRadioLabel: __('GitLab (self-managed)'),
+ instanceURLInputLabel: s__('JiraService|GitLab instance URL'),
+ instanceURLInputDescription: s__('JiraService|For example: https://gitlab.example.com'),
+ },
+};
+</script>
+
+<template>
+ <gl-form class="gl-max-w-62 gl-mx-auto" @submit.prevent="onSubmit">
+ <gl-form-radio-group v-model="selected" class="gl-mb-3" name="gitlab_version">
+ <gl-form-radio :value="$options.radioOptions.saas">
+ {{ $options.i18n.saasRadioLabel }}
+ <template #help>
+ {{ $options.i18n.saasRadioHelp }}
+ </template>
+ </gl-form-radio>
+ <gl-form-radio :value="$options.radioOptions.selfManaged">
+ {{ $options.i18n.selfManagedRadioLabel }}
+ </gl-form-radio>
+ </gl-form-radio-group>
+
+ <gl-form-group
+ v-if="isSelfManagedSelected"
+ class="gl-ml-6"
+ :label="$options.i18n.instanceURLInputLabel"
+ :description="$options.i18n.instanceURLInputDescription"
+ label-for="self-managed-instance-input"
+ >
+ <gl-form-input id="self-managed-instance-input" v-model="selfManagedBasePathInput" required />
+ </gl-form-group>
+
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button variant="confirm" type="submit">{{ __('Save') }}</gl-button>
+ </div>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_page.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_page.vue
new file mode 100644
index 00000000000..f4c59b2184e
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_page.vue
@@ -0,0 +1,35 @@
+<script>
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import SignInGitlabCom from './sign_in_gitlab_com.vue';
+import SignInGitlabMultiversion from './sign_in_gitlab_multiversion/index.vue';
+
+export default {
+ name: 'SignInPage',
+ components: { SignInGitlabCom, SignInGitlabMultiversion },
+ mixins: [glFeatureFlagMixin()],
+ props: {
+ hasSubscriptions: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ isOauthSelfManagedEnabled() {
+ return this.glFeatures.jiraConnectOauth && this.glFeatures.jiraConnectOauthSelfManaged;
+ },
+ },
+};
+</script>
+<template>
+ <sign-in-gitlab-multiversion
+ v-if="isOauthSelfManagedEnabled"
+ @sign-in-oauth="$emit('sign-in-oauth', $event)"
+ @error="$emit('error', $event)"
+ />
+ <sign-in-gitlab-com
+ v-else
+ :has-subscriptions="hasSubscriptions"
+ @sign-in-oauth="$emit('sign-in-oauth')"
+ @error="$emit('error', $event)"
+ />
+</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions.vue
deleted file mode 100644
index 426f2999370..00000000000
--- a/app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<script>
-import { GlEmptyState } from '@gitlab/ui';
-import SubscriptionsList from '../components/subscriptions_list.vue';
-import AddNamespaceButton from '../components/add_namespace_button.vue';
-
-export default {
- name: 'SubscriptionsPage',
- components: {
- GlEmptyState,
- SubscriptionsList,
- AddNamespaceButton,
- },
- props: {
- hasSubscriptions: {
- type: Boolean,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div v-if="hasSubscriptions">
- <div class="gl-display-flex gl-justify-content-end">
- <add-namespace-button />
- </div>
-
- <subscriptions-list />
- </div>
- <gl-empty-state
- v-else
- :title="s__('Integrations|No linked namespaces')"
- :description="
- s__(
- 'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.',
- )
- "
- >
- <template #actions>
- <add-namespace-button />
- </template>
- </gl-empty-state>
-</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions_page.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions_page.vue
new file mode 100644
index 00000000000..b1c1ae73e14
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions_page.vue
@@ -0,0 +1,54 @@
+<script>
+import { mapState } from 'vuex';
+import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
+
+import SubscriptionsList from '../components/subscriptions_list.vue';
+import AddNamespaceButton from '../components/add_namespace_button.vue';
+
+export default {
+ name: 'SubscriptionsPage',
+ components: {
+ GlEmptyState,
+ GlLoadingIcon,
+ SubscriptionsList,
+ AddNamespaceButton,
+ },
+ props: {
+ hasSubscriptions: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['subscriptionsLoading', 'subscriptionsError']),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
+
+ <gl-loading-icon v-if="subscriptionsLoading" size="md" />
+ <div v-else-if="hasSubscriptions && !subscriptionsError">
+ <div class="gl-display-flex gl-justify-content-end gl-mb-3">
+ <add-namespace-button />
+ </div>
+
+ <subscriptions-list />
+ </div>
+ <gl-empty-state
+ v-else
+ :title="s__('Integrations|No linked namespaces')"
+ :description="
+ s__(
+ 'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.',
+ )
+ "
+ >
+ <template #actions>
+ <add-namespace-button />
+ </template>
+ </gl-empty-state>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/store/actions.js b/app/assets/javascripts/jira_connect/subscriptions/store/actions.js
new file mode 100644
index 00000000000..4a83ee8671d
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/store/actions.js
@@ -0,0 +1,73 @@
+import { fetchSubscriptions as fetchSubscriptionsREST } from '~/jira_connect/subscriptions/api';
+import { getCurrentUser } from '~/rest_api';
+import { addJiraConnectSubscription } from '~/api/integrations_api';
+import {
+ I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_TITLE,
+ I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_MESSAGE,
+ INTEGRATIONS_DOC_LINK,
+ I18N_DEFAULT_SUBSCRIPTIONS_ERROR_MESSAGE,
+} from '../constants';
+import { getJwt } from '../utils';
+import {
+ SET_SUBSCRIPTIONS,
+ SET_SUBSCRIPTIONS_LOADING,
+ SET_SUBSCRIPTIONS_ERROR,
+ ADD_SUBSCRIPTION_LOADING,
+ ADD_SUBSCRIPTION_ERROR,
+ SET_ALERT,
+ SET_CURRENT_USER,
+ SET_CURRENT_USER_ERROR,
+} from './mutation_types';
+
+export const fetchSubscriptions = async ({ commit }, subscriptionsPath) => {
+ commit(SET_SUBSCRIPTIONS_LOADING, true);
+
+ try {
+ const data = await fetchSubscriptionsREST(subscriptionsPath);
+ commit(SET_SUBSCRIPTIONS, data.data.subscriptions);
+ } catch {
+ commit(SET_SUBSCRIPTIONS_ERROR, true);
+ commit(SET_ALERT, { message: I18N_DEFAULT_SUBSCRIPTIONS_ERROR_MESSAGE, variant: 'danger' });
+ } finally {
+ commit(SET_SUBSCRIPTIONS_LOADING, false);
+ }
+};
+
+export const loadCurrentUser = async ({ commit }, accessToken) => {
+ try {
+ const { data: user } = await getCurrentUser({
+ headers: { Authorization: `Bearer ${accessToken}` },
+ });
+
+ commit(SET_CURRENT_USER, user);
+ } catch (e) {
+ commit(SET_CURRENT_USER_ERROR, e);
+ }
+};
+
+export const addSubscription = async (
+ { commit, state, dispatch },
+ { namespacePath, subscriptionsPath },
+) => {
+ try {
+ commit(ADD_SUBSCRIPTION_LOADING, true);
+
+ await addJiraConnectSubscription(namespacePath, {
+ jwt: await getJwt(),
+ accessToken: state.accessToken,
+ });
+
+ commit(SET_ALERT, {
+ title: I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_TITLE,
+ message: I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_MESSAGE,
+ linkUrl: INTEGRATIONS_DOC_LINK,
+ variant: 'success',
+ });
+
+ dispatch('fetchSubscriptions', subscriptionsPath);
+ } catch (e) {
+ commit(ADD_SUBSCRIPTION_ERROR, e);
+ } finally {
+ commit(ADD_SUBSCRIPTION_LOADING, false);
+ }
+};
diff --git a/app/assets/javascripts/jira_connect/subscriptions/store/index.js b/app/assets/javascripts/jira_connect/subscriptions/store/index.js
index de830e3891a..abad1920bcc 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/store/index.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/store/index.js
@@ -1,12 +1,15 @@
import Vue from 'vue';
import Vuex from 'vuex';
+import * as actions from './actions';
import mutations from './mutations';
-import state from './state';
+import createState from './state';
Vue.use(Vuex);
-export default () =>
- new Vuex.Store({
+export default function createStore(initialState) {
+ return new Vuex.Store({
mutations,
- state,
+ actions,
+ state: createState(initialState),
});
+}
diff --git a/app/assets/javascripts/jira_connect/subscriptions/store/mutation_types.js b/app/assets/javascripts/jira_connect/subscriptions/store/mutation_types.js
index 15f36b824d9..d4893fbcaf6 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/store/mutation_types.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/store/mutation_types.js
@@ -1 +1,13 @@
export const SET_ALERT = 'SET_ALERT';
+
+export const SET_SUBSCRIPTIONS = 'SET_SUBSCRIPTIONS';
+export const SET_SUBSCRIPTIONS_LOADING = 'SET_SUBSCRIPTIONS_LOADING';
+export const SET_SUBSCRIPTIONS_ERROR = 'SET_SUBSCRIPTIONS_ERROR';
+
+export const ADD_SUBSCRIPTION_LOADING = 'ADD_SUBSCRIPTION_LOADING';
+export const ADD_SUBSCRIPTION_ERROR = 'ADD_SUBSCRIPTION_ERROR';
+
+export const SET_CURRENT_USER = 'SET_CURRENT_USER';
+export const SET_CURRENT_USER_ERROR = 'SET_CURRENT_USER_ERROR';
+
+export const SET_ACCESS_TOKEN = 'SET_ACCESS_TOKEN';
diff --git a/app/assets/javascripts/jira_connect/subscriptions/store/mutations.js b/app/assets/javascripts/jira_connect/subscriptions/store/mutations.js
index 2a25e0fe25f..60076c918fd 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/store/mutations.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/store/mutations.js
@@ -1,7 +1,45 @@
-import { SET_ALERT } from './mutation_types';
+import {
+ SET_ALERT,
+ SET_SUBSCRIPTIONS,
+ SET_SUBSCRIPTIONS_LOADING,
+ SET_SUBSCRIPTIONS_ERROR,
+ ADD_SUBSCRIPTION_LOADING,
+ ADD_SUBSCRIPTION_ERROR,
+ SET_CURRENT_USER,
+ SET_CURRENT_USER_ERROR,
+ SET_ACCESS_TOKEN,
+} from './mutation_types';
export default {
[SET_ALERT](state, { title, message, variant, linkUrl } = {}) {
state.alert = { title, message, variant, linkUrl };
},
+
+ [SET_SUBSCRIPTIONS](state, subscriptions = []) {
+ state.subscriptions = subscriptions;
+ },
+ [SET_SUBSCRIPTIONS_LOADING](state, subscriptionsLoading) {
+ state.subscriptionsLoading = subscriptionsLoading;
+ },
+ [SET_SUBSCRIPTIONS_ERROR](state, subscriptionsError) {
+ state.subscriptionsError = subscriptionsError;
+ },
+
+ [ADD_SUBSCRIPTION_LOADING](state, loading) {
+ state.addSubscriptionLoading = loading;
+ },
+ [ADD_SUBSCRIPTION_ERROR](state, error) {
+ state.addSubscriptionError = error;
+ },
+
+ [SET_CURRENT_USER](state, currentUser) {
+ state.currentUser = currentUser;
+ },
+ [SET_CURRENT_USER_ERROR](state, currentUserError) {
+ state.currentUserError = currentUserError;
+ },
+
+ [SET_ACCESS_TOKEN](state, accessToken) {
+ state.accessToken = accessToken;
+ },
};
diff --git a/app/assets/javascripts/jira_connect/subscriptions/store/state.js b/app/assets/javascripts/jira_connect/subscriptions/store/state.js
index c807df03f00..03a83f18b4c 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/store/state.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/store/state.js
@@ -1,3 +1,17 @@
-export default () => ({
- alert: undefined,
-});
+export default function createState({ subscriptions = [], subscriptionsLoading = false } = {}) {
+ return {
+ alert: undefined,
+
+ subscriptions,
+ subscriptionsLoading,
+ subscriptionsError: false,
+
+ addSubscriptionLoading: false,
+ addSubscriptionError: false,
+
+ currentUser: null,
+ currentUserError: null,
+
+ accessToken: null,
+ };
+}
diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue
index af4a26a7352..8a36a4d2466 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_form.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue
@@ -304,7 +304,10 @@ export default {
:text="data.value || $options.currentUsername"
class="w-100"
:aria-label="
- sprintf($options.dropdownLabel, { jiraDisplayName: data.item.jiraDisplayName })
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf($options.dropdownLabel, {
+ jiraDisplayName: data.item.jiraDisplayName,
+ }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
"
@hide="resetDropdown"
>
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index 85fe5ed7e26..396b015ad83 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -94,7 +94,7 @@ export default {
'emptyStateIllustration',
'isScrollingDown',
'emptyStateAction',
- 'hasRunnersForProject',
+ 'hasOfflineRunnersForProject',
]),
shouldRenderContent() {
@@ -220,7 +220,7 @@ export default {
<!-- Body Section -->
<stuck-block
v-if="job.stuck"
- :has-no-runners-for-project="hasRunnersForProject"
+ :has-offline-runners-for-project="hasOfflineRunnersForProject"
:tags="job.tags"
:runners-path="runnerSettingsUrl"
/>
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index 1b4c9ebdf7d..cc099dba72f 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -125,6 +125,7 @@ export default {
:title="$options.i18n.cancelJobButtonLabel"
:aria-label="$options.i18n.cancelJobButtonLabel"
:href="job.cancel_path"
+ variant="danger"
icon="cancel"
data-method="post"
data-testid="cancel-button"
diff --git a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
index 5428f657252..2ba531c9e95 100644
--- a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
@@ -56,7 +56,7 @@ export default {
});
},
runnerId() {
- const { id, short_sha: token, description } = this.job?.runner;
+ const { id, short_sha: token, description } = this.job.runner;
return `#${id} (${token}) ${description}`;
},
@@ -71,11 +71,20 @@ export default {
return '';
}
- return sprintf(__(` (from %{timeoutSource})`), {
+ return sprintf(__(' (from %{timeoutSource})'), {
timeoutSource: this.job.metadata.timeout_source,
});
},
},
+ i18n: {
+ COVERAGE: __('Coverage'),
+ FINISHED: __('Finished'),
+ ERASED: __('Erased'),
+ QUEUED: __('Queued'),
+ RUNNER: __('Runner'),
+ TAGS: __('Tags:'),
+ TIMEOUT: __('Timeout'),
+ },
};
</script>
@@ -86,22 +95,22 @@ export default {
v-if="job.finished_at"
:value="finishedAt"
data-testid="job-finished"
- title="Finished"
+ :title="$options.i18n.FINISHED"
/>
- <detail-row v-if="job.erased_at" :value="erasedAt" title="Erased" />
- <detail-row v-if="job.queued" :value="queued" title="Queued" />
+ <detail-row v-if="job.erased_at" :value="erasedAt" :title="$options.i18n.ERASED" />
+ <detail-row v-if="job.queued" :value="queued" :title="$options.i18n.QUEUED" />
<detail-row
v-if="hasTimeout"
:help-url="runnerHelpUrl"
:value="timeout"
data-testid="job-timeout"
- title="Timeout"
+ :title="$options.i18n.TIMEOUT"
/>
- <detail-row v-if="job.runner" :value="runnerId" title="Runner" />
- <detail-row v-if="job.coverage" :value="coverage" title="Coverage" />
+ <detail-row v-if="job.runner" :value="runnerId" :title="$options.i18n.RUNNER" />
+ <detail-row v-if="job.coverage" :value="coverage" :title="$options.i18n.COVERAGE" />
<p v-if="hasTags" class="build-detail-row" data-testid="job-tags">
- <span class="font-weight-bold">{{ __('Tags:') }}</span>
+ <span class="font-weight-bold">{{ $options.i18n.TAGS }}</span>
<gl-badge v-for="(tag, i) in job.tags" :key="i" variant="info">{{ tag }}</gl-badge>
</p>
</div>
diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue
index abd0c13702a..f9cde61e917 100644
--- a/app/assets/javascripts/jobs/components/stuck_block.vue
+++ b/app/assets/javascripts/jobs/components/stuck_block.vue
@@ -11,7 +11,7 @@ export default {
GlLink,
},
props: {
- hasNoRunnersForProject: {
+ hasOfflineRunnersForProject: {
type: Boolean,
required: true,
},
@@ -37,7 +37,7 @@ export default {
dataTestId: 'job-stuck-with-tags',
showTags: true,
};
- } else if (this.hasNoRunnersForProject) {
+ } else if (this.hasOfflineRunnersForProject) {
return {
text: s__(`Job|This job is stuck because the project
doesn't have any runners online assigned to it.`),
diff --git a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
index f16e0287d5d..02aeb46a22b 100644
--- a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
+++ b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton, GlButtonGroup, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+import { redirectTo } from '~/lib/utils/url_utility';
import {
ACTIONS_DOWNLOAD_ARTIFACTS,
ACTIONS_START_NOW,
@@ -108,11 +109,11 @@ export default {
},
},
methods: {
- async postJobAction(name, mutation) {
+ async postJobAction(name, mutation, redirect = false) {
try {
const {
data: {
- [name]: { errors },
+ [name]: { errors, job },
},
} = await this.$apollo.mutate({
mutation,
@@ -121,6 +122,10 @@ export default {
if (errors.length > 0) {
reportMessageToSentry(this.$options.name, errors.join(', '), {});
this.showToastMessage();
+ } else if (redirect) {
+ // Retry and Play actions redirect to job detail view
+ // we don't need to refetch with jobActionPerformed event
+ redirectTo(job.detailedStatus.detailsPath);
} else {
eventHub.$emit('jobActionPerformed');
}
@@ -147,12 +152,12 @@ export default {
retryJob() {
this.retryBtnDisabled = true;
- this.postJobAction(this.$options.jobRetry, retryJobMutation);
+ this.postJobAction(this.$options.jobRetry, retryJobMutation, true);
},
playJob() {
this.playManualBtnDisabled = true;
- this.postJobAction(this.$options.jobPlay, playJobMutation);
+ this.postJobAction(this.$options.jobPlay, playJobMutation, true);
},
unscheduleJob() {
this.unscheduleBtnDisabled = true;
diff --git a/app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql b/app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql
index 06b065a86ce..3038216fdfc 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql
+++ b/app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql
@@ -1,3 +1,7 @@
fragment Job on CiJob {
id
+ detailedStatus {
+ id
+ detailsPath
+ }
}
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue b/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue
index 0a25dc5bea5..27e3b8028b7 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue
@@ -54,7 +54,9 @@ export default {
<gl-tab
v-for="tab in tabs"
:key="tab.text"
- :title-link-attributes="{ 'data-testid': tab.testId }"
+ :title-link-attributes="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ 'data-testid': tab.testId,
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@click="$emit('fetchJobsByStatus', tab.scope)"
>
<template #title>
diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js
index 9d255822250..a0f9db7409d 100644
--- a/app/assets/javascripts/jobs/store/getters.js
+++ b/app/assets/javascripts/jobs/store/getters.js
@@ -1,7 +1,7 @@
-import { isEmpty, isString } from 'lodash';
+import { isEmpty } from 'lodash';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
-export const headerTime = (state) => (state.job.started ? state.job.started : state.job.created_at);
+export const headerTime = (state) => state.job.started_at || state.job.created_at;
export const hasForwardDeploymentFailure = (state) =>
state?.job?.failure_reason === 'forward_deployment_failure';
@@ -13,10 +13,10 @@ export const shouldRenderCalloutMessage = (state) =>
!isEmpty(state.job.status) && !isEmpty(state.job.callout_message);
/**
- * When job has not started the key will be null
- * When job started the key will be a string with a date.
+ * When the job has not started the value of job.started_at will be null
+ * When job has started the value of job.started_at will be a string with a date.
*/
-export const shouldRenderTriggeredLabel = (state) => isString(state.job.started);
+export const shouldRenderTriggeredLabel = (state) => Boolean(state.job.started_at);
export const hasEnvironment = (state) => !isEmpty(state.job.deployment_status);
@@ -46,5 +46,5 @@ export const shouldRenderSharedRunnerLimitWarning = (state) =>
export const isScrollingDown = (state) => isScrolledToBottom() && !state.isJobLogComplete;
-export const hasRunnersForProject = (state) =>
+export const hasOfflineRunnersForProject = (state) =>
state?.job?.runners?.available && !state?.job?.runners?.online;
diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js
index 47568f0ecff..4959550e273 100644
--- a/app/assets/javascripts/lib/dompurify.js
+++ b/app/assets/javascripts/lib/dompurify.js
@@ -7,6 +7,7 @@ const defaultConfig = {
// Prevent possible XSS attacks with data-* attributes used by @rails/ujs
// See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1421
FORBID_ATTR: ['data-remote', 'data-url', 'data-type', 'data-method'],
+ FORBID_TAGS: ['style', 'mstyle'],
};
// Only icons urls from `gon` are allowed
diff --git a/app/assets/javascripts/lib/gfm/index.js b/app/assets/javascripts/lib/gfm/index.js
index 07388f1fdfa..4e704eb69b2 100644
--- a/app/assets/javascripts/lib/gfm/index.js
+++ b/app/assets/javascripts/lib/gfm/index.js
@@ -32,7 +32,7 @@ const compilerFactory = (renderer) =>
* the MDast tree
*/
export const render = async ({ markdown, renderer }) => {
- const { value } = await createParser().use(compilerFactory(renderer)).process(markdown);
+ const { result } = await createParser().use(compilerFactory(renderer)).process(markdown);
- return value;
+ return result;
};
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 96d019f62f2..1ed0cc3130b 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -4,15 +4,15 @@
import { GlBreakpointInstance as breakpointInstance } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
-import Cookies from 'js-cookie';
import { isFunction, defer } from 'lodash';
+import Cookies from '~/lib/utils/cookies';
import { SCOPED_LABEL_DELIMITER } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import { convertToCamelCase, convertToSnakeCase } from './text_utility';
import { isObject } from './type_utility';
import { getLocationHash } from './url_utility';
export const getPagePath = (index = 0) => {
- const { page = '' } = document?.body?.dataset;
+ const { page = '' } = document.body.dataset;
return page.split(':')[index];
};
@@ -105,7 +105,7 @@ export const handleLocationHash = () => {
}
if (isInIssuePage()) {
- adjustment -= fixedIssuableTitle?.offsetHeight;
+ adjustment -= fixedIssuableTitle.offsetHeight;
}
if (isInMRPage()) {
@@ -157,7 +157,7 @@ export const contentTop = () => {
() => getOuterHeight('#js-peek'),
() => getOuterHeight('.navbar-gitlab'),
({ desktop }) => {
- const container = document.querySelector('.line-resolve-all-container');
+ const container = document.querySelector('.discussions-counter');
let size = 0;
if (!desktop && container) {
@@ -282,23 +282,51 @@ export const getSelectedFragment = (restrictToNode) => {
return documentFragment;
};
+function execInsertText(text) {
+ if (text === '') return document.execCommand('delete');
+
+ return document.execCommand('insertText', false, text);
+}
+
+/**
+ * This method inserts text into a textarea/input field.
+ * Uses `execCommand` if supported
+ *
+ * @param {HTMLElement} target - textarea/input to have text inserted into
+ * @param {String | function} text - text to be inserted
+ */
export const insertText = (target, text) => {
- // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
const { selectionStart, selectionEnd, value } = target;
-
const textBefore = value.substring(0, selectionStart);
const textAfter = value.substring(selectionEnd, value.length);
-
const insertedText = text instanceof Function ? text(textBefore, textAfter) : text;
- const newText = textBefore + insertedText + textAfter;
- // eslint-disable-next-line no-param-reassign
- target.value = newText;
- // eslint-disable-next-line no-param-reassign
- target.selectionStart = selectionStart + insertedText.length;
-
- // eslint-disable-next-line no-param-reassign
- target.selectionEnd = selectionStart + insertedText.length;
+ // The `execCommand` is officially deprecated. However, for `insertText`,
+ // there is currently no alternative. We need to use it in order to trigger
+ // the browser's undo tracking when we insert text.
+ // Per https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand on 2022-04-11,
+ // The Clipboard API can be used instead of execCommand in many cases,
+ // but execCommand is still sometimes useful. In particular, the Clipboard
+ // API doesn't replace the insertText command
+ // So we attempt to use it if possible. Otherwise, fall back to just replacing
+ // the value as before. In this case, Undo will be broken with inserted text.
+ // Testing on older versions of Firefox:
+ // 87 and below: does not work and falls through to just replacing value.
+ // 87 was released in Mar of 2021
+ // 89 and above: works well
+ // 89 was released in May of 2021
+ if (!execInsertText(insertedText)) {
+ const newText = textBefore + insertedText + textAfter;
+
+ // eslint-disable-next-line no-param-reassign
+ target.value = newText;
+
+ // eslint-disable-next-line no-param-reassign
+ target.selectionStart = selectionStart + insertedText.length;
+
+ // eslint-disable-next-line no-param-reassign
+ target.selectionEnd = selectionStart + insertedText.length;
+ }
// Trigger autosave
target.dispatchEvent(new Event('input'));
diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
index 1d8eb73d3d7..3788d8ab20c 100644
--- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
+++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
@@ -3,7 +3,6 @@ import { GlModal, GlSafeHtmlDirective } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
- cancelAction: { text: __('Cancel') },
directives: {
SafeHtml: GlSafeHtmlDirective,
},
@@ -36,6 +35,16 @@ export default {
required: false,
default: 'confirm',
},
+ cancelText: {
+ type: String,
+ required: false,
+ default: __('Cancel'),
+ },
+ cancelVariant: {
+ type: String,
+ required: false,
+ default: 'default',
+ },
modalHtmlMessage: {
type: String,
required: false,
@@ -71,7 +80,14 @@ export default {
};
},
cancelAction() {
- return this.hideCancel ? null : this.$options.cancelAction;
+ return this.hideCancel
+ ? null
+ : {
+ text: this.cancelText,
+ attributes: {
+ variant: this.cancelVariant,
+ },
+ };
},
shouldShowHeader() {
return Boolean(this.title?.length);
diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
index 1adb6f9c26f..173116062c9 100644
--- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
+++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
@@ -7,6 +7,8 @@ export function confirmAction(
primaryBtnText,
secondaryBtnVariant,
secondaryBtnText,
+ cancelBtnVariant,
+ cancelBtnText,
modalHtmlMessage,
title,
hideCancel,
@@ -28,6 +30,8 @@ export function confirmAction(
secondaryVariant: secondaryBtnVariant,
primaryVariant: primaryBtnVariant,
primaryText: primaryBtnText,
+ cancelVariant: cancelBtnVariant,
+ cancelText: cancelBtnText,
title,
modalHtmlMessage,
hideCancel,
diff --git a/app/assets/javascripts/lib/utils/cookies.js b/app/assets/javascripts/lib/utils/cookies.js
new file mode 100644
index 00000000000..be0491376c9
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/cookies.js
@@ -0,0 +1,8 @@
+import CookiesBuilder from 'js-cookie';
+
+// set default path for cookies
+const Cookies = CookiesBuilder.withAttributes({
+ path: gon.relative_url_root || '/',
+});
+
+export default Cookies;
diff --git a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
index 095a29a2eff..05f34db662a 100644
--- a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
@@ -7,7 +7,7 @@ import { formatDate } from './date_format_utility';
*
* see https://github.com/hustcc/timeago.js/tree/v3.0.0/locales
*/
-const timeagoLanguageCode = languageCode().replace(/-/g, '_');
+export const timeagoLanguageCode = languageCode().replace(/-/g, '_');
/**
* Registers timeago locales
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index b52a736f153..4262329aae7 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -90,6 +90,20 @@ export const getParents = (element) => {
return parents;
};
+export const getParentByTagName = (element, tagName) => {
+ let parent = element.parentNode;
+
+ do {
+ if (parent.nodeName?.toLowerCase() === tagName?.toLowerCase()) {
+ return parent;
+ }
+
+ parent = parent.parentElement;
+ } while (parent);
+
+ return undefined;
+};
+
/**
* This method takes a HTML element and an object of attributes
* to save repeated calls to `setAttribute` when multiple
diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js
index 6b9be34235b..c5190592bb6 100644
--- a/app/assets/javascripts/lib/utils/http_status.js
+++ b/app/assets/javascripts/lib/utils/http_status.js
@@ -22,6 +22,7 @@ const httpStatusCodes = {
METHOD_NOT_ALLOWED: 405,
CONFLICT: 409,
GONE: 410,
+ PAYLOAD_TOO_LARGE: 413,
UNPROCESSABLE_ENTITY: 422,
TOO_MANY_REQUESTS: 429,
INTERNAL_SERVER_ERROR: 500,
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 52fa90c7791..243de48948c 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -14,6 +14,8 @@ const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isUl>[*+-])|(?<isOl
// detect a horizontal rule that might be mistaken for a list item (not full pattern for an <hr>)
const HR_PATTERN = /^((\s{0,3}-+\s*-+\s*-+\s*[\s-]*)|(\s{0,3}\*+\s*\*+\s*\*+\s*[\s*]*))$/;
+let compositioningNoteText = false;
+
function selectedText(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
}
@@ -363,10 +365,11 @@ function continueOlText(result, nextLineResult) {
}
function handleContinueList(e, textArea) {
- if (!gon.features?.markdownContinueLists) return;
if (!(e.key === 'Enter')) return;
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
if (textArea.selectionStart !== textArea.selectionEnd) return;
+ // prevent unintended line breaks were inserted using Japanese IME on MacOS
+ if (compositioningNoteText) return;
const currentLine = lineBefore(textArea.value, textArea, false);
const result = currentLine.match(LIST_LINE_HEAD_PATTERN);
@@ -420,6 +423,14 @@ export function keypressNoteText(e) {
handleSurroundSelectedText(e, textArea);
}
+export function compositionStartNoteText() {
+ compositioningNoteText = true;
+}
+
+export function compositionEndNoteText() {
+ compositioningNoteText = false;
+}
+
export function updateTextForToolbarBtn($toolbarBtn) {
return updateText({
textArea: $toolbarBtn.closest('.md-area').find('textarea'),
@@ -435,6 +446,8 @@ export function updateTextForToolbarBtn($toolbarBtn) {
export function addMarkdownListeners(form) {
$('.markdown-area', form)
.on('keydown', keypressNoteText)
+ .on('compositionstart', compositionStartNoteText)
+ .on('compositionend', compositionEndNoteText)
.each(function attachTextareaShortcutHandlers() {
Shortcuts.initMarkdownEditorShortcuts($(this), updateTextForToolbarBtn);
});
@@ -474,6 +487,8 @@ export function addEditorMarkdownListeners(editor) {
export function removeMarkdownListeners(form) {
$('.markdown-area', form)
.off('keydown', keypressNoteText)
+ .off('compositionstart', compositionStartNoteText)
+ .off('compositionend', compositionEndNoteText)
.each(function removeTextareaShortcutHandlers() {
Shortcuts.removeMarkdownEditorShortcuts($(this));
});
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 419afa0a0a9..dad9cbcb6f6 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -6,6 +6,10 @@ import {
} from '~/lib/utils/constants';
import { allSingleQuotes } from '~/lib/utils/regexp';
+export const COLON = ':';
+export const HYPHEN = '-';
+export const NEWLINE = '\n';
+
/**
* Adds a , to a string composed by numbers, at every 3 chars.
*
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 335cd6a16e5..ff60fd2aecb 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -420,6 +420,19 @@ export function isSafeURL(url) {
}
/**
+ * Returns the sanitized url when not safe
+ *
+ * @param {String} url
+ * @returns {String}
+ */
+export function sanitizeUrl(url) {
+ if (!isSafeURL(url)) {
+ return 'about:blank';
+ }
+ return url;
+}
+
+/**
* Returns a normalized url
*
* https://gitlab.com/foo/../baz => https://gitlab.com/baz
diff --git a/app/assets/javascripts/lib/utils/users_cache.js b/app/assets/javascripts/lib/utils/users_cache.js
index 54f69ef8e1b..bd000bb26fe 100644
--- a/app/assets/javascripts/lib/utils/users_cache.js
+++ b/app/assets/javascripts/lib/utils/users_cache.js
@@ -35,6 +35,17 @@ class UsersCache extends Cache {
// missing catch is intentional, error handling depends on use case
}
+ updateById(userId, data) {
+ if (!this.hasData(userId)) {
+ return;
+ }
+
+ this.internalStorage[userId] = {
+ ...this.internalStorage[userId],
+ ...data,
+ };
+ }
+
retrieveStatusById(userId) {
if (this.hasData(userId) && this.get(userId).status) {
return Promise.resolve(this.get(userId).status);
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 8fc54be9c28..2f3cdc525a7 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -1,7 +1,6 @@
/* global $ */
import jQuery from 'jquery';
-import Cookies from 'js-cookie';
// bootstrap webpack, common libs, polyfills, and behaviors
import './webpack';
@@ -178,9 +177,6 @@ initUserTracking();
initLayoutNav();
initAlertHandler();
-// Set the default path for all cookies to GitLab's root directory
-Cookies.defaults.path = gon.relative_url_root || '/';
-
// `hashchange` is not triggered when link target is already in window.location
$body.on('click', 'a[href^="#"]', function clickHashLinkCallback() {
const href = this.getAttribute('href');
@@ -199,7 +195,11 @@ $body.on('click', 'a[href^="#"]', function clickHashLinkCallback() {
* Quick fix: Get rid of jQuery for this implementation
*/
const isBoardsPage = /(projects|groups):boards:show/.test(document.body.dataset.page);
-if (!isBoardsPage && (bootstrapBreakpoint === 'sm' || bootstrapBreakpoint === 'xs')) {
+if (
+ !isBoardsPage &&
+ !window.gon?.features?.movedMrSidebar &&
+ (bootstrapBreakpoint === 'sm' || bootstrapBreakpoint === 'xs')
+) {
const $rightSidebar = $('aside.right-sidebar');
const $layoutPage = $('.layout-page');
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 0b97ce7e33e..14d628e455c 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -232,12 +232,10 @@ export default {
v-bind="tableAttrs.table"
class="members-table"
data-testid="members-table"
- head-variant="white"
stacked="lg"
:fields="filteredAndModifiedFields"
:items="members"
primary-key="id"
- thead-class="border-bottom"
:empty-text="__('No members found')"
show-empty
:tbody-tr-attr="tbodyTrAttr"
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 829e2264152..960b25bb552 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -5,6 +5,7 @@ import createFlash from '~/flash';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
import eventHub from '~/vue_merge_request_widget/event_hub';
+import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
import axios from './lib/utils/axios_utils';
import { addDelimiter } from './lib/utils/text_utility';
import { getParameterValues, setUrlParams } from './lib/utils/url_utility';
@@ -31,8 +32,16 @@ function MergeRequest(opts) {
selector: '.detail-page-description',
lockVersion: this.$el.data('lockVersion'),
onSuccess: (result) => {
- document.querySelector('#task_status').innerText = result.task_status;
- document.querySelector('#task_status_short').innerText = result.task_status_short;
+ const taskStatus = document.querySelector('#task_status');
+ const taskStatusShort = document.querySelector('#task_status_short');
+
+ if (taskStatus) {
+ taskStatus.innerText = result.task_status;
+ }
+
+ if (taskStatusShort) {
+ document.querySelector('#task_status_short').innerText = result.task_status_short;
+ }
},
onError: () => {
createFlash({
@@ -72,8 +81,7 @@ MergeRequest.prototype.initMRBtnListeners = function () {
const wipEvent = getParameterValues('merge_request[wip_event]', url)[0];
const mobileDropdown = draftToggle.closest('.dropdown.show');
- const loader = document.createElement('span');
- loader.classList.add('gl-spinner', 'gl-mr-3');
+ const loader = loadingIconForLegacyJS({ inline: true, classes: ['gl-mr-3'] });
if (mobileDropdown) {
$(mobileDropdown.firstElementChild).dropdown('toggle');
@@ -90,10 +98,13 @@ MergeRequest.prototype.initMRBtnListeners = function () {
MergeRequest.toggleDraftStatus(data.title, wipEvent === 'unwip');
})
.catch(() => {
- draftToggle.removeAttribute('disabled');
createFlash({
message: __('Something went wrong. Please try again.'),
});
+ })
+ .finally(() => {
+ draftToggle.removeAttribute('disabled');
+ loader.remove();
});
});
});
@@ -145,7 +156,11 @@ MergeRequest.toggleDraftStatus = function (title, isReady) {
} else {
toast(__('Marked as draft. Can only be merged when marked as ready.'));
}
- const titleEl = document.querySelector('.merge-request .detail-page-description .title');
+ const titleEl = document.querySelector(
+ `.merge-request .detail-page-${
+ window.gon?.features?.updatedMrHeader ? 'header' : 'description'
+ } .title`,
+ );
if (titleEl) {
titleEl.textContent = title;
@@ -162,7 +177,9 @@ MergeRequest.toggleDraftStatus = function (title, isReady) {
);
draftToggle.setAttribute('href', url);
- draftToggle.textContent = isReady ? __('Mark as draft') : __('Mark as ready');
+ draftToggle.querySelector('.gl-new-dropdown-item-text-wrapper').textContent = isReady
+ ? __('Mark as draft')
+ : __('Mark as ready');
});
}
};
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 61f7a079d77..e02109d1fd1 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,5 +1,4 @@
/* eslint-disable no-new, class-methods-use-this */
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
import Vue from 'vue';
import { getCookie, isMetaClick, parseBoolean, scrollToElement } from '~/lib/utils/common_utils';
@@ -176,6 +175,8 @@ export default class MergeRequestTabs {
: null;
this.navbar = document.querySelector('.navbar-gitlab');
this.peek = document.getElementById('js-peek');
+ this.sidebar = document.querySelector('.js-right-sidebar');
+ this.pageLayout = document.querySelector('.layout-page');
this.paddingTop = 16;
this.scrollPositions = {};
@@ -282,7 +283,7 @@ export default class MergeRequestTabs {
if (action === 'commits') {
this.loadCommits(href);
- this.expandView();
+ // this.hideSidebar();
this.resetViewContainer();
this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
} else if (action === 'new') {
@@ -301,13 +302,12 @@ export default class MergeRequestTabs {
*/
this.loadDiff(href);
}
- if (bp.getBreakpointSize() !== 'xl') {
- this.shrinkView();
- }
+ // this.hideSidebar();
this.expandViewContainer();
this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
this.commitsTab.classList.remove('active');
} else if (action === 'pipelines') {
+ // this.hideSidebar();
this.resetViewContainer();
this.mountPipelinesView();
} else {
@@ -320,9 +320,7 @@ export default class MergeRequestTabs {
notesTab.classList.add('active');
}
- if (bp.getBreakpointSize() !== 'xs') {
- this.expandView();
- }
+ // this.showSidebar();
this.resetViewContainer();
this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
}
@@ -509,19 +507,6 @@ export default class MergeRequestTabs {
}
}
- shrinkView() {
- const $gutterBtn = $('.js-sidebar-toggle:visible');
- const $expandSvg = $gutterBtn.find('.js-sidebar-expand');
-
- // Wait until listeners are set
- setTimeout(() => {
- // Only when sidebar is expanded
- if ($expandSvg.length && $expandSvg.hasClass('hidden')) {
- $gutterBtn.trigger('click', [true]);
- }
- }, 0);
- }
-
// Expand the issuable sidebar unless the user explicitly collapsed it
expandView() {
if (parseBoolean(getCookie('collapsed_gutter'))) {
@@ -538,4 +523,24 @@ export default class MergeRequestTabs {
}
}, 0);
}
+
+ hideSidebar() {
+ if (!isInVueNoteablePage() || this.cachedPageLayoutClasses) return;
+
+ this.cachedPageLayoutClasses = this.pageLayout.className;
+ this.pageLayout.classList.remove(
+ 'right-sidebar-collapsed',
+ 'right-sidebar-expanded',
+ 'page-with-icon-sidebar',
+ );
+ this.sidebar.style.width = '0px';
+ }
+
+ showSidebar() {
+ if (!isInVueNoteablePage() || !this.cachedPageLayoutClasses) return;
+
+ this.pageLayout.className = this.cachedPageLayoutClasses;
+ this.sidebar.style.width = '';
+ delete this.cachedPageLayoutClasses;
+ }
}
diff --git a/app/assets/javascripts/milestones/components/milestone_combobox.vue b/app/assets/javascripts/milestones/components/milestone_combobox.vue
index d7ffdfd7c5f..59d2a2b29b3 100644
--- a/app/assets/javascripts/milestones/components/milestone_combobox.vue
+++ b/app/assets/javascripts/milestones/components/milestone_combobox.vue
@@ -187,7 +187,7 @@ export default {
ref="searchBox"
v-model.trim="searchQuery"
class="gl-m-3"
- :placeholder="this.$options.translations.searchMilestones"
+ :placeholder="$options.translations.searchMilestones"
@input="onSearchBoxInput"
@keydown.enter.prevent="onSearchBoxEnter"
/>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index c4392dd3748..6a85833db27 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -492,7 +492,9 @@ export default {
v-if="!groupSingleEmptyState(groupData.key)"
:value="groupData.panels"
group="metrics-dashboard"
- :component-data="{ attrs: { class: 'row mx-0 w-100' } }"
+ :component-data="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ attrs: { class: 'row mx-0 w-100' },
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:disabled="!isRearrangingPanels"
@input="updatePanels(groupData.key, $event)"
>
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
index 78e3b15913a..ff8ccded83b 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -19,6 +19,7 @@ import invalidUrl from '~/lib/utils/invalid_url';
import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility';
import { __, n__ } from '~/locale';
import TrackEventDirective from '~/vue_shared/directives/track_event';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { panelTypes } from '../constants';
import { graphDataToCsv } from '../csv_export';
@@ -57,6 +58,7 @@ export default {
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
clipboardText: {
type: String,
@@ -141,6 +143,9 @@ export default {
return metrics.some(({ loading }) => loading);
},
logsPathWithTimeRange() {
+ if (!this.glFeatures.monitorLogging) {
+ return null;
+ }
const timeRange = this.zoomedTimeRange || this.timeRange;
if (this.logsPath && this.logsPath !== invalidUrl && timeRange) {
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index a1377415efe..7424c011052 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -31,6 +31,8 @@ export default function initMrNotes() {
const el = document.getElementById('js-vue-discussion-counter');
if (el) {
+ const { blocksMerge } = el.dataset;
+
// eslint-disable-next-line no-new
new Vue({
el,
@@ -40,7 +42,11 @@ export default function initMrNotes() {
},
store,
render(createElement) {
- return createElement('discussion-counter');
+ return createElement('discussion-counter', {
+ props: {
+ blocksMerge: blocksMerge === 'true',
+ },
+ });
},
});
}
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index a9948fed3b6..4e03bed8737 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -6,7 +6,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import Autosave from '~/autosave';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import createFlash from '~/flash';
-import { statusBoxState } from '~/issuable/components/status_box.vue';
+import { badgeState } from '~/issuable/components/status_box.vue';
import httpStatusCodes from '~/lib/utils/http_status';
import {
capitalizeFirstCharacter,
@@ -90,9 +90,16 @@ export default {
return this.getUserData.id;
},
commentButtonTitle() {
- return this.noteType === constants.COMMENT
- ? this.$options.i18n.comment
- : this.$options.i18n.startThread;
+ const { comment, internalComment, startThread, startInternalThread } = this.$options.i18n;
+ if (this.noteIsConfidential) {
+ return this.noteType === constants.COMMENT ? internalComment : startInternalThread;
+ }
+ return this.noteType === constants.COMMENT ? comment : startThread;
+ },
+ textareaPlaceholder() {
+ return this.noteIsConfidential
+ ? this.$options.i18n.bodyPlaceholderInternal
+ : this.$options.i18n.bodyPlaceholder;
},
discussionsRequireResolution() {
return this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE;
@@ -266,7 +273,7 @@ export default {
const toggleState = this.isOpen ? this.closeIssuable : this.reopenIssuable;
toggleState()
- .then(() => statusBoxState.updateStatus && statusBoxState.updateStatus())
+ .then(() => badgeState.updateStatus && badgeState.updateStatus())
.then(refreshUserMergeRequestCounts)
.catch(() =>
createFlash({
@@ -371,7 +378,7 @@ export default {
data-testid="comment-field"
data-supports-quick-actions="true"
:aria-label="$options.i18n.comment"
- :placeholder="$options.i18n.bodyPlaceholder"
+ :placeholder="textareaPlaceholder"
@keydown.up="editCurrentUserLastNote()"
@keydown.meta.enter="handleEnter()"
@keydown.ctrl.enter="handleEnter()"
@@ -386,7 +393,7 @@ export default {
data-testid="add-to-review-button"
type="submit"
category="primary"
- variant="success"
+ variant="confirm"
@click.prevent="handleSaveDraft()"
>{{ __('Add to review') }}</gl-button
>
@@ -419,6 +426,7 @@ export default {
class="gl-mr-3"
:disabled="disableSubmitButton"
:tracking-label="trackingLabel"
+ :is-internal-note="noteIsConfidential"
:noteable-display-name="noteableDisplayName"
:discussions-require-resolution="discussionsRequireResolution"
@click="handleSave"
diff --git a/app/assets/javascripts/notes/components/comment_type_dropdown.vue b/app/assets/javascripts/notes/components/comment_type_dropdown.vue
index 30ea5d3532e..543be838920 100644
--- a/app/assets/javascripts/notes/components/comment_type_dropdown.vue
+++ b/app/assets/javascripts/notes/components/comment_type_dropdown.vue
@@ -32,6 +32,11 @@ export default {
required: false,
default: false,
},
+ isInternalNote: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
noteableDisplayName: {
type: String,
required: true,
@@ -48,18 +53,43 @@ export default {
isNoteTypeDiscussion() {
return this.noteType === constants.DISCUSSION;
},
+ dropdownCommentButtonTitle() {
+ const { comment, internalComment } = this.$options.i18n.submitButton;
+
+ return this.isInternalNote ? internalComment : comment;
+ },
+ dropdownStartThreadButtonTitle() {
+ const { startThread, startInternalThread } = this.$options.i18n.submitButton;
+
+ return this.isInternalNote ? startInternalThread : startThread;
+ },
commentButtonTitle() {
- return this.noteType === constants.COMMENT
- ? this.$options.i18n.comment
- : this.$options.i18n.startThread;
+ const { comment, internalComment, startThread, startInternalThread } = this.$options.i18n;
+
+ if (this.isInternalNote) {
+ return this.noteType === constants.COMMENT ? internalComment : startInternalThread;
+ }
+ return this.noteType === constants.COMMENT ? comment : startThread;
},
startDiscussionDescription() {
- return this.discussionsRequireResolution
- ? this.$options.i18n.discussionThatNeedsResolution
- : this.$options.i18n.discussion;
+ const {
+ discussionThatNeedsResolution,
+ internalDiscussionThatNeedsResolution,
+ discussion,
+ internalDiscussion,
+ } = this.$options.i18n;
+
+ if (this.isInternalNote) {
+ return this.discussionsRequireResolution
+ ? internalDiscussionThatNeedsResolution
+ : internalDiscussion;
+ }
+ return this.discussionsRequireResolution ? discussionThatNeedsResolution : discussion;
},
commentDescription() {
- return sprintf(this.$options.i18n.submitButton.commentHelp, {
+ const { commentHelp, internalCommentHelp } = this.$options.i18n.submitButton;
+
+ return sprintf(this.isInternalNote ? internalCommentHelp : commentHelp, {
noteableDisplayName: this.noteableDisplayName,
});
},
@@ -101,7 +131,7 @@ export default {
:is-checked="isNoteTypeComment"
@click.stop.prevent="setNoteTypeToComment"
>
- <strong>{{ $options.i18n.submitButton.comment }}</strong>
+ <strong>{{ dropdownCommentButtonTitle }}</strong>
<p class="gl-m-0">{{ commentDescription }}</p>
</gl-dropdown-item>
<gl-dropdown-divider />
@@ -111,7 +141,7 @@ export default {
data-qa-selector="discussion_menu_item"
@click.stop.prevent="setNoteTypeToDiscussion"
>
- <strong>{{ $options.i18n.submitButton.startThread }}</strong>
+ <strong>{{ dropdownStartThreadButtonTitle }}</strong>
<p class="gl-m-0">{{ startDiscussionDescription }}</p>
</gl-dropdown-item>
</gl-dropdown>
diff --git a/app/assets/javascripts/notes/components/diff_discussion_header.vue b/app/assets/javascripts/notes/components/diff_discussion_header.vue
index 45d97f278dc..5210d2ca287 100644
--- a/app/assets/javascripts/notes/components/diff_discussion_header.vue
+++ b/app/assets/javascripts/notes/components/diff_discussion_header.vue
@@ -98,13 +98,14 @@ export default {
<template>
<div class="discussion-header note-wrapper">
- <div v-once class="timeline-icon align-self-start flex-shrink-0">
+ <div v-once class="timeline-icon gl-align-self-start gl-flex-shrink-0 gl-flex-shrink gl-mr-4">
<user-avatar-link
v-if="author"
:link-href="author.path"
:img-src="author.avatar_url"
:img-alt="author.name"
- :img-size="40"
+ :img-size="32"
+ :img-css-classes="'gl-mr-0!' /* NOTE: this is needed only while we migrate user-avatar-image to GlAvatar (https://gitlab.com/groups/gitlab-org/-/epics/7731) */"
/>
</div>
<div class="timeline-content w-100">
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 33819c78c0f..f746f7ed0ed 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective, GlIcon, GlButton, GlButtonGroup } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlButtonGroup } from '@gitlab/ui';
import { mapGetters, mapActions } from 'vuex';
import { __ } from '~/locale';
import discussionNavigation from '../mixins/discussion_navigation';
@@ -9,46 +9,41 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- GlIcon,
GlButton,
GlButtonGroup,
},
mixins: [discussionNavigation],
+ props: {
+ blocksMerge: {
+ type: Boolean,
+ required: true,
+ },
+ },
computed: {
...mapGetters([
- 'getUserData',
'getNoteableData',
'resolvableDiscussionsCount',
'unresolvedDiscussionsCount',
- 'discussions',
+ 'allResolvableDiscussions',
]),
- isLoggedIn() {
- return this.getUserData.id;
- },
allResolved() {
return this.unresolvedDiscussionsCount === 0;
},
- resolveAllDiscussionsIssuePath() {
- return this.getNoteableData.create_issue_to_resolve_discussions_path;
- },
- toggeableDiscussions() {
- return this.discussions.filter((discussion) => !discussion.individual_note);
- },
allExpanded() {
- return this.toggeableDiscussions.every((discussion) => discussion.expanded);
- },
- lineResolveClass() {
- return this.allResolved ? 'line-resolve-btn is-active' : 'line-resolve-text';
+ return this.allResolvableDiscussions.every((discussion) => discussion.expanded);
},
toggleThreadsLabel() {
return this.allExpanded ? __('Collapse all threads') : __('Expand all threads');
},
+ resolveAllDiscussionsIssuePath() {
+ return this.getNoteableData.create_issue_to_resolve_discussions_path;
+ },
},
methods: {
...mapActions(['setExpandDiscussions']),
handleExpandDiscussions() {
this.setExpandDiscussions({
- discussionIds: this.toggeableDiscussions.map((discussion) => discussion.id),
+ discussionIds: this.allResolvableDiscussions.map((discussion) => discussion.id),
expanded: !this.allExpanded,
});
},
@@ -60,21 +55,61 @@ export default {
<div
v-if="resolvableDiscussionsCount > 0"
ref="discussionCounter"
- class="line-resolve-all-container full-width-mobile gl-display-flex d-sm-flex"
+ class="gl-display-flex discussions-counter"
>
- <div class="line-resolve-all">
- <span :class="lineResolveClass">
- <template v-if="allResolved">
- <gl-icon name="check-circle-filled" />
- {{ __('All threads resolved') }}
- </template>
- <template v-else>
- {{ n__('%d unresolved thread', '%d unresolved threads', unresolvedDiscussionsCount) }}
- </template>
- </span>
+ <div
+ class="gl-display-flex gl-align-items-center gl-pl-4 gl-rounded-base gl-mr-3"
+ :class="{
+ 'gl-bg-orange-50': blocksMerge && !allResolved,
+ 'gl-bg-gray-50': !blocksMerge || allResolved,
+ 'gl-pr-4': allResolved,
+ 'gl-pr-2': !allResolved,
+ }"
+ data-testid="discussions-counter-text"
+ >
+ <template v-if="allResolved">
+ {{ __('All threads resolved!') }}
+ </template>
+ <template v-else>
+ {{ n__('%d unresolved thread', '%d unresolved threads', unresolvedDiscussionsCount) }}
+ <gl-button-group class="gl-ml-3">
+ <gl-button
+ v-gl-tooltip.hover
+ :title="__('Jump to previous unresolved thread')"
+ :aria-label="__('Jump to previous unresolved thread')"
+ class="discussion-previous-btn gl-rounded-base! gl-px-2!"
+ data-track-action="click_button"
+ data-track-label="mr_previous_unresolved_thread"
+ data-track-property="click_previous_unresolved_thread_top"
+ icon="angle-up"
+ category="tertiary"
+ @click="jumpToPreviousDiscussion"
+ />
+ <gl-button
+ v-gl-tooltip.hover
+ :title="__('Jump to next unresolved thread')"
+ :aria-label="__('Jump to next unresolved thread')"
+ class="discussion-next-btn gl-rounded-base! gl-px-2!"
+ data-track-action="click_button"
+ data-track-label="mr_next_unresolved_thread"
+ data-track-property="click_next_unresolved_thread_top"
+ icon="angle-down"
+ category="tertiary"
+ @click="jumpToNextDiscussion"
+ />
+ </gl-button-group>
+ </template>
</div>
<gl-button-group>
<gl-button
+ v-gl-tooltip
+ :title="toggleThreadsLabel"
+ :aria-label="toggleThreadsLabel"
+ class="toggle-all-discussions-btn"
+ :icon="allExpanded ? 'collapse' : 'expand'"
+ @click="handleExpandDiscussions"
+ />
+ <gl-button
v-if="resolveAllDiscussionsIssuePath && !allResolved"
v-gl-tooltip
:href="resolveAllDiscussionsIssuePath"
@@ -83,26 +118,6 @@ export default {
class="new-issue-for-discussion discussion-create-issue-btn"
icon="issue-new"
/>
- <gl-button
- v-if="isLoggedIn && !allResolved"
- v-gl-tooltip
- :title="__('Jump to next unresolved thread')"
- :aria-label="__('Jump to next unresolved thread')"
- class="discussion-next-btn"
- data-track-action="click_button"
- data-track-label="mr_next_unresolved_thread"
- data-track-property="click_next_unresolved_thread_top"
- icon="comment-next"
- @click="jumpToNextDiscussion"
- />
- <gl-button
- v-gl-tooltip
- :title="toggleThreadsLabel"
- :aria-label="toggleThreadsLabel"
- class="toggle-all-discussions-btn"
- :icon="allExpanded ? 'angle-up' : 'angle-down'"
- @click="handleExpandDiscussions"
- />
</gl-button-group>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index fe17a061c0a..6c9bc4461c2 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -4,6 +4,7 @@ import { GlSafeHtmlDirective } from '@gitlab/ui';
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
+import { __ } from '~/locale';
import '~/behaviors/markdown/render_gfm';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import autosave from '../mixins/autosave';
@@ -69,6 +70,9 @@ export default {
noteBody() {
return this.note.note;
},
+ saveButtonTitle() {
+ return this.note.confidential ? __('Save internal note') : __('Save comment');
+ },
hasSuggestion() {
return this.note.suggestions && this.note.suggestions.length;
},
@@ -180,6 +184,7 @@ export default {
:note-id="note.id"
:line="line"
:note="note"
+ :save-button-title="saveButtonTitle"
:help-page-path="helpPagePath"
:discussion="discussion"
:resolve-discussion="note.resolve_discussion"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index c1e763d81ee..5dd032abd72 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -8,9 +8,11 @@ import markdownField from '~/vue_shared/components/markdown/field.vue';
import eventHub from '../event_hub';
import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable';
+import { COMMENT_FORM } from '../i18n';
import CommentFieldLayout from './comment_field_layout.vue';
export default {
+ i18n: COMMENT_FORM,
name: 'NoteForm',
components: {
markdownField,
@@ -133,6 +135,11 @@ export default {
.some((n) => n.current_user?.can_resolve_discussion) || this.isDraft
);
},
+ textareaPlaceholder() {
+ return this.discussionNote?.confidential
+ ? this.$options.i18n.bodyPlaceholderInternal
+ : this.$options.i18n.bodyPlaceholder;
+ },
noteHash() {
if (this.noteId) {
return `#note_${this.noteId}`;
@@ -350,7 +357,7 @@ export default {
data-qa-selector="reply_field"
dir="auto"
:aria-label="__('Reply to comment')"
- :placeholder="__('Write a comment or drag your files here…')"
+ :placeholder="textareaPlaceholder"
@keydown.meta.enter="handleKeySubmit()"
@keydown.ctrl.enter="handleKeySubmit()"
@keydown.exact.up="editMyLastNote()"
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 11b427b9346..1ad9d593ccc 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -1,6 +1,7 @@
<script>
import {
GlIcon,
+ GlBadge,
GlLoadingIcon,
GlTooltipDirective,
GlSafeHtmlDirective as SafeHtml,
@@ -10,8 +11,6 @@ import { __, s__ } from '~/locale';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
-import { NOTEABLE_TYPE_MAPPING } from '../constants';
-
export default {
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
components: {
@@ -19,6 +18,7 @@ export default {
GitlabTeamMemberBadge: () =>
import('ee_component/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue'),
GlIcon,
+ GlBadge,
GlLoadingIcon,
UserNameWithStatus,
},
@@ -111,13 +111,7 @@ export default {
return this.author.name;
},
noteConfidentialityTooltip() {
- if (
- this.noteableType === NOTEABLE_TYPE_MAPPING.Issue ||
- this.noteableType === NOTEABLE_TYPE_MAPPING.MergeRequest
- ) {
- return s__('Notes|This comment is confidential and only visible to project members');
- }
- return s__('Notes|This comment is confidential and only visible to group members');
+ return s__('Notes|This internal note will always remain confidential');
},
},
mounted() {
@@ -236,15 +230,16 @@ export default {
</a>
<time-ago-tooltip v-else ref="noteTimestamp" :time="createdAt" tooltip-placement="bottom" />
</template>
- <gl-icon
+ <gl-badge
v-if="isConfidential"
v-gl-tooltip:tooltipcontainer.bottom
- data-testid="confidentialIndicator"
- name="eye-slash"
- :size="16"
+ data-testid="internalNoteIndicator"
+ variant="warning"
+ size="sm"
:title="noteConfidentialityTooltip"
- class="gl-ml-1 gl-text-orange-700 align-middle"
- />
+ >
+ {{ __('Internal note') }}
+ </gl-badge>
<slot name="extra-controls"></slot>
<gl-loading-icon
v-if="showSpinner"
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 000eb3bdff3..0f5a517a4c5 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -7,7 +7,7 @@ import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
-import { s__, __ } from '~/locale';
+import { s__, __, sprintf } from '~/locale';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -95,6 +95,9 @@ export default {
isLoggedIn() {
return isLoggedIn();
},
+ commentType() {
+ return this.discussion.confidential ? __('internal note') : __('comment');
+ },
autosaveKey() {
return getDiscussionReplyKey(this.firstNote.noteable_type, this.discussion.id);
},
@@ -104,6 +107,9 @@ export default {
firstNote() {
return this.discussion.notes.slice(0, 1)[0];
},
+ saveButtonTitle() {
+ return this.discussion.confidential ? __('Reply internally') : __('Comment');
+ },
shouldShowJumpToNextDiscussion() {
return this.showJumpToNextDiscussion(this.discussionsByDiffOrder ? 'diff' : 'discussion');
},
@@ -174,9 +180,15 @@ export default {
},
cancelReplyForm: ignoreWhilePending(async function cancelReplyForm(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
- const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
+ const msg = sprintf(
+ s__('Notes|Are you sure you want to cancel creating this %{commentType}?'),
+ { commentType: this.commentType },
+ );
- const confirmed = await confirmAction(msg);
+ const confirmed = await confirmAction(msg, {
+ primaryBtnText: __('Discard changes'),
+ cancelBtnText: __('Continue editing'),
+ });
if (!confirmed) {
return;
@@ -308,7 +320,7 @@ export default {
ref="noteForm"
:discussion="discussion"
:line="diffLine"
- save-button-title="Comment"
+ :save-button-title="saveButtonTitle"
:autosave-key="autosaveKey"
@handleFormUpdateAddToReview="addReplyToReview"
@handleFormUpdate="saveReply"
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index a2fbb242222..cda22b58c5b 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -108,6 +108,9 @@ export default {
author() {
return this.note.author;
},
+ commentType() {
+ return this.note.confidential ? __('internal note') : __('comment');
+ },
classNameBindings() {
return {
[`note-row-${this.note.id}`]: true,
@@ -246,14 +249,19 @@ export default {
this.$emit('handleEdit');
},
async deleteHandler() {
- const typeOfComment = this.note.isDraft ? __('pending comment') : __('comment');
+ let { commentType } = this;
+
+ if (this.note.isDraft) {
+ // Draft internal notes (i.e. MR review comments) are not supported.
+ commentType = __('pending comment');
+ }
- const msg = sprintf(__('Are you sure you want to delete this %{typeOfComment}?'), {
- typeOfComment,
+ const msg = sprintf(__('Are you sure you want to delete this %{commentType}?'), {
+ commentType,
});
const confirmed = await confirmAction(msg, {
primaryBtnVariant: 'danger',
- primaryBtnText: __('Delete Comment'),
+ primaryBtnText: this.note.confidential ? __('Delete Internal Note') : __('Delete Comment'),
});
if (confirmed) {
@@ -356,7 +364,9 @@ export default {
isDirty,
}) {
if (shouldConfirm && isDirty) {
- const msg = __('Are you sure you want to cancel editing this comment?');
+ const msg = sprintf(__('Are you sure you want to cancel editing this %{commentType}?'), {
+ commentType: this.commentType,
+ });
const confirmed = await confirmAction(msg, {
primaryBtnText: __('Cancel editing'),
primaryBtnVariant: 'danger',
diff --git a/app/assets/javascripts/notes/i18n.js b/app/assets/javascripts/notes/i18n.js
index 951fa9733d4..4c0ee81bec0 100644
--- a/app/assets/javascripts/notes/i18n.js
+++ b/app/assets/javascripts/notes/i18n.js
@@ -6,19 +6,26 @@ export const COMMENT_FORM = {
),
note: __('Note'),
comment: __('Comment'),
+ internalComment: __('Add internal note'),
issue: __('issue'),
startThread: __('Start thread'),
+ startInternalThread: __('Start internal thread'),
mergeRequest: __('merge request'),
epic: __('epic'),
bodyPlaceholder: __('Write a comment or drag your files here…'),
- confidential: s__('Notes|Make this comment confidential'),
+ bodyPlaceholderInternal: __('Write an internal note or drag your files here…'),
+ confidential: s__('Notes|Make this an internal note'),
confidentialVisibility: s__(
- 'Notes|Confidential comments are only visible to members with the role of Reporter or higher',
+ 'Notes|Internal notes are only visible to the author, assignees, and members with the role of Reporter or higher',
),
discussionThatNeedsResolution: __(
'Discuss a specific suggestion or question that needs to be resolved.',
),
+ internalDiscussionThatNeedsResolution: __(
+ 'Discuss a specific suggestion or question internally that needs to be resolved.',
+ ),
discussion: __('Discuss a specific suggestion or question.'),
+ internalDiscussion: __('Discuss a specific suggestion or question internally.'),
actionButtonWithNote: __('%{actionText} & %{openOrClose} %{noteable}'),
actionButton: {
withNote: {
@@ -32,7 +39,10 @@ export const COMMENT_FORM = {
},
submitButton: {
startThread: __('Start thread'),
+ startInternalThread: __('Start internal thread'),
comment: __('Comment'),
+ internalComment: __('Add internal note'),
commentHelp: __('Add a general comment to this %{noteableDisplayName}.'),
+ internalCommentHelp: __('Add a confidential internal note to this %{noteableDisplayName}.'),
},
};
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 204e704e504..0cfc17a6ae9 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -17,6 +17,7 @@ import { mergeUrlParams } from '~/lib/utils/url_utility';
import sidebarTimeTrackingEventHub from '~/sidebar/event_hub';
import TaskList from '~/task_list';
import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
import * as constants from '../constants';
import eventHub from '../event_hub';
import * as types from './mutation_types';
@@ -369,7 +370,14 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
}
const processQuickActions = (res) => {
- const { errors: { commands_only: message } = { commands_only: null } } = res;
+ const {
+ errors: { commands_only: commandsOnly, command_names: commandNames } = {
+ commands_only: null,
+ command_names: [],
+ },
+ } = res;
+ let message = commandsOnly;
+
/*
The following reply means that quick actions have been successfully applied:
@@ -387,6 +395,13 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
confidentialWidget.setConfidentiality();
}
+ const commands = ['approve', 'merge', 'assign_reviewer', 'assign'];
+ const commandUpdatesAttentionRequest = commandNames[0].some((c) => commands.includes(c));
+
+ if (commandUpdatesAttentionRequest && SidebarStore.singleton.currentUserHasAttention) {
+ message = sprintf(__('%{message}. Your attention request was removed.'), { message });
+ }
+
$('.js-gfm-input').trigger('clear-commands-cache.atwho');
createFlash({
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index a710ac0ccf5..1fe82d96435 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -1,5 +1,6 @@
import { flattenDeep, clone } from 'lodash';
-import { statusBoxState } from '~/issuable/components/status_box.vue';
+import { match } from '~/diffs/utils/diff_file';
+import { badgeState } from '~/issuable/components/status_box.vue';
import { isInMRPage } from '~/lib/utils/common_utils';
import * as constants from '../constants';
import { collapseSystemNotes } from './collapse_utils';
@@ -84,8 +85,7 @@ export const getBlockedByIssues = (state) => state.noteableData.blocked_by_issue
export const userCanReply = (state) => Boolean(state.noteableData.current_user.can_create_note);
-export const openState = (state) =>
- isInMRPage() ? statusBoxState.state : state.noteableData.state;
+export const openState = (state) => (isInMRPage() ? badgeState.state : state.noteableData.state);
export const getUserData = (state) => state.userData || {};
@@ -179,29 +179,42 @@ export const unresolvedDiscussionsIdsByDate = (state, getters) =>
// Sorts the array of resolvable yet unresolved discussions by
// comparing file names first. If file names are the same, compares
// line numbers.
-export const unresolvedDiscussionsIdsByDiff = (state, getters) =>
- getters.allResolvableDiscussions
+export const unresolvedDiscussionsIdsByDiff = (state, getters, allState) => {
+ const authoritativeFiles = allState.diffs.diffFiles;
+
+ return getters.allResolvableDiscussions
.filter((d) => !d.resolved && d.active)
.sort((a, b) => {
+ let order = 0;
+
if (!a.diff_file || !b.diff_file) {
- return 0;
+ return order;
}
- // Get file names comparison result
- const filenameComparison = a.diff_file.file_path.localeCompare(b.diff_file.file_path);
+ const authoritativeA = authoritativeFiles.find((source) =>
+ match({ fileA: source, fileB: a.diff_file, mode: 'mr' }),
+ );
+ const authoritativeB = authoritativeFiles.find((source) =>
+ match({ fileA: source, fileB: b.diff_file, mode: 'mr' }),
+ );
+
+ if (authoritativeA && authoritativeB) {
+ order = authoritativeA.order - authoritativeB.order;
+ }
// Get the line numbers, to compare within the same file
const aLines = [a.position.new_line, a.position.old_line];
const bLines = [b.position.new_line, b.position.old_line];
- return filenameComparison < 0 ||
- (filenameComparison === 0 &&
+ return order < 0 ||
+ (order === 0 &&
// .max() because one of them might be zero (if removed/added)
Math.max(aLines[0], aLines[1]) < Math.max(bLines[0], bLines[1]))
? -1
: 1;
})
.map((d) => d.id);
+};
export const resolvedDiscussionCount = (state, getters) => {
const resolvedMap = getters.resolvedDiscussionsById;
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue
index 7a8a1bbcf09..2da8ca2d8a8 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue
@@ -83,11 +83,13 @@ export default {
modal-id="delete-tag-modal"
ok-variant="danger"
size="sm"
- :action-primary="{
+ :action-primary="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: __('Delete'),
attributes: [{ variant: 'danger' }, { disabled: disablePrimaryButton }],
- }"
- :action-cancel="{ text: __('Cancel') }"
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ :action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ text: __('Cancel'),
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@primary="$emit('confirmDelete')"
@cancel="$emit('cancelDelete')"
@change="projectPath = ''"
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 7659ba5f9ea..9e8eb92d87a 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
@@ -168,7 +168,9 @@ export default {
<div>
<persisted-search
class="gl-mb-5"
- :sortable-fields="[$options.searchConfig.NAME_SORT_FIELD]"
+ :sortable-fields="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
+ $options.searchConfig.NAME_SORT_FIELD,
+ ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:default-order="$options.searchConfig.NAME_SORT_FIELD.orderBy"
default-sort="asc"
@update="handleSearchUpdate"
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue
index 1f52e319ad0..3ae69731537 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue
@@ -1,7 +1,9 @@
<script>
-import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlIcon, GlPopover, GlLink, GlSprintf } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { timeTilRun } from '../../utils';
import {
- CLEANUP_TIMED_OUT_ERROR_MESSAGE,
+ PARTIAL_CLEANUP_CONTINUE_MESSAGE,
CLEANUP_STATUS_SCHEDULED,
CLEANUP_STATUS_ONGOING,
CLEANUP_STATUS_UNFINISHED,
@@ -15,9 +17,9 @@ export default {
name: 'CleanupStatus',
components: {
GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
+ GlPopover,
+ GlLink,
+ GlSprintf,
},
props: {
status: {
@@ -29,12 +31,17 @@ export default {
);
},
},
+ expirationPolicy: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
},
i18n: {
CLEANUP_STATUS_SCHEDULED,
CLEANUP_STATUS_ONGOING,
CLEANUP_STATUS_UNFINISHED,
- CLEANUP_TIMED_OUT_ERROR_MESSAGE,
+ PARTIAL_CLEANUP_CONTINUE_MESSAGE,
},
computed: {
showStatus() {
@@ -46,26 +53,57 @@ export default {
statusText() {
return this.$options.i18n[`CLEANUP_STATUS_${this.status}`];
},
- expireIconClass() {
- return this.failedDelete ? 'gl-text-orange-500' : '';
+ calculatedTimeTilNextRun() {
+ return timeTilRun(this.expirationPolicy?.next_run);
},
},
+ statusPopoverOptions: {
+ triggers: 'hover',
+ placement: 'top',
+ },
+ cleanupPolicyHelpPage: helpPagePath(
+ 'user/packages/container_registry/reduce_container_registry_storage.html',
+ { anchor: 'how-the-cleanup-policy-works' },
+ ),
};
</script>
<template>
- <div v-if="showStatus" class="gl-display-inline-flex gl-align-items-center">
- <gl-icon name="expire" data-testid="main-icon" :class="expireIconClass" />
+ <div
+ v-if="showStatus"
+ id="status-popover-container"
+ class="gl-display-inline-flex gl-align-items-center"
+ >
+ <div class="gl-display-inline-flex gl-align-items-center">
+ <gl-icon name="expire" data-testid="main-icon" />
+ </div>
<span class="gl-mx-2">
{{ statusText }}
</span>
<gl-icon
v-if="failedDelete"
- v-gl-tooltip="{ title: $options.i18n.CLEANUP_TIMED_OUT_ERROR_MESSAGE }"
+ id="status-info"
:size="14"
- class="gl-text-black-normal"
+ class="gl-text-gray-500"
data-testid="extra-info"
- name="information"
+ name="information-o"
/>
+ <gl-popover
+ target="status-info"
+ container="status-popover-container"
+ v-bind="$options.statusPopoverOptions"
+ >
+ <template #title>
+ {{ $options.i18n.CLEANUP_STATUS_UNFINISHED }}
+ </template>
+ <gl-sprintf :message="$options.i18n.PARTIAL_CLEANUP_CONTINUE_MESSAGE">
+ <template #time>{{ calculatedTimeTilNextRun }}</template
+ ><template #link="{ content }"
+ ><gl-link :href="$options.cleanupPolicyHelpPage" class="gl-font-sm" target="_blank">{{
+ content
+ }}</gl-link></template
+ >
+ </gl-sprintf>
+ </gl-popover>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue
index 5bd13322ebb..6f1f67e251f 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue
@@ -22,6 +22,11 @@ export default {
type: Object,
required: true,
},
+ expirationPolicy: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
},
computed: {
showPagination() {
@@ -38,6 +43,7 @@ export default {
:key="index"
:item="listItem"
:metadata-loading="metadataLoading"
+ :expiration-policy="expirationPolicy"
@delete="$emit('delete', $event)"
/>
<div class="gl-display-flex gl-justify-content-center">
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue
index 484903354e8..d76a8245b63 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue
@@ -6,12 +6,10 @@ import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import {
- ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
LIST_DELETE_BUTTON_DISABLED,
LIST_DELETE_BUTTON_DISABLED_FOR_MIGRATION,
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
- CLEANUP_TIMED_OUT_ERROR_MESSAGE,
IMAGE_DELETE_SCHEDULED_STATUS,
IMAGE_FAILED_DELETED_STATUS,
IMAGE_MIGRATING_STATE,
@@ -45,6 +43,11 @@ export default {
default: false,
required: false,
},
+ expirationPolicy: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
},
i18n: {
REMOVE_REPOSITORY_LABEL,
@@ -73,15 +76,6 @@ export default {
this.item.tagsCount,
);
},
- warningIconText() {
- if (this.failedDelete) {
- return ASYNC_DELETE_IMAGE_ERROR_MESSAGE;
- }
- if (this.item.expirationPolicyStartedAt) {
- return CLEANUP_TIMED_OUT_ERROR_MESSAGE;
- }
- return null;
- },
imageName() {
return this.item.name ? this.item.path : `${this.item.path}/ ${ROOT_IMAGE_TEXT}`;
},
@@ -140,6 +134,7 @@ export default {
v-if="item.expirationPolicyCleanupStatus"
class="ml-2"
:status="item.expirationPolicyCleanupStatus"
+ :expiration-policy="expirationPolicy"
/>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue
index 154e176dc6e..4ffd8390e4d 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue
@@ -7,7 +7,6 @@ import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import {
CONTAINER_REGISTRY_TITLE,
- LIST_INTRO_TEXT,
EXPIRATION_POLICY_WILL_RUN_IN,
EXPIRATION_POLICY_DISABLED_TEXT,
SET_UP_CLEANUP,
@@ -87,19 +86,12 @@ export default {
? sprintf(EXPIRATION_POLICY_WILL_RUN_IN, { time: this.timeTillRun })
: EXPIRATION_POLICY_DISABLED_TEXT;
},
- infoMessages() {
- return [{ text: LIST_INTRO_TEXT, link: this.helpPagePath }];
- },
},
};
</script>
<template>
- <title-area
- :title="$options.i18n.CONTAINER_REGISTRY_TITLE"
- :info-messages="infoMessages"
- :metadata-loading="metadataLoading"
- >
+ <title-area :title="$options.i18n.CONTAINER_REGISTRY_TITLE" :metadata-loading="metadataLoading">
<template #right-actions>
<slot name="commands"></slot>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
index 3c7f7ca9aa8..2a58933cd64 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
@@ -87,7 +87,7 @@ export const CLEANUP_DISABLED_TOOLTIP = s__(
export const CLEANUP_STATUS_SCHEDULED = s__('ContainerRegistry|Cleanup will run soon');
export const CLEANUP_STATUS_ONGOING = s__('ContainerRegistry|Cleanup is ongoing');
-export const CLEANUP_STATUS_UNFINISHED = s__('ContainerRegistry|Cleanup timed out');
+export const CLEANUP_STATUS_UNFINISHED = s__('ContainerRegistry|Partial cleanup complete');
export const DETAILS_DELETE_IMAGE_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while scheduling the image for deletion.',
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js
index e584da23edb..9d0ecfd2dcb 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js
@@ -10,7 +10,7 @@ export const DELETE_ALERT_TITLE = s__('ContainerRegistry|Some tags were not dele
export const DELETE_ALERT_LINK_TEXT = s__(
'ContainerRegistry|The cleanup policy timed out before it could delete all tags. An administrator can %{adminLinkStart}manually run cleanup now%{adminLinkEnd} or you can wait for the cleanup policy to automatically run again. %{docLinkStart}More information%{docLinkEnd}',
);
-export const CLEANUP_TIMED_OUT_ERROR_MESSAGE = s__(
- 'ContainerRegistry|Cleanup timed out before it could delete all tags',
+export const PARTIAL_CLEANUP_CONTINUE_MESSAGE = s__(
+ 'ContainerRegistry|The cleanup will continue within %{time}. %{linkStart}Learn more%{linkEnd}',
);
export const SET_UP_CLEANUP = s__('ContainerRegistry|Set up cleanup');
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js
index c7022d6070f..ceaf8a65a10 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js
@@ -8,9 +8,6 @@ export const CONNECTION_ERROR_TITLE = s__('ContainerRegistry|Docker connection e
export const CONNECTION_ERROR_MESSAGE = s__(
`ContainerRegistry|We are having trouble connecting to the Container Registry. Please try refreshing the page. If this error persists, please review %{docLinkStart}the troubleshooting documentation%{docLinkEnd}.`,
);
-export const LIST_INTRO_TEXT = s__(
- `ContainerRegistry|With the GitLab Container Registry, every project can have its own space to store images. %{docLinkStart}More information%{docLinkEnd}`,
-);
export const LIST_DELETE_BUTTON_DISABLED = s__(
'ContainerRegistry|Missing or insufficient permission, delete button disabled',
);
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql
index 916740f41b8..e2036d9e63d 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql
@@ -1,4 +1,4 @@
-query getContainerRepositoryDetails($id: ID!) {
+query getContainerRepositoryDetails($id: ContainerRepositoryID!) {
containerRepository(id: $id) {
id
name
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_metadata.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_metadata.query.graphql
index f1f67b98407..1faa9dec795 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_metadata.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_metadata.query.graphql
@@ -1,4 +1,4 @@
-query getContainerRepositoryMetadata($id: ID!) {
+query getContainerRepositoryMetadata($id: ContainerRepositoryID!) {
containerRepository(id: $id) {
id
tagsCount
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
index 8c577cc7b17..e57ac2a9efe 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
@@ -1,7 +1,7 @@
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
query getContainerRepositoryTags(
- $id: ID!
+ $id: ContainerRepositoryID!
$first: Int
$last: Int
$after: String
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 d1cab406984..c1bd71de646 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
@@ -336,6 +336,7 @@ export default {
:images="images"
:metadata-loading="$apollo.queries.additionalDetails.loading"
:page-info="pageInfo"
+ :expiration-policy="config.expirationPolicy"
@delete="deleteImage"
@prev-page="fetchPreviousPage"
@next-page="fetchNextPage"
@@ -370,7 +371,10 @@ export default {
ref="deleteModal"
size="sm"
modal-id="delete-image-modal"
- :action-primary="{ text: __('Remove'), attributes: { variant: 'danger' } }"
+ :action-primary="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ text: __('Remove'),
+ attributes: { variant: 'danger' },
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@primary="doDelete"
@cancel="track('cancel_delete')"
>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/utils.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/utils.js
new file mode 100644
index 00000000000..ffdaf9f2f17
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/utils.js
@@ -0,0 +1,8 @@
+import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility';
+
+export const timeTilRun = (time) => {
+ if (!time) return '';
+
+ const difference = calculateRemainingMilliseconds(time);
+ return approximateDuration(difference / 1000);
+};
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
index 67c2ca02d20..1faff1ff4de 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
@@ -16,10 +16,7 @@ import Api from '~/api';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
-import {
- DEPENDENCY_PROXY_SETTINGS_DESCRIPTION,
- DEPENDENCY_PROXY_DOCS_PATH,
-} from '~/packages_and_registries/settings/group/constants';
+import { DEPENDENCY_PROXY_DOCS_PATH } from '~/packages_and_registries/settings/group/constants';
import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql';
@@ -42,11 +39,8 @@ export default {
directives: {
GlModalDirective,
},
- inject: ['groupPath', 'groupId', 'dependencyProxyAvailable', 'noManifestsIllustration'],
+ inject: ['groupPath', 'groupId', 'noManifestsIllustration'],
i18n: {
- proxyNotAvailableText: s__(
- 'DependencyProxy|Dependency Proxy feature is limited to public groups for now.',
- ),
proxyImagePrefix: s__('DependencyProxy|Dependency Proxy image prefix'),
copyImagePrefixText: s__('DependencyProxy|Copy prefix'),
blobCountAndSize: s__('DependencyProxy|Contains %{count} blobs of images (%{size})'),
@@ -80,32 +74,20 @@ export default {
apollo: {
group: {
query: getDependencyProxyDetailsQuery,
- skip() {
- return !this.dependencyProxyAvailable;
- },
variables() {
return this.queryVariables;
},
},
},
computed: {
- infoMessages() {
- return [
- {
- text: DEPENDENCY_PROXY_SETTINGS_DESCRIPTION,
- link: DEPENDENCY_PROXY_DOCS_PATH,
- },
- ];
- },
-
queryVariables() {
return { fullPath: this.groupPath, first: GRAPHQL_PAGE_SIZE };
},
pageInfo() {
- return this.group.dependencyProxyManifests.pageInfo;
+ return this.group.dependencyProxyManifests?.pageInfo;
},
manifests() {
- return this.group.dependencyProxyManifests.nodes;
+ return this.group.dependencyProxyManifests?.nodes;
},
modalTitleWithCount() {
return sprintf(
@@ -132,7 +114,10 @@ export default {
);
},
showDeleteDropdown() {
- return this.group.dependencyProxyBlobCount > 0;
+ return this.group.dependencyProxyManifests?.nodes.length > 0;
+ },
+ showDependencyProxyImagePrefix() {
+ return this.group.dependencyProxyImagePrefix?.length > 0;
},
},
methods: {
@@ -181,7 +166,7 @@ export default {
>
{{ deleteCacheAlertMessage }}
</gl-alert>
- <title-area :title="$options.i18n.pageTitle" :info-messages="infoMessages">
+ <title-area :title="$options.i18n.pageTitle">
<template v-if="showDeleteDropdown" #right-actions>
<gl-dropdown
icon="ellipsis_v"
@@ -198,41 +183,34 @@ export default {
</gl-dropdown>
</template>
</title-area>
- <gl-alert
- v-if="!dependencyProxyAvailable"
- :dismissible="false"
- data-testid="proxy-not-available"
- >
- {{ $options.i18n.proxyNotAvailableText }}
- </gl-alert>
- <gl-skeleton-loader v-else-if="$apollo.queries.group.loading" />
-
- <div v-else data-testid="main-area">
- <gl-form-group :label="$options.i18n.proxyImagePrefix">
- <gl-form-input-group
- readonly
- :value="group.dependencyProxyImagePrefix"
- class="gl-layout-w-limited"
- data-testid="proxy-url"
- >
- <template #append>
- <clipboard-button
- :text="group.dependencyProxyImagePrefix"
- :title="$options.i18n.copyImagePrefixText"
- />
- </template>
- </gl-form-input-group>
- <template #description>
- <span data-qa-selector="dependency_proxy_count" data-testid="proxy-count">
- <gl-sprintf :message="$options.i18n.blobCountAndSize">
- <template #count>{{ group.dependencyProxyBlobCount }}</template>
- <template #size>{{ group.dependencyProxyTotalSize }}</template>
- </gl-sprintf>
- </span>
+ <gl-form-group v-if="showDependencyProxyImagePrefix" :label="$options.i18n.proxyImagePrefix">
+ <gl-form-input-group
+ readonly
+ :value="group.dependencyProxyImagePrefix"
+ class="gl-layout-w-limited"
+ data-testid="proxy-url"
+ >
+ <template #append>
+ <clipboard-button
+ :text="group.dependencyProxyImagePrefix"
+ :title="$options.i18n.copyImagePrefixText"
+ />
</template>
- </gl-form-group>
+ </gl-form-input-group>
+ <template #description>
+ <span data-qa-selector="dependency_proxy_count" data-testid="proxy-count">
+ <gl-sprintf :message="$options.i18n.blobCountAndSize">
+ <template #count>{{ group.dependencyProxyBlobCount }}</template>
+ <template #size>{{ group.dependencyProxyTotalSize }}</template>
+ </gl-sprintf>
+ </span>
+ </template>
+ </gl-form-group>
+ <gl-skeleton-loader v-if="$apollo.queries.group.loading" />
+
+ <div v-else data-testid="main-area">
<manifests-list
v-if="manifests && manifests.length"
:manifests="manifests"
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifest_row.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifest_row.vue
index 78880b6e3f4..1bbd0c32dc4 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifest_row.vue
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifest_row.vue
@@ -1,5 +1,6 @@
<script>
-import { GlSprintf } from '@gitlab/ui';
+import { GlIcon, GlSprintf } from '@gitlab/ui';
+import { MANIFEST_PENDING_DESTRUCTION_STATUS } from '~/packages_and_registries/dependency_proxy/constants';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { s__ } from '~/locale';
@@ -7,6 +8,7 @@ import { s__ } from '~/locale';
export default {
name: 'ManifestRow',
components: {
+ GlIcon,
GlSprintf,
ListItem,
TimeagoTooltip,
@@ -24,17 +26,31 @@ export default {
version() {
return this.manifest?.imageName.split(':')[1];
},
+ isErrorStatus() {
+ return this.manifest?.status === MANIFEST_PENDING_DESTRUCTION_STATUS;
+ },
+ disabledRowStyle() {
+ return this.isErrorStatus ? 'gl-font-weight-normal gl-text-gray-500' : '';
+ },
},
i18n: {
cachedAgoMessage: s__('DependencyProxy|Cached %{time}'),
+ scheduledForDeletion: s__('DependencyProxy|Scheduled for deletion'),
},
};
</script>
<template>
- <list-item>
- <template #left-primary> {{ name }} </template>
- <template #left-secondary> {{ version }} </template>
+ <list-item :disabled="isErrorStatus">
+ <template #left-primary>
+ <span :class="disabledRowStyle">{{ name }}</span>
+ </template>
+ <template #left-secondary>
+ {{ version }}
+ <span v-if="isErrorStatus" class="gl-ml-4" data-testid="status"
+ ><gl-icon name="clock" /> {{ $options.i18n.scheduledForDeletion }}</span
+ >
+ </template>
<template #right-primary> &nbsp; </template>
<template #right-secondary>
<timeago-tooltip :time="manifest.createdAt" data-testid="cached-message">
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js
index 3c6ede6fdce..fdad69204ba 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js
@@ -1 +1,2 @@
export const GRAPHQL_PAGE_SIZE = 20;
+export const MANIFEST_PENDING_DESTRUCTION_STATUS = 'PENDING_DESTRUCTION';
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql
index 5c43b10a5e3..c1597625964 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql
@@ -20,6 +20,7 @@ query getDependencyProxyDetails(
id
createdAt
imageName
+ status
}
pageInfo {
...PageInfo
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js
index dc73470e07d..14789aafdb7 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js
@@ -1,5 +1,4 @@
import Vue from 'vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
import app from '~/packages_and_registries/dependency_proxy/app.vue';
import { apolloProvider } from '~/packages_and_registries/dependency_proxy/graphql';
import Translate from '~/vue_shared/translate';
@@ -11,12 +10,11 @@ export const initDependencyProxyApp = () => {
if (!el) {
return null;
}
- const { dependencyProxyAvailable, ...dataset } = el.dataset;
+ const { ...dataset } = el.dataset;
return new Vue({
el,
apolloProvider,
provide: {
- dependencyProxyAvailable: parseBoolean(dependencyProxyAvailable),
...dataset,
},
render(createElement) {
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 f198d2e1bfa..425fb4596fd 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
@@ -191,7 +191,10 @@ export default {
<package-list-row
v-for="v in packageEntity.versions"
:key="v.id"
- :package-entity="{ name: packageEntity.name, ...v }"
+ :package-entity="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ name: packageEntity.name,
+ ...v,
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:package-link="v.id.toString()"
:disable-delete="true"
:show-package-type="false"
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue
index c611f92036d..d3c38da1531 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue
@@ -33,7 +33,7 @@ export default {
<registry-search
:filter="filter"
:sorting="sorting"
- :tokens="[]"
+ :tokens="[] /* eslint-disable-line @gitlab/vue-no-new-non-primitive-in-template */"
:sortable-fields="sortableFields"
@sorting:changed="updateSorting"
@filter:changed="setFilter"
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 118c509828c..f5946797626 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
@@ -1,5 +1,5 @@
<script>
-import { GlIcon, GlSprintf, GlBadge, GlResizeObserverDirective } from '@gitlab/ui';
+import { GlSprintf, GlBadge, GlResizeObserverDirective } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
@@ -14,7 +14,6 @@ export default {
name: 'PackageTitle',
components: {
TitleArea,
- GlIcon,
GlSprintf,
PackageTags,
MetadataItem,
@@ -84,7 +83,6 @@ export default {
data-qa-selector="package_title"
>
<template #sub-header>
- <gl-icon name="eye" class="gl-mr-3" />
<span data-testid="sub-header">
<gl-sprintf :message="$options.i18n.packageInfo">
<template #version>
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 6222c2e73d7..04faff1a75b 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
@@ -1,5 +1,12 @@
<script>
-import { GlButton, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui';
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlIcon,
+ GlSprintf,
+ GlTooltipDirective,
+ GlTruncate,
+} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import {
@@ -17,7 +24,9 @@ import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
name: 'PackageListRow',
components: {
- GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlIcon,
GlSprintf,
GlTruncate,
PackageTags,
@@ -50,31 +59,42 @@ export default {
pipelineUser() {
return this.pipeline?.user?.name;
},
- showWarningIcon() {
+ errorStatusRow() {
return this.packageEntity.status === PACKAGE_ERROR_STATUS;
},
showTags() {
return Boolean(this.packageEntity.tags?.nodes?.length);
},
- disabledRow() {
+ nonDefaultRow() {
return this.packageEntity.status && this.packageEntity.status !== PACKAGE_DEFAULT_STATUS;
},
routerLinkEvent() {
- return this.disabledRow ? '' : 'click';
+ return this.nonDefaultRow ? '' : 'click';
+ },
+ errorPackageStyle() {
+ return {
+ 'gl-text-red-500': this.errorStatusRow,
+ 'gl-font-weight-normal': this.errorStatusRow,
+ };
},
},
i18n: {
erroredPackageText: s__('PackageRegistry|Invalid Package: failed metadata extraction'),
createdAt: __('Created %{timestamp}'),
+ deletePackage: s__('PackageRegistry|Delete package'),
+ errorPublishing: s__('PackageRegistry|Error publishing'),
+ warning: __('Warning'),
+ moreActions: __('More actions'),
},
};
</script>
<template>
- <list-item data-qa-selector="package_row" :disabled="disabledRow">
+ <list-item data-qa-selector="package_row">
<template #left-primary>
<div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0">
<router-link
+ :class="errorPackageStyle"
class="gl-text-body gl-min-w-0"
data-testid="details-link"
data-qa-selector="package_link"
@@ -84,16 +104,6 @@ export default {
<gl-truncate :text="packageEntity.name" />
</router-link>
- <gl-button
- v-if="showWarningIcon"
- v-gl-tooltip="{ title: $options.i18n.erroredPackageText }"
- class="gl-hover-bg-transparent!"
- icon="warning"
- category="tertiary"
- data-testid="warning-icon"
- :aria-label="__('Warning')"
- />
-
<package-tags
v-if="showTags"
class="gl-ml-3"
@@ -104,7 +114,7 @@ export default {
</div>
</template>
<template #left-secondary>
- <div class="gl-display-flex" data-testid="left-secondary-infos">
+ <div v-if="!errorStatusRow" class="gl-display-flex" data-testid="left-secondary-infos">
<span>{{ packageEntity.version }}</span>
<div v-if="pipelineUser" class="gl-display-none gl-sm-display-flex gl-ml-2">
@@ -120,9 +130,19 @@ export default {
<package-path
v-if="isGroupPage"
:path="packageEntity.project.fullPath"
- :disabled="disabledRow"
+ :disabled="nonDefaultRow"
/>
</div>
+ <div v-else>
+ <gl-icon
+ v-gl-tooltip="{ title: $options.i18n.erroredPackageText }"
+ name="warning"
+ class="gl-text-red-500"
+ :aria-label="$options.i18n.warning"
+ data-testid="warning-icon"
+ />
+ <span class="gl-text-red-500">{{ $options.i18n.errorPublishing }}</span>
+ </div>
</template>
<template #right-primary>
@@ -139,16 +159,22 @@ export default {
</span>
</template>
- <template v-if="!disabledRow" #right-action>
- <gl-button
- data-testid="action-delete"
- icon="remove"
- category="secondary"
- variant="danger"
- :title="s__('PackageRegistry|Remove package')"
- :aria-label="s__('PackageRegistry|Remove package')"
- @click="$emit('packageToDelete', packageEntity)"
- />
+ <template v-if="packageEntity.canDestroy" #right-action>
+ <gl-dropdown
+ data-testid="delete-dropdown"
+ icon="ellipsis_v"
+ :text="$options.i18n.moreActions"
+ :text-sr-only="true"
+ category="tertiary"
+ no-caret
+ >
+ <gl-dropdown-item
+ data-testid="action-delete"
+ variant="danger"
+ @click="$emit('packageToDelete', packageEntity)"
+ >{{ $options.i18n.deletePackage }}</gl-dropdown-item
+ >
+ </gl-dropdown>
</template>
</list-item>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue
index bf41c36e09b..440e11a99f2 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue
@@ -27,21 +27,15 @@ export default {
packageAmountText() {
return n__(`%d Package`, `%d Packages`, this.count);
},
- infoMessages() {
- return [{ text: this.$options.i18n.LIST_INTRO_TEXT, link: this.helpUrl }];
- },
},
i18n: {
LIST_TITLE_TEXT: s__('PackageRegistry|Package Registry'),
- LIST_INTRO_TEXT: s__(
- 'PackageRegistry|Publish and share packages for a variety of common package managers. %{docLinkStart}More information%{docLinkEnd}',
- ),
},
};
</script>
<template>
- <title-area :title="$options.i18n.LIST_TITLE_TEXT" :info-messages="infoMessages">
+ <title-area :title="$options.i18n.LIST_TITLE_TEXT">
<template #metadata-amount>
<metadata-item v-if="showPackageCount" icon="package" :text="packageAmountText" />
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
index 298ed9bccdb..1aff23bc112 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
@@ -1,18 +1,20 @@
<script>
-import { GlModal, GlSprintf, GlKeysetPagination } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { GlAlert, GlModal, GlSprintf, GlKeysetPagination } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import {
DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
+ PACKAGE_ERROR_STATUS,
} from '~/packages_and_registries/package_registry/constants';
import { packageTypeToTrackCategory } from '~/packages_and_registries/package_registry/utils';
import Tracking from '~/tracking';
export default {
components: {
+ GlAlert,
GlKeysetPagination,
GlModal,
GlSprintf,
@@ -40,6 +42,7 @@ export default {
data() {
return {
itemToBeDeleted: null,
+ errorPackages: [],
};
},
computed: {
@@ -70,6 +73,24 @@ export default {
}
},
},
+ errorTitleAlert() {
+ return sprintf(
+ s__('PackageRegistry|There was an error publishing a %{packageName} package'),
+ { packageName: this.errorPackages[0].name },
+ );
+ },
+ showErrorPackageAlert() {
+ return this.errorPackages.length > 0;
+ },
+ },
+ watch: {
+ list(newVal) {
+ this.errorPackages = newVal.filter((pkg) => pkg.status === PACKAGE_ERROR_STATUS);
+ },
+ },
+ created() {
+ this.errorPackages =
+ this.list.length > 0 ? this.list.filter((pkg) => pkg.status === PACKAGE_ERROR_STATUS) : [];
},
methods: {
setItemToBeDeleted(item) {
@@ -83,12 +104,19 @@ export default {
deleteItemCanceled() {
this.track(CANCEL_DELETE_PACKAGE_TRACKING_ACTION);
},
+ showConfirmationModal() {
+ this.setItemToBeDeleted(this.errorPackages[0]);
+ },
},
i18n: {
deleteModalContent: s__(
'PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?',
),
modalAction: s__('PackageRegistry|Delete package'),
+ errorMessageBodyAlert: s__(
+ 'PackageRegistry|There was a timeout and the package was not published. Delete this package and try again.',
+ ),
+ deleteThisPackage: s__('PackageRegistry|Delete this package'),
},
};
</script>
@@ -102,6 +130,14 @@ export default {
</div>
<template v-else>
+ <gl-alert
+ v-if="showErrorPackageAlert"
+ variant="danger"
+ :title="errorTitleAlert"
+ :primary-button-text="$options.i18n.deleteThisPackage"
+ @primaryAction="showConfirmationModal"
+ >{{ $options.i18n.errorMessageBodyAlert }}</gl-alert
+ >
<div data-qa-selector="packages-table">
<packages-list-row
v-for="packageEntity in list"
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql
index 66315fda9e9..b5695a01376 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql
@@ -5,6 +5,7 @@ fragment PackageData on Package {
packageType
createdAt
status
+ canDestroy
tags {
nodes {
id
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
index c45cbe56e00..41b0c285fff 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
@@ -1,4 +1,4 @@
-query getPackageDetails($id: ID!) {
+query getPackageDetails($id: PackagesPackageID!) {
package(id: $id) {
id
name
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/bundle.js b/app/assets/javascripts/packages_and_registries/settings/group/bundle.js
index 85a7aeb5561..482a3ef2ead 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/bundle.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/bundle.js
@@ -21,7 +21,6 @@ export default () => {
groupPath: el.dataset.groupPath,
groupDependencyProxyPath: el.dataset.groupDependencyProxyPath,
defaultExpanded: parseBoolean(el.dataset.defaultExpanded),
- dependencyProxyAvailable: parseBoolean(el.dataset.dependencyProxyAvailable),
},
render(createElement) {
return createElement(SettingsApp);
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
index fd62fe144b2..a5189201112 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
@@ -13,7 +13,6 @@ import {
import {
DEPENDENCY_PROXY_HEADER,
- DEPENDENCY_PROXY_SETTINGS_DESCRIPTION,
DEPENDENCY_PROXY_DOCS_PATH,
} from '~/packages_and_registries/settings/group/constants';
@@ -28,7 +27,6 @@ export default {
},
i18n: {
DEPENDENCY_PROXY_HEADER,
- DEPENDENCY_PROXY_SETTINGS_DESCRIPTION,
enabledProxyLabel: s__('DependencyProxy|Enable Dependency Proxy'),
enabledProxyHelpText: s__(
'DependencyProxy|To see the image prefix and what is in the cache, visit the %{linkStart}Dependency Proxy%{linkEnd}',
@@ -140,19 +138,6 @@ export default {
data-qa-selector="dependency_proxy_settings_content"
>
<template #title> {{ $options.i18n.DEPENDENCY_PROXY_HEADER }} </template>
- <template #description>
- <span data-testid="description">
- <gl-sprintf :message="$options.i18n.DEPENDENCY_PROXY_SETTINGS_DESCRIPTION">
- <template #docLink="{ content }">
- <gl-link
- data-testid="description-link"
- :href="$options.links.DEPENDENCY_PROXY_DOCS_PATH"
- >{{ content }}</gl-link
- >
- </template>
- </gl-sprintf>
- </span>
- </template>
<template #default>
<div>
<gl-toggle
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
index 64c12b4be6a..f285dfc0755 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
@@ -13,7 +13,7 @@ export default {
PackagesSettings,
DependencyProxySettings,
},
- inject: ['groupPath', 'dependencyProxyAvailable'],
+ inject: ['groupPath'],
apollo: {
group: {
query: getGroupPackagesSettingsQuery,
@@ -83,7 +83,6 @@ export default {
/>
<dependency-proxy-settings
- v-if="dependencyProxyAvailable"
:dependency-proxy-settings="dependencyProxySettings"
:dependency-proxy-image-ttl-policy="dependencyProxyImageTtlPolicy"
:is-loading="isLoading"
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 ee922457993..0249b475e46 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
@@ -19,13 +19,10 @@ export const DUPLICATES_SETTINGS_EXCEPTION_LEGEND = s__(
);
export const DEPENDENCY_PROXY_HEADER = s__('DependencyProxy|Dependency Proxy');
-export const DEPENDENCY_PROXY_SETTINGS_DESCRIPTION = s__(
- 'DependencyProxy|Create a local proxy for storing frequently used upstream images. %{docLinkStart}Learn more%{docLinkEnd} about dependency proxies.',
-);
// Parameters
-export const PACKAGES_DOCS_PATH = helpPagePath('user/packages');
+export const PACKAGES_DOCS_PATH = helpPagePath('user/packages/index');
export const MAVEN_DUPLICATES_ALLOWED = 'mavenDuplicatesAllowed';
export const MAVEN_DUPLICATE_EXCEPTION_REGEX = 'mavenDuplicateExceptionRegex';
diff --git a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
index 3ef75b3ef0e..5ecacb84d65 100644
--- a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
+++ b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
@@ -112,9 +112,7 @@ export default {
},
signupEnabledHelpText() {
const text = sprintf(
- s__(
- 'ApplicationSettings|When enabled, any user visiting %{host} will be able to create an account.',
- ),
+ s__('ApplicationSettings|Any user that visits %{host} can create an account.'),
{
host: this.host,
},
@@ -125,7 +123,7 @@ export default {
requireAdminApprovalHelpText() {
const text = sprintf(
s__(
- 'ApplicationSettings|When enabled, any user visiting %{host} and creating an account will have to be explicitly approved by an admin before they can sign in. This setting is effective only if sign-ups are enabled.',
+ 'ApplicationSettings|Any user that visits %{host} and creates an account must be explicitly approved by an administrator before they can sign in. Only effective if sign-ups are enabled.',
),
{
host: this.host,
@@ -197,32 +195,34 @@ export default {
),
domainAllowListLabel: s__('ApplicationSettings|Allowed domains for sign-ups'),
domainAllowListDescription: s__(
- 'ApplicationSettings|ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com',
+ 'ApplicationSettings|Only users with e-mail addresses that match these domain(s) can sign up. Wildcards allowed. Use separate lines for multiple entries. Example: domain.com, *.domain.com',
),
userCapLabel: s__('ApplicationSettings|User cap'),
userCapDescription: s__(
- 'ApplicationSettings|Once the instance reaches the user cap, any user who is added or requests access will have to be approved by an admin. Leave the field empty for unlimited.',
+ 'ApplicationSettings|After the instance reaches the user cap, any user who is added or requests access must be approved by an administrator. Leave blank for unlimited.',
),
domainDenyListGroupLabel: s__('ApplicationSettings|Domain denylist'),
- domainDenyListLabel: s__('ApplicationSettings|Enable domain denylist for sign ups'),
+ domainDenyListLabel: s__('ApplicationSettings|Enable domain denylist for sign-ups'),
domainDenyListTypeFileLabel: s__('ApplicationSettings|Upload denylist file'),
domainDenyListTypeRawLabel: s__('ApplicationSettings|Enter denylist manually'),
domainDenyListFileLabel: s__('ApplicationSettings|Denylist file'),
domainDenyListFileDescription: s__(
- 'ApplicationSettings|Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.',
+ 'ApplicationSettings|Users with e-mail addresses that match these domain(s) cannot sign up. Wildcards allowed. Use separate lines or commas for multiple entries.',
),
domainDenyListListLabel: s__('ApplicationSettings|Denied domains for sign-ups'),
domainDenyListListDescription: s__(
- 'ApplicationSettings|Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com',
+ 'ApplicationSettings|Users with e-mail addresses that match these domain(s) cannot sign up. Wildcards allowed. Use separate lines for multiple entries. Example: domain.com, *.domain.com',
),
domainPlaceholder: s__('ApplicationSettings|domain.com'),
emailRestrictionsEnabledGroupLabel: s__('ApplicationSettings|Email restrictions'),
emailRestrictionsEnabledLabel: s__(
- 'ApplicationSettings|Enable email restrictions for sign ups',
+ 'ApplicationSettings|Enable email restrictions for sign-ups',
),
emailRestrictionsGroupLabel: s__('ApplicationSettings|Email restrictions for sign-ups'),
- afterSignUpTextGroupLabel: s__('ApplicationSettings|After sign up text'),
- afterSignUpTextGroupDescription: s__('ApplicationSettings|Markdown enabled'),
+ afterSignUpTextGroupLabel: s__('ApplicationSettings|After sign-up text'),
+ afterSignUpTextGroupDescription: s__(
+ 'ApplicationSettings|Text shown after a user signs up. Markdown enabled.',
+ ),
},
};
</script>
@@ -288,19 +288,21 @@ export default {
name="application_setting[minimum_password_length]"
/>
- <gl-sprintf
- :message="
- s__(
- 'ApplicationSettings|See GitLab\'s %{linkStart}Password Policy Guidelines%{linkEnd}',
- )
- "
- >
- <template #link="{ content }">
- <gl-link :href="form.minimumPasswordLengthHelpLink" target="_blank">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
+ <template #description>
+ <gl-sprintf
+ :message="
+ s__(
+ 'ApplicationSettings|See GitLab\'s %{linkStart}Password Policy Guidelines%{linkEnd}.',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link :href="form.minimumPasswordLengthHelpLink" target="_blank">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
</gl-form-group>
<gl-form-group
@@ -380,17 +382,19 @@ export default {
name="application_setting[email_restrictions]"
></textarea>
- <gl-sprintf
- :message="
- s__(
- 'ApplicationSettings|Restricts sign-ups for email addresses that match the given regex. See the %{linkStart}supported syntax%{linkEnd} for more information.',
- )
- "
- >
- <template #link="{ content }">
- <gl-link :href="form.supportedSyntaxLinkUrl" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
+ <template #description>
+ <gl-sprintf
+ :message="
+ s__(
+ 'ApplicationSettings|Restricts sign-ups for email addresses that match the given regex. %{linkStart}What is the supported syntax?%{linkEnd}',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link :href="form.supportedSyntaxLinkUrl" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
</gl-form-group>
<gl-form-group
diff --git a/app/assets/javascripts/pages/admin/background_migrations/index.js b/app/assets/javascripts/pages/admin/background_migrations/index.js
new file mode 100644
index 00000000000..4c59613140b
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/background_migrations/index.js
@@ -0,0 +1,3 @@
+import { initBackgroundMigrationsApp } from '~/admin/background_migrations';
+
+initBackgroundMigrationsApp();
diff --git a/app/assets/javascripts/pages/admin/clusters/index.js b/app/assets/javascripts/pages/admin/clusters/index.js
deleted file mode 100644
index f398b1cee82..00000000000
--- a/app/assets/javascripts/pages/admin/clusters/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initCreateCluster from '~/create_cluster/init_create_cluster';
-
-initCreateCluster(document, gon);
diff --git a/app/assets/javascripts/pages/groups/clusters/index.js b/app/assets/javascripts/pages/groups/clusters/index.js
index 4d48bd4be2b..99fb7fa68a9 100644
--- a/app/assets/javascripts/pages/groups/clusters/index.js
+++ b/app/assets/javascripts/pages/groups/clusters/index.js
@@ -1,5 +1,3 @@
import initIntegrationForm from '~/clusters/forms/show/index';
-import initCreateCluster from '~/create_cluster/init_create_cluster';
-initCreateCluster(document, gon);
initIntegrationForm();
diff --git a/app/assets/javascripts/pages/groups/new/components/app.vue b/app/assets/javascripts/pages/groups/new/components/app.vue
index c3ac074cd7a..713287f65b4 100644
--- a/app/assets/javascripts/pages/groups/new/components/app.vue
+++ b/app/assets/javascripts/pages/groups/new/components/app.vue
@@ -21,9 +21,7 @@ const PANELS = [
name: 'import-group-pane',
selector: '#import-group-pane',
title: s__('GroupsNew|Import group'),
- description: s__(
- 'GroupsNew|Export groups with all their related data and move to a new GitLab instance.',
- ),
+ description: s__('GroupsNew|Import a group and related data from another GitLab instance.'),
illustration: importGroupIllustration,
details: 'Migrate your existing groups from another instance of GitLab.',
},
diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
index 5d8ee146e62..52add416f38 100644
--- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
@@ -1,24 +1,9 @@
import initVariableList from '~/ci_variable_list';
-import GroupRunnersFilteredSearchTokenKeys from '~/filtered_search/group_runners_filtered_search_token_keys';
import initSharedRunnersForm from '~/group_settings/mount_shared_runners';
-import { FILTERED_SEARCH } from '~/filtered_search/constants';
-import initFilteredSearch from '~/pages/search/init_filtered_search';
-import { initRunnerAwsDeployments } from '~/pages/shared/mount_runner_aws_deployments';
-import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
import initSettingsPanels from '~/settings_panels';
// Initialize expandable settings panels
initSettingsPanels();
-initFilteredSearch({
- page: FILTERED_SEARCH.ADMIN_RUNNERS,
- filteredSearchTokenKeys: GroupRunnersFilteredSearchTokenKeys,
- anchor: FILTERED_SEARCH.GROUP_RUNNERS_ANCHOR,
- useDefaultState: false,
-});
-
initSharedRunnersForm();
initVariableList();
-
-initInstallRunner();
-initRunnerAwsDeployments();
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 9a4054eb110..35a8d3d979a 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
@@ -9,6 +9,7 @@ import { getBulkImportsHistory } from '~/rest_api';
import ImportStatus from '~/import_entities/components/import_status.vue';
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 { DEFAULT_ERROR } from '../utils/error_messages';
@@ -16,6 +17,8 @@ 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 HISTORY_PAGINATION_SIZE_PERSIST_KEY = 'gl-bulk-imports-history-per-page';
+
const tableCell = (config) => ({
thClass: `${DEFAULT_TH_CLASSES}`,
tdClass: (value, key, item) => {
@@ -37,6 +40,7 @@ export default {
PaginationBar,
ImportStatus,
TimeAgo,
+ LocalStorageSync,
},
data() {
@@ -59,7 +63,7 @@ export default {
}),
tableCell({
key: 'destination_name',
- label: s__('BulkImport|New group'),
+ label: s__('BulkImport|Destination group'),
thClass: `${DEFAULT_TH_CLASSES} gl-w-40p`,
}),
tableCell({
@@ -85,10 +89,13 @@ export default {
this.loadHistoryItems();
},
deep: true,
- immediate: true,
},
},
+ mounted() {
+ this.loadHistoryItems();
+ },
+
methods: {
async loadHistoryItems() {
try {
@@ -116,6 +123,7 @@ export default {
},
gitlabLogo: window.gon.gitlab_logo,
+ historyPaginationSizePersistKey: HISTORY_PAGINATION_SIZE_PERSIST_KEY,
};
</script>
@@ -171,5 +179,9 @@ export default {
@set-page-size="paginationConfig.perPage = $event"
/>
</template>
+ <local-storage-sync
+ v-model="paginationConfig.perPage"
+ :storage-key="$options.historyPaginationSizePersistKey"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/pages/projects/clusters/index.js b/app/assets/javascripts/pages/projects/clusters/index.js
deleted file mode 100644
index f398b1cee82..00000000000
--- a/app/assets/javascripts/pages/projects/clusters/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initCreateCluster from '~/create_cluster/init_create_cluster';
-
-initCreateCluster(document, gon);
diff --git a/app/assets/javascripts/pages/projects/clusters/show/index.js b/app/assets/javascripts/pages/projects/clusters/show/index.js
index 71ab5a0b19c..0b34f374abc 100644
--- a/app/assets/javascripts/pages/projects/clusters/show/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/show/index.js
@@ -1,6 +1,6 @@
import ClustersBundle from '~/clusters/clusters_bundle';
import initIntegrationForm from '~/clusters/forms/show';
-import initGkeNamespace from '~/create_cluster/gke_cluster_namespace';
+import initGkeNamespace from '~/clusters/gke_cluster_namespace';
import initClusterHealth from './cluster_health';
new ClustersBundle(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
index 1667f2c3576..1912477758b 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
@@ -1,5 +1,6 @@
<script>
-import { GlLink, GlIcon, GlButton, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+import { GlLink, GlIcon, GlButton, GlPopover, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import { isExperimentVariant } from '~/experimentation/utils';
import eventHub from '~/invite_members/event_hub';
@@ -12,6 +13,7 @@ export default {
GlLink,
GlIcon,
GlButton,
+ GlPopover,
GitlabExperiment,
},
directives: {
@@ -19,6 +21,8 @@ export default {
},
i18n: {
trialOnly: s__('LearnGitlab|Trial only'),
+ contactAdmin: s__('LearnGitlab|Contact your administrator to start a free Ultimate trial.'),
+ viewAdminList: s__('LearnGitlab|View administrator list'),
watchHow: __('Watch how'),
},
props: {
@@ -31,6 +35,11 @@ export default {
type: Object,
},
},
+ data() {
+ return {
+ popoverId: uniqueId('contact-admin-'),
+ };
+ },
computed: {
linkTitle() {
return ACTION_LABELS[this.action].title;
@@ -78,7 +87,7 @@ export default {
{{ linkTitle }}
</gl-link>
<gl-link
- v-else
+ v-else-if="value.enabled"
:target="openInNewTab ? '_blank' : '_self'"
:href="value.url"
data-testid="uncompleted-learn-gitlab-link"
@@ -87,6 +96,33 @@ export default {
>
{{ linkTitle }}
</gl-link>
+ <template v-else>
+ <div data-testid="disabled-learn-gitlab-link">{{ linkTitle }}</div>
+ <gl-button
+ :id="popoverId"
+ category="tertiary"
+ icon="question-o"
+ class="ml-auto"
+ :aria-label="$options.i18n.contactAdmin"
+ size="small"
+ data-testid="contact-admin-popover-trigger"
+ />
+ <gl-popover
+ :target="popoverId"
+ placement="top"
+ triggers="hover focus"
+ data-testid="contact-admin-popover"
+ >
+ <p>{{ $options.i18n.contactAdmin }}</p>
+ <gl-link
+ :href="value.url"
+ class="font-size-inherit"
+ data-testid="view-administrator-link-text"
+ >
+ {{ $options.i18n.viewAdminList }}
+ </gl-link>
+ </gl-popover>
+ </template>
<gitlab-experiment name="video_tutorials_continuous_onboarding">
<template #control></template>
<template #candidate>
@@ -100,6 +136,7 @@ export default {
:href="linkToVideoTutorial"
target="_blank"
class="ml-auto"
+ size="small"
data-testid="video-tutorial-link"
data-track-action="click_video_link"
:data-track-label="linkTitle"
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js
index 9ba5e17237a..05bacd9b350 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js
@@ -30,6 +30,7 @@ export const ACTION_LABELS = {
description: s__('LearnGitLab|Try all GitLab features for 30 days, no credit card required.'),
section: 'workspace',
position: 3,
+ openInNewTab: true,
},
codeOwnersEnabled: {
title: s__('LearnGitLab|Add code owners'),
@@ -40,6 +41,7 @@ export const ACTION_LABELS = {
trialRequired: true,
section: 'workspace',
position: 4,
+ openInNewTab: true,
videoTutorial: 'https://vimeo.com/670896787',
},
requiredMrApprovalsEnabled: {
@@ -49,6 +51,7 @@ export const ACTION_LABELS = {
trialRequired: true,
section: 'workspace',
position: 5,
+ openInNewTab: true,
videoTutorial: 'https://vimeo.com/670904904',
},
mergeRequestCreated: {
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index 0e0c1475eda..48e360ce762 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -8,6 +8,7 @@ import createDefaultClient from '~/lib/graphql';
import initSourcegraph from '~/sourcegraph';
import ZenMode from '~/zen_mode';
import initAwardsApp from '~/emoji/awards_app';
+import MrWidgetHowToMergeModal from '~/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue';
import getStateQuery from './queries/get_state.query.graphql';
export default function initMergeRequestShow() {
@@ -19,22 +20,45 @@ export default function initMergeRequestShow() {
initAwardsApp(document.getElementById('js-vue-awards-block'));
const el = document.querySelector('.js-mr-status-box');
+ const { iid, issuableType, projectPath } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
// eslint-disable-next-line no-new
new Vue({
el,
+ name: 'IssuableStatusBoxRoot',
apolloProvider,
provide: {
query: getStateQuery,
- projectPath: el.dataset.projectPath,
- iid: el.dataset.iid,
+ iid,
+ projectPath,
},
render(h) {
return h(StatusBox, {
props: {
initialState: el.dataset.state,
+ issuableType,
+ },
+ });
+ },
+ });
+
+ const modalEl = document.getElementById('js-check-out-modal');
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: modalEl,
+ render(h) {
+ return h(MrWidgetHowToMergeModal, {
+ props: {
+ canMerge: modalEl.dataset.canMerge === 'true',
+ isFork: modalEl.dataset.isFork === 'true',
+ sourceBranch: modalEl.dataset.sourceBranch,
+ sourceProjectPath: modalEl.dataset.sourceProjectPath,
+ targetBranch: modalEl.dataset.targetBranch,
+ sourceProjectDefaultUrl: modalEl.dataset.sourceProjectDefaultUrl,
+ reviewingDocsPath: modalEl.dataset.reviewingDocsPath,
},
});
},
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index 4f57e1308df..032e2410233 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -65,6 +65,7 @@ export default class Project {
const fieldName = $dropdown.data('fieldName');
const shouldVisit = Boolean($dropdown.data('visit'));
const $form = $dropdown.closest('form');
+ const path = $form.find('#path').val();
const action = $form.attr('action');
const linkTarget = mergeUrlParams(serializeForm($form[0]), action);
@@ -116,20 +117,21 @@ export default class Project {
},
clicked(options) {
const { e } = options;
- e.preventDefault();
- // Since this page does not reload when changing directories in a repo
- // the rendered links do not have the path to the current directory.
- // This updates the path based on the current url and then opens
- // the the url with the updated path parameter.
- if (shouldVisit) {
+ if (!shouldVisit) {
+ e.preventDefault();
+ }
+
+ // Some pages need to dynamically get the current path
+ // so they can opt-in to JS getting the path from the
+ // current URL by not setting a path in the dropdown form
+ if (shouldVisit && path === undefined) {
+ e.preventDefault();
+
const selectedUrl = new URL(e.target.href);
const loc = window.location.href;
if (loc.includes('/-/')) {
- // Since the current ref in renderRow is outdated on page changes
- // (To be addressed in: https://gitlab.com/gitlab-org/gitlab/-/issues/327085)
- // We are deciphering the current ref from the dropdown data instead
const currentRef = $dropdown.data('ref');
// The split and startWith is to ensure an exact word match
// and avoid partial match ie. currentRef is "dev" and loc is "development"
diff --git a/app/assets/javascripts/pages/projects/serverless/index.js b/app/assets/javascripts/pages/projects/serverless/index.js
deleted file mode 100644
index 9ae81b327b1..00000000000
--- a/app/assets/javascripts/pages/projects/serverless/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import ServerlessBundle from '~/serverless/serverless_bundle';
-
-new ServerlessBundle(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/services/edit/index.js b/app/assets/javascripts/pages/projects/services/edit/index.js
index 2048d3dfc37..64df0d07d74 100644
--- a/app/assets/javascripts/pages/projects/services/edit/index.js
+++ b/app/assets/javascripts/pages/projects/services/edit/index.js
@@ -1,5 +1,4 @@
import initIntegrationSettingsForm from '~/integrations/edit';
-import PrometheusAlerts from '~/prometheus_alerts';
import CustomMetrics from '~/prometheus_metrics/custom_metrics';
initIntegrationSettingsForm();
@@ -10,5 +9,3 @@ if (prometheusSettingsWrapper) {
const customMetrics = new CustomMetrics(prometheusSettingsSelector);
customMetrics.init();
}
-
-PrometheusAlerts();
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index 71c6773c176..e2b1a702560 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -1,8 +1,5 @@
-import initTree from 'ee_else_ce/repository';
-import Activities from '~/activities';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import { BlobViewer } from '~/blob/viewer';
-import { initUploadForm } from '~/blob_edit/blob_bundle';
+
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import leaveByUrl from '~/namespaces/leave_by_url';
@@ -10,33 +7,38 @@ import initVueNotificationsDropdown from '~/notifications';
import Star from '~/projects/star';
import { initUploadFileTrigger } from '~/projects/upload_file';
import initReadMore from '~/read_more';
-import UserCallout from '~/user_callout';
-
-initReadMore();
-new Star(); // eslint-disable-line no-new
-
-// eslint-disable-next-line no-new
-new UserCallout({
- setCalloutPerProject: false,
- className: 'js-autodevops-banner',
-});
// Project show page loads different overview content based on user preferences
-
if (document.querySelector('.js-upload-blob-form')) {
- initUploadForm();
+ import(/* webpackChunkName: 'blobBundle' */ '~/blob_edit/blob_bundle')
+ .then(({ initUploadForm }) => {
+ initUploadForm();
+ })
+ .catch(() => {});
}
if (document.getElementById('js-tree-list')) {
- initTree();
+ import(/* webpackChunkName: 'treeList' */ 'ee_else_ce/repository')
+ .then(({ default: initTree }) => {
+ initTree();
+ })
+ .catch(() => {});
}
if (document.querySelector('.blob-viewer')) {
- new BlobViewer(); // eslint-disable-line no-new
+ import(/* webpackChunkName: 'blobViewer' */ '~/blob/viewer')
+ .then(({ BlobViewer }) => {
+ new BlobViewer(); // eslint-disable-line no-new
+ })
+ .catch(() => {});
}
if (document.querySelector('.project-show-activity')) {
- new Activities(); // eslint-disable-line no-new
+ import(/* webpackChunkName: 'activitiesList' */ '~/activities')
+ .then(({ default: Activities }) => {
+ new Activities(); // eslint-disable-line no-new
+ })
+ .catch(() => {});
}
leaveByUrl('project');
@@ -48,3 +50,18 @@ new ShortcutsNavigation(); // eslint-disable-line no-new
initUploadFileTrigger();
initInviteMembersModal();
initInviteMembersTrigger();
+
+initReadMore();
+new Star(); // eslint-disable-line no-new
+
+if (document.querySelector('.js-autodevops-banner')) {
+ import(/* webpackChunkName: 'userCallOut' */ '~/user_callout')
+ .then(({ default: UserCallout }) => {
+ // eslint-disable-next-line no-new
+ new UserCallout({
+ setCalloutPerProject: false,
+ className: 'js-autodevops-banner',
+ });
+ })
+ .catch(() => {});
+}
diff --git a/app/assets/javascripts/pages/projects/wikis/edit/index.js b/app/assets/javascripts/pages/projects/wikis/edit/index.js
deleted file mode 100644
index b2288c2655c..00000000000
--- a/app/assets/javascripts/pages/projects/wikis/edit/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import { mountApplications } from '~/pages/shared/wikis/edit';
-
-mountApplications();
diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js
index 83fcd348ddf..692baee383b 100644
--- a/app/assets/javascripts/pages/projects/wikis/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/index.js
@@ -1,3 +1,6 @@
import Wikis from '~/pages/shared/wikis/wikis';
+import { mountApplications } from '~/pages/shared/wikis/async_edit';
+
+mountApplications();
export default new Wikis();
diff --git a/app/assets/javascripts/pages/projects/wikis/show/index.js b/app/assets/javascripts/pages/projects/wikis/show/index.js
index 7ca5f6964cd..288f6b616cc 100644
--- a/app/assets/javascripts/pages/projects/wikis/show/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/show/index.js
@@ -1,5 +1,3 @@
import { mountApplications } from '~/pages/shared/wikis/show';
-import { mountApplications as mountEditApplications } from '~/pages/shared/wikis/async_edit';
mountApplications();
-mountEditApplications();
diff --git a/app/assets/javascripts/pages/registrations/new/index.js b/app/assets/javascripts/pages/registrations/new/index.js
index 8bbe81a9ed5..94a5c1cb29b 100644
--- a/app/assets/javascripts/pages/registrations/new/index.js
+++ b/app/assets/javascripts/pages/registrations/new/index.js
@@ -3,9 +3,14 @@ import { trackNewRegistrations } from '~/google_tag_manager';
import NoEmojiValidator from '~/emoji/no_emoji_validator';
import LengthValidator from '~/pages/sessions/new/length_validator';
import UsernameValidator from '~/pages/sessions/new/username_validator';
+import Tracking from '~/tracking';
new UsernameValidator(); // eslint-disable-line no-new
new LengthValidator(); // eslint-disable-line no-new
new NoEmojiValidator(); // eslint-disable-line no-new
trackNewRegistrations();
+
+Tracking.enableFormTracking({
+ forms: { allow: ['new_user'] },
+});
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index dee832c01d5..100ffc0664b 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -13,6 +13,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-approaching-seats-count-threshold',
'.js-storage-enforcement-banner',
'.js-user-over-limit-free-plan-alert',
+ '.js-minute-limit-banner',
];
const initCallouts = () => {
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
index 8536db78dfb..d9da238358f 100644
--- a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
@@ -54,15 +54,15 @@ export default {
return {
message: this.defaultMessage,
openMergeRequest: false,
- targetBranch: this.currentBranch,
+ sourceBranch: this.currentBranch,
};
},
computed: {
isCommitFormFilledOut() {
- return this.message && this.targetBranch;
+ return this.message && this.sourceBranch;
},
- isCurrentBranchTarget() {
- return this.targetBranch === this.currentBranch;
+ isCurrentBranchSourceBranch() {
+ return this.sourceBranch === this.currentBranch;
},
isSubmitDisabled() {
return !this.isCommitFormFilledOut || (!this.hasUnsavedChanges && !this.isNewCiConfigFile);
@@ -79,7 +79,7 @@ export default {
onSubmit() {
this.$emit('submit', {
message: this.message,
- targetBranch: this.targetBranch,
+ sourceBranch: this.sourceBranch,
openMergeRequest: this.openMergeRequest,
});
},
@@ -93,7 +93,7 @@ export default {
},
i18n: {
commitMessage: __('Commit message'),
- targetBranch: __('Target Branch'),
+ sourceBranch: __('Branch'),
startMergeRequest: __('Start a %{new_merge_request} with these changes'),
newMergeRequest: __('new merge request'),
commitChanges: __('Commit changes'),
@@ -120,20 +120,20 @@ export default {
/>
</gl-form-group>
<gl-form-group
- id="target-branch-group"
- :label="$options.i18n.targetBranch"
+ id="source-branch-group"
+ :label="$options.i18n.sourceBranch"
label-cols-sm="2"
- label-for="target-branch-field"
+ label-for="source-branch-field"
>
<gl-form-input
- id="target-branch-field"
- v-model="targetBranch"
+ id="source-branch-field"
+ v-model="sourceBranch"
class="gl-font-monospace!"
required
- data-qa-selector="target_branch_field"
+ data-qa-selector="source_branch_field"
/>
<gl-form-checkbox
- v-if="!isCurrentBranchTarget"
+ v-if="!isCurrentBranchSourceBranch"
v-model="openMergeRequest"
data-testid="new-mr-checkbox"
data-qa-selector="new_mr_checkbox"
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
index 4ef598d6ff3..9cbf60b1c8f 100644
--- a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
@@ -75,7 +75,7 @@ export default {
},
},
methods: {
- async onCommitSubmit({ message, targetBranch, openMergeRequest }) {
+ async onCommitSubmit({ message, sourceBranch, openMergeRequest }) {
this.isSaving = true;
try {
@@ -88,7 +88,7 @@ export default {
variables: {
action: this.action,
projectPath: this.projectFullPath,
- branch: targetBranch,
+ branch: sourceBranch,
startBranch: this.currentBranch,
message,
filePath: this.ciConfigPath,
@@ -104,12 +104,11 @@ export default {
if (errors?.length) {
this.$emit('showError', { type: COMMIT_FAILURE, reasons: errors });
} else {
- const commitBranch = targetBranch;
const params = openMergeRequest
? {
type: COMMIT_SUCCESS_WITH_REDIRECT,
params: {
- sourceBranch: commitBranch,
+ sourceBranch,
targetBranch: this.currentBranch,
},
}
@@ -119,10 +118,10 @@ export default {
...params,
});
- this.updateLastCommitBranch(targetBranch);
- this.updateCurrentBranch(targetBranch);
+ this.updateLastCommitBranch(sourceBranch);
+ this.updateCurrentBranch(sourceBranch);
- if (this.currentBranch === targetBranch) {
+ if (this.currentBranch === sourceBranch) {
this.$emit('updateCommitSha');
}
}
diff --git a/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue b/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue
index 853e839a7ab..42e2d34fa3a 100644
--- a/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue
+++ b/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue
@@ -46,7 +46,9 @@ export default {
:value="mergedYaml"
:file-name="ciConfigPath"
:file-global-id="fileGlobalId"
- :editor-options="{ readOnly: true }"
+ :editor-options="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ readOnly: true,
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
v-on="$listeners"
/>
</div>
diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
index 83b074dd55c..58df98d0fb7 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
+++ b/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
@@ -1,26 +1,74 @@
<script>
+import { GlButton } from '@gitlab/ui';
+import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { EDITOR_APP_STATUS_EMPTY, EDITOR_APP_STATUS_LOADING } from '../../constants';
+import FileTreePopover from '../popovers/file_tree_popover.vue';
import BranchSwitcher from './branch_switcher.vue';
export default {
components: {
BranchSwitcher,
+ FileTreePopover,
+ GlButton,
},
+ mixins: [glFeatureFlagMixin()],
props: {
hasUnsavedChanges: {
type: Boolean,
required: false,
default: false,
},
+ isNewCiConfigFile: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
shouldLoadNewBranch: {
type: Boolean,
required: false,
default: false,
},
},
+ apollo: {
+ appStatus: {
+ query: getAppStatus,
+ update(data) {
+ return data.app.status;
+ },
+ },
+ },
+ computed: {
+ isAppLoading() {
+ return this.appStatus === EDITOR_APP_STATUS_LOADING;
+ },
+ showFileTreeToggle() {
+ return (
+ this.glFeatures.pipelineEditorFileTree &&
+ !this.isNewCiConfigFile &&
+ this.appStatus !== EDITOR_APP_STATUS_EMPTY
+ );
+ },
+ },
+ methods: {
+ onFileTreeBtnClick() {
+ this.$emit('toggle-file-tree');
+ },
+ },
};
</script>
<template>
<div class="gl-mb-4">
+ <gl-button
+ v-if="showFileTreeToggle"
+ id="file-tree-toggle"
+ icon="file-tree"
+ data-testid="file-tree-toggle"
+ :aria-label="__('File Tree')"
+ :loading="isAppLoading"
+ @click="onFileTreeBtnClick"
+ />
+ <file-tree-popover v-if="showFileTreeToggle" />
<branch-switcher
:has-unsaved-changes="hasUnsavedChanges"
:should-load-new-branch="shouldLoadNewBranch"
diff --git a/app/assets/javascripts/pipeline_editor/components/file_tree/container.vue b/app/assets/javascripts/pipeline_editor/components/file_tree/container.vue
new file mode 100644
index 00000000000..280cd729a43
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/file_tree/container.vue
@@ -0,0 +1,78 @@
+<script>
+import { GlAlert, GlTooltipDirective } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import { FILE_TREE_TIP_DISMISSED_KEY } from '../../constants';
+import FileItem from './file_item.vue';
+
+const i18n = {
+ tipBtn: __('Learn more'),
+ tipDescription: s__(
+ 'PipelineEditorFileTree|When you use the include keyword to add pipeline configuration from files in the project, those files will be listed here.',
+ ),
+ tipTitle: s__('PipelineEditorFileTree|Configuration files added with the include keyword'),
+};
+
+export default {
+ i18n,
+ name: 'PipelineEditorFileTreeContainer',
+ components: {
+ FileIcon,
+ FileItem,
+ GlAlert,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: ['ciConfigPath', 'includesHelpPagePath'],
+ props: {
+ includes: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ canShowTip: localStorage.getItem(FILE_TREE_TIP_DISMISSED_KEY) !== 'true',
+ };
+ },
+ computed: {
+ showTip() {
+ return this.includes.length === 0 && this.canShowTip;
+ },
+ },
+ methods: {
+ dismissTip() {
+ this.canShowTip = false;
+ localStorage.setItem(FILE_TREE_TIP_DISMISSED_KEY, 'true');
+ },
+ },
+};
+</script>
+<template>
+ <aside class="file-tree-container gl-mr-5 gl-mb-5">
+ <div
+ v-gl-tooltip
+ :title="ciConfigPath"
+ class="gl-bg-gray-50 gl-py-2 gl-px-3 gl-mb-3 gl-rounded-base"
+ >
+ <span class="file-row-name gl-str-truncated" :title="ciConfigPath">
+ <file-icon class="file-row-icon" :file-name="ciConfigPath" />
+ <span data-testid="current-config-filename">{{ ciConfigPath }}</span>
+ </span>
+ </div>
+ <gl-alert
+ v-if="showTip"
+ variant="tip"
+ :title="$options.i18n.tipTitle"
+ :secondary-button-text="$options.i18n.tipBtn"
+ :secondary-button-link="includesHelpPagePath"
+ @dismiss="dismissTip"
+ >
+ {{ $options.i18n.tipDescription }}
+ </gl-alert>
+ <div class="gl-overflow-y-auto">
+ <file-item v-for="file in includes" :key="file.location" :file="file" />
+ </div>
+ </aside>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/file_tree/file_item.vue b/app/assets/javascripts/pipeline_editor/components/file_tree/file_item.vue
new file mode 100644
index 00000000000..786d483b5b9
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/file_tree/file_item.vue
@@ -0,0 +1,45 @@
+<script>
+import { GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+
+export default {
+ name: 'PipelineEditorFileItem',
+ components: {
+ FileIcon,
+ GlIcon,
+ GlLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ fileName() {
+ return this.file.location;
+ },
+ filePath() {
+ return this.file.blob || this.file.raw;
+ },
+ },
+};
+</script>
+<template>
+ <gl-link
+ v-gl-tooltip
+ :href="filePath"
+ :title="fileName"
+ target="_blank"
+ class="file-tree-includes-link gl-display-flex gl-justify-content-space-between gl-hover-bg-gray-50 gl-text-body gl-hover-text-gray-900 gl-hover-text-decoration-none gl-py-2 gl-px-3 gl-rounded-base"
+ >
+ <span class="file-row-name gl-str-truncated" :title="fileName">
+ <file-icon class="file-row-icon" :file-name="fileName" />
+ <span>{{ fileName }}</span>
+ </span>
+ <gl-icon class="gl-display-none gl-relative gl-text-gray-500" name="external-link" />
+ </gl-link>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
index 25a78aab933..7beabcfe403 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
@@ -47,6 +47,12 @@ export default {
downstreamPipelines() {
return this.linkedPipelines?.downstream?.nodes || [];
},
+ hasDownstreamPipelines() {
+ return this.downstreamPipelines.length > 0;
+ },
+ hasPipelineStages() {
+ return this.pipelineStages.length > 0;
+ },
pipelinePath() {
return this.pipeline.detailedStatus?.detailsPath || '';
},
@@ -73,9 +79,6 @@ export default {
};
});
},
- showDownstreamPipelines() {
- return this.downstreamPipelines.length > 0;
- },
upstreamPipeline() {
return this.linkedPipelines?.upstream;
},
@@ -84,15 +87,20 @@ export default {
</script>
<template>
- <div v-if="pipelineStages.length > 0" class="stage-cell gl-mr-5">
+ <div
+ v-if="hasPipelineStages"
+ class="gl-align-items-center gl-display-inline-flex gl-flex-wrap stage-cell gl-mr-5"
+ >
<linked-pipelines-mini-list
v-if="upstreamPipeline"
- :triggered-by="[upstreamPipeline]"
+ :triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
+ upstreamPipeline,
+ ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
data-testid="pipeline-editor-mini-graph-upstream"
/>
- <pipeline-mini-graph class="gl-display-inline" :stages="pipelineStages" />
+ <pipeline-mini-graph :stages="pipelineStages" />
<linked-pipelines-mini-list
- v-if="showDownstreamPipelines"
+ v-if="hasDownstreamPipelines"
:triggered="downstreamPipelines"
:pipeline-path="pipelinePath"
data-testid="pipeline-editor-mini-graph-downstream"
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
index d50e6f9a623..da31fc62d09 100644
--- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -23,7 +23,7 @@ import CiEditorHeader from './editor/ci_editor_header.vue';
import TextEditor from './editor/text_editor.vue';
import CiLint from './lint/ci_lint.vue';
import EditorTab from './ui/editor_tab.vue';
-import WalkthroughPopover from './walkthrough_popover.vue';
+import WalkthroughPopover from './popovers/walkthrough_popover.vue';
export default {
i18n: {
diff --git a/app/assets/javascripts/pipeline_editor/components/popovers/file_tree_popover.vue b/app/assets/javascripts/pipeline_editor/components/popovers/file_tree_popover.vue
new file mode 100644
index 00000000000..6270429535d
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/popovers/file_tree_popover.vue
@@ -0,0 +1,59 @@
+<script>
+import { GlLink, GlPopover, GlOutsideDirective as Outside, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { FILE_TREE_POPOVER_DISMISSED_KEY } from '../../constants';
+
+export default {
+ name: 'PipelineEditorFileTreePopover',
+ directives: { Outside },
+ i18n: {
+ description: s__(
+ 'pipelineEditorWalkthrough|You can use the file tree to view your pipeline configuration files. %{linkStart}Learn more%{linkEnd}',
+ ),
+ },
+ components: {
+ GlLink,
+ GlPopover,
+ GlSprintf,
+ },
+ inject: ['includesHelpPagePath'],
+ data() {
+ return {
+ showPopover: false,
+ };
+ },
+ mounted() {
+ this.showPopover = localStorage.getItem(FILE_TREE_POPOVER_DISMISSED_KEY) !== 'true';
+ },
+ methods: {
+ closePopover() {
+ this.showPopover = false;
+ },
+ dismissPermanently() {
+ this.closePopover();
+ localStorage.setItem(FILE_TREE_POPOVER_DISMISSED_KEY, 'true');
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-popover
+ v-if="showPopover"
+ show
+ show-close-button
+ target="file-tree-toggle"
+ triggers="manual"
+ placement="right"
+ data-qa-selector="file_tree_popover"
+ @close-button-clicked="dismissPermanently"
+ >
+ <div v-outside="closePopover" class="gl-font-base gl-mb-3">
+ <gl-sprintf :message="$options.i18n.description">
+ <template #link="{ content }">
+ <gl-link :href="includesHelpPagePath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ </gl-popover>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/walkthrough_popover.vue b/app/assets/javascripts/pipeline_editor/components/popovers/walkthrough_popover.vue
index 5742b11b841..c636d8b8e34 100644
--- a/app/assets/javascripts/pipeline_editor/components/walkthrough_popover.vue
+++ b/app/assets/javascripts/pipeline_editor/components/popovers/walkthrough_popover.vue
@@ -76,7 +76,7 @@ export default {
@click="handleClickCta"
>
<gl-emoji data-name="rocket" />
- {{ this.$options.i18n.ctaText }}
+ {{ $options.i18n.ctaText }}
</gl-button>
</div>
</gl-popover>
diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js
index 9b4732b26d2..ff7c742f588 100644
--- a/app/assets/javascripts/pipeline_editor/constants.js
+++ b/app/assets/javascripts/pipeline_editor/constants.js
@@ -49,6 +49,10 @@ export const BRANCH_PAGINATION_LIMIT = 20;
export const BRANCH_SEARCH_DEBOUNCE = '500';
export const SOURCE_EDITOR_DEBOUNCE = 500;
+export const FILE_TREE_DISPLAY_KEY = 'pipeline_editor_file_tree_display';
+export const FILE_TREE_POPOVER_DISMISSED_KEY = 'pipeline_editor_file_tree_popover_dismissed';
+export const FILE_TREE_TIP_DISMISSED_KEY = 'pipeline_editor_file_tree_tip_dismissed';
+
export const STARTER_TEMPLATE_NAME = 'Getting-Started';
export const pipelineEditorTrackingOptions = {
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql
index df7de6a1f54..5354ed7c2d5 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql
@@ -3,6 +3,12 @@
query getCiConfigData($projectPath: ID!, $sha: String, $content: String!) {
ciConfig(projectPath: $projectPath, sha: $sha, content: $content) {
errors
+ includes {
+ location
+ type
+ blob
+ raw
+ }
mergedYaml
status
stages {
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index 732fc665c9e..e13d9cf9df0 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -30,6 +30,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
defaultBranch,
emptyStateIllustrationPath,
helpPaths,
+ includesHelpPagePath,
lintHelpPagePath,
lintUnavailableHelpPagePath,
needsHelpPagePath,
@@ -41,7 +42,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
runnerHelpPagePath,
totalBranches,
ymlHelpPagePath,
- } = el?.dataset;
+ } = el.dataset;
const configurationPaths = Object.fromEntries(
Object.entries(CODE_SNIPPET_SOURCE_SETTINGS).map(([source, { datasetKey }]) => [
@@ -118,6 +119,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
defaultBranch,
emptyStateIllustrationPath,
helpPaths,
+ includesHelpPagePath,
lintHelpPagePath,
lintUnavailableHelpPagePath,
needsHelpPagePath,
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index 4e6a4ffa6d2..3fd31edec2c 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -104,6 +104,7 @@ export default {
this.setAppStatus(EDITOR_APP_STATUS_EMPTY);
}
+ this.isNewCiConfigFile = false;
if (!hasCIFile) {
if (this.shouldSkipStartScreen) {
this.setNewEmptyCiConfigFile();
@@ -381,7 +382,7 @@ export default {
</script>
<template>
- <div class="gl-mt-4 gl-relative">
+ <div class="gl-mt-4 gl-relative" data-qa-selector="pipeline_editor_app">
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
<pipeline-editor-empty-state
v-else-if="showStartScreen"
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
index 23e3ce10d5a..59022a91322 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
@@ -1,12 +1,14 @@
<script>
import { GlModal } from '@gitlab/ui';
import { __ } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import CommitSection from './components/commit/commit_section.vue';
import PipelineEditorDrawer from './components/drawer/pipeline_editor_drawer.vue';
import PipelineEditorFileNav from './components/file_nav/pipeline_editor_file_nav.vue';
+import PipelineEditorFileTree from './components/file_tree/container.vue';
import PipelineEditorHeader from './components/header/pipeline_editor_header.vue';
import PipelineEditorTabs from './components/pipeline_editor_tabs.vue';
-import { CREATE_TAB } from './constants';
+import { CREATE_TAB, FILE_TREE_DISPLAY_KEY } from './constants';
export default {
commitSectionRef: 'commitSectionRef',
@@ -28,9 +30,11 @@ export default {
GlModal,
PipelineEditorDrawer,
PipelineEditorFileNav,
+ PipelineEditorFileTree,
PipelineEditorHeader,
PipelineEditorTabs,
},
+ mixins: [glFeatureFlagMixin()],
props: {
ciConfigData: {
type: Object,
@@ -61,6 +65,7 @@ export default {
scrollToCommitForm: false,
shouldLoadNewBranch: false,
showDrawer: false,
+ showFileTree: false,
showSwitchBranchModal: false,
};
},
@@ -68,6 +73,15 @@ export default {
showCommitForm() {
return this.currentTab === CREATE_TAB;
},
+ includesFiles() {
+ return this.ciConfigData?.includes || [];
+ },
+ isFileTreeVisible() {
+ return this.showFileTree && this.glFeatures.pipelineEditorFileTree;
+ },
+ },
+ mounted() {
+ this.showFileTree = JSON.parse(localStorage.getItem(FILE_TREE_DISPLAY_KEY)) || false;
},
methods: {
closeBranchModal() {
@@ -82,6 +96,10 @@ export default {
openDrawer() {
this.showDrawer = true;
},
+ toggleFileTree() {
+ this.showFileTree = !this.showFileTree;
+ localStorage.setItem(FILE_TREE_DISPLAY_KEY, this.showFileTree);
+ },
switchBranch() {
this.showSwitchBranchModal = false;
this.shouldLoadNewBranch = true;
@@ -114,28 +132,39 @@ export default {
</gl-modal>
<pipeline-editor-file-nav
:has-unsaved-changes="hasUnsavedChanges"
+ :is-new-ci-config-file="isNewCiConfigFile"
:should-load-new-branch="shouldLoadNewBranch"
@select-branch="handleConfirmSwitchBranch"
+ @toggle-file-tree="toggleFileTree"
v-on="$listeners"
/>
- <pipeline-editor-header
- :ci-config-data="ciConfigData"
- :commit-sha="commitSha"
- :is-new-ci-config-file="isNewCiConfigFile"
- v-on="$listeners"
- />
- <pipeline-editor-tabs
- :ci-config-data="ciConfigData"
- :ci-file-content="ciFileContent"
- :commit-sha="commitSha"
- :is-new-ci-config-file="isNewCiConfigFile"
- :show-drawer="showDrawer"
- v-on="$listeners"
- @open-drawer="openDrawer"
- @close-drawer="closeDrawer"
- @set-current-tab="setCurrentTab"
- @walkthrough-popover-cta-clicked="setScrollToCommitForm"
- />
+ <div class="gl-display-flex gl-w-full gl-sm-flex-direction-column">
+ <pipeline-editor-file-tree
+ v-if="isFileTreeVisible"
+ class="gl-flex-shrink-0"
+ :includes="includesFiles"
+ />
+ <div class="gl-flex-grow-1 gl-min-w-0">
+ <pipeline-editor-header
+ :ci-config-data="ciConfigData"
+ :commit-sha="commitSha"
+ :is-new-ci-config-file="isNewCiConfigFile"
+ v-on="$listeners"
+ />
+ <pipeline-editor-tabs
+ :ci-config-data="ciConfigData"
+ :ci-file-content="ciFileContent"
+ :commit-sha="commitSha"
+ :is-new-ci-config-file="isNewCiConfigFile"
+ :show-drawer="showDrawer"
+ v-on="$listeners"
+ @open-drawer="openDrawer"
+ @close-drawer="closeDrawer"
+ @set-current-tab="setCurrentTab"
+ @walkthrough-popover-cta-clicked="setScrollToCommitForm"
+ />
+ </div>
+ </div>
<commit-section
v-if="showCommitForm"
:ref="$options.commitSectionRef"
diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
index 32e1e18b684..d84fc724d38 100644
--- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
@@ -313,7 +313,7 @@ export default {
errors = [],
warnings = [],
total_warnings: totalWarnings = 0,
- } = err?.response?.data;
+ } = err.response.data;
const [error] = errors;
this.reportError({
diff --git a/app/assets/javascripts/pipeline_new/index.js b/app/assets/javascripts/pipeline_new/index.js
index a645ea8603b..927eeb5e144 100644
--- a/app/assets/javascripts/pipeline_new/index.js
+++ b/app/assets/javascripts/pipeline_new/index.js
@@ -17,7 +17,7 @@ export default () => {
fileParam,
settingsLink,
maxWarnings,
- } = el?.dataset;
+ } = el.dataset;
const variableParams = JSON.parse(varParam);
const fileParams = JSON.parse(fileParam);
diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/pipelines/components/dag/dag.vue
index 16fb931ec2b..475dd3bf36e 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag.vue
+++ b/app/assets/javascripts/pipelines/components/dag/dag.vue
@@ -238,7 +238,7 @@ export default {
</div>
</template>
<template v-if="dagDocPath" #actions>
- <gl-button :href="dagDocPath" target="__blank" variant="success">
+ <gl-button :href="dagDocPath" target="_blank" variant="confirm">
{{ $options.emptyStateTexts.button }}
</gl-button>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js
index 0b59612b25c..85ca52f633e 100644
--- a/app/assets/javascripts/pipelines/components/graph/constants.js
+++ b/app/assets/javascripts/pipelines/components/graph/constants.js
@@ -15,4 +15,8 @@ export const VIEW_TYPE_KEY = 'pipeline_graph_view_type';
export const SINGLE_JOB = 'single_job';
export const JOB_DROPDOWN = 'job_dropdown';
+export const BUILD_KIND = 'BUILD';
+export const BRIDGE_KIND = 'BRIDGE';
+
+export const ACTION_FAILURE = 'action_failure';
export const IID_FAILURE = 'missing_iid';
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 015f0519c72..31a34ab4fb5 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -233,6 +233,7 @@ export default {
:view-type="viewType"
@downstreamHovered="setSourceJob"
@pipelineExpandToggle="togglePipelineExpanded"
+ @refreshPipelineGraph="$emit('refreshPipelineGraph')"
@scrollContainer="slidePipelineContainer"
@error="onError"
/>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
index 534ad25a35d..f822e2c0874 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
@@ -8,7 +8,7 @@ import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants';
import DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql';
import getPipelineQuery from '../../graphql/queries/get_pipeline_header_data.query.graphql';
import { reportToSentry, reportMessageToSentry } from '../../utils';
-import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants';
+import { ACTION_FAILURE, IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants';
import PipelineGraph from './graph_component.vue';
import GraphViewSelector from './graph_view_selector.vue';
import {
@@ -57,13 +57,29 @@ export default {
showLinks: false,
};
},
- errorTexts: {
- [DRAW_FAILURE]: __('An error occurred while drawing job relationship links.'),
- [IID_FAILURE]: __(
- 'The data in this pipeline is too old to be rendered as a graph. Please check the Jobs tab to access historical data.',
- ),
- [LOAD_FAILURE]: __('We are currently unable to fetch data for this pipeline.'),
- [DEFAULT]: __('An unknown error occurred while loading this graph.'),
+ errors: {
+ [ACTION_FAILURE]: {
+ text: __('An error occurred while performing this action.'),
+ variant: 'danger',
+ },
+ [DRAW_FAILURE]: {
+ text: __('An error occurred while drawing job relationship links.'),
+ variant: 'danger',
+ },
+ [IID_FAILURE]: {
+ text: __(
+ 'The data in this pipeline is too old to be rendered as a graph. Please check the Jobs tab to access historical data.',
+ ),
+ variant: 'info',
+ },
+ [LOAD_FAILURE]: {
+ text: __('Currently unable to fetch data for this pipeline.'),
+ variant: 'danger',
+ },
+ [DEFAULT]: {
+ text: __('An unknown error occurred while loading this graph.'),
+ variant: 'danger',
+ },
},
apollo: {
callouts: {
@@ -154,28 +170,12 @@ export default {
},
computed: {
alert() {
- switch (this.alertType) {
- case DRAW_FAILURE:
- return {
- text: this.$options.errorTexts[DRAW_FAILURE],
- variant: 'danger',
- };
- case IID_FAILURE:
- return {
- text: this.$options.errorTexts[IID_FAILURE],
- variant: 'info',
- };
- case LOAD_FAILURE:
- return {
- text: this.$options.errorTexts[LOAD_FAILURE],
- variant: 'danger',
- };
- default:
- return {
- text: this.$options.errorTexts[DEFAULT],
- variant: 'danger',
- };
- }
+ const { errors } = this.$options;
+
+ return {
+ text: errors[this.alertType]?.text ?? errors[DEFAULT].text,
+ variant: errors[this.alertType]?.variant ?? errors[DEFAULT].variant,
+ };
},
configPaths() {
return {
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index f69b25dfa7c..362571930d6 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import { GlBadge, GlLink, GlTooltipDirective } from '@gitlab/ui';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { sprintf, __ } from '~/locale';
@@ -7,7 +7,7 @@ import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '../../utils';
import ActionComponent from '../jobs_shared/action_component.vue';
import JobNameComponent from '../jobs_shared/job_name_component.vue';
-import { SINGLE_JOB } from './constants';
+import { BRIDGE_KIND, SINGLE_JOB } from './constants';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
@@ -35,11 +35,16 @@ import { SINGLE_JOB } from './constants';
*/
export default {
+ i18n: {
+ bridgeBadgeText: __('Trigger job'),
+ unauthorizedTooltip: __('You are not authorized to run this manual job'),
+ },
hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500',
components: {
ActionComponent,
CiIcon,
JobNameComponent,
+ GlBadge,
GlLink,
},
directives: {
@@ -113,6 +118,12 @@ export default {
isSingleItem() {
return this.type === SINGLE_JOB;
},
+ isBridge() {
+ return this.kind === BRIDGE_KIND;
+ },
+ kind() {
+ return this.job?.kind || '';
+ },
nameComponent() {
return this.hasDetails ? 'gl-link' : 'div';
},
@@ -187,6 +198,7 @@ export default {
[this.$options.hoverClass]:
this.relatedDownstreamHovered || this.relatedDownstreamExpanded,
},
+ { 'gl-rounded-lg': this.isBridge },
this.cssClassJobName,
];
},
@@ -213,9 +225,6 @@ export default {
this.$emit('pipelineActionRequestComplete');
},
},
- i18n: {
- unauthorizedTooltip: __('You are not authorized to run this manual job'),
- },
};
</script>
<template>
@@ -253,6 +262,9 @@ export default {
</div>
</div>
</div>
+ <gl-badge v-if="isBridge" class="gl-mt-3" variant="info" size="sm">
+ {{ $options.i18n.bridgeBadgeText }}
+ </gl-badge>
</component>
<action-component
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index d59802196af..9f76d4cec50 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -1,10 +1,22 @@
<script>
-import { GlBadge, GlButton, GlLink, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlBadge,
+ GlButton,
+ GlLink,
+ GlLoadingIcon,
+ GlTooltip,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
+import CancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
+import RetryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { PIPELINE_GRAPHQL_TYPE } from '../../constants';
import { reportToSentry } from '../../utils';
-import { DOWNSTREAM, UPSTREAM } from './constants';
+import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from './constants';
export default {
directives: {
@@ -16,7 +28,14 @@ export default {
GlButton,
GlLink,
GlLoadingIcon,
+ GlTooltip,
},
+ styles: {
+ actionSizeClasses: ['gl-h-7 gl-w-7'],
+ flatLeftBorder: ['gl-rounded-bottom-left-none!', 'gl-rounded-top-left-none!'],
+ flatRightBorder: ['gl-rounded-bottom-right-none!', 'gl-rounded-top-right-none!'],
+ },
+ mixins: [glFeatureFlagMixin()],
props: {
columnTitle: {
type: String,
@@ -39,15 +58,44 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ hasActionTooltip: false,
+ isActionLoading: false,
+ };
+ },
computed: {
- buttonBorderClass() {
- return this.isUpstream ? 'gl-border-r-1!' : 'gl-border-l-1!';
+ action() {
+ if (this.glFeatures?.downstreamRetryAction && this.isDownstream) {
+ if (this.isCancelable) {
+ return {
+ icon: 'cancel',
+ method: this.cancelPipeline,
+ ariaLabel: __('Cancel downstream pipeline'),
+ };
+ } else if (this.isRetryable) {
+ return {
+ icon: 'retry',
+ method: this.retryPipeline,
+ ariaLabel: __('Retry downstream pipeline'),
+ };
+ }
+ }
+
+ return {};
+ },
+ buttonBorderClasses() {
+ return this.isUpstream
+ ? ['gl-border-r-0!', ...this.$options.styles.flatRightBorder]
+ : ['gl-border-l-0!', ...this.$options.styles.flatLeftBorder];
},
buttonId() {
return `js-linked-pipeline-${this.pipeline.id}`;
},
- cardSpacingClass() {
- return this.isDownstream ? 'gl-pr-0' : '';
+ cardClasses() {
+ return this.isDownstream
+ ? this.$options.styles.flatRightBorder
+ : this.$options.styles.flatLeftBorder;
},
expandedIcon() {
if (this.isUpstream) {
@@ -64,9 +112,21 @@ export default {
flexDirection() {
return this.isUpstream ? 'gl-flex-direction-row-reverse' : 'gl-flex-direction-row';
},
+ graphqlPipelineId() {
+ return convertToGraphQLId(PIPELINE_GRAPHQL_TYPE, this.pipeline.id);
+ },
+ hasUpdatePipelinePermissions() {
+ return Boolean(this.pipeline?.userPermissions?.updatePipeline);
+ },
+ isCancelable() {
+ return Boolean(this.pipeline?.cancelable && this.hasUpdatePipelinePermissions);
+ },
isDownstream() {
return this.type === DOWNSTREAM;
},
+ isRetryable() {
+ return Boolean(this.pipeline?.retryable && this.hasUpdatePipelinePermissions);
+ },
isSameProject() {
return !this.pipeline.multiproject;
},
@@ -93,13 +153,19 @@ export default {
projectName() {
return this.pipeline.project.name;
},
+ showAction() {
+ return Boolean(this.action?.method && this.action?.icon && this.action?.ariaLabel);
+ },
+ showCardTooltip() {
+ return !this.hasActionTooltip;
+ },
sourceJobName() {
return this.pipeline.sourceJob?.name ?? '';
},
sourceJobInfo() {
return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : '';
},
- tooltipText() {
+ cardTooltipText() {
return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} -
${this.sourceJobInfo}`;
},
@@ -108,6 +174,26 @@ export default {
reportToSentry('linked_pipeline', `error: ${err}, info: ${info}`);
},
methods: {
+ cancelPipeline() {
+ this.executePipelineAction(CancelPipelineMutation);
+ },
+ async executePipelineAction(mutation) {
+ try {
+ this.isActionLoading = true;
+
+ await this.$apollo.mutate({
+ mutation,
+ variables: {
+ id: this.graphqlPipelineId,
+ },
+ });
+ this.$emit('refreshPipelineGraph');
+ } catch {
+ this.$emit('error', { type: ACTION_FAILURE });
+ } finally {
+ this.isActionLoading = false;
+ }
+ },
hideTooltips() {
this.$root.$emit(BV_HIDE_TOOLTIP);
},
@@ -122,6 +208,12 @@ export default {
onDownstreamHoverLeave() {
this.$emit('downstreamHovered', '');
},
+ retryPipeline() {
+ this.executePipelineAction(RetryPipelineMutation);
+ },
+ setActionTooltip(flag) {
+ this.hasActionTooltip = flag;
+ },
},
};
</script>
@@ -129,33 +221,48 @@ export default {
<template>
<div
ref="linkedPipeline"
- v-gl-tooltip
- class="gl-h-full gl-display-flex! gl-border-solid gl-border-gray-100 gl-border-1"
+ class="gl-h-full gl-display-flex!"
:class="flexDirection"
- :title="tooltipText"
data-qa-selector="linked_pipeline_container"
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
>
- <div class="gl-w-full gl-bg-white gl-p-3" :class="cardSpacingClass">
- <div class="gl-display-flex gl-pr-3">
- <ci-status
- v-if="!pipelineIsLoading"
- :status="pipelineStatus"
- :size="24"
- css-classes="gl-top-0 gl-pr-2"
- />
+ <gl-tooltip v-if="showCardTooltip" :target="() => $refs.linkedPipeline">
+ {{ cardTooltipText }}
+ </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-status v-if="!pipelineIsLoading" :status="pipelineStatus" :size="24" css-classes="" />
<div v-else class="gl-pr-3"><gl-loading-icon size="sm" inline /></div>
- <div class="gl-display-flex gl-flex-direction-column gl-downstream-pipeline-job-width">
+ <div
+ class="gl-display-flex gl-downstream-pipeline-job-width gl-flex-direction-column gl-line-height-normal"
+ >
<span class="gl-text-truncate" data-testid="downstream-title">
{{ downstreamTitle }}
</span>
<div class="gl-text-truncate">
- <gl-link class="gl-text-blue-500!" :href="pipeline.path" data-testid="pipelineLink"
+ <gl-link
+ class="gl-text-blue-500! gl-font-sm"
+ :href="pipeline.path"
+ data-testid="pipelineLink"
>#{{ pipeline.id }}</gl-link
>
</div>
</div>
+ <gl-button
+ v-if="showAction"
+ v-gl-tooltip
+ :title="action.ariaLabel"
+ :loading="isActionLoading"
+ :icon="action.icon"
+ class="gl-rounded-full!"
+ :class="$options.styles.actionSizeClasses"
+ :aria-label="action.ariaLabel"
+ @click="action.method"
+ @mouseover="setActionTooltip(true)"
+ @mouseout="setActionTooltip(false)"
+ />
+ <div v-else :class="$options.styles.actionSizeClasses"></div>
</div>
<div class="gl-pt-2">
<gl-badge size="sm" variant="info" data-testid="downstream-pipeline-label">
@@ -166,8 +273,8 @@ export default {
<div class="gl-display-flex">
<gl-button
:id="buttonId"
- class="gl-shadow-none! gl-rounded-0!"
- :class="`js-pipeline-expand-${pipeline.id} ${buttonBorderClass}`"
+ class="gl-border! gl-shadow-none! gl-rounded-lg!"
+ :class="[`js-pipeline-expand-${pipeline.id}`, buttonBorderClasses]"
:icon="expandedIcon"
:aria-label="__('Expand pipeline')"
data-testid="expand-pipeline-button"
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
index 3c1208afbf0..b06c2f15042 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -66,14 +66,13 @@ export default {
columnClass() {
const positionValues = {
right: 'gl-ml-6',
- left: 'gl-mr-6',
+ left: 'gl-mx-6',
};
+
return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
},
computedTitleClasses() {
- const positionalClasses = this.isUpstream
- ? ['gl-w-full', 'gl-text-right', 'gl-linked-pipeline-padding']
- : [];
+ const positionalClasses = this.isUpstream ? ['gl-w-full', 'gl-linked-pipeline-padding'] : [];
return [...this.$options.titleClasses, ...positionalClasses];
},
@@ -202,7 +201,7 @@ export default {
<li
v-for="pipeline in linkedPipelines"
:key="pipeline.id"
- class="gl-display-flex gl-mb-4"
+ class="gl-display-flex gl-mb-3"
:class="{ 'gl-flex-direction-row-reverse': isUpstream }"
>
<linked-pipeline
@@ -215,6 +214,7 @@ export default {
@downstreamHovered="onDownstreamHovered"
@pipelineClicked="onPipelineClick(pipeline)"
@pipelineExpandToggle="onPipelineExpandToggle"
+ @refreshPipelineGraph="$emit('refreshPipelineGraph')"
/>
<div
v-if="showContainer(pipeline.id)"
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index 04b78b8aa23..37878f3fb6d 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -93,6 +93,7 @@ export default {
data() {
return {
pipeline: null,
+ failureMessages: [],
failureType: null,
isCanceling: false,
isRetrying: false,
@@ -159,8 +160,9 @@ export default {
},
},
methods: {
- reportFailure(errorType) {
+ reportFailure(errorType, errorMessages = []) {
this.failureType = errorType;
+ this.failureMessages = errorMessages;
},
async postPipelineAction(name, mutation) {
try {
@@ -176,7 +178,7 @@ export default {
if (errors.length > 0) {
this.isRetrying = false;
- this.reportFailure(POST_FAILURE);
+ this.reportFailure(POST_FAILURE, errors);
} else {
await this.$apollo.queries.pipeline.refetch();
if (!this.isFinished) {
@@ -214,7 +216,7 @@ export default {
});
if (errors.length > 0) {
- this.reportFailure(DELETE_FAILURE);
+ this.reportFailure(DELETE_FAILURE, errors);
this.isDeleting = false;
} else {
redirectTo(setUrlFragment(this.paths.pipelinesPath, 'delete_success'));
@@ -231,9 +233,11 @@ export default {
</script>
<template>
<div class="js-pipeline-header-container">
- <gl-alert v-if="hasError" :variant="failure.variant" :dismissible="false">{{
- failure.text
- }}</gl-alert>
+ <gl-alert v-if="hasError" :title="failure.text" :variant="failure.variant" :dismissible="false">
+ <div v-for="(failureMessage, index) in failureMessages" :key="`failure-message-${index}`">
+ {{ failureMessage }}
+ </div>
+ </gl-alert>
<ci-header
v-if="shouldRenderContent"
:status="pipeline.detailedStatus"
@@ -261,6 +265,7 @@ export default {
v-if="canCancelPipeline"
:loading="isCanceling"
:disabled="isCanceling"
+ class="gl-ml-3"
variant="danger"
data-testid="cancelPipeline"
@click="cancelPipeline()"
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue
new file mode 100644
index 00000000000..9e886fd7a48
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue
@@ -0,0 +1,73 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import createFlash from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import GetFailedJobsQuery from '../../graphql/queries/get_failed_jobs.query.graphql';
+import { prepareFailedJobs } from './utils';
+import FailedJobsTable from './failed_jobs_table.vue';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ FailedJobsTable,
+ },
+ inject: {
+ fullPath: {
+ default: '',
+ },
+ pipelineIid: {
+ default: '',
+ },
+ },
+ props: {
+ failedJobsSummary: {
+ type: Array,
+ required: true,
+ },
+ },
+ apollo: {
+ failedJobs: {
+ query: GetFailedJobsQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ pipelineIid: this.pipelineIid,
+ };
+ },
+ update({ project }) {
+ if (project?.pipeline?.jobs?.nodes) {
+ return project.pipeline.jobs.nodes.map((job) => {
+ return { normalizedId: getIdFromGraphQLId(job.id), ...job };
+ });
+ }
+ return [];
+ },
+ result() {
+ this.preparedFailedJobs = prepareFailedJobs(this.failedJobs, this.failedJobsSummary);
+ },
+ error() {
+ createFlash({ message: s__('Jobs|There was a problem fetching the failed jobs.') });
+ },
+ },
+ },
+ data() {
+ return {
+ failedJobs: [],
+ preparedFailedJobs: [],
+ };
+ },
+ computed: {
+ loading() {
+ return this.$apollo.queries.failedJobs.loading;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon v-if="loading" size="lg" class="gl-mt-4" />
+ <failed-jobs-table v-else :failed-jobs="preparedFailedJobs" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
new file mode 100644
index 00000000000..1c646bdf3d6
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
@@ -0,0 +1,111 @@
+<script>
+import { GlButton, GlLink, GlSafeHtmlDirective, GlTableLite } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import createFlash from '~/flash';
+import { redirectTo } from '~/lib/utils/url_utility';
+import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import RetryFailedJobMutation from '../../graphql/mutations/retry_failed_job.mutation.graphql';
+import { DEFAULT_FIELDS } from '../../constants';
+
+export default {
+ fields: DEFAULT_FIELDS,
+ retry: __('Retry'),
+ components: {
+ CiBadge,
+ GlButton,
+ GlLink,
+ GlTableLite,
+ },
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
+ props: {
+ failedJobs: {
+ type: Array,
+ required: true,
+ },
+ },
+ methods: {
+ async retryJob(id) {
+ try {
+ const {
+ data: {
+ jobRetry: { errors, job },
+ },
+ } = await this.$apollo.mutate({
+ mutation: RetryFailedJobMutation,
+ variables: { id },
+ });
+ if (errors.length > 0) {
+ this.showErrorMessage();
+ } else {
+ redirectTo(job.detailedStatus.detailsPath);
+ }
+ } catch {
+ this.showErrorMessage();
+ }
+ },
+ canRetryJob(job) {
+ return job.retryable && job.userPermissions.updateBuild;
+ },
+ showErrorMessage() {
+ createFlash({ message: s__('Job|There was a problem retrying the failed job.') });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-table-lite :items="failedJobs" :fields="$options.fields" stacked="lg" fixed>
+ <template #table-colgroup="{ fields }">
+ <col v-for="field in fields" :key="field.key" :class="field.columnClass" />
+ </template>
+
+ <template #cell(name)="{ item }">
+ <div
+ class="gl-display-flex gl-align-items-center gl-lg-justify-content-start gl-justify-content-end"
+ >
+ <ci-badge :status="item.detailedStatus" :show-text="false" class="gl-mr-3" />
+ <div class="gl-text-truncate">
+ <gl-link
+ :href="item.detailedStatus.detailsPath"
+ class="gl-font-weight-bold gl-text-gray-900!"
+ >
+ {{ item.name }}
+ </gl-link>
+ </div>
+ </div>
+ </template>
+
+ <template #cell(stage)="{ item }">
+ <div class="gl-text-truncate">
+ <span>{{ item.stage.name }}</span>
+ </div>
+ </template>
+
+ <template #cell(failure)="{ item }">
+ <span>{{ item.failure }}</span>
+ </template>
+
+ <template #cell(actions)="{ item }">
+ <gl-button
+ v-if="canRetryJob(item)"
+ icon="repeat"
+ :title="$options.retry"
+ :aria-label="$options.retry"
+ @click="retryJob(item.id)"
+ />
+ </template>
+
+ <template #row-details="{ item }">
+ <pre
+ v-if="item.userPermissions.readBuild"
+ class="gl-w-full gl-text-left gl-border-none"
+ data-testid="job-log"
+ >
+ <code v-safe-html="item.failureSummary" class="gl-reset-bg gl-p-0" >
+ </code>
+ </pre>
+ </template>
+ </gl-table-lite>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/jobs/utils.js b/app/assets/javascripts/pipelines/components/jobs/utils.js
new file mode 100644
index 00000000000..c8414d44d14
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/jobs/utils.js
@@ -0,0 +1,33 @@
+/*
+ We get the failure and failure summary from Rails which has
+ a summary failure log. Here we combine that data with the data
+ from GraphQL to display the log.
+
+ failedJobs is from GraphQL
+ failedJobsSummary is from Rails
+ */
+
+export const prepareFailedJobs = (failedJobs = [], failedJobsSummary = []) => {
+ const combinedJobs = [];
+
+ if (failedJobs.length > 0 && failedJobsSummary.length > 0) {
+ failedJobs.forEach((failedJob) => {
+ const foundJob = failedJobsSummary.find(
+ (failedJobSummary) => failedJob.normalizedId === failedJobSummary.id,
+ );
+
+ if (foundJob) {
+ combinedJobs.push({
+ ...failedJob,
+ failure: foundJob?.failure,
+ failureSummary: foundJob?.failure_summary,
+ // this field is needed for the slot row-details
+ // on the failed_jobs_table.vue component
+ _showDetails: true,
+ });
+ }
+ });
+ }
+
+ return combinedJobs;
+};
diff --git a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue b/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
index 62c785d7ad2..66d30c10362 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
@@ -1,6 +1,7 @@
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import { __ } from '~/locale';
+import { failedJobsTabName, jobsTabName, needsTabName, testReportTabName } from '../constants';
import PipelineGraphWrapper from './graph/graph_component_wrapper.vue';
import Dag from './dag/dag.vue';
import JobsApp from './jobs/jobs_app.vue';
@@ -16,6 +17,12 @@ export default {
testsTitle: __('Tests'),
},
},
+ tabNames: {
+ needs: needsTabName,
+ jobs: jobsTabName,
+ failures: failedJobsTabName,
+ tests: testReportTabName,
+ },
components: {
Dag,
GlTab,
@@ -25,24 +32,47 @@ export default {
PipelineGraphWrapper,
TestReports,
},
+ inject: ['defaultTabValue'],
+ methods: {
+ isActive(tabName) {
+ return tabName === this.defaultTabValue;
+ },
+ },
};
</script>
<template>
<gl-tabs>
- <gl-tab :title="$options.i18n.tabs.pipelineTitle" data-testid="pipeline-tab">
+ <gl-tab ref="pipelineTab" :title="$options.i18n.tabs.pipelineTitle" data-testid="pipeline-tab">
<pipeline-graph-wrapper />
</gl-tab>
- <gl-tab :title="$options.i18n.tabs.needsTitle" data-testid="dag-tab">
+ <gl-tab
+ ref="dagTab"
+ :title="$options.i18n.tabs.needsTitle"
+ :active="isActive($options.tabNames.needs)"
+ data-testid="dag-tab"
+ >
<dag />
</gl-tab>
- <gl-tab :title="$options.i18n.tabs.jobsTitle" data-testid="jobs-tab">
+ <gl-tab
+ :title="$options.i18n.tabs.jobsTitle"
+ :active="isActive($options.tabNames.jobs)"
+ data-testid="jobs-tab"
+ >
<jobs-app />
</gl-tab>
- <gl-tab :title="$options.i18n.tabs.failedJobsTitle" data-testid="failed-jobs-tab">
+ <gl-tab
+ :title="$options.i18n.tabs.failedJobsTitle"
+ :active="isActive($options.tabNames.failures)"
+ data-testid="failed-jobs-tab"
+ >
<failed-jobs-app />
</gl-tab>
- <gl-tab :title="$options.i18n.tabs.testsTitle" data-testid="tests-tab">
+ <gl-tab
+ :title="$options.i18n.tabs.testsTitle"
+ :active="isActive($options.tabNames.tests)"
+ data-testid="tests-tab"
+ >
<test-reports />
</gl-tab>
<slot></slot>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
index 5a9c85a0f10..3bbdfc73e1b 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
@@ -1,7 +1,9 @@
<script>
import { GlEmptyState } from '@gitlab/ui';
import { s__ } from '~/locale';
+import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import PipelinesCiTemplates from './empty_state/pipelines_ci_templates.vue';
+import IosTemplates from './empty_state/ios_templates.vue';
export default {
i18n: {
@@ -10,7 +12,9 @@ export default {
name: 'PipelinesEmptyState',
components: {
GlEmptyState,
+ GitlabExperiment,
PipelinesCiTemplates,
+ IosTemplates,
},
props: {
emptyStateSvgPath: {
@@ -21,26 +25,24 @@ export default {
type: Boolean,
required: true,
},
- ciRunnerSettingsPath: {
+ registrationToken: {
type: String,
required: false,
default: null,
},
- anyRunnersAvailable: {
- type: Boolean,
- required: false,
- default: true,
- },
},
};
</script>
<template>
<div>
- <pipelines-ci-templates
- v-if="canSetCi"
- :ci-runner-settings-path="ciRunnerSettingsPath"
- :any-runners-available="anyRunnersAvailable"
- />
+ <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=""
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue
index 3b312e78d11..64d4414eb94 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue
@@ -12,15 +12,31 @@ export default {
},
mixins: [Tracking.mixin()],
inject: ['pipelineEditorPath', 'suggestedCiTemplates'],
+ props: {
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ filterTemplates: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
data() {
- const templates = this.suggestedCiTemplates.map(({ name, logo }) => {
- return {
- name,
- logo,
- link: mergeUrlParams({ template: name }, this.pipelineEditorPath),
- description: sprintf(this.$options.i18n.description, { name }),
- };
- });
+ const templates = this.suggestedCiTemplates
+ .filter(
+ (template) => !this.filterTemplates.length || this.filterTemplates.includes(template.name),
+ )
+ .map(({ name, logo, title }) => {
+ return {
+ name: title || name,
+ logo,
+ link: mergeUrlParams({ template: name }, this.pipelineEditorPath),
+ description: sprintf(this.$options.i18n.description, { name: title || name }),
+ };
+ });
return {
templates,
@@ -34,7 +50,9 @@ export default {
},
},
i18n: {
- description: s__('Pipelines|CI/CD template to test and deploy your %{name} project.'),
+ description: s__(
+ 'Pipelines|Continuous integration and deployment template to test and deploy your %{name} project.',
+ ),
cta: s__('Pipelines|Use template'),
},
AVATAR_SHAPE_OPTION_RECT,
@@ -67,6 +85,7 @@ export default {
</div>
</div>
<gl-button
+ :disabled="disabled"
category="primary"
variant="confirm"
:href="template.link"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue
new file mode 100644
index 00000000000..8ff311e90e7
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue
@@ -0,0 +1,220 @@
+<script>
+import { GlButton, GlCard, GlSprintf, GlLink, GlPopover, GlModalDirective } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import apolloProvider from '~/pipelines/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: 'https://docs.gitlab.com/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/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue
index be46a7f5cec..3eafb36bd1d 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue
@@ -33,19 +33,7 @@ export default {
RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT,
RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT,
I18N,
- inject: ['pipelineEditorPath'],
- props: {
- ciRunnerSettingsPath: {
- type: String,
- required: false,
- default: null,
- },
- anyRunnersAvailable: {
- type: Boolean,
- required: false,
- default: true,
- },
- },
+ inject: ['anyRunnersAvailable', 'pipelineEditorPath', 'ciRunnerSettingsPath'],
data() {
return {
gettingStartedTemplateUrl: mergeUrlParams(
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue
index 2b33467e948..e35fccf2d7e 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue
@@ -36,7 +36,7 @@ export default {
};
</script>
<template>
- <div data-testid="pipeline-mini-graph">
+ <div data-testid="pipeline-mini-graph" class="gl-display-inline gl-vertical-align-middle gl-my-1">
<div
v-for="stage in stages"
:key="stage.name"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue
index afcb04cd7eb..53e21d4ce8b 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue
@@ -12,7 +12,8 @@
* 4. Commit widget
*/
-import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlDropdown, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
@@ -21,7 +22,7 @@ import JobItem from './job_item.vue';
export default {
components: {
- GlIcon,
+ CiIcon,
GlLoadingIcon,
GlDropdown,
JobItem,
@@ -51,14 +52,6 @@ export default {
dropdownContent: [],
};
},
- computed: {
- triggerButtonClass() {
- return `ci-status-icon-${this.stage.status.group}`;
- },
- borderlessIcon() {
- return `${this.stage.status.icon}_borderless`;
- },
- },
watch: {
updateDropdown() {
if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) {
@@ -114,15 +107,21 @@ export default {
variant="link"
:aria-label="stageAriaLabel(stage.title)"
:lazy="true"
- :popper-opts="{ placement: 'bottom' }"
- :toggle-class="['mini-pipeline-graph-dropdown-toggle', triggerButtonClass]"
+ :popper-opts="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ placement: 'bottom',
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ :toggle-class="['gl-rounded-full!']"
menu-class="mini-pipeline-graph-dropdown-menu"
@show="onShowDropdown"
>
<template #button-content>
- <span class="gl-pointer-events-none">
- <gl-icon :name="borderlessIcon" />
- </span>
+ <ci-icon
+ is-interactive
+ css-classes="gl-rounded-full"
+ :size="24"
+ :status="stage.status"
+ class="gl-align-items-center gl-display-inline-flex"
+ />
</template>
<gl-loading-icon v-if="isLoading" size="sm" />
<ul
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
index db9dc74863d..485e338f639 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
@@ -107,16 +107,11 @@ export default {
type: Object,
required: true,
},
- ciRunnerSettingsPath: {
+ registrationToken: {
type: String,
required: false,
default: null,
},
- anyRunnersAvailable: {
- type: Boolean,
- required: false,
- default: true,
- },
},
data() {
return {
@@ -386,8 +381,7 @@ export default {
v-else-if="stateToRender === $options.stateMap.emptyState"
:empty-state-svg-path="emptyStateSvgPath"
:can-set-ci="canCreatePipeline"
- :ci-runner-settings-path="ciRunnerSettingsPath"
- :any-runners-available="anyRunnersAvailable"
+ :registration-token="registrationToken"
/>
<gl-empty-state
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
index 77b9c2b5203..53da98434b0 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
@@ -174,12 +174,13 @@ export default {
<div></div>
<linked-pipelines-mini-list
v-if="item.triggered_by"
- :triggered-by="[item.triggered_by]"
+ :triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
+ item.triggered_by,
+ ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
data-testid="mini-graph-upstream"
/>
<pipeline-mini-graph
v-if="item.details && item.details.stages && item.details.stages.length > 0"
- class="gl-display-inline"
:stages="item.details.stages"
:update-dropdown="updateGraphDropdown"
@pipelineActionRequestComplete="onPipelineActionRequestComplete"
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
index 51373e712ff..9b0e6560c53 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
@@ -35,7 +35,7 @@ export default {
},
computed: {
...mapState(['pageInfo']),
- ...mapGetters(['getSuiteTests', 'getSuiteTestCount']),
+ ...mapGetters(['getSuiteTests', 'getSuiteTestCount', 'getSuiteArtifactsExpired']),
hasSuites() {
return this.getSuiteTests.length > 0;
},
@@ -80,7 +80,8 @@ export default {
<div
v-for="(testCase, index) in getSuiteTests"
:key="index"
- class="gl-responsive-table-row rounded align-items-md-start mt-xs-3 js-case-row"
+ class="gl-responsive-table-row rounded align-items-md-start"
+ data-testid="test-case-row"
>
<div class="table-section section-20 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Suite') }}</div>
@@ -157,7 +158,16 @@ export default {
</div>
<div v-else>
- <p class="js-no-test-cases">{{ s__('TestReports|There are no test cases to display.') }}</p>
+ <p data-testid="no-test-cases">
+ {{ s__('TestReports|There are no test cases to display.') }}
+ </p>
+ <p v-if="getSuiteArtifactsExpired" data-testid="artifacts-expired">
+ {{
+ s__(
+ 'TestReports|Test details are populated by job artifacts. The job artifacts from this pipeline are expired.',
+ )
+ }}
+ </p>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index 36f708ff2af..0510992e962 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -9,6 +9,7 @@ export const FILTER_TAG_IDENTIFIER = 'tag';
export const SCHEDULE_ORIGIN = 'schedule';
export const NEEDS_PROPERTY = 'needs';
export const EXPLICIT_NEEDS_PROPERTY = 'previousStageJobsOrNeeds';
+export const PIPELINE_GRAPHQL_TYPE = 'Ci::Pipeline';
export const ICONS = {
TAG: 'tag',
@@ -44,6 +45,28 @@ export const UNSUPPORTED_DATA = 'unsupported_data';
export const CHILD_VIEW = 'child';
+// Pipeline tabs
+
+export const TAB_QUERY_PARAM = 'tab';
+
+export const needsTabName = 'dag';
+export const jobsTabName = 'builds';
+export const failedJobsTabName = 'failures';
+export const testReportTabName = 'test_report';
+export const securityTabName = 'security';
+export const licensesTabName = 'licenses';
+export const codeQualityTabName = 'codequality_report';
+
+export const validPipelineTabNames = [
+ needsTabName,
+ jobsTabName,
+ failedJobsTabName,
+ testReportTabName,
+ securityTabName,
+ licensesTabName,
+ codeQualityTabName,
+];
+
// Constants for the ID and IID selection dropdown
export const PipelineKeyOptions = [
{
@@ -62,3 +85,27 @@ export const TOAST_MESSAGE = s__('Pipeline|Creating pipeline.');
export const BUTTON_TOOLTIP_RETRY = __('Retry failed jobs');
export const BUTTON_TOOLTIP_CANCEL = __('Cancel');
+
+export const DEFAULT_FIELDS = [
+ {
+ key: 'name',
+ label: __('Name'),
+ columnClass: 'gl-w-20p',
+ },
+ {
+ key: 'stage',
+ label: __('Stage'),
+ columnClass: 'gl-w-20p',
+ },
+ {
+ key: 'failure',
+ label: __('Failure'),
+ columnClass: 'gl-w-40p',
+ },
+ {
+ key: 'actions',
+ label: '',
+ tdClass: 'gl-text-right',
+ columnClass: 'gl-w-20p',
+ },
+];
diff --git a/app/assets/javascripts/pipelines/graphql/mutations/retry_failed_job.mutation.graphql b/app/assets/javascripts/pipelines/graphql/mutations/retry_failed_job.mutation.graphql
new file mode 100644
index 00000000000..1955cc9b0ac
--- /dev/null
+++ b/app/assets/javascripts/pipelines/graphql/mutations/retry_failed_job.mutation.graphql
@@ -0,0 +1,12 @@
+mutation retryFailedJob($id: CiBuildID!) {
+ jobRetry(input: { id: $id }) {
+ job {
+ id
+ detailedStatus {
+ id
+ detailsPath
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/pipelines/graphql/provider.js b/app/assets/javascripts/pipelines/graphql/provider.js
new file mode 100644
index 00000000000..ef96b443da8
--- /dev/null
+++ b/app/assets/javascripts/pipelines/graphql/provider.js
@@ -0,0 +1,9 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+
+Vue.use(VueApollo);
+
+export default new VueApollo({
+ defaultClient: createDefaultClient(),
+});
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql
new file mode 100644
index 00000000000..14e9a838f4b
--- /dev/null
+++ b/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql
@@ -0,0 +1,41 @@
+query getFailedJobs($fullPath: ID!, $pipelineIid: ID!) {
+ project(fullPath: $fullPath) {
+ id
+ pipeline(iid: $pipelineIid) {
+ id
+ jobs(statuses: FAILED) {
+ nodes {
+ status
+ detailedStatus {
+ id
+ detailsPath
+ group
+ icon
+ label
+ text
+ tooltip
+ action {
+ id
+ buttonTitle
+ icon
+ method
+ path
+ title
+ }
+ }
+ id
+ stage {
+ id
+ name
+ }
+ name
+ retryable
+ userPermissions {
+ readBuild
+ updateBuild
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 338de65e795..fd869014570 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -1,10 +1,11 @@
import createFlash from '~/flash';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
import createDagApp from './pipeline_details_dag';
import { createPipelinesDetailApp } from './pipeline_details_graph';
import { createPipelineHeaderApp } from './pipeline_details_header';
import { createPipelineNotificationApp } from './pipeline_details_notification';
import { createPipelineJobsApp } from './pipeline_details_jobs';
+import { createPipelineFailedJobsApp } from './pipeline_details_failed_jobs';
import { apolloProvider } from './pipeline_shared_client';
import { createTestDetails } from './pipeline_test_details';
@@ -16,6 +17,7 @@ const SELECTORS = {
PIPELINE_TABS: '#js-pipeline-tabs',
PIPELINE_TESTS: '#js-pipeline-tests-detail',
PIPELINE_JOBS: '#js-pipeline-jobs-vue',
+ PIPELINE_FAILED_JOBS: '#js-pipeline-failed-jobs-vue',
};
export default async function initPipelineDetailsBundle() {
@@ -79,5 +81,15 @@ export default async function initPipelineDetailsBundle() {
message: __('An error occurred while loading the Jobs tab.'),
});
}
+
+ if (gon.features?.failedJobsTabVue) {
+ try {
+ createPipelineFailedJobsApp(SELECTORS.PIPELINE_FAILED_JOBS);
+ } catch {
+ createFlash({
+ message: s__('Jobs|An error occurred while loading the Failed Jobs tab.'),
+ });
+ }
+ }
}
}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_dag.js b/app/assets/javascripts/pipelines/pipeline_details_dag.js
index e2835ecc4d1..b2cb0457c4d 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_dag.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_dag.js
@@ -17,7 +17,7 @@ const createDagApp = (apolloProvider) => {
emptySvgPath,
pipelineProjectPath,
pipelineIid,
- } = el?.dataset;
+ } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
diff --git a/app/assets/javascripts/pipelines/pipeline_details_failed_jobs.js b/app/assets/javascripts/pipelines/pipeline_details_failed_jobs.js
new file mode 100644
index 00000000000..7bf3b64bf47
--- /dev/null
+++ b/app/assets/javascripts/pipelines/pipeline_details_failed_jobs.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import FailedJobsApp from './components/jobs/failed_jobs_app.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export const createPipelineFailedJobsApp = (selector) => {
+ const containerEl = document.querySelector(selector);
+
+ if (!containerEl) {
+ return false;
+ }
+
+ const { fullPath, pipelineIid, failedJobsSummaryData } = containerEl.dataset;
+
+ return new Vue({
+ el: containerEl,
+ apolloProvider,
+ provide: {
+ fullPath,
+ pipelineIid,
+ },
+ render(createElement) {
+ return createElement(FailedJobsApp, {
+ props: {
+ failedJobsSummary: JSON.parse(failedJobsSummaryData),
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/pipelines/pipeline_details_header.js b/app/assets/javascripts/pipelines/pipeline_details_header.js
index 1c619768764..2fedd7e7a98 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_header.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_header.js
@@ -11,7 +11,7 @@ export const createPipelineHeaderApp = (elSelector, apolloProvider, graphqlResou
return;
}
- const { fullPath, pipelineId, pipelineIid, pipelinesPath } = el?.dataset;
+ const { fullPath, pipelineId, pipelineIid, pipelinesPath } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
diff --git a/app/assets/javascripts/pipelines/pipeline_details_notification.js b/app/assets/javascripts/pipelines/pipeline_details_notification.js
index 0061be843c5..b480fc7c713 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_notification.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_notification.js
@@ -11,7 +11,7 @@ export const createPipelineNotificationApp = (elSelector, apolloProvider) => {
return;
}
- const { deprecatedKeywordsDocPath, fullPath, pipelineIid } = el?.dataset;
+ const { deprecatedKeywordsDocPath, fullPath, pipelineIid } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
diff --git a/app/assets/javascripts/pipelines/pipeline_tabs.js b/app/assets/javascripts/pipelines/pipeline_tabs.js
index ff88c6215e5..530917f0402 100644
--- a/app/assets/javascripts/pipelines/pipeline_tabs.js
+++ b/app/assets/javascripts/pipelines/pipeline_tabs.js
@@ -1,7 +1,10 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import PipelineTabs from 'ee_else_ce/pipelines/components/pipeline_tabs.vue';
-import { reportToSentry } from './utils';
+import { removeParams, updateHistory } from '~/lib/utils/url_utility';
+import { TAB_QUERY_PARAM } from '~/pipelines/constants';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { getPipelineDefaultTab, reportToSentry } from './utils';
Vue.use(VueApollo);
@@ -17,7 +20,19 @@ const createPipelineTabs = (selector, apolloProvider) => {
downloadablePathForReportType,
exposeSecurityDashboard,
exposeLicenseScanningData,
+ graphqlResourceEtag,
+ pipelineIid,
+ pipelineProjectPath,
} = dataset;
+
+ const defaultTabValue = getPipelineDefaultTab(window.location.href);
+
+ updateHistory({
+ url: removeParams([TAB_QUERY_PARAM]),
+ title: document.title,
+ replace: true,
+ });
+
// eslint-disable-next-line no-new
new Vue({
el: selector,
@@ -26,11 +41,15 @@ const createPipelineTabs = (selector, apolloProvider) => {
},
apolloProvider,
provide: {
- canGenerateCodequalityReports: JSON.parse(canGenerateCodequalityReports),
+ canGenerateCodequalityReports: parseBoolean(canGenerateCodequalityReports),
codequalityReportDownloadPath,
+ defaultTabValue,
downloadablePathForReportType,
- exposeSecurityDashboard: JSON.parse(exposeSecurityDashboard),
- exposeLicenseScanningData: JSON.parse(exposeLicenseScanningData),
+ exposeSecurityDashboard: parseBoolean(exposeSecurityDashboard),
+ exposeLicenseScanningData: parseBoolean(exposeLicenseScanningData),
+ graphqlResourceEtag,
+ pipelineIid,
+ pipelineProjectPath,
},
errorCaptured(err, _vm, info) {
reportToSentry('pipeline_tabs', `error: ${err}, info: ${info}`);
diff --git a/app/assets/javascripts/pipelines/pipelines_index.js b/app/assets/javascripts/pipelines/pipelines_index.js
index f4d9a44a754..6dccdb1a3e6 100644
--- a/app/assets/javascripts/pipelines/pipelines_index.js
+++ b/app/assets/javascripts/pipelines/pipelines_index.js
@@ -40,6 +40,8 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
params,
ciRunnerSettingsPath,
anyRunnersAvailable,
+ iosRunnersAvailable,
+ registrationToken,
} = el.dataset;
return new Vue({
@@ -49,6 +51,9 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
artifactsEndpoint,
artifactsEndpointPlaceholder,
suggestedCiTemplates: JSON.parse(suggestedCiTemplates),
+ ciRunnerSettingsPath,
+ anyRunnersAvailable: parseBoolean(anyRunnersAvailable),
+ iosRunnersAvailable: parseBoolean(iosRunnersAvailable),
},
data() {
return {
@@ -78,8 +83,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
projectId,
defaultBranchName,
params: JSON.parse(params),
- ciRunnerSettingsPath,
- anyRunnersAvailable: parseBoolean(anyRunnersAvailable),
+ registrationToken,
},
});
},
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/actions.js b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
index b7f590a7b3c..f0556f3d12e 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/actions.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
@@ -38,11 +38,7 @@ export const fetchTestSuite = ({ state, commit, dispatch }, index) => {
return axios
.get(state.suiteEndpoint, { params: { build_ids } })
.then(({ data }) => commit(types.SET_SUITE, { suite: data, index }))
- .catch(() => {
- createFlash({
- message: s__('TestReports|There was an error fetching the test suite.'),
- });
- })
+ .catch((error) => commit(types.SET_SUITE_ERROR, error))
.finally(() => {
dispatch('toggleLoading');
});
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/constants.js b/app/assets/javascripts/pipelines/stores/test_reports/constants.js
new file mode 100644
index 00000000000..8eebfb6b208
--- /dev/null
+++ b/app/assets/javascripts/pipelines/stores/test_reports/constants.js
@@ -0,0 +1 @@
+export const ARTIFACTS_EXPIRED_ERROR_MESSAGE = 'Test report artifacts have expired';
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
index 03680de0fa9..e6a88bb4175 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/getters.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
@@ -1,4 +1,5 @@
import { addIconStatus, formatFilePath, formattedTime } from './utils';
+import { ARTIFACTS_EXPIRED_ERROR_MESSAGE } from './constants';
export const getTestSuites = (state) => {
const { test_suites: testSuites = [] } = state.testReports;
@@ -29,3 +30,6 @@ export const getSuiteTests = (state) => {
};
export const getSuiteTestCount = (state) => getSelectedSuite(state)?.test_cases?.length || 0;
+
+export const getSuiteArtifactsExpired = (state) =>
+ state.errorMessage === ARTIFACTS_EXPIRED_ERROR_MESSAGE;
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js b/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
index 803f6bf60b1..7651a2f4327 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
@@ -2,4 +2,5 @@ export const SET_PAGE = 'SET_PAGE';
export const SET_SELECTED_SUITE_INDEX = 'SET_SELECTED_SUITE_INDEX';
export const SET_SUMMARY = 'SET_SUMMARY';
export const SET_SUITE = 'SET_SUITE';
+export const SET_SUITE_ERROR = 'SET_SUITE_ERROR';
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
index cf0bf8483dd..68ee063dda7 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
@@ -1,3 +1,5 @@
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
import * as types from './mutation_types';
export default {
@@ -13,6 +15,18 @@ export default {
state.testReports.test_suites[index] = { ...suite, hasFullSuite: true };
},
+ [types.SET_SUITE_ERROR](state, error) {
+ const errorMessage = error.response?.data?.errors;
+
+ if (errorMessage) {
+ state.errorMessage = errorMessage;
+ } else {
+ createFlash({
+ message: s__('TestReports|There was an error fetching the test suite.'),
+ });
+ }
+ },
+
[types.SET_SELECTED_SUITE_INDEX](state, selectedSuiteIndex) {
Object.assign(state, { selectedSuiteIndex });
},
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/state.js b/app/assets/javascripts/pipelines/stores/test_reports/state.js
index 0ee6f53fa58..3ec9418c14e 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/state.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/state.js
@@ -5,6 +5,7 @@ export default ({ blobPath = '', summaryEndpoint = '', suiteEndpoint = '' }) =>
testReports: {},
selectedSuiteIndex: null,
isLoading: false,
+ errorMessage: null,
pageInfo: {
page: 1,
perPage: 20,
diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js
index f6e1c8b7412..588d15495ab 100644
--- a/app/assets/javascripts/pipelines/utils.js
+++ b/app/assets/javascripts/pipelines/utils.js
@@ -1,7 +1,12 @@
import * as Sentry from '@sentry/browser';
import { pickBy } from 'lodash';
-import { SUPPORTED_FILTER_PARAMETERS, NEEDS_PROPERTY } from './constants';
-
+import { getParameterValues } from '~/lib/utils/url_utility';
+import {
+ NEEDS_PROPERTY,
+ SUPPORTED_FILTER_PARAMETERS,
+ TAB_QUERY_PARAM,
+ validPipelineTabNames,
+} from './constants';
/*
The following functions are the main engine in transforming the data as
received from the endpoint into the format the d3 graph expects.
@@ -138,3 +143,13 @@ export const reportMessageToSentry = (component, message, context) => {
Sentry.captureMessage(message);
});
};
+
+export const getPipelineDefaultTab = (url) => {
+ const [tabQueryValue] = getParameterValues(TAB_QUERY_PARAM, url);
+
+ if (tabQueryValue && validPipelineTabNames.includes(tabQueryValue)) {
+ return tabQueryValue;
+ }
+
+ return null;
+};
diff --git a/app/assets/javascripts/project_select_combo_button.js b/app/assets/javascripts/project_select_combo_button.js
index 09dbf2cee04..ad80032c551 100644
--- a/app/assets/javascripts/project_select_combo_button.js
+++ b/app/assets/javascripts/project_select_combo_button.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import { sprintf, __ } from '~/locale';
+import { sanitizeUrl } from '~/lib/utils/url_utility';
import AccessorUtilities from './lib/utils/accessor';
import { loadCSSFile } from './lib/utils/css_utils';
@@ -80,7 +81,7 @@ export default class ProjectSelectComboButton {
setNewItemBtnAttributes(project) {
if (project) {
- this.newItemBtn.attr('href', project.url);
+ this.newItemBtn.attr('href', sanitizeUrl(project.url));
this.newItemBtn.text(
sprintf(__('New %{type} in %{project}'), {
type: this.resourceLabel,
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
index 8511f9bdb0f..9bd78b7c89e 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
@@ -121,20 +121,18 @@ export default {
</script>
<template>
- <div>
+ <div class="gl-pt-2">
<gl-loading-icon v-if="$apollo.queries.pipeline.loading" />
- <div v-else>
+ <div v-else class="gl-align-items-center gl-display-flex">
<linked-pipelines-mini-list
v-if="upstreamPipeline"
- :triggered-by="[upstreamPipeline]"
+ :triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
+ upstreamPipeline,
+ ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
data-testid="commit-box-mini-graph-upstream"
/>
- <pipeline-mini-graph
- :stages="formattedStages"
- class="gl-display-inline"
- data-testid="commit-box-mini-graph"
- />
+ <pipeline-mini-graph :stages="formattedStages" data-testid="commit-box-mini-graph" />
<linked-pipelines-mini-list
v-if="hasDownstream"
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 3e1c471f015..2bf13941f6f 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { debounce } from 'lodash';
-import DEFAULT_PROJECT_TEMPLATES from 'ee_else_ce/projects/default_project_templates';
+import DEFAULT_PROJECT_TEMPLATES from 'any_else_ce/projects/default_project_templates';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '../lib/utils/constants';
import { ENTER_KEY } from '../lib/utils/keys';
@@ -286,9 +286,6 @@ const bindEvents = () => {
});
$('.js-import-git-toggle-button').on('click', () => {
- const $projectMirror = $('#project_mirror');
-
- $projectMirror.attr('disabled', !$projectMirror.attr('disabled'));
setProjectNamePathHandlers(
$('.tab-pane.active #project_name'),
$('.tab-pane.active #project_path'),
diff --git a/app/assets/javascripts/projects/settings/init_access_dropdown.js b/app/assets/javascripts/projects/settings/init_access_dropdown.js
index 11272652b63..941efaef3bc 100644
--- a/app/assets/javascripts/projects/settings/init_access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/init_access_dropdown.js
@@ -4,7 +4,7 @@ import AccessDropdown from './components/access_dropdown.vue';
export const initAccessDropdown = (el, options) => {
if (!el) {
- return false;
+ return null;
}
const { accessLevelsData, accessLevel } = options;
diff --git a/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue b/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue
index d4c97cbf038..9c8de9bef2d 100644
--- a/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue
+++ b/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue
@@ -85,7 +85,7 @@ export default {
<gl-avatar-labeled
:src="dropdownItem.avatarUrl"
:entity-name="dropdownItem.name"
- :label="dropdownItem.name"
+ :label="dropdownItem.title"
:size="32"
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
/>
diff --git a/app/assets/javascripts/projects/settings/topics/queries/project_topics_search.query.graphql b/app/assets/javascripts/projects/settings/topics/queries/project_topics_search.query.graphql
index b193165062a..0c0a874d950 100644
--- a/app/assets/javascripts/projects/settings/topics/queries/project_topics_search.query.graphql
+++ b/app/assets/javascripts/projects/settings/topics/queries/project_topics_search.query.graphql
@@ -3,6 +3,7 @@ query searchProjectTopics($search: String) {
nodes {
id
name
+ title
avatarUrl
}
}
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 e5ddfe82e3b..8a9a0b541f3 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
@@ -128,7 +128,7 @@ export default {
this.selectedTemplate = selectedTemplate;
},
validateProjectKey() {
- if (this.projectKey && !new RegExp(/^[a-z0-9_]+$/).test(this.projectKey)) {
+ if (this.projectKey && !/^[a-z0-9_]+$/.test(this.projectKey)) {
this.projectKeyError = __('Only use lowercase letters, numbers, and underscores.');
return;
}
@@ -270,18 +270,16 @@ export default {
</template>
</gl-form-group>
- <div class="gl-display-flex gl-justify-content-end">
- <gl-button
- variant="success"
- class="gl-mt-5"
- data-testid="save_service_desk_settings_button"
- data-qa-selector="save_service_desk_settings_button"
- :disabled="isTemplateSaving"
- @click="onSaveTemplate"
- >
- {{ __('Save changes') }}
- </gl-button>
- </div>
+ <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"
+ @click="onSaveTemplate"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
deleted file mode 100644
index befbca48736..00000000000
--- a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
+++ /dev/null
@@ -1,149 +0,0 @@
-<script>
-import {
- GlButton,
- GlFormGroup,
- GlFormInput,
- GlModal,
- GlModalDirective,
- GlSprintf,
- GlLink,
-} from '@gitlab/ui';
-import createFlash from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-
-export default {
- copyToClipboard: __('Copy'),
- components: {
- GlButton,
- GlFormGroup,
- GlFormInput,
- GlModal,
- ClipboardButton,
- GlSprintf,
- GlLink,
- },
- directives: {
- 'gl-modal': GlModalDirective,
- },
- props: {
- initialAuthorizationKey: {
- type: String,
- required: false,
- default: '',
- },
- changeKeyUrl: {
- type: String,
- required: true,
- },
- notifyUrl: {
- type: String,
- required: true,
- },
- learnMoreUrl: {
- type: String,
- required: true,
- },
- disabled: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- authorizationKey: this.initialAuthorizationKey,
- };
- },
- methods: {
- resetKey() {
- axios
- .post(this.changeKeyUrl)
- .then((res) => {
- this.authorizationKey = res.data.token;
- })
- .catch(() => {
- createFlash({
- message: __('Failed to reset key. Please try again.'),
- });
- });
- },
- },
-};
-</script>
-
-<template>
- <div class="row py-4 border-top js-prometheus-alerts">
- <div class="col-lg-3">
- <h4 class="mt-0">
- {{ __('Alerts') }}
- </h4>
- <p>
- {{ __('Receive alerts from manually configured Prometheus servers.') }}
- </p>
- </div>
- <div class="col-lg-9">
- <gl-sprintf
- :message="
- __(
- 'To receive alerts from manually configured Prometheus services, add the following URL and Authorization key to your Prometheus webhook config file. Learn more about %{linkStart}configuring Prometheus%{linkEnd} to send alerts to GitLab.',
- )
- "
- >
- <template #link="{ content }">
- <gl-link :href="learnMoreUrl" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- <gl-form-group :label="__('URL')" label-for="notify-url" label-class="label-bold">
- <div class="input-group">
- <gl-form-input id="notify-url" :readonly="true" :value="notifyUrl" />
- <span class="input-group-append">
- <clipboard-button
- :text="notifyUrl"
- :title="$options.copyToClipboard"
- :disabled="disabled"
- />
- </span>
- </div>
- </gl-form-group>
- <gl-form-group
- :label="__('Authorization key')"
- label-for="authorization-key"
- label-class="label-bold"
- >
- <div class="input-group">
- <gl-form-input id="authorization-key" :readonly="true" :value="authorizationKey" />
- <span class="input-group-append">
- <clipboard-button
- :text="authorizationKey"
- :title="$options.copyToClipboard"
- :disabled="disabled"
- />
- </span>
- </div>
- </gl-form-group>
- <template v-if="authorizationKey.length > 0">
- <gl-modal
- modal-id="authKeyModal"
- :title="__('Reset authorization key?')"
- :ok-title="__('Reset authorization key')"
- ok-variant="danger"
- @ok="resetKey"
- >
- {{
- __(
- 'Resetting the authorization key will invalidate the previous key. Existing alert configurations will need to be updated with the new key.',
- )
- }}
- </gl-modal>
- <gl-button v-gl-modal.authKeyModal class="js-reset-auth-key" :disabled="disabled">{{
- __('Reset key')
- }}</gl-button>
- </template>
- <gl-button v-else :disabled="disabled" class="js-reset-auth-key" @click="resetKey">{{
- __('Generate key')
- }}</gl-button>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/prometheus_alerts/index.js b/app/assets/javascripts/prometheus_alerts/index.js
deleted file mode 100644
index 7efe6ed186b..00000000000
--- a/app/assets/javascripts/prometheus_alerts/index.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import Vue from 'vue';
-import ResetKey from './components/reset_key.vue';
-
-export default () => {
- const el = document.querySelector('#js-settings-prometheus-alerts');
-
- if (!el) {
- return;
- }
-
- const { authorizationKey, changeKeyUrl, notifyUrl, learnMoreUrl, disabled } = el.dataset;
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- render(createElement) {
- return createElement(ResetKey, {
- props: {
- initialAuthorizationKey: authorizationKey,
- changeKeyUrl,
- notifyUrl,
- learnMoreUrl,
- disabled,
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/related_issues/components/add_issuable_form.vue b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
index 9ee2e7a4ffd..42de419aec4 100644
--- a/app/assets/javascripts/related_issues/components/add_issuable_form.vue
+++ b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
@@ -195,7 +195,10 @@ export default {
:path-id-separator="pathIdSeparator"
:input-value="inputValue"
:auto-complete-sources="transformedAutocompleteSources"
- :auto-complete-options="{ issues: autoCompleteIssues, epics: autoCompleteEpics }"
+ :auto-complete-options="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ issues: autoCompleteIssues,
+ epics: autoCompleteEpics,
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:issuable-type="issuableType"
@pendingIssuableRemoveRequest="onPendingIssuableRemoveRequest"
@formCancel="onFormCancel"
diff --git a/app/assets/javascripts/related_issues/components/related_issues_root.vue b/app/assets/javascripts/related_issues/components/related_issues_root.vue
index 40d58c04753..da049d68467 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_root.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_root.vue
@@ -220,7 +220,8 @@ export default {
const startsWithNumber = String(touchedReference).match(/^[0-9]/) !== null;
if (startsWithNumber) {
- this.inputValue = `#${touchedReference}`;
+ const { pathIdSeparator } = this;
+ this.inputValue = `${pathIdSeparator}${touchedReference}`;
} else {
this.inputValue = `${touchedReference}`;
}
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index 8365e6a5ab0..327da1fb2a1 100644
--- a/app/assets/javascripts/releases/components/app_edit_new.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlFormInput, GlFormGroup, GlSprintf } from '@gitlab/ui';
+import { GlButton, GlFormCheckbox, GlFormInput, GlFormGroup, GlLink, GlSprintf } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
import { isSameOriginUrl, getParameterByName } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -12,9 +12,11 @@ import TagField from './tag_field.vue';
export default {
name: 'ReleaseEditNewApp',
components: {
+ GlFormCheckbox,
GlFormInput,
GlFormGroup,
GlButton,
+ GlLink,
GlSprintf,
MarkdownField,
AssetLinksForm,
@@ -28,6 +30,7 @@ export default {
'fetchError',
'markdownDocsPath',
'markdownPreviewPath',
+ 'editReleaseDocsPath',
'releasesPagePath',
'release',
'newMilestonePath',
@@ -35,8 +38,9 @@ export default {
'projectId',
'groupId',
'groupMilestonesAvailable',
+ 'tagNotes',
]),
- ...mapGetters('editNew', ['isValid', 'isExistingRelease']),
+ ...mapGetters('editNew', ['isValid', 'isExistingRelease', 'formattedReleaseNotes']),
showForm() {
return Boolean(!this.isFetchingRelease && !this.fetchError && this.release);
},
@@ -64,6 +68,14 @@ export default {
this.updateReleaseMilestones(milestones);
},
},
+ includeTagNotes: {
+ get() {
+ return this.$store.state.editNew.includeTagNotes;
+ },
+ set(includeTagNotes) {
+ this.updateIncludeTagNotes(includeTagNotes);
+ },
+ },
cancelPath() {
const backUrl = getParameterByName(BACK_URL_PARAM);
@@ -105,6 +117,7 @@ export default {
'updateReleaseTitle',
'updateReleaseNotes',
'updateReleaseMilestones',
+ 'updateIncludeTagNotes',
]),
submitForm() {
if (!this.isFormSubmissionDisabled) {
@@ -161,7 +174,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:add-spacing-classes="false"
- :textarea-value="releaseNotes"
+ :textarea-value="formattedReleaseNotes"
class="gl-mt-3 gl-mb-3"
>
<template #textarea>
@@ -178,6 +191,25 @@ export default {
</markdown-field>
</div>
</gl-form-group>
+ <gl-form-group v-if="!isExistingRelease">
+ <gl-form-checkbox v-model="includeTagNotes">
+ {{ s__('Release|Include message from the annotated tag.') }}
+
+ <template #help>
+ <gl-sprintf
+ :message="
+ s__(
+ 'Release|You can edit the content later by editing the release. %{linkStart}How do I edit a release?%{linkEnd}',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link :href="editReleaseDocsPath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-form-checkbox>
+ </gl-form-group>
<asset-links-form />
diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue
index 9e05d00a98d..d3b6d07590f 100644
--- a/app/assets/javascripts/releases/components/tag_field_new.vue
+++ b/app/assets/javascripts/releases/components/tag_field_new.vue
@@ -52,7 +52,10 @@ export default {
},
},
showTagNameValidationError() {
- return this.isInputDirty && this.validationErrors.isTagNameEmpty;
+ return (
+ this.isInputDirty &&
+ (this.validationErrors.isTagNameEmpty || this.validationErrors.existingRelease)
+ );
},
tagNameInputId() {
return uniqueId('tag-name-input-');
@@ -60,9 +63,14 @@ export default {
createFromSelectorId() {
return uniqueId('create-from-selector-');
},
+ tagFeedback() {
+ return this.validationErrors.existingRelease
+ ? __('Selected tag is already in use. Choose another option.')
+ : __('Tag name is required.');
+ },
},
methods: {
- ...mapActions('editNew', ['updateReleaseTagName', 'updateCreateFrom']),
+ ...mapActions('editNew', ['updateReleaseTagName', 'updateCreateFrom', 'fetchTagNotes']),
markInputAsDirty() {
this.isInputDirty = true;
},
@@ -112,7 +120,7 @@ export default {
<gl-form-group
data-testid="tag-name-field"
:state="!showTagNameValidationError"
- :invalid-feedback="__('Tag name is required')"
+ :invalid-feedback="tagFeedback"
:label="$options.translations.tagName.label"
:label-for="tagNameInputId"
:label-description="$options.translations.tagName.labelDescription"
@@ -125,6 +133,7 @@ export default {
:translations="$options.translations.tagName"
:enabled-ref-types="$options.tagNameEnabledRefTypes"
:state="!showTagNameValidationError"
+ @input="fetchTagNotes"
@hide.once="markInputAsDirty"
>
<template #footer="{ isLoading, matches, query }">
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
index b3ba4f9263a..08197377f61 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
@@ -1,3 +1,4 @@
+import { getTag } from '~/rest_api';
import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
@@ -7,6 +8,7 @@ import deleteReleaseAssetLinkMutation from '~/releases/graphql/mutations/delete_
import updateReleaseMutation from '~/releases/graphql/mutations/update_release.mutation.graphql';
import oneReleaseForEditingQuery from '~/releases/graphql/queries/one_release_for_editing.query.graphql';
import { gqClient, convertOneReleaseGraphQLResponse } from '~/releases/util';
+
import * as types from './mutation_types';
export const initializeRelease = ({ commit, dispatch, getters }) => {
@@ -224,3 +226,23 @@ export const updateRelease = async ({ commit, dispatch, state, getters }) => {
});
}
};
+
+export const fetchTagNotes = ({ commit, state }, tagName) => {
+ commit(types.REQUEST_TAG_NOTES);
+
+ return getTag(state.projectId, tagName)
+ .then(({ data }) => {
+ commit(types.RECEIVE_TAG_NOTES_SUCCESS, data);
+ })
+ .catch((error) => {
+ createFlash({
+ message: s__('Release|Unable to fetch the tag notes.'),
+ });
+
+ commit(types.RECEIVE_TAG_NOTES_ERROR, error);
+ });
+};
+
+export const updateIncludeTagNotes = ({ commit }, includeTagNotes) => {
+ commit(types.UPDATE_INCLUDE_TAG_NOTES, includeTagNotes);
+};
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
index d4f49e53619..0ca5eb9931a 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
@@ -1,4 +1,5 @@
import { isEmpty } from 'lodash';
+import { s__ } from '~/locale';
import { hasContent } from '~/lib/utils/text_utility';
import { getDuplicateItemsFromArray } from '~/lib/utils/array_utility';
@@ -52,6 +53,10 @@ export const validationErrors = (state) => {
errors.isTagNameEmpty = true;
}
+ if (state.existingRelease) {
+ errors.existingRelease = true;
+ }
+
// Each key of this object is a URL, and the value is an
// array of Release link objects that share this URL.
// This is used for detecting duplicate URLs.
@@ -113,11 +118,15 @@ export const validationErrors = (state) => {
/** Returns whether or not the release object is valid */
export const isValid = (_state, getters) => {
const errors = getters.validationErrors;
- return Object.values(errors.assets.links).every(isEmpty) && !errors.isTagNameEmpty;
+ return (
+ Object.values(errors.assets.links).every(isEmpty) &&
+ !errors.isTagNameEmpty &&
+ !errors.existingRelease
+ );
};
/** Returns all the variables for a `releaseUpdate` GraphQL mutation */
-export const releaseUpdateMutatationVariables = (state) => {
+export const releaseUpdateMutatationVariables = (state, getters) => {
const name = state.release.name?.trim().length > 0 ? state.release.name.trim() : null;
// Milestones may be either a list of milestone objects OR just a list
@@ -129,7 +138,9 @@ export const releaseUpdateMutatationVariables = (state) => {
projectPath: state.projectPath,
tagName: state.release.tagName,
name,
- description: state.release.description,
+ description: state.includeTagNotes
+ ? getters.formattedReleaseNotes
+ : state.release.description,
milestones,
},
};
@@ -151,3 +162,8 @@ export const releaseCreateMutatationVariables = (state, getters) => {
},
};
};
+
+export const formattedReleaseNotes = ({ includeTagNotes, release: { description }, tagNotes }) =>
+ includeTagNotes && tagNotes
+ ? `${description}\n\n### ${s__('Releases|Tag message')}\n\n${tagNotes}\n`
+ : description;
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
index 1b2f5f33f02..daa077309a1 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
@@ -20,3 +20,9 @@ export const UPDATE_ASSET_LINK_URL = 'UPDATE_ASSET_LINK_URL';
export const UPDATE_ASSET_LINK_NAME = 'UPDATE_ASSET_LINK_NAME';
export const UPDATE_ASSET_LINK_TYPE = 'UPDATE_ASSET_LINK_TYPE';
export const REMOVE_ASSET_LINK = 'REMOVE_ASSET_LINK';
+
+export const REQUEST_TAG_NOTES = 'REQUEST_TAG_NOTES';
+export const RECEIVE_TAG_NOTES_SUCCESS = 'RECEIVE_TAG_NOTES_SUCCESS';
+export const RECEIVE_TAG_NOTES_ERROR = 'RECEIVE_TAG_NOTES_ERROR';
+
+export const UPDATE_INCLUDE_TAG_NOTES = 'UPDATE_INCLUDE_TAG_NOTES';
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
index cf282f9ab2c..6b22468bbfe 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
@@ -95,4 +95,22 @@ export default {
[types.REMOVE_ASSET_LINK](state, linkIdToRemove) {
state.release.assets.links = state.release.assets.links.filter((l) => l.id !== linkIdToRemove);
},
+
+ [types.REQUEST_TAG_NOTES](state) {
+ state.isFetchingTagNotes = true;
+ },
+ [types.RECEIVE_TAG_NOTES_SUCCESS](state, data) {
+ state.fetchError = undefined;
+ state.isFetchingTagNotes = false;
+ state.tagNotes = data.message;
+ state.existingRelease = data.release;
+ },
+ [types.RECEIVE_TAG_NOTES_ERROR](state, error) {
+ state.fetchError = error;
+ state.isFetchingTagNotes = false;
+ state.tagNotes = '';
+ },
+ [types.UPDATE_INCLUDE_TAG_NOTES](state, includeTagNotes) {
+ state.includeTagNotes = includeTagNotes;
+ },
};
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/state.js b/app/assets/javascripts/releases/stores/modules/edit_new/state.js
index 315d07ac664..33cb3ee06d0 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/state.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/state.js
@@ -9,6 +9,7 @@ export default ({
manageMilestonesPath,
newMilestonePath,
releasesPagePath,
+ editReleaseDocsPath,
tagName = null,
defaultBranch = null,
@@ -23,6 +24,7 @@ export default ({
manageMilestonesPath,
newMilestonePath,
releasesPagePath,
+ editReleaseDocsPath,
/**
* The name of the tag associated with the release, provided by the backend.
@@ -48,4 +50,8 @@ export default ({
isUpdatingRelease: false,
updateError: null,
+
+ tagNotes: '',
+ includeTagNotes: false,
+ existingRelease: null,
});
diff --git a/app/assets/javascripts/reports/codequality_report/store/getters.js b/app/assets/javascripts/reports/codequality_report/store/getters.js
index 4712f8cbefe..70d11e96a54 100644
--- a/app/assets/javascripts/reports/codequality_report/store/getters.js
+++ b/app/assets/javascripts/reports/codequality_report/store/getters.js
@@ -1,5 +1,5 @@
import { spriteIcon } from '~/lib/utils/common_utils';
-import { sprintf, s__ } from '~/locale';
+import { sprintf, s__, n__ } from '~/locale';
import { LOADING, ERROR, SUCCESS, STATUS_NOT_FOUND } from '../../constants';
export const hasCodequalityIssues = (state) =>
@@ -29,9 +29,17 @@ export const codequalityText = (state) => {
},
);
} else if (resolvedIssues.length) {
- text = s__(`ciReport|Code quality improved`);
+ text = n__(
+ `ciReport|Code quality improved due to 1 resolved issue`,
+ `ciReport|Code quality improved due to %d resolved issues`,
+ resolvedIssues.length,
+ );
} else if (newIssues.length) {
- text = s__(`ciReport|Code quality degraded`);
+ text = n__(
+ `ciReport|Code quality degraded due to 1 new issue`,
+ `ciReport|Code quality degraded due to %d new issues`,
+ newIssues.length,
+ );
}
return text;
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index c9e4aab1db1..3729bd4c601 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -8,7 +8,7 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo, getLocationHash } from '~/lib/utils/url_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
import CodeIntelligence from '~/code_navigation/components/app.vue';
@@ -17,15 +17,12 @@ import getRefMixin from '../mixins/get_ref';
import blobInfoQuery from '../queries/blob_info.query.graphql';
import userInfoQuery from '../queries/user_info.query.graphql';
import applicationInfoQuery from '../queries/application_info.query.graphql';
-import { DEFAULT_BLOB_INFO, TEXT_FILE_TYPE, LFS_STORAGE } from '../constants';
+import { DEFAULT_BLOB_INFO, TEXT_FILE_TYPE, LFS_STORAGE, LEGACY_FILE_TYPES } from '../constants';
import BlobButtonGroup from './blob_button_group.vue';
import ForkSuggestion from './fork_suggestion.vue';
import { loadViewer } from './blob_viewers';
export default {
- i18n: {
- pipelineEditor: __('Pipeline Editor'),
- },
components: {
BlobHeader,
BlobButtonGroup,
@@ -132,7 +129,8 @@ export default {
return this.shouldLoadLegacyViewer ? null : loadViewer(fileType, this.isUsingLfs);
},
shouldLoadLegacyViewer() {
- return this.viewer.fileType === TEXT_FILE_TYPE && !this.glFeatures.highlightJs;
+ const isTextFile = this.viewer.fileType === TEXT_FILE_TYPE && !this.glFeatures.highlightJs;
+ return isTextFile || LEGACY_FILE_TYPES.includes(this.blobInfo.fileType);
},
legacyViewerLoaded() {
return (
@@ -199,11 +197,20 @@ export default {
this.legacyRichViewer = html;
}
+ this.scrollToHash();
this.isBinary = binary;
this.isLoadingLegacyViewer = false;
})
.catch(() => this.displayError());
},
+ scrollToHash() {
+ const hash = getLocationHash();
+ if (hash) {
+ // Ensures the browser's native scroll to hash is triggered for async content
+ window.location.hash = '';
+ window.location.hash = hash;
+ }
+ },
displayError() {
createFlash({ message: __('An error occurred while loading the file. Please try again.') });
},
diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue
index 84c9f9d0bbe..20888db80a9 100644
--- a/app/assets/javascripts/repository/components/breadcrumbs.vue
+++ b/app/assets/javascripts/repository/components/breadcrumbs.vue
@@ -269,6 +269,9 @@ export default {
renderAddToTreeDropdown() {
return !this.isBlobPath && (this.canCollaborate || this.canCreateMrFromFork);
},
+ newDirectoryPath() {
+ return joinPaths(this.newDirPath, this.currentPath);
+ },
},
methods: {
isLast(i) {
@@ -332,7 +335,7 @@ export default {
:commit-message="__('Add new directory')"
:target-branch="selectedBranch"
:original-branch="originalBranch"
- :path="newDirPath"
+ :path="newDirectoryPath"
/>
</nav>
</template>
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 2810db33e64..03dd7c6fada 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -14,6 +14,7 @@ import CiIcon from '~/vue_shared/components/ci_icon.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import getRefMixin from '../mixins/get_ref';
import projectPathQuery from '../queries/project_path.query.graphql';
@@ -27,6 +28,7 @@ export default {
GlButtonGroup,
GlLink,
GlLoadingIcon,
+ UserAvatarImage,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -111,24 +113,24 @@ export default {
</script>
<template>
- <div class="well-segment commit gl-p-5 gl-w-full">
+ <div class="well-segment commit gl-p-5 gl-w-full gl-display-flex">
<gl-loading-icon v-if="isLoading" size="md" color="dark" class="m-auto" />
<template v-else-if="commit">
<user-avatar-link
v-if="commit.author"
:link-href="commit.author.webPath"
:img-src="commit.author.avatarUrl"
- :img-size="40"
- class="avatar-cell"
+ :img-size="32"
+ :img-css-classes="'gl-mr-0!' /* NOTE: this is needed only while we migrate user-avatar-image to GlAvatar (7731 epics) */"
+ class="gl-my-2 gl-mr-4"
+ />
+ <user-avatar-image
+ v-else
+ class="gl-my-2 gl-mr-4"
+ :img-src="commit.authorGravatar || $options.defaultAvatarUrl"
+ :css-classes="'gl-mr-0!' /* NOTE: this is needed only while we migrate user-avatar-image to GlAvatar (7731 epics) */"
+ :size="32"
/>
- <span v-else class="avatar-cell user-avatar-link">
- <img
- :src="commit.authorGravatar || $options.defaultAvatarUrl"
- width="40"
- height="40"
- class="avatar s40"
- />
- </span>
<div class="commit-detail flex-list">
<div class="commit-content qa-commit-content">
<gl-link
@@ -168,7 +170,10 @@ export default {
class="commit-row-description gl-mb-3"
></pre>
</div>
- <div class="commit-actions flex-row">
+ <div class="gl-flex-grow-1"></div>
+ <div
+ class="commit-actions gl-display-flex gl-flex-align gl-align-items-center gl-flex-direction-row"
+ >
<div
v-if="commit.signatureHtml"
v-html="commit.signatureHtml /* eslint-disable-line vue/no-v-html */"
diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js
index bb9d3180be8..2cafeed2ef4 100644
--- a/app/assets/javascripts/repository/constants.js
+++ b/app/assets/javascripts/repository/constants.js
@@ -86,3 +86,24 @@ export const DEFAULT_BLOB_INFO = {
export const TEXT_FILE_TYPE = 'text';
export const LFS_STORAGE = 'lfs';
+
+/**
+ * We have some features (like linking to external dependencies) that our frontend highlighter
+ * do not yet support.
+ * These are file types that we want the legacy (backend) syntax highlighter to highlight.
+ */
+export const LEGACY_FILE_TYPES = [
+ 'package_json',
+ 'gemfile',
+ 'gemspec',
+ 'composer_json',
+ 'podfile',
+ 'podspec',
+ 'podspec_json',
+ 'cartfile',
+ 'godeps_json',
+ 'requirements_txt',
+ 'cargo_toml',
+ 'go_mod',
+ 'go_sum',
+];
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index b38a1cfdc7b..8f8735a6371 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -151,12 +151,20 @@ export default function setupVueRepositoryList() {
const treeHistoryLinkEl = document.getElementById('js-tree-history-link');
const { historyLink } = treeHistoryLinkEl.dataset;
+ let { isProjectOverview } = treeHistoryLinkEl.dataset;
+
+ const isProjectOverviewAfterEach = router.afterEach(() => {
+ isProjectOverview = false;
+ isProjectOverviewAfterEach();
+ });
// eslint-disable-next-line no-new
new Vue({
el: treeHistoryLinkEl,
router,
render(h) {
+ if (parseBoolean(isProjectOverview) && !this.$route.params.path) return null;
+
return h(
GlButton,
{
diff --git a/app/assets/javascripts/rest_api.js b/app/assets/javascripts/rest_api.js
index 48a15954035..0f8e6945bf9 100644
--- a/app/assets/javascripts/rest_api.js
+++ b/app/assets/javascripts/rest_api.js
@@ -4,6 +4,7 @@ export * from './api/user_api';
export * from './api/markdown_api';
export * from './api/bulk_imports_api';
export * from './api/namespaces_api';
+export * from './api/tags_api';
// Note: It's not possible to spy on methods imported from this file in
// Jest tests.
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 009afe03ea6..a3abc8b8e90 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -7,6 +7,18 @@ import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { sprintf, s__, __ } from './locale';
+const updateSidebarClasses = (layoutPage, rightSidebar) => {
+ if (window.innerWidth >= 768) {
+ layoutPage.classList.remove('right-sidebar-expanded', 'right-sidebar-collapsed');
+ rightSidebar.classList.remove('right-sidebar-collapsed');
+ rightSidebar.classList.add('right-sidebar-expanded');
+ } else {
+ layoutPage.classList.add('right-sidebar-collapsed', 'is-merge-request');
+ rightSidebar.classList.add('right-sidebar-collapsed');
+ rightSidebar.classList.remove('right-sidebar-expanded');
+ }
+};
+
function Sidebar() {
this.toggleTodo = this.toggleTodo.bind(this);
this.sidebar = $('aside');
@@ -42,13 +54,22 @@ Sidebar.prototype.addEventListeners = function () {
this.sidebar.on('hiddenGlDropdown', this, this.onSidebarDropdownHidden);
$document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked);
- return $(document)
- .off('click', '.js-issuable-todo')
- .on('click', '.js-issuable-todo', this.toggleTodo);
+ $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo);
+
+ if (window.gon?.features?.movedMrSidebar) {
+ const layoutPage = document.querySelector('.layout-page');
+ const rightSidebar = document.querySelector('.js-right-sidebar');
+
+ updateSidebarClasses(layoutPage, rightSidebar);
+ window.addEventListener('resize', () => updateSidebarClasses(layoutPage, rightSidebar));
+ }
};
Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
const $this = $(this);
+
+ if ($this.hasClass('right-sidebar-merge-requests')) return;
+
const $collapseIcon = $('.js-sidebar-collapse');
const $expandIcon = $('.js-sidebar-expand');
const $toggleContainer = $('.js-sidebar-toggle-container');
diff --git a/app/assets/javascripts/runner/admin_runner_edit/admin_runner_edit_app.vue b/app/assets/javascripts/runner/admin_runner_edit/admin_runner_edit_app.vue
index c2db3b9facd..40787cf72da 100644
--- a/app/assets/javascripts/runner/admin_runner_edit/admin_runner_edit_app.vue
+++ b/app/assets/javascripts/runner/admin_runner_edit/admin_runner_edit_app.vue
@@ -5,7 +5,7 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '../components/runner_header.vue';
import RunnerUpdateForm from '../components/runner_update_form.vue';
import { I18N_FETCH_ERROR } from '../constants';
-import runnerQuery from '../graphql/details/runner.query.graphql';
+import runnerFormQuery from '../graphql/edit/runner_form.query.graphql';
import { captureException } from '../sentry_utils';
export default {
@@ -19,6 +19,11 @@ export default {
type: String,
required: true,
},
+ runnerPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -27,7 +32,7 @@ export default {
},
apollo: {
runner: {
- query: runnerQuery,
+ query: runnerFormQuery,
variables() {
return {
id: convertToGraphQLId(TYPE_CI_RUNNER, this.runnerId),
@@ -40,6 +45,11 @@ export default {
},
},
},
+ computed: {
+ loading() {
+ return this.$apollo.queries.runner.loading;
+ },
+ },
errorCaptured(error) {
this.reportToSentry(error);
},
@@ -53,6 +63,11 @@ export default {
<template>
<div>
<runner-header v-if="runner" :runner="runner" />
- <runner-update-form :runner="runner" class="gl-my-5" />
+ <runner-update-form
+ :loading="loading"
+ :runner="runner"
+ :runner-path="runnerPath"
+ class="gl-my-5"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/runner/admin_runner_edit/index.js b/app/assets/javascripts/runner/admin_runner_edit/index.js
index adb420f9963..a2ac5731a62 100644
--- a/app/assets/javascripts/runner/admin_runner_edit/index.js
+++ b/app/assets/javascripts/runner/admin_runner_edit/index.js
@@ -12,7 +12,7 @@ export const initAdminRunnerEdit = (selector = '#js-admin-runner-edit') => {
return null;
}
- const { runnerId } = el.dataset;
+ const { runnerId, runnerPath } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
@@ -25,6 +25,7 @@ export const initAdminRunnerEdit = (selector = '#js-admin-runner-edit') => {
return h(AdminRunnerEditApp, {
props: {
runnerId,
+ runnerPath,
},
});
},
diff --git a/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue b/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue
index 86ad912f017..c3f317b40b0 100644
--- a/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue
+++ b/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue
@@ -1,19 +1,23 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
-import { createAlert } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { redirectTo } from '~/lib/utils/url_utility';
+import RunnerDeleteButton from '../components/runner_delete_button.vue';
import RunnerEditButton from '../components/runner_edit_button.vue';
import RunnerPauseButton from '../components/runner_pause_button.vue';
import RunnerHeader from '../components/runner_header.vue';
import RunnerDetails from '../components/runner_details.vue';
import { I18N_FETCH_ERROR } from '../constants';
-import runnerQuery from '../graphql/details/runner.query.graphql';
+import runnerQuery from '../graphql/show/runner.query.graphql';
import { captureException } from '../sentry_utils';
+import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage';
export default {
name: 'AdminRunnerShowApp',
components: {
+ RunnerDeleteButton,
RunnerEditButton,
RunnerPauseButton,
RunnerHeader,
@@ -27,6 +31,10 @@ export default {
type: String,
required: true,
},
+ runnersPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -52,6 +60,9 @@ export default {
canUpdate() {
return this.runner.userPermissions?.updateRunner;
},
+ canDelete() {
+ return this.runner.userPermissions?.deleteRunner;
+ },
},
errorCaptured(error) {
this.reportToSentry(error);
@@ -60,6 +71,10 @@ export default {
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
+ onDeleted({ message }) {
+ saveAlertToLocalStorage({ message, variant: VARIANT_SUCCESS });
+ redirectTo(this.runnersPath);
+ },
},
};
</script>
@@ -69,6 +84,7 @@ export default {
<template #actions>
<runner-edit-button v-if="canUpdate && runner.editAdminUrl" :href="runner.editAdminUrl" />
<runner-pause-button v-if="canUpdate" :runner="runner" />
+ <runner-delete-button v-if="canDelete" :runner="runner" @deleted="onDeleted" />
</template>
</runner-header>
diff --git a/app/assets/javascripts/runner/admin_runner_show/index.js b/app/assets/javascripts/runner/admin_runner_show/index.js
index a781898cf8d..ea455416648 100644
--- a/app/assets/javascripts/runner/admin_runner_show/index.js
+++ b/app/assets/javascripts/runner/admin_runner_show/index.js
@@ -1,18 +1,21 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import { showAlertFromLocalStorage } from '../local_storage_alert/show_alert_from_local_storage';
import AdminRunnerShowApp from './admin_runner_show_app.vue';
Vue.use(VueApollo);
export const initAdminRunnerShow = (selector = '#js-admin-runner-show') => {
+ showAlertFromLocalStorage();
+
const el = document.querySelector(selector);
if (!el) {
return null;
}
- const { runnerId } = el.dataset;
+ const { runnerId, runnersPath } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
@@ -25,6 +28,7 @@ export const initAdminRunnerShow = (selector = '#js-admin-runner-show') => {
return h(AdminRunnerShowApp, {
props: {
runnerId,
+ runnersPath,
},
});
},
diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
index accc9926a57..c2bb635e056 100644
--- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
@@ -38,7 +38,7 @@ import {
} from '../runner_search_utils';
import { captureException } from '../sentry_utils';
-const runnersCountSmartQuery = {
+const countSmartQuery = () => ({
query: runnersAdminCountQuery,
fetchPolicy: fetchPolicies.NETWORK_ONLY,
update(data) {
@@ -47,6 +47,39 @@ const runnersCountSmartQuery = {
error(error) {
this.reportToSentry(error);
},
+});
+
+const tabCountSmartQuery = ({ type }) => {
+ return {
+ ...countSmartQuery(),
+ variables() {
+ return {
+ ...this.countVariables,
+ type,
+ };
+ },
+ };
+};
+
+const statusCountSmartQuery = ({ status, name }) => {
+ return {
+ ...countSmartQuery(),
+ skip() {
+ // skip if filtering by status and not using _this_ status as filter
+ if (this.countVariables.status && this.countVariables.status !== status) {
+ // reset count for given status
+ this[name] = null;
+ return true;
+ }
+ return false;
+ },
+ variables() {
+ return {
+ ...this.countVariables,
+ status,
+ };
+ },
+ };
};
export default {
@@ -101,65 +134,30 @@ export default {
this.reportToSentry(error);
},
},
+
+ // Tabs counts
allRunnersCount: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- ...this.countVariables,
- type: null,
- };
- },
+ ...tabCountSmartQuery({ type: null }),
},
instanceRunnersCount: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- ...this.countVariables,
- type: INSTANCE_TYPE,
- };
- },
+ ...tabCountSmartQuery({ type: INSTANCE_TYPE }),
},
groupRunnersCount: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- ...this.countVariables,
- type: GROUP_TYPE,
- };
- },
+ ...tabCountSmartQuery({ type: GROUP_TYPE }),
},
projectRunnersCount: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- ...this.countVariables,
- type: PROJECT_TYPE,
- };
- },
+ ...tabCountSmartQuery({ type: PROJECT_TYPE }),
},
+
+ // Runner stats
onlineRunnersTotal: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- status: STATUS_ONLINE,
- };
- },
+ ...statusCountSmartQuery({ status: STATUS_ONLINE, name: 'onlineRunnersTotal' }),
},
offlineRunnersTotal: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- status: STATUS_OFFLINE,
- };
- },
+ ...statusCountSmartQuery({ status: STATUS_OFFLINE, name: 'offlineRunnersTotal' }),
},
staleRunnersTotal: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- status: STATUS_STALE,
- };
- },
+ ...statusCountSmartQuery({ status: STATUS_STALE, name: 'staleRunnersTotal' }),
},
},
computed: {
@@ -263,12 +261,6 @@ export default {
</script>
<template>
<div>
- <runner-stats
- :online-runners-count="onlineRunnersTotal"
- :offline-runners-count="offlineRunnersTotal"
- :stale-runners-count="staleRunnersTotal"
- />
-
<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"
>
@@ -300,6 +292,12 @@ export default {
:namespace="$options.filteredSearchNamespace"
/>
+ <runner-stats
+ :online-runners-count="onlineRunnersTotal"
+ :offline-runners-count="offlineRunnersTotal"
+ :stale-runners-count="staleRunnersTotal"
+ />
+
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
{{ __('No runners found') }}
</div>
diff --git a/app/assets/javascripts/runner/admin_runners/index.js b/app/assets/javascripts/runner/admin_runners/index.js
index 12e2cb2ee9f..b1d8442bb32 100644
--- a/app/assets/javascripts/runner/admin_runners/index.js
+++ b/app/assets/javascripts/runner/admin_runners/index.js
@@ -5,12 +5,15 @@ import { visitUrl } from '~/lib/utils/url_utility';
import { updateOutdatedUrl } from '~/runner/runner_search_utils';
import createDefaultClient from '~/lib/graphql';
import { createLocalState } from '../graphql/list/local_state';
+import { showAlertFromLocalStorage } from '../local_storage_alert/show_alert_from_local_storage';
import AdminRunnersApp from './admin_runners_app.vue';
Vue.use(GlToast);
Vue.use(VueApollo);
export const initAdminRunners = (selector = '#js-admin-runners') => {
+ showAlertFromLocalStorage();
+
const el = document.querySelector(selector);
if (!el) {
diff --git a/app/assets/javascripts/runner/components/registration/registration_dropdown.vue b/app/assets/javascripts/runner/components/registration/registration_dropdown.vue
index 3fbe3c1be74..bb2a8ddf151 100644
--- a/app/assets/javascripts/runner/components/registration/registration_dropdown.vue
+++ b/app/assets/javascripts/runner/components/registration/registration_dropdown.vue
@@ -96,7 +96,7 @@ export default {
<runner-instructions-modal
v-if="instructionsModalOpened"
ref="runnerInstructionsModal"
- :registration-token="registrationToken"
+ :registration-token="currentRegistrationToken"
data-testid="runner-instructions-modal"
/>
</gl-dropdown-item>
diff --git a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue
index 1234054c660..09d46ce3e66 100644
--- a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue
+++ b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue
@@ -115,14 +115,14 @@ export default {
<gl-modal
size="sm"
:modal-id="$options.modalId"
- :action-primary="{
+ :action-primary="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: $options.i18n.modalAction,
attributes: [{ variant: 'danger' }],
- }"
- :action-secondary="{
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ :action-secondary="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: $options.i18n.modalCancel,
attributes: [{ variant: 'default' }],
- }"
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:title="$options.i18n.modalTitle"
@primary="handleModalPrimary"
>
diff --git a/app/assets/javascripts/runner/components/runner_delete_button.vue b/app/assets/javascripts/runner/components/runner_delete_button.vue
index b58665ecbc9..62382891df0 100644
--- a/app/assets/javascripts/runner/components/runner_delete_button.vue
+++ b/app/assets/javascripts/runner/components/runner_delete_button.vue
@@ -165,6 +165,8 @@ export default {
:loading="deleting"
:disabled="disabled"
variant="danger"
+ category="secondary"
+ v-bind="$attrs"
>
{{ buttonContent }}
</gl-button>
diff --git a/app/assets/javascripts/runner/components/runner_details.vue b/app/assets/javascripts/runner/components/runner_details.vue
index b6a5ffc7a64..3734f436034 100644
--- a/app/assets/javascripts/runner/components/runner_details.vue
+++ b/app/assets/javascripts/runner/components/runner_details.vue
@@ -84,6 +84,9 @@ export default {
</runner-detail>
<runner-detail :label="s__('Runners|Version')" :value="runner.version" />
<runner-detail :label="s__('Runners|IP Address')" :value="runner.ipAddress" />
+ <runner-detail :label="s__('Runners|Executor')" :value="runner.executorName" />
+ <runner-detail :label="s__('Runners|Architecture')" :value="runner.architectureName" />
+ <runner-detail :label="s__('Runners|Platform')" :value="runner.platformName" />
<runner-detail :label="s__('Runners|Configuration')">
<template #value>
<gl-intersperse v-if="configTextProtected || configTextUntagged">
diff --git a/app/assets/javascripts/runner/components/runner_jobs.vue b/app/assets/javascripts/runner/components/runner_jobs.vue
index b25d92d049e..4eb1312b204 100644
--- a/app/assets/javascripts/runner/components/runner_jobs.vue
+++ b/app/assets/javascripts/runner/components/runner_jobs.vue
@@ -1,7 +1,7 @@
<script>
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { createAlert } from '~/flash';
-import runnerJobsQuery from '../graphql/details/runner_jobs.query.graphql';
+import runnerJobsQuery from '../graphql/show/runner_jobs.query.graphql';
import { I18N_FETCH_ERROR, I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '../constants';
import { captureException } from '../sentry_utils';
import { getPaginationVariables } from '../utils';
diff --git a/app/assets/javascripts/runner/components/runner_pagination.vue b/app/assets/javascripts/runner/components/runner_pagination.vue
index b683a7f2330..cfc21d1407b 100644
--- a/app/assets/javascripts/runner/components/runner_pagination.vue
+++ b/app/assets/javascripts/runner/components/runner_pagination.vue
@@ -21,10 +21,10 @@ export default {
},
computed: {
prevPage() {
- return this.pageInfo?.hasPreviousPage ? this.value?.page - 1 : null;
+ return this.pageInfo?.hasPreviousPage ? this.value.page - 1 : null;
},
nextPage() {
- return this.pageInfo?.hasNextPage ? this.value?.page + 1 : null;
+ return this.pageInfo?.hasNextPage ? this.value.page + 1 : null;
},
},
methods: {
diff --git a/app/assets/javascripts/runner/components/runner_projects.vue b/app/assets/javascripts/runner/components/runner_projects.vue
index d080d34fdd3..daca718e2b5 100644
--- a/app/assets/javascripts/runner/components/runner_projects.vue
+++ b/app/assets/javascripts/runner/components/runner_projects.vue
@@ -2,7 +2,7 @@
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { sprintf, formatNumber } from '~/locale';
import { createAlert } from '~/flash';
-import runnerProjectsQuery from '../graphql/details/runner_projects.query.graphql';
+import runnerProjectsQuery from '../graphql/show/runner_projects.query.graphql';
import {
I18N_ASSIGNED_PROJECTS,
I18N_NONE,
diff --git a/app/assets/javascripts/runner/components/runner_update_form.vue b/app/assets/javascripts/runner/components/runner_update_form.vue
index 119e5236f85..56c9007a781 100644
--- a/app/assets/javascripts/runner/components/runner_update_form.vue
+++ b/app/assets/javascripts/runner/components/runner_update_form.vue
@@ -1,10 +1,12 @@
<script>
import {
GlButton,
+ GlIcon,
GlForm,
GlFormCheckbox,
GlFormGroup,
GlFormInputGroup,
+ GlSkeletonLoader,
GlTooltipDirective,
} from '@gitlab/ui';
import {
@@ -12,19 +14,23 @@ import {
runnerToModel,
} from 'ee_else_ce/runner/runner_update_form_utils';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
+import { redirectTo } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { captureException } from '~/runner/sentry_utils';
import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants';
-import runnerUpdateMutation from '../graphql/details/runner_update.mutation.graphql';
+import runnerUpdateMutation from '../graphql/edit/runner_update.mutation.graphql';
+import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage';
export default {
name: 'RunnerUpdateForm',
components: {
GlButton,
+ GlIcon,
GlForm,
GlFormCheckbox,
GlFormGroup,
GlFormInputGroup,
+ GlSkeletonLoader,
RunnerUpdateCostFactorFields: () =>
import('ee_component/runner/components/runner_update_cost_factor_fields.vue'),
},
@@ -37,6 +43,16 @@ export default {
required: false,
default: null,
},
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ runnerPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -48,9 +64,6 @@ export default {
canBeLockedToProject() {
return this.runner?.runnerType === PROJECT_TYPE;
},
- readonlyIpAddress() {
- return this.runner?.ipAddress;
- },
},
watch: {
runner(newVal, oldVal) {
@@ -74,24 +87,23 @@ export default {
});
if (errors?.length) {
- // Validation errors need not be thrown
- createAlert({ message: errors[0] });
- return;
+ this.onError(errors[0]);
+ } else {
+ this.onSuccess();
}
-
- this.onSuccess();
} catch (error) {
const { message } = error;
-
- createAlert({ message });
+ this.onError(message);
captureException({ error, component: this.$options.name });
- } finally {
- this.saving = false;
}
},
onSuccess() {
- createAlert({ message: __('Changes saved.'), variant: VARIANT_SUCCESS });
- this.model = runnerToModel(this.runner);
+ saveAlertToLocalStorage({ message: __('Changes saved.'), variant: VARIANT_SUCCESS });
+ redirectTo(this.runnerPath);
+ },
+ onError(message) {
+ this.saving = false;
+ createAlert({ message });
},
},
ACCESS_LEVEL_NOT_PROTECTED,
@@ -100,104 +112,108 @@ export default {
</script>
<template>
<gl-form @submit.prevent="onSubmit">
- <gl-form-checkbox
- v-model="model.active"
- data-testid="runner-field-paused"
- :value="false"
- :unchecked-value="true"
- >
- {{ __('Paused') }}
- <template #help>
- {{ s__('Runners|Stop the runner from accepting new jobs.') }}
- </template>
- </gl-form-checkbox>
+ <h4 class="gl-font-lg gl-my-5">{{ s__('Runners|Details') }}</h4>
- <gl-form-checkbox
- v-model="model.accessLevel"
- data-testid="runner-field-protected"
- :value="$options.ACCESS_LEVEL_REF_PROTECTED"
- :unchecked-value="$options.ACCESS_LEVEL_NOT_PROTECTED"
- >
- {{ __('Protected') }}
- <template #help>
- {{ s__('Runners|Use the runner on pipelines for protected branches only.') }}
- </template>
- </gl-form-checkbox>
+ <gl-skeleton-loader v-if="loading" />
+ <gl-form-group v-else :label="__('Description')" data-testid="runner-field-description">
+ <gl-form-input-group v-model="model.description" />
+ </gl-form-group>
- <gl-form-checkbox v-model="model.runUntagged" data-testid="runner-field-run-untagged">
- {{ __('Run untagged jobs') }}
- <template #help>
- {{ s__('Runners|Use the runner for jobs without tags, in addition to tagged jobs.') }}
- </template>
- </gl-form-checkbox>
+ <hr />
- <gl-form-checkbox
- v-if="canBeLockedToProject"
- v-model="model.locked"
- data-testid="runner-field-locked"
- >
- {{ __('Lock to current projects') }}
- <template #help>
- {{
- s__(
- 'Runners|Use the runner for the currently assigned projects only. Only administrators can change the assigned projects.',
- )
- }}
- </template>
- </gl-form-checkbox>
+ <h4 class="gl-font-lg gl-my-5">{{ s__('Runners|Configuration') }}</h4>
- <gl-form-group :label="__('IP Address')" data-testid="runner-field-ip-address">
- <gl-form-input-group :value="readonlyIpAddress" readonly select-on-click>
- <template #append>
- <gl-button
- v-gl-tooltip.hover
- :title="__('Copy IP Address')"
- :aria-label="__('Copy IP Address')"
- :data-clipboard-text="readonlyIpAddress"
- icon="copy-to-clipboard"
- class="d-inline-flex"
- />
- </template>
- </gl-form-input-group>
- </gl-form-group>
+ <template v-if="loading">
+ <gl-skeleton-loader v-for="i in 3" :key="i" />
+ </template>
+ <template v-else>
+ <div class="gl-mb-5">
+ <gl-form-checkbox
+ v-model="model.active"
+ data-testid="runner-field-paused"
+ :value="false"
+ :unchecked-value="true"
+ >
+ {{ __('Paused') }}
+ <template #help>
+ {{ s__('Runners|Stop the runner from accepting new jobs.') }}
+ </template>
+ </gl-form-checkbox>
- <gl-form-group :label="__('Description')" data-testid="runner-field-description">
- <gl-form-input-group v-model="model.description" />
- </gl-form-group>
+ <gl-form-checkbox
+ v-model="model.accessLevel"
+ data-testid="runner-field-protected"
+ :value="$options.ACCESS_LEVEL_REF_PROTECTED"
+ :unchecked-value="$options.ACCESS_LEVEL_NOT_PROTECTED"
+ >
+ {{ __('Protected') }}
+ <template #help>
+ {{ s__('Runners|Use the runner on pipelines for protected branches only.') }}
+ </template>
+ </gl-form-checkbox>
- <gl-form-group
- data-testid="runner-field-max-timeout"
- :label="__('Maximum job timeout')"
- :description="
- s__(
- 'Runners|Enter the number of seconds. This timeout takes precedence over lower timeouts set for the project.',
- )
- "
- >
- <gl-form-input-group v-model.number="model.maximumTimeout" type="number" />
- </gl-form-group>
+ <gl-form-checkbox v-model="model.runUntagged" data-testid="runner-field-run-untagged">
+ {{ __('Run untagged jobs') }}
+ <template #help>
+ {{ s__('Runners|Use the runner for jobs without tags, in addition to tagged jobs.') }}
+ </template>
+ </gl-form-checkbox>
- <gl-form-group
- data-testid="runner-field-tags"
- :label="__('Tags')"
- :description="
- __('You can set up jobs to only use runners with specific tags. Separate tags with commas.')
- "
- >
- <gl-form-input-group v-model="model.tagList" />
- </gl-form-group>
+ <gl-form-checkbox
+ v-if="canBeLockedToProject"
+ v-model="model.locked"
+ data-testid="runner-field-locked"
+ >
+ {{ __('Lock to current projects') }} <gl-icon name="lock" />
+ <template #help>
+ {{
+ s__(
+ 'Runners|Use the runner for the currently assigned projects only. Only administrators can change the assigned projects.',
+ )
+ }}
+ </template>
+ </gl-form-checkbox>
+ </div>
- <runner-update-cost-factor-fields v-model="model" />
+ <gl-form-group
+ data-testid="runner-field-max-timeout"
+ :label="__('Maximum job timeout')"
+ :description="
+ s__(
+ 'Runners|Enter the number of seconds. This timeout takes precedence over lower timeouts set for the project.',
+ )
+ "
+ >
+ <gl-form-input-group v-model.number="model.maximumTimeout" type="number" />
+ </gl-form-group>
+
+ <gl-form-group
+ data-testid="runner-field-tags"
+ :label="__('Tags')"
+ :description="
+ __(
+ 'You can set up jobs to only use runners with specific tags. Separate tags with commas.',
+ )
+ "
+ >
+ <gl-form-input-group v-model="model.tagList" />
+ </gl-form-group>
+
+ <runner-update-cost-factor-fields v-model="model" />
+ </template>
- <div class="form-actions">
+ <div class="gl-mt-6">
<gl-button
type="submit"
variant="confirm"
class="js-no-auto-disable"
- :loading="saving || !runner"
+ :loading="loading || saving"
>
{{ __('Save changes') }}
</gl-button>
+ <gl-button :href="runnerPath">
+ {{ __('Cancel') }}
+ </gl-button>
</div>
</gl-form>
</template>
diff --git a/app/assets/javascripts/runner/graphql/details/runner.query.graphql b/app/assets/javascripts/runner/graphql/details/runner.query.graphql
deleted file mode 100644
index 4792a186160..00000000000
--- a/app/assets/javascripts/runner/graphql/details/runner.query.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-#import "ee_else_ce/runner/graphql/details/runner_details.fragment.graphql"
-
-query getRunner($id: CiRunnerID!) {
- # We have an id in deeply nested fragment
- # eslint-disable-next-line @graphql-eslint/require-id-when-available
- runner(id: $id) {
- ...RunnerDetails
- }
-}
diff --git a/app/assets/javascripts/runner/graphql/details/runner_details.fragment.graphql b/app/assets/javascripts/runner/graphql/details/runner_details.fragment.graphql
deleted file mode 100644
index 2449ee0fc0f..00000000000
--- a/app/assets/javascripts/runner/graphql/details/runner_details.fragment.graphql
+++ /dev/null
@@ -1,5 +0,0 @@
-#import "./runner_details_shared.fragment.graphql"
-
-fragment RunnerDetails on CiRunner {
- ...RunnerDetailsShared
-}
diff --git a/app/assets/javascripts/runner/graphql/details/runner_details_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/details/runner_details_shared.fragment.graphql
deleted file mode 100644
index d8c67728fac..00000000000
--- a/app/assets/javascripts/runner/graphql/details/runner_details_shared.fragment.graphql
+++ /dev/null
@@ -1,35 +0,0 @@
-fragment RunnerDetailsShared on CiRunner {
- __typename
- id
- runnerType
- active
- accessLevel
- runUntagged
- locked
- ipAddress
- description
- maximumTimeout
- jobCount
- tagList
- createdAt
- status(legacyMode: null)
- contactedAt
- version
- editAdminUrl
- userPermissions {
- updateRunner
- deleteRunner
- }
- groups {
- # Only a single group can be loaded here, while projects
- # are loaded separately using the query with pagination
- # parameters `runner_projects.query.graphql`.
- nodes {
- id
- avatarUrl
- name
- fullName
- webUrl
- }
- }
-}
diff --git a/app/assets/javascripts/runner/graphql/details/runner_update.mutation.graphql b/app/assets/javascripts/runner/graphql/details/runner_update.mutation.graphql
deleted file mode 100644
index e4bf51e2c30..00000000000
--- a/app/assets/javascripts/runner/graphql/details/runner_update.mutation.graphql
+++ /dev/null
@@ -1,15 +0,0 @@
-#import "ee_else_ce/runner/graphql/details/runner_details.fragment.graphql"
-
-# Mutation for updates from the runner form, loads
-# attributes shown in the runner details.
-
-mutation runnerUpdate($input: RunnerUpdateInput!) {
- runnerUpdate(input: $input) {
- # We have an id in deep nested fragment
- # eslint-disable-next-line @graphql-eslint/require-id-when-available
- runner {
- ...RunnerDetails
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/runner/graphql/edit/runner_fields.fragment.graphql b/app/assets/javascripts/runner/graphql/edit/runner_fields.fragment.graphql
new file mode 100644
index 00000000000..b732d587d70
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/edit/runner_fields.fragment.graphql
@@ -0,0 +1,5 @@
+#import "./runner_fields_shared.fragment.graphql"
+
+fragment RunnerFields on CiRunner {
+ ...RunnerFieldsShared
+}
diff --git a/app/assets/javascripts/runner/graphql/edit/runner_fields_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/edit/runner_fields_shared.fragment.graphql
new file mode 100644
index 00000000000..f900a0450e5
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/edit/runner_fields_shared.fragment.graphql
@@ -0,0 +1,15 @@
+fragment RunnerFieldsShared on CiRunner {
+ __typename
+ id
+ shortSha
+ runnerType
+ active
+ accessLevel
+ runUntagged
+ locked
+ description
+ maximumTimeout
+ tagList
+ createdAt
+ status(legacyMode: null)
+}
diff --git a/app/assets/javascripts/runner/graphql/edit/runner_form.query.graphql b/app/assets/javascripts/runner/graphql/edit/runner_form.query.graphql
new file mode 100644
index 00000000000..0bf66c223fc
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/edit/runner_form.query.graphql
@@ -0,0 +1,7 @@
+#import "ee_else_ce/runner/graphql/edit/runner_fields.fragment.graphql"
+
+query getRunnerForm($id: CiRunnerID!) {
+ runner(id: $id) {
+ ...RunnerFields
+ }
+}
diff --git a/app/assets/javascripts/runner/graphql/edit/runner_update.mutation.graphql b/app/assets/javascripts/runner/graphql/edit/runner_update.mutation.graphql
new file mode 100644
index 00000000000..8694a51b5a4
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/edit/runner_update.mutation.graphql
@@ -0,0 +1,13 @@
+#import "ee_else_ce/runner/graphql/edit/runner_fields.fragment.graphql"
+
+# Mutation for updates from the runner form, loads
+# attributes shown in the runner details.
+
+mutation runnerUpdate($input: RunnerUpdateInput!) {
+ runnerUpdate(input: $input) {
+ runner {
+ ...RunnerFields
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/runner/graphql/show/runner.query.graphql b/app/assets/javascripts/runner/graphql/show/runner.query.graphql
new file mode 100644
index 00000000000..178816b58bd
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/show/runner.query.graphql
@@ -0,0 +1,41 @@
+query getRunner($id: CiRunnerID!) {
+ runner(id: $id) {
+ __typename
+ id
+ shortSha
+ runnerType
+ active
+ accessLevel
+ runUntagged
+ locked
+ ipAddress
+ executorName
+ architectureName
+ platformName
+ description
+ maximumTimeout
+ jobCount
+ tagList
+ createdAt
+ status(legacyMode: null)
+ contactedAt
+ version
+ editAdminUrl
+ userPermissions {
+ updateRunner
+ deleteRunner
+ }
+ groups {
+ # Only a single group can be loaded here, while projects
+ # are loaded separately using the query with pagination
+ # parameters `runner_projects.query.graphql`.
+ nodes {
+ id
+ avatarUrl
+ name
+ fullName
+ webUrl
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/runner/graphql/details/runner_jobs.query.graphql b/app/assets/javascripts/runner/graphql/show/runner_jobs.query.graphql
index 14585e62bf2..14585e62bf2 100644
--- a/app/assets/javascripts/runner/graphql/details/runner_jobs.query.graphql
+++ b/app/assets/javascripts/runner/graphql/show/runner_jobs.query.graphql
diff --git a/app/assets/javascripts/runner/graphql/details/runner_projects.query.graphql b/app/assets/javascripts/runner/graphql/show/runner_projects.query.graphql
index cb27de7c200..cb27de7c200 100644
--- a/app/assets/javascripts/runner/graphql/details/runner_projects.query.graphql
+++ b/app/assets/javascripts/runner/graphql/show/runner_projects.query.graphql
diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
index b299d7c40fe..b5bd4b111fd 100644
--- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
@@ -34,7 +34,7 @@ import {
} from '../runner_search_utils';
import { captureException } from '../sentry_utils';
-const runnersCountSmartQuery = {
+const countSmartQuery = () => ({
query: groupRunnersCountQuery,
fetchPolicy: fetchPolicies.NETWORK_ONLY,
update(data) {
@@ -43,6 +43,39 @@ const runnersCountSmartQuery = {
error(error) {
this.reportToSentry(error);
},
+});
+
+const tabCountSmartQuery = ({ type }) => {
+ return {
+ ...countSmartQuery(),
+ variables() {
+ return {
+ ...this.countVariables,
+ type,
+ };
+ },
+ };
+};
+
+const statusCountSmartQuery = ({ status, name }) => {
+ return {
+ ...countSmartQuery(),
+ skip() {
+ // skip if filtering by status and not using _this_ status as filter
+ if (this.countVariables.status && this.countVariables.status !== status) {
+ // reset count for given status
+ this[name] = null;
+ return true;
+ }
+ return false;
+ },
+ variables() {
+ return {
+ ...this.countVariables,
+ status,
+ };
+ },
+ };
};
export default {
@@ -116,59 +149,27 @@ export default {
this.reportToSentry(error);
},
},
- onlineRunnersTotal: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- groupFullPath: this.groupFullPath,
- status: STATUS_ONLINE,
- };
- },
- },
- offlineRunnersTotal: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- groupFullPath: this.groupFullPath,
- status: STATUS_OFFLINE,
- };
- },
- },
- staleRunnersTotal: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- groupFullPath: this.groupFullPath,
- status: STATUS_STALE,
- };
- },
- },
+
+ // Tabs counts
allRunnersCount: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- ...this.countVariables,
- type: null,
- };
- },
+ ...tabCountSmartQuery({ type: null }),
},
groupRunnersCount: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- ...this.countVariables,
- type: GROUP_TYPE,
- };
- },
+ ...tabCountSmartQuery({ type: GROUP_TYPE }),
},
projectRunnersCount: {
- ...runnersCountSmartQuery,
- variables() {
- return {
- ...this.countVariables,
- type: PROJECT_TYPE,
- };
- },
+ ...tabCountSmartQuery({ type: PROJECT_TYPE }),
+ },
+
+ // Runner status summary
+ onlineRunnersTotal: {
+ ...statusCountSmartQuery({ status: STATUS_ONLINE, name: 'onlineRunnersTotal' }),
+ },
+ offlineRunnersTotal: {
+ ...statusCountSmartQuery({ status: STATUS_OFFLINE, name: 'offlineRunnersTotal' }),
+ },
+ staleRunnersTotal: {
+ ...statusCountSmartQuery({ status: STATUS_STALE, name: 'staleRunnersTotal' }),
},
},
computed: {
@@ -263,12 +264,6 @@ export default {
<template>
<div>
- <runner-stats
- :online-runners-count="onlineRunnersTotal"
- :offline-runners-count="offlineRunnersTotal"
- :stale-runners-count="staleRunnersTotal"
- />
-
<div class="gl-display-flex gl-align-items-center">
<runner-type-tabs
v-model="search"
@@ -298,6 +293,12 @@ export default {
:namespace="filteredSearchNamespace"
/>
+ <runner-stats
+ :online-runners-count="onlineRunnersTotal"
+ :offline-runners-count="offlineRunnersTotal"
+ :stale-runners-count="staleRunnersTotal"
+ />
+
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
{{ __('No runners found') }}
</div>
diff --git a/app/assets/javascripts/runner/local_storage_alert/constants.js b/app/assets/javascripts/runner/local_storage_alert/constants.js
new file mode 100644
index 00000000000..69b7418f898
--- /dev/null
+++ b/app/assets/javascripts/runner/local_storage_alert/constants.js
@@ -0,0 +1 @@
+export const LOCAL_STORAGE_ALERT_KEY = 'local-storage-alert';
diff --git a/app/assets/javascripts/runner/local_storage_alert/save_alert_to_local_storage.js b/app/assets/javascripts/runner/local_storage_alert/save_alert_to_local_storage.js
new file mode 100644
index 00000000000..ca7c627459a
--- /dev/null
+++ b/app/assets/javascripts/runner/local_storage_alert/save_alert_to_local_storage.js
@@ -0,0 +1,8 @@
+import AccessorUtilities from '~/lib/utils/accessor';
+import { LOCAL_STORAGE_ALERT_KEY } from './constants';
+
+export const saveAlertToLocalStorage = (alertOptions) => {
+ if (AccessorUtilities.canUseLocalStorage()) {
+ localStorage.setItem(LOCAL_STORAGE_ALERT_KEY, JSON.stringify(alertOptions));
+ }
+};
diff --git a/app/assets/javascripts/runner/local_storage_alert/show_alert_from_local_storage.js b/app/assets/javascripts/runner/local_storage_alert/show_alert_from_local_storage.js
new file mode 100644
index 00000000000..d768a06494a
--- /dev/null
+++ b/app/assets/javascripts/runner/local_storage_alert/show_alert_from_local_storage.js
@@ -0,0 +1,18 @@
+import AccessorUtilities from '~/lib/utils/accessor';
+import { LOCAL_STORAGE_ALERT_KEY } from './constants';
+
+export const showAlertFromLocalStorage = async () => {
+ if (AccessorUtilities.canUseLocalStorage()) {
+ const alertOptions = localStorage.getItem(LOCAL_STORAGE_ALERT_KEY);
+
+ if (alertOptions) {
+ try {
+ const { createAlert } = await import('~/flash');
+ createAlert(JSON.parse(alertOptions));
+ } catch {
+ // ignore when the alert data cannot be parsed
+ }
+ }
+ localStorage.removeItem(LOCAL_STORAGE_ALERT_KEY);
+ }
+};
diff --git a/app/assets/javascripts/runner/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js
index 5e3c412ddb6..0d688ed65ef 100644
--- a/app/assets/javascripts/runner/runner_search_utils.js
+++ b/app/assets/javascripts/runner/runner_search_utils.js
@@ -18,7 +18,6 @@ import {
PARAM_KEY_BEFORE,
DEFAULT_SORT,
RUNNER_PAGE_SIZE,
- STATUS_NEVER_CONTACTED,
} from './constants';
import { getPaginationVariables } from './utils';
@@ -84,7 +83,6 @@ const getPaginationFromParams = (params) => {
};
// Outdated URL parameters
-const STATUS_NOT_CONNECTED = 'NOT_CONNECTED';
const STATUS_ACTIVE = 'ACTIVE';
const STATUS_PAUSED = 'PAUSED';
@@ -116,10 +114,6 @@ export const updateOutdatedUrl = (url = window.location.href) => {
const status = params[PARAM_KEY_STATUS]?.[0] || null;
switch (status) {
- case STATUS_NOT_CONNECTED:
- return updateUrlParams(url, {
- [PARAM_KEY_STATUS]: [STATUS_NEVER_CONTACTED],
- });
case STATUS_ACTIVE:
return updateUrlParams(url, {
[PARAM_KEY_PAUSED]: ['false'],
diff --git a/app/assets/javascripts/runner/utils.js b/app/assets/javascripts/runner/utils.js
index 1f7794720de..cb2917a92fd 100644
--- a/app/assets/javascripts/runner/utils.js
+++ b/app/assets/javascripts/runner/utils.js
@@ -1,5 +1,4 @@
import { formatNumber } from '~/locale';
-import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants';
import { RUNNER_JOB_COUNT_LIMIT } from './constants';
/**
@@ -28,7 +27,7 @@ export const tableField = ({ key, label = '', thClasses = [], ...options }) => {
return {
key,
label,
- thClass: [DEFAULT_TH_CLASSES, ...thClasses],
+ thClass: thClasses,
tdAttr: {
'data-testid': `td-${key}`,
},
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index ba0120a0a70..d0c4ad3646c 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -1,15 +1,15 @@
<script>
import { GlTab, GlTabs, GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
import { __, s__ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import SectionLayout from '~/vue_shared/security_configuration/components/section_layout.vue';
+import currentLicenseQuery from '~/security_configuration/graphql/current_license.query.graphql';
import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
-import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
+import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY, LICENSE_ULTIMATE } from './constants';
import FeatureCard from './feature_card.vue';
import TrainingProviderList from './training_provider_list.vue';
-import SectionLayout from './section_layout.vue';
import UpgradeBanner from './upgrade_banner.vue';
export const i18n = {
@@ -50,8 +50,18 @@ export default {
UserCalloutDismisser,
TrainingProviderList,
},
- mixins: [glFeatureFlagsMixin()],
inject: ['projectFullPath', 'vulnerabilityTrainingDocsPath'],
+ apollo: {
+ currentLicensePlan: {
+ query: currentLicenseQuery,
+ update({ currentLicense }) {
+ return currentLicense?.plan;
+ },
+ error() {
+ this.hasCurrentLicenseFetchError = true;
+ },
+ },
+ },
props: {
augmentedSecurityFeatures: {
type: Array,
@@ -91,6 +101,8 @@ export default {
return {
autoDevopsEnabledAlertDismissedProjects: [],
errorMessage: '',
+ currentLicensePlan: '',
+ hasCurrentLicenseFetchError: false,
};
},
computed: {
@@ -111,6 +123,12 @@ export default {
!this.autoDevopsEnabledAlertDismissedProjects.includes(this.projectFullPath)
);
},
+ shouldShowVulnerabilityManagementTab() {
+ // if the query fails (if the plan is `null` also means an error has occurred) we still want to show the feature
+ const hasQueryError = this.hasCurrentLicenseFetchError || this.currentLicensePlan === null;
+
+ return hasQueryError || this.currentLicensePlan === LICENSE_ULTIMATE;
+ },
},
methods: {
dismissAutoDevopsEnabledAlert() {
@@ -175,7 +193,7 @@ export default {
@dismiss="dismissAutoDevopsEnabledAlert"
/>
- <section-layout :heading="$options.i18n.securityTesting">
+ <section-layout class="gl-border-b-0" :heading="$options.i18n.securityTesting">
<template #description>
<p>
<span data-testid="latest-pipeline-info-security">
@@ -252,7 +270,7 @@ export default {
</section-layout>
</gl-tab>
<gl-tab
- v-if="glFeatures.secureVulnerabilityTraining"
+ v-if="shouldShowVulnerabilityManagementTab"
data-testid="vulnerability-management-tab"
:title="$options.i18n.vulnerabilityManagement"
query-param-value="vulnerability-management"
diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js
index 6db28ef0fad..5b04ad6f9ba 100644
--- a/app/assets/javascripts/security_configuration/components/constants.js
+++ b/app/assets/javascripts/security_configuration/components/constants.js
@@ -55,7 +55,7 @@ export const DAST_DESCRIPTION = s__(
);
export const DAST_HELP_PATH = helpPagePath('user/application_security/dast/index');
export const DAST_CONFIG_HELP_PATH = helpPagePath('user/application_security/dast/index', {
- anchor: 'enable-dast',
+ anchor: 'enable-automatic-dast-run',
});
export const DAST_BADGE_TEXT = __('Available on-demand');
export const DAST_BADGE_TOOLTIP = __(
@@ -126,7 +126,7 @@ export const COVERAGE_FUZZING_HELP_PATH = helpPagePath(
);
export const COVERAGE_FUZZING_CONFIG_HELP_PATH = helpPagePath(
'user/application_security/coverage_fuzzing/index',
- { anchor: 'configuration' },
+ { anchor: 'enable-coverage-guided-fuzz-testing' },
);
export const CORPUS_MANAGEMENT_NAME = __('Corpus Management');
@@ -310,3 +310,7 @@ export const TEMP_PROVIDER_URLS = {
Kontra: 'https://application.security/',
[__('Secure Code Warrior')]: 'https://www.securecodewarrior.com/',
};
+
+export const LICENSE_ULTIMATE = 'ultimate';
+export const LICENSE_FREE = 'free';
+export const LICENSE_PREMIUM = 'premium';
diff --git a/app/assets/javascripts/security_configuration/components/feature_card.vue b/app/assets/javascripts/security_configuration/components/feature_card.vue
index 309e5f21445..19b412d66ca 100644
--- a/app/assets/javascripts/security_configuration/components/feature_card.vue
+++ b/app/assets/javascripts/security_configuration/components/feature_card.vue
@@ -119,8 +119,10 @@ export default {
/>
<template v-if="enabled">
- <gl-icon name="check-circle-filled" />
- <span class="gl-text-green-700">{{ $options.i18n.enabled }}</span>
+ <span>
+ <gl-icon name="check-circle-filled" />
+ <span class="gl-text-green-700">{{ $options.i18n.enabled }}</span>
+ </span>
</template>
<template v-else-if="available">
diff --git a/app/assets/javascripts/security_configuration/components/section_layout.vue b/app/assets/javascripts/security_configuration/components/section_layout.vue
deleted file mode 100644
index 1fe8dd862a0..00000000000
--- a/app/assets/javascripts/security_configuration/components/section_layout.vue
+++ /dev/null
@@ -1,23 +0,0 @@
-<script>
-export default {
- name: 'SectionLayout',
- props: {
- heading: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div class="row gl-line-height-20 gl-pt-6">
- <div class="col-lg-4">
- <h2 class="gl-font-size-h2 gl-mt-0">{{ heading }}</h2>
- <slot name="description"></slot>
- </div>
- <div class="col-lg-8">
- <slot name="features"></slot>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/security_configuration/graphql/current_license.query.graphql b/app/assets/javascripts/security_configuration/graphql/current_license.query.graphql
new file mode 100644
index 00000000000..9ab4f4d4347
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/graphql/current_license.query.graphql
@@ -0,0 +1,6 @@
+query getCurrentLicensePlan {
+ currentLicense {
+ id
+ plan
+ }
+}
diff --git a/app/assets/javascripts/security_configuration/graphql/security_training_vulnerability.query.graphql b/app/assets/javascripts/security_configuration/graphql/security_training_vulnerability.query.graphql
index f0474614dab..891e0dda312 100644
--- a/app/assets/javascripts/security_configuration/graphql/security_training_vulnerability.query.graphql
+++ b/app/assets/javascripts/security_configuration/graphql/security_training_vulnerability.query.graphql
@@ -5,6 +5,7 @@ query getSecurityTrainingUrls($projectFullPath: ID!, $identifierExternalIds: [St
name
status
url
+ identifier
}
}
}
diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js
index 65cf1ec27a3..dcc41a38067 100644
--- a/app/assets/javascripts/security_configuration/index.js
+++ b/app/assets/javascripts/security_configuration/index.js
@@ -37,6 +37,7 @@ export const initSecurityConfiguration = (el) => {
return new Vue({
el,
apolloProvider,
+ name: 'SecurityConfigurationRoot',
provide: {
projectFullPath,
upgradePath,
diff --git a/app/assets/javascripts/serverless/components/area.vue b/app/assets/javascripts/serverless/components/area.vue
deleted file mode 100644
index a9584c070fe..00000000000
--- a/app/assets/javascripts/serverless/components/area.vue
+++ /dev/null
@@ -1,145 +0,0 @@
-<script>
-import { GlAreaChart } from '@gitlab/ui/dist/charts';
-import dateFormat from 'dateformat';
-import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
-import { __ } from '~/locale';
-import { X_INTERVAL } from '../constants';
-import { validateGraphData } from '../utils';
-
-let debouncedResize;
-
-export default {
- components: {
- GlAreaChart,
- },
- inheritAttrs: false,
- props: {
- graphData: {
- type: Object,
- required: true,
- validator: validateGraphData,
- },
- containerWidth: {
- type: Number,
- required: true,
- },
- },
- data() {
- return {
- tooltipPopoverTitle: '',
- tooltipPopoverContent: '',
- width: this.containerWidth,
- };
- },
- computed: {
- chartData() {
- return this.graphData.queries.reduce((accumulator, query) => {
- accumulator[query.unit] = query.result.reduce((acc, res) => acc.concat(res.values), []);
- return accumulator;
- }, {});
- },
- extractTimeData() {
- return this.chartData.requests.map((data) => data.time);
- },
- generateSeries() {
- return {
- name: __('Invocations'),
- type: 'line',
- data: this.chartData.requests.map((data) => [data.time, data.value]),
- symbolSize: 0,
- };
- },
- getInterval() {
- const { result } = this.graphData.queries[0];
-
- if (result.length === 0) {
- return 1;
- }
-
- const split = result[0].values.reduce(
- (acc, pair) => (pair.value > acc ? pair.value : acc),
- 1,
- );
-
- return split < X_INTERVAL ? split : X_INTERVAL;
- },
- chartOptions() {
- return {
- xAxis: {
- name: 'time',
- type: 'time',
- axisLabel: {
- formatter: (date) => dateFormat(date, 'h:MM TT'),
- },
- data: this.extractTimeData,
- nameTextStyle: {
- padding: [18, 0, 0, 0],
- },
- },
- yAxis: {
- name: this.yAxisLabel,
- nameTextStyle: {
- padding: [0, 0, 36, 0],
- },
- splitNumber: this.getInterval,
- },
- legend: {
- formatter: this.xAxisLabel,
- },
- series: this.generateSeries,
- };
- },
- xAxisLabel() {
- return this.graphData.queries.map((query) => query.label).join(', ');
- },
- yAxisLabel() {
- const [query] = this.graphData.queries;
- return `${this.graphData.y_label} (${query.unit})`;
- },
- },
- watch: {
- containerWidth: 'onResize',
- },
- beforeDestroy() {
- window.removeEventListener('resize', debouncedResize);
- },
- created() {
- debouncedResize = debounceByAnimationFrame(this.onResize);
- window.addEventListener('resize', debouncedResize);
- },
- methods: {
- formatTooltipText(params) {
- const [seriesData] = params.seriesData;
- this.tooltipPopoverTitle = dateFormat(params.value, 'dd mmm yyyy, h:MMTT');
- this.tooltipPopoverContent = `${this.yAxisLabel}: ${seriesData.value[1]}`;
- },
- onResize() {
- const { width } = this.$refs.areaChart.$el.getBoundingClientRect();
- this.width = width;
- },
- },
-};
-</script>
-
-<template>
- <div class="prometheus-graph">
- <div class="prometheus-graph-header">
- <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5>
- <div ref="graphWidgets" class="prometheus-graph-widgets">
- <slot></slot>
- </div>
- </div>
- <gl-area-chart
- ref="areaChart"
- v-bind="$attrs"
- :data="[]"
- :option="chartOptions"
- :format-tooltip-text="formatTooltipText"
- :width="width"
- :include-legend-avg-max="false"
- >
- <template #tooltip-title>{{ tooltipPopoverTitle }}</template>
- <template #tooltip-content>{{ tooltipPopoverContent }}</template>
- </gl-area-chart>
- </div>
-</template>
diff --git a/app/assets/javascripts/serverless/components/empty_state.vue b/app/assets/javascripts/serverless/components/empty_state.vue
deleted file mode 100644
index 6d1cea519c4..00000000000
--- a/app/assets/javascripts/serverless/components/empty_state.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-<script>
-import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
-import { mapState } from 'vuex';
-import { s__ } from '~/locale';
-import { DEPRECATION_POST_LINK } from '../constants';
-
-export default {
- components: {
- GlEmptyState,
- GlLink,
- GlSprintf,
- },
- i18n: {
- title: s__('Serverless|Getting started with serverless'),
- description: s__(
- 'Serverless|Serverless was %{postLinkStart}deprecated%{postLinkEnd}. But if you opt to use it, you must install Knative in your Kubernetes cluster first. %{linkStart}Learn more.%{linkEnd}',
- ),
- },
- deprecationPostLink: DEPRECATION_POST_LINK,
- computed: {
- ...mapState(['emptyImagePath', 'helpPath']),
- },
-};
-</script>
-
-<template>
- <gl-empty-state :svg-path="emptyImagePath" :title="$options.i18n.title">
- <template #description>
- <gl-sprintf :message="$options.i18n.description">
- <template #postLink="{ content }">
- <gl-link :href="$options.deprecationPostLink" target="_blank">{{ content }}</gl-link>
- </template>
- <template #link="{ content }">
- <gl-link :href="helpPath">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </template>
- </gl-empty-state>
-</template>
diff --git a/app/assets/javascripts/serverless/components/environment_row.vue b/app/assets/javascripts/serverless/components/environment_row.vue
deleted file mode 100644
index 01030172ea8..00000000000
--- a/app/assets/javascripts/serverless/components/environment_row.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
-import ItemCaret from '~/groups/components/item_caret.vue';
-import FunctionRow from './function_row.vue';
-
-export default {
- components: {
- ItemCaret,
- FunctionRow,
- },
- props: {
- env: {
- type: Array,
- required: true,
- },
- envName: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- isOpen: true,
- };
- },
- computed: {
- envId() {
- if (this.envName === '*') {
- return 'env-global';
- }
-
- return `env-${this.envName}`;
- },
- isOpenClass() {
- return {
- 'is-open': this.isOpen,
- };
- },
- },
- methods: {
- toggleOpen() {
- this.isOpen = !this.isOpen;
- },
- },
-};
-</script>
-
-<template>
- <li :id="envId" :class="isOpenClass" class="group-row has-children">
- <div
- class="group-row-contents d-flex justify-content-end align-items-center py-2"
- role="button"
- @click.stop="toggleOpen"
- >
- <div class="folder-toggle-wrap d-flex align-items-center">
- <item-caret :is-group-open="isOpen" />
- </div>
- <div class="group-text flex-grow title namespace-title gl-ml-3">
- {{ envName }}
- </div>
- </div>
- <ul v-if="isOpen" class="content-list group-list-tree">
- <function-row v-for="(f, index) in env" :key="f.name" :index="index" :func="f" />
- </ul>
- </li>
-</template>
diff --git a/app/assets/javascripts/serverless/components/function_details.vue b/app/assets/javascripts/serverless/components/function_details.vue
deleted file mode 100644
index d2306c2d8bd..00000000000
--- a/app/assets/javascripts/serverless/components/function_details.vue
+++ /dev/null
@@ -1,94 +0,0 @@
-<script>
-import { isString } from 'lodash';
-import { mapState, mapActions, mapGetters } from 'vuex';
-import AreaChart from './area.vue';
-import MissingPrometheus from './missing_prometheus.vue';
-import PodBox from './pod_box.vue';
-import Url from './url.vue';
-
-export default {
- components: {
- PodBox,
- Url,
- AreaChart,
- MissingPrometheus,
- },
- props: {
- func: {
- type: Object,
- required: true,
- },
- hasPrometheus: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- elWidth: 0,
- };
- },
- computed: {
- name() {
- return this.func.name;
- },
- description() {
- return isString(this.func.description) ? this.func.description : '';
- },
- funcUrl() {
- return this.func.url;
- },
- podCount() {
- return Number(this.func.podcount) || 0;
- },
- ...mapState(['graphData', 'hasPrometheusData']),
- ...mapGetters(['hasPrometheusMissingData']),
- },
- created() {
- this.fetchMetrics({
- metricsPath: this.func.metricsUrl,
- hasPrometheus: this.hasPrometheus,
- });
- },
- mounted() {
- this.elWidth = this.$el.clientWidth;
- },
- methods: {
- ...mapActions(['fetchMetrics']),
- },
-};
-</script>
-
-<template>
- <section id="serverless-function-details">
- <h3 class="serverless-function-name">{{ name }}</h3>
- <div class="gl-mb-3 serverless-function-description">
- <div v-for="(line, index) in description.split('\n')" :key="index">{{ line }}</div>
- </div>
- <url :uri="funcUrl" />
-
- <h4>{{ s__('ServerlessDetails|Kubernetes Pods') }}</h4>
- <div v-if="podCount > 0">
- <p>
- <b v-if="podCount == 1">{{ podCount }} {{ s__('ServerlessDetails|pod in use') }}</b>
- <b v-else>{{ podCount }} {{ s__('ServerlessDetails|pods in use') }}</b>
- </p>
- <pod-box :count="podCount" />
- <p>
- {{
- s__('ServerlessDetails|Number of Kubernetes pods in use over time based on necessity.')
- }}
- </p>
- </div>
- <div v-else>
- <p>{{ s__('ServerlessDetails|No pods loaded at this time.') }}</p>
- </div>
-
- <area-chart v-if="hasPrometheusData" :graph-data="graphData" :container-width="elWidth" />
- <missing-prometheus
- v-if="!hasPrometheus || hasPrometheusMissingData"
- :missing-data="hasPrometheusMissingData"
- />
- </section>
-</template>
diff --git a/app/assets/javascripts/serverless/components/function_row.vue b/app/assets/javascripts/serverless/components/function_row.vue
deleted file mode 100644
index fab9c0a75e7..00000000000
--- a/app/assets/javascripts/serverless/components/function_row.vue
+++ /dev/null
@@ -1,77 +0,0 @@
-<script>
-import { isString } from 'lodash';
-import { visitUrl } from '~/lib/utils/url_utility';
-import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
-import Url from './url.vue';
-
-export default {
- components: {
- Timeago,
- Url,
- },
- props: {
- func: {
- type: Object,
- required: true,
- },
- },
- computed: {
- name() {
- return this.func.name;
- },
- description() {
- if (!isString(this.func.description)) {
- return '';
- }
-
- const desc = this.func.description.split('\n');
- if (desc.length > 1) {
- return desc[1];
- }
-
- return desc[0];
- },
- detailUrl() {
- return this.func.detail_url;
- },
- targetUrl() {
- return this.func.url;
- },
- image() {
- return this.func.image;
- },
- timestamp() {
- return this.func.created_at;
- },
- },
- methods: {
- checkClass(element) {
- if (element.closest('.no-expand') === null) {
- return true;
- }
-
- return false;
- },
- openDetails(e) {
- if (this.checkClass(e.target)) {
- visitUrl(this.detailUrl);
- }
- },
- },
-};
-</script>
-
-<template>
- <li :id="name" class="group-row">
- <div class="group-row-contents py-2" role="button" @click="openDetails">
- <p class="float-right text-right">
- <span>{{ image }}</span
- ><br />
- <timeago :time="timestamp" />
- </p>
- <b>{{ name }}</b>
- <div v-for="line in description.split('\n')" :key="line">{{ line }}</div>
- <url :uri="targetUrl" class="gl-mt-3 no-expand" />
- </div>
- </li>
-</template>
diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
deleted file mode 100644
index e9461aa3ead..00000000000
--- a/app/assets/javascripts/serverless/components/functions.vue
+++ /dev/null
@@ -1,139 +0,0 @@
-<script>
-import {
- GlLink,
- GlAlert,
- GlSprintf,
- GlLoadingIcon,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
-import { mapState, mapActions, mapGetters } from 'vuex';
-import { sprintf, s__ } from '~/locale';
-import { CHECKING_INSTALLED, DEPRECATION_POST_LINK } from '../constants';
-import EmptyState from './empty_state.vue';
-import EnvironmentRow from './environment_row.vue';
-
-export default {
- components: {
- EnvironmentRow,
- EmptyState,
- GlLink,
- GlAlert,
- GlSprintf,
- GlLoadingIcon,
- },
- directives: {
- SafeHtml,
- },
- deprecationPostLink: DEPRECATION_POST_LINK,
- computed: {
- ...mapState(['installed', 'isLoading', 'hasFunctionData', 'helpPath', 'statusPath']),
- ...mapGetters(['getFunctions']),
-
- checkingInstalled() {
- return this.installed === CHECKING_INSTALLED;
- },
- isInstalled() {
- return this.installed === true;
- },
- noServerlessConfigFile() {
- return sprintf(
- s__(
- 'Serverless|Your repository does not have a corresponding %{startTag}serverless.yml%{endTag} file.',
- ),
- { startTag: '<code>', endTag: '</code>' },
- false,
- );
- },
- noGitlabYamlConfigured() {
- return sprintf(
- s__('Serverless|Your %{startTag}.gitlab-ci.yml%{endTag} file is not properly configured.'),
- { startTag: '<code>', endTag: '</code>' },
- false,
- );
- },
- mismatchedServerlessFunctions() {
- return sprintf(
- s__(
- "Serverless|The functions listed in the %{startTag}serverless.yml%{endTag} file don't match the namespace of your cluster.",
- ),
- { startTag: '<code>', endTag: '</code>' },
- false,
- );
- },
- },
- created() {
- this.fetchFunctions({
- functionsPath: this.statusPath,
- });
- },
- methods: {
- ...mapActions(['fetchFunctions']),
- },
-};
-</script>
-
-<template>
- <section id="serverless-functions" class="flex-grow">
- <gl-alert class="gl-mt-6" variant="warning" :dismissible="false">
- <gl-sprintf
- :message="s__('Serverless|Serverless was %{linkStart}deprecated%{linkEnd} in GitLab 14.3.')"
- ><template #link="{ content }"
- ><gl-link :href="$options.deprecationPostLink" target="_blank">{{
- content
- }}</gl-link></template
- ></gl-sprintf
- >
- </gl-alert>
-
- <gl-loading-icon v-if="checkingInstalled" size="lg" class="gl-mt-3 gl-mb-3" />
-
- <div v-else-if="isInstalled">
- <div v-if="hasFunctionData">
- <div class="groups-list-tree-container js-functions-wrapper">
- <ul class="content-list group-list-tree">
- <environment-row
- v-for="(env, index) in getFunctions"
- :key="index"
- :env="env"
- :env-name="index"
- />
- </ul>
- </div>
- <gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3 gl-mb-3 js-functions-loader" />
- </div>
- <div v-else class="empty-state js-empty-state">
- <div class="text-content">
- <h4 class="state-title text-center">{{ s__('Serverless|No functions available') }}</h4>
- <p class="state-description">
- {{
- s__(
- 'Serverless|There is currently no function data available from Knative. This could be for a variety of reasons including:',
- )
- }}
- </p>
- <ul>
- <li v-safe-html="noServerlessConfigFile"></li>
- <li v-safe-html="noGitlabYamlConfigured"></li>
- <li v-safe-html="mismatchedServerlessFunctions"></li>
- <li>{{ s__('Serverless|The deploy job has not finished.') }}</li>
- </ul>
-
- <p>
- {{
- s__(
- 'Serverless|If you believe none of these apply, please check back later as the function data may be in the process of becoming available.',
- )
- }}
- </p>
- <div class="text-center">
- <gl-link :href="helpPath" class="btn btn-success">{{
- s__('Serverless|Learn more about Serverless')
- }}</gl-link>
- </div>
- </div>
- </div>
- </div>
-
- <empty-state v-else />
- </section>
-</template>
diff --git a/app/assets/javascripts/serverless/components/missing_prometheus.vue b/app/assets/javascripts/serverless/components/missing_prometheus.vue
deleted file mode 100644
index d9e6bb5009e..00000000000
--- a/app/assets/javascripts/serverless/components/missing_prometheus.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<script>
-import { GlButton, GlLink } from '@gitlab/ui';
-import { mapState } from 'vuex';
-import { s__ } from '~/locale';
-
-export default {
- components: {
- GlButton,
- GlLink,
- },
- props: {
- missingData: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- ...mapState(['clustersPath', 'helpPath']),
- missingStateClass() {
- return this.missingData ? 'missing-prometheus-state' : 'empty-prometheus-state';
- },
- prometheusHelpPath() {
- return `${this.helpPath}#prometheus-support`;
- },
- description() {
- return this.missingData
- ? s__(`ServerlessDetails|Invocation metrics loading or not available at this time.`)
- : s__(
- `ServerlessDetails|Function invocation metrics require the Prometheus cluster integration.`,
- );
- },
- },
-};
-</script>
-
-<template>
- <div class="row" :class="missingStateClass">
- <div class="col-12">
- <div class="text-content">
- <h4 class="state-title text-left">{{ s__(`ServerlessDetails|Invocations`) }}</h4>
- <p class="state-description">
- {{ description }}
- <gl-link :href="prometheusHelpPath">{{
- s__(`ServerlessDetails|More information`)
- }}</gl-link
- >.
- </p>
-
- <div v-if="!missingData" class="text-left">
- <gl-button :href="clustersPath" variant="success" category="primary">
- {{ s__('ServerlessDetails|Configure cluster.') }}
- </gl-button>
- </div>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/serverless/components/pod_box.vue b/app/assets/javascripts/serverless/components/pod_box.vue
deleted file mode 100644
index 04d3641bce3..00000000000
--- a/app/assets/javascripts/serverless/components/pod_box.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<script>
-export default {
- props: {
- count: {
- type: Number,
- required: true,
- },
- color: {
- type: String,
- required: false,
- default: 'green',
- },
- },
- methods: {
- boxOffset(i) {
- return 20 * (i - 1);
- },
- },
-};
-</script>
-
-<template>
- <svg :width="boxOffset(count + 1)" :height="20">
- <rect
- v-for="i in count"
- :key="i"
- width="15"
- height="15"
- rx="5"
- ry="5"
- :fill="color"
- :x="boxOffset(i)"
- y="0"
- />
- </svg>
-</template>
diff --git a/app/assets/javascripts/serverless/components/url.vue b/app/assets/javascripts/serverless/components/url.vue
deleted file mode 100644
index b105f49e475..00000000000
--- a/app/assets/javascripts/serverless/components/url.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<script>
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-
-export default {
- components: {
- ClipboardButton,
- },
- props: {
- uri: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div class="clipboard-group">
- <div class="gl-cursor-text label label-monospace monospace" data-testid="url-text-field">
- {{ uri }}
- </div>
- <clipboard-button
- :text="uri"
- :title="s__('ServerlessURL|Copy URL')"
- class="input-group-text js-clipboard-btn"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/serverless/constants.js b/app/assets/javascripts/serverless/constants.js
deleted file mode 100644
index 42c9ee983b4..00000000000
--- a/app/assets/javascripts/serverless/constants.js
+++ /dev/null
@@ -1,10 +0,0 @@
-export const MAX_REQUESTS = 3; // max number of times to retry
-
-export const X_INTERVAL = 5; // Reflects the number of verticle bars on the x-axis
-
-export const CHECKING_INSTALLED = 'checking'; // The backend is still determining whether or not Knative is installed
-
-export const TIMEOUT = 'timeout';
-
-export const DEPRECATION_POST_LINK =
- 'https://about.gitlab.com/releases/2021/09/22/gitlab-14-3-released/#gitlab-serverless';
diff --git a/app/assets/javascripts/serverless/event_hub.js b/app/assets/javascripts/serverless/event_hub.js
deleted file mode 100644
index e31806ad199..00000000000
--- a/app/assets/javascripts/serverless/event_hub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import createEventHub from '~/helpers/event_hub_factory';
-
-export default createEventHub();
diff --git a/app/assets/javascripts/serverless/serverless_bundle.js b/app/assets/javascripts/serverless/serverless_bundle.js
deleted file mode 100644
index e8d87a40fc7..00000000000
--- a/app/assets/javascripts/serverless/serverless_bundle.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import Vue from 'vue';
-import FunctionDetails from './components/function_details.vue';
-import Functions from './components/functions.vue';
-import { createStore } from './store';
-
-export default class Serverless {
- constructor() {
- if (document.querySelector('.js-serverless-function-details-page') != null) {
- const entryPointData = document.querySelector('.js-serverless-function-details-page').dataset;
- const store = createStore(entryPointData);
-
- const {
- serviceName,
- serviceDescription,
- serviceEnvironment,
- serviceUrl,
- serviceNamespace,
- servicePodcount,
- serviceMetricsUrl,
- prometheus,
- } = entryPointData;
- const el = document.querySelector('#js-serverless-function-details');
-
- const service = {
- name: serviceName,
- description: serviceDescription,
- environment: serviceEnvironment,
- url: serviceUrl,
- namespace: serviceNamespace,
- podcount: servicePodcount,
- metricsUrl: serviceMetricsUrl,
- };
-
- this.functionDetails = new Vue({
- el,
- store,
- render(createElement) {
- return createElement(FunctionDetails, {
- props: {
- func: service,
- hasPrometheus: prometheus !== undefined,
- },
- });
- },
- });
- } else {
- const entryPointData = document.querySelector('.js-serverless-functions-page').dataset;
- const store = createStore(entryPointData);
-
- const el = document.querySelector('#js-serverless-functions');
- this.functions = new Vue({
- el,
- store,
- render(createElement) {
- return createElement(Functions);
- },
- });
- }
- }
-
- destroy() {
- this.destroyed = true;
-
- this.functions.$destroy();
- this.functionDetails.$destroy();
- }
-}
diff --git a/app/assets/javascripts/serverless/store/actions.js b/app/assets/javascripts/serverless/store/actions.js
deleted file mode 100644
index 166cd796680..00000000000
--- a/app/assets/javascripts/serverless/store/actions.js
+++ /dev/null
@@ -1,131 +0,0 @@
-import createFlash from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import { backOff } from '~/lib/utils/common_utils';
-import statusCodes from '~/lib/utils/http_status';
-import { __ } from '~/locale';
-import { MAX_REQUESTS, CHECKING_INSTALLED, TIMEOUT } from '../constants';
-import * as types from './mutation_types';
-
-export const requestFunctionsLoading = ({ commit }) => commit(types.REQUEST_FUNCTIONS_LOADING);
-export const receiveFunctionsSuccess = ({ commit }, data) =>
- commit(types.RECEIVE_FUNCTIONS_SUCCESS, data);
-export const receiveFunctionsPartial = ({ commit }, data) =>
- commit(types.RECEIVE_FUNCTIONS_PARTIAL, data);
-export const receiveFunctionsTimeout = ({ commit }, data) =>
- commit(types.RECEIVE_FUNCTIONS_TIMEOUT, data);
-export const receiveFunctionsNoDataSuccess = ({ commit }, data) =>
- commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS, data);
-export const receiveFunctionsError = ({ commit }, error) =>
- commit(types.RECEIVE_FUNCTIONS_ERROR, error);
-
-export const receiveMetricsSuccess = ({ commit }, data) =>
- commit(types.RECEIVE_METRICS_SUCCESS, data);
-export const receiveMetricsNoPrometheus = ({ commit }) =>
- commit(types.RECEIVE_METRICS_NO_PROMETHEUS);
-export const receiveMetricsNoDataSuccess = ({ commit }, data) =>
- commit(types.RECEIVE_METRICS_NODATA_SUCCESS, data);
-export const receiveMetricsError = ({ commit }, error) =>
- commit(types.RECEIVE_METRICS_ERROR, error);
-
-export const fetchFunctions = ({ dispatch }, { functionsPath }) => {
- let retryCount = 0;
-
- const functionsPartiallyFetched = (data) => {
- if (data.functions !== null && data.functions.length) {
- dispatch('receiveFunctionsPartial', data);
- }
- };
-
- dispatch('requestFunctionsLoading');
-
- backOff((next, stop) => {
- axios
- .get(functionsPath)
- .then((response) => {
- if (response.data.knative_installed === CHECKING_INSTALLED) {
- retryCount += 1;
- if (retryCount < MAX_REQUESTS) {
- functionsPartiallyFetched(response.data);
- next();
- } else {
- stop(TIMEOUT);
- }
- } else {
- stop(response.data);
- }
- })
- .catch(stop);
- })
- .then((data) => {
- if (data === TIMEOUT) {
- dispatch('receiveFunctionsTimeout');
- createFlash({
- message: __('Loading functions timed out. Please reload the page to try again.'),
- });
- } else if (data.functions !== null && data.functions.length) {
- dispatch('receiveFunctionsSuccess', data);
- } else {
- dispatch('receiveFunctionsNoDataSuccess', data);
- }
- })
- .catch((error) => {
- dispatch('receiveFunctionsError', error);
- createFlash({
- message: error,
- });
- });
-};
-
-export const fetchMetrics = ({ dispatch }, { metricsPath, hasPrometheus }) => {
- let retryCount = 0;
-
- if (!hasPrometheus) {
- dispatch('receiveMetricsNoPrometheus');
- return;
- }
-
- backOff((next, stop) => {
- axios
- .get(metricsPath)
- .then((response) => {
- if (response.status === statusCodes.NO_CONTENT) {
- retryCount += 1;
- if (retryCount < MAX_REQUESTS) {
- next();
- } else {
- dispatch('receiveMetricsNoDataSuccess');
- stop(null);
- }
- } else {
- stop(response.data);
- }
- })
- .catch(stop);
- })
- .then((data) => {
- if (data === null) {
- return;
- }
-
- const updatedMetric = data.metrics;
- const queries = data.metrics.queries.map((query) => ({
- ...query,
- result: query.result.map((result) => ({
- ...result,
- values: result.values.map(([timestamp, value]) => ({
- time: new Date(timestamp * 1000).toISOString(),
- value: Number(value),
- })),
- })),
- }));
-
- updatedMetric.queries = queries;
- dispatch('receiveMetricsSuccess', updatedMetric);
- })
- .catch((error) => {
- dispatch('receiveMetricsError', error);
- createFlash({
- message: error,
- });
- });
-};
diff --git a/app/assets/javascripts/serverless/store/getters.js b/app/assets/javascripts/serverless/store/getters.js
deleted file mode 100644
index da975c56e5d..00000000000
--- a/app/assets/javascripts/serverless/store/getters.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { translate } from '../utils';
-
-export const hasPrometheusMissingData = (state) => state.hasPrometheus && !state.hasPrometheusData;
-
-// Convert the function list into a k/v grouping based on the environment scope
-
-export const getFunctions = (state) => translate(state.functions);
diff --git a/app/assets/javascripts/serverless/store/index.js b/app/assets/javascripts/serverless/store/index.js
deleted file mode 100644
index 6f32d85201e..00000000000
--- a/app/assets/javascripts/serverless/store/index.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import createState from './state';
-
-Vue.use(Vuex);
-
-export const createStore = (entryPointData = {}) =>
- new Vuex.Store({
- actions,
- getters,
- mutations,
- state: createState(entryPointData),
- });
-
-export default createStore;
diff --git a/app/assets/javascripts/serverless/store/mutation_types.js b/app/assets/javascripts/serverless/store/mutation_types.js
deleted file mode 100644
index b8fa9ea1a01..00000000000
--- a/app/assets/javascripts/serverless/store/mutation_types.js
+++ /dev/null
@@ -1,11 +0,0 @@
-export const REQUEST_FUNCTIONS_LOADING = 'REQUEST_FUNCTIONS_LOADING';
-export const RECEIVE_FUNCTIONS_SUCCESS = 'RECEIVE_FUNCTIONS_SUCCESS';
-export const RECEIVE_FUNCTIONS_PARTIAL = 'RECEIVE_FUNCTIONS_PARTIAL';
-export const RECEIVE_FUNCTIONS_TIMEOUT = 'RECEIVE_FUNCTIONS_TIMEOUT';
-export const RECEIVE_FUNCTIONS_NODATA_SUCCESS = 'RECEIVE_FUNCTIONS_NODATA_SUCCESS';
-export const RECEIVE_FUNCTIONS_ERROR = 'RECEIVE_FUNCTIONS_ERROR';
-
-export const RECEIVE_METRICS_NO_PROMETHEUS = 'RECEIVE_METRICS_NO_PROMETHEUS';
-export const RECEIVE_METRICS_SUCCESS = 'RECEIVE_METRICS_SUCCESS';
-export const RECEIVE_METRICS_NODATA_SUCCESS = 'RECEIVE_METRICS_NODATA_SUCCESS';
-export const RECEIVE_METRICS_ERROR = 'RECEIVE_METRICS_ERROR';
diff --git a/app/assets/javascripts/serverless/store/mutations.js b/app/assets/javascripts/serverless/store/mutations.js
deleted file mode 100644
index 2685a5b11ff..00000000000
--- a/app/assets/javascripts/serverless/store/mutations.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import * as types from './mutation_types';
-
-export default {
- [types.REQUEST_FUNCTIONS_LOADING](state) {
- state.isLoading = true;
- },
- [types.RECEIVE_FUNCTIONS_SUCCESS](state, data) {
- state.functions = data.functions;
- state.installed = data.knative_installed;
- state.isLoading = false;
- state.hasFunctionData = true;
- },
- [types.RECEIVE_FUNCTIONS_PARTIAL](state, data) {
- state.functions = data.functions;
- state.installed = true;
- state.isLoading = true;
- state.hasFunctionData = true;
- },
- [types.RECEIVE_FUNCTIONS_TIMEOUT](state) {
- state.isLoading = false;
- },
- [types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state, data) {
- state.isLoading = false;
- state.installed = data.knative_installed;
- state.hasFunctionData = false;
- },
- [types.RECEIVE_FUNCTIONS_ERROR](state, error) {
- state.error = error;
- state.hasFunctionData = false;
- state.isLoading = false;
- },
- [types.RECEIVE_METRICS_SUCCESS](state, data) {
- state.isLoading = false;
- state.hasPrometheusData = true;
- state.graphData = data;
- },
- [types.RECEIVE_METRICS_NODATA_SUCCESS](state) {
- state.isLoading = false;
- state.hasPrometheusData = false;
- },
- [types.RECEIVE_METRICS_ERROR](state, error) {
- state.hasPrometheusData = false;
- state.error = error;
- },
- [types.RECEIVE_METRICS_NO_PROMETHEUS](state) {
- state.hasPrometheusData = false;
- state.hasPrometheus = false;
- },
-};
diff --git a/app/assets/javascripts/serverless/store/state.js b/app/assets/javascripts/serverless/store/state.js
deleted file mode 100644
index 353bfcf3fed..00000000000
--- a/app/assets/javascripts/serverless/store/state.js
+++ /dev/null
@@ -1,22 +0,0 @@
-export default (
- initialState = { clustersPath: null, helpPath: null, emptyImagePath: null, statusPath: null },
-) => ({
- clustersPath: initialState.clustersPath,
- error: null,
- helpPath: initialState.helpPath,
- installed: 'checking',
- isLoading: true,
-
- // functions
- functions: [],
- hasFunctionData: true,
- statusPath: initialState.statusPath,
-
- // function_details
- hasPrometheus: true,
- hasPrometheusData: false,
- graphData: {},
-
- // empty_state
- emptyImagePath: initialState.emptyImagePath,
-});
diff --git a/app/assets/javascripts/serverless/utils.js b/app/assets/javascripts/serverless/utils.js
deleted file mode 100644
index e218a9aa3fd..00000000000
--- a/app/assets/javascripts/serverless/utils.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// Validate that the object coming in has valid query details and results
-export const validateGraphData = (data) =>
- data.queries &&
- Array.isArray(data.queries) &&
- data.queries.filter((query) => {
- if (Array.isArray(query.result)) {
- return query.result.filter((res) => Array.isArray(res.values)).length === query.result.length;
- }
-
- return false;
- }).length === data.queries.length;
-
-export const translate = (functions) =>
- functions.reduce(
- (acc, func) =>
- Object.assign(acc, {
- [func.environment_scope]: (acc[func.environment_scope] || []).concat([func]),
- }),
- {},
- );
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
index 578c344da02..ef40de82d01 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
@@ -101,14 +101,15 @@ export default {
<template>
<!-- must be `d-inline-block` or parent flex-basis causes width issues -->
<gl-link
- v-gl-tooltip="tooltipOption"
:href="assigneeUrl"
:title="tooltipTitle"
- class="gl-display-inline-block"
+ :data-user-id="user.id"
+ data-placement="left"
+ class="gl-display-inline-block js-user-link"
>
<!-- use d-flex so that slot can be appropriately styled -->
<span class="gl-display-flex">
- <assignee-avatar :user="user" :img-size="32" :issuable-type="issuableType" />
+ <assignee-avatar :user="user" :img-size="24" :issuable-type="issuableType" />
<slot></slot>
</span>
</gl-link>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
index f98aa0dc77d..6e18cf36690 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
@@ -1,6 +1,7 @@
<script>
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { n__, __ } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'AssigneeTitle',
@@ -8,6 +9,7 @@ export default {
GlLoadingIcon,
GlIcon,
},
+ mixins: [glFeatureFlagMixin()],
props: {
loading: {
type: Boolean,
@@ -45,7 +47,7 @@ export default {
};
</script>
<template>
- <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900">
+ <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-font-weight-bold">
{{ assigneeTitle }}
<gl-loading-icon v-if="loading" size="sm" inline class="align-bottom" />
<a
@@ -63,6 +65,7 @@ export default {
v-if="showToggle"
:aria-label="__('Toggle sidebar')"
class="gutter-toggle float-right js-sidebar-toggle"
+ :class="{ 'gl-display-block gl-md-display-none!': glFeatures.movedMrSidebar }"
href="#"
role="button"
>
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
index 856687c00ae..50b1955abcc 100644
--- a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
@@ -124,7 +124,11 @@ export default {
:issuable-type="issuableType"
/>
<button v-if="hasMoreThanTwoAssignees" class="btn-link" type="button">
- <span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span>
+ <span
+ class="avatar-counter sidebar-avatar-counter gl-display-flex gl-align-items-center gl-pl-3"
+ >
+ {{ sidebarAvatarCounter }}
+ </span>
<gl-icon
v-if="isMergeRequest && !allAssigneesCanMerge"
name="warning-solid"
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue
index 19f588b28be..e9c68008143 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue
@@ -3,6 +3,11 @@ import { GlAvatarLabeled, GlAvatarLink, GlIcon } from '@gitlab/ui';
import { IssuableType } from '~/issues/constants';
import { s__, sprintf } from '~/locale';
+const AVAILABILITY_STATUS = {
+ NOT_SET: 'NOT_SET',
+ BUSY: 'BUSY',
+};
+
export default {
components: {
GlAvatarLabeled,
@@ -22,12 +27,17 @@ export default {
},
computed: {
userLabel() {
- if (!this.user.status) {
- return this.user.name;
+ const { name, status } = this.user;
+ if (!status || status?.availability !== AVAILABILITY_STATUS.BUSY) {
+ return name;
}
- return sprintf(s__('UserAvailability|%{author} (Busy)'), {
- author: this.user.name,
- });
+ return sprintf(
+ s__('UserAvailability|%{author} (Busy)'),
+ {
+ author: name,
+ },
+ false,
+ );
},
hasCannotMergeIcon() {
return this.issuableType === IssuableType.MergeRequest && !this.user.canMerge;
diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
index 8717d205dcb..01d29da5486 100644
--- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -35,13 +35,6 @@ export default {
firstUser() {
return this.users[0];
},
- hasOneUser() {
- if (this.showVerticalList) {
- return false;
- }
-
- return this.users.length === 1;
- },
hiddenAssigneesLabel() {
const { numberOfHiddenAssignees } = this;
return sprintf(__('+ %{numberOfHiddenAssignees} more'), { numberOfHiddenAssignees });
@@ -90,30 +83,15 @@ export default {
</script>
<template>
- <assignee-avatar-link
- v-if="hasOneUser"
- tooltip-placement="left"
- :tooltip-has-name="false"
- :user="firstUser"
- :issuable-type="issuableType"
- >
- <div class="ml-2 gl-line-height-normal">
- <user-name-with-status :name="firstUser.name" :availability="userAvailability(firstUser)" />
- <div>{{ username }}</div>
- </div>
- </assignee-avatar-link>
- <div v-else>
+ <div>
<div class="gl-display-flex gl-flex-wrap">
<div
v-for="(user, index) in uncollapsedUsers"
:key="user.id"
:class="{
- 'user-item': !showVerticalList,
- 'gl-display-inline-block': !showVerticalList,
- 'gl-display-grid gl-align-items-center': showVerticalList,
- 'gl-mb-3': index !== users.length - 1 && showVerticalList,
+ 'gl-mb-3': index !== users.length - 1,
}"
- class="assignee-grid"
+ class="assignee-grid gl-display-grid gl-align-items-center gl-w-full"
>
<assignee-avatar-link
:user="user"
@@ -123,12 +101,10 @@ export default {
data-css-area="user"
>
<div
- v-if="showVerticalList"
- class="gl-ml-3 gl-line-height-normal gl-display-grid"
+ class="gl-ml-3 gl-line-height-normal gl-display-grid gl-align-items-center"
data-testid="username"
>
<user-name-with-status :name="user.name" :availability="userAvailability(user)" />
- <span>@{{ user.username }}</span>
</div>
</assignee-avatar-link>
<attention-requested-toggle
diff --git a/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue
index cdc1c65a516..031de669489 100644
--- a/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue
+++ b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue
@@ -5,9 +5,8 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
export default {
i18n: {
- attentionRequestedReviewer: __('Request attention to review'),
- attentionRequestedAssignee: __('Request attention'),
- removeAttentionRequested: __('Remove attention request'),
+ addAttentionRequest: __('Add attention request'),
+ removeAttentionRequest: __('Remove attention request'),
attentionRequestedNoPermission: __('Attention requested'),
noAttentionRequestedNoPermission: __('No attention request'),
},
@@ -36,20 +35,35 @@ export default {
tooltipTitle() {
if (this.user.attention_requested) {
if (this.user.can_update_merge_request) {
- return this.$options.i18n.removeAttentionRequested;
+ return this.$options.i18n.removeAttentionRequest;
}
return this.$options.i18n.attentionRequestedNoPermission;
}
if (this.user.can_update_merge_request) {
- return this.type === 'reviewer'
- ? this.$options.i18n.attentionRequestedReviewer
- : this.$options.i18n.attentionRequestedAssignee;
+ return this.$options.i18n.addAttentionRequest;
}
return this.$options.i18n.noAttentionRequestedNoPermission;
},
+ request() {
+ const state = {
+ variant: 'default',
+ icon: 'attention',
+ direction: 'add',
+ };
+
+ if (this.user.attention_requested) {
+ Object.assign(state, {
+ variant: 'warning',
+ icon: 'attention-solid',
+ direction: 'remove',
+ });
+ }
+
+ return state;
+ },
},
methods: {
toggleAttentionRequired() {
@@ -60,6 +74,7 @@ export default {
this.$emit('toggle-attention-requested', {
user: this.user,
callback: this.toggleAttentionRequiredComplete,
+ direction: this.request.direction,
});
},
toggleAttentionRequiredComplete() {
@@ -77,8 +92,8 @@ export default {
>
<gl-button
:loading="loading"
- :variant="user.attention_requested ? 'warning' : 'default'"
- :icon="user.attention_requested ? 'attention-solid' : 'attention'"
+ :variant="request.variant"
+ :icon="request.icon"
:aria-label="tooltipTitle"
:class="{ 'gl-pointer-events-none': !user.can_update_merge_request }"
size="small"
diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_content.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_content.vue
index 37a44eb8f01..6afaee91d7a 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_content.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_content.vue
@@ -1,10 +1,13 @@
<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
+import { GlIcon, GlAlert, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { IssuableType, WorkspaceType } from '~/issues/constants';
+import { confidentialityInfoText } from '~/vue_shared/constants';
export default {
components: {
GlIcon,
+ GlAlert,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -20,12 +23,11 @@ export default {
},
},
computed: {
- confidentialText() {
- return this.confidential
- ? sprintf(__('This %{issuableType} is confidential'), {
- issuableType: this.issuableType,
- })
- : __('Not confidential');
+ confidentialBodyText() {
+ return confidentialityInfoText(
+ this.issuableType === IssuableType.Epic ? WorkspaceType.group : WorkspaceType.project,
+ this.issuableType,
+ );
},
confidentialIcon() {
return this.confidential ? 'eye-slash' : 'eye';
@@ -59,6 +61,17 @@ export default {
class="sidebar-item-icon inline hide-collapsed"
:class="{ 'is-active': confidential }"
/>
- <span class="hide-collapsed" data-testid="confidential-text">{{ confidentialText }}</span>
+ <span class="hide-collapsed" data-testid="confidential-text">
+ {{ tooltipLabel }}
+ <gl-alert
+ v-if="confidential"
+ :show-icon="false"
+ :dismissible="false"
+ variant="warning"
+ class="gl-mt-3"
+ >
+ {{ confidentialBodyText }}
+ </gl-alert>
+ </span>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
index 209d1cca360..71e40fde77d 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
@@ -8,7 +8,7 @@ import { confidentialityQueries } from '~/sidebar/constants';
export default {
i18n: {
confidentialityOnWarning: __(
- 'You are going to turn on confidentiality. Only team members with %{strongStart}at least Reporter access%{strongEnd} will be able to see and leave comments on the %{issuableType}.',
+ 'You are going to turn on confidentiality. Only %{context} members with %{strongStart}at least Reporter role%{strongEnd} can view or be notified about this %{issuableType}.',
),
confidentialityOffWarning: __(
'You are going to turn off the confidentiality. This means %{strongStart}everyone%{strongEnd} will be able to see and leave a comment on this %{issuableType}.',
@@ -53,6 +53,9 @@ export default {
? this.$options.i18n.confidentialityOffWarning
: this.$options.i18n.confidentialityOnWarning;
},
+ context() {
+ return this.issuableType === IssuableType.Issue ? __('project') : __('group');
+ },
workspacePath() {
return this.issuableType === IssuableType.Issue
? {
@@ -119,6 +122,7 @@ export default {
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
+ <template #context>{{ context }}</template>
<template #issuableType>{{ issuableType }}</template>
</gl-sprintf>
</p>
diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
index 950647f1cb2..67f36f65b5d 100644
--- a/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
+++ b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
@@ -2,7 +2,7 @@
import { GlIcon, GlLink, GlPopover, GlTooltipDirective } from '@gitlab/ui';
import { __, n__, sprintf } from '~/locale';
import createFlash from '~/flash';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ISSUE } from '~/graphql_shared/constants';
import getIssueCrmContactsQuery from './queries/get_issue_crm_contacts.query.graphql';
import issueCrmContactsSubscription from './queries/issue_crm_contacts.subscription.graphql';
@@ -21,6 +21,10 @@ export default {
type: String,
required: true,
},
+ groupIssuesPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -85,6 +89,10 @@ export default {
Boolean,
);
},
+ getIssuesPath(contactId) {
+ const id = getIdFromGraphQLId(contactId);
+ return `${this.groupIssuesPath}?crm_contact_id=${id}`;
+ },
},
};
</script>
@@ -100,7 +108,7 @@ export default {
><gl-icon name="question-o"
/></gl-link>
</div>
- <div class="title hide-collapsed gl-mb-2 gl-line-height-20">
+ <div class="title hide-collapsed gl-mb-2 gl-line-height-20 gl-font-weight-bold">
{{ contactsLabel }}
</div>
<div class="hide-collapsed gl-display-flex gl-flex-wrap">
@@ -110,8 +118,8 @@ export default {
:key="index"
class="gl-pr-2"
>
- <span :id="`contact_${index}`" class="gl-font-weight-bold"
- >{{ contact.firstName }} {{ contact.lastName }}{{ divider(index) }}</span
+ <gl-link :id="`contact_${index}`" :href="getIssuesPath(contact.id)"
+ >{{ contact.firstName }} {{ contact.lastName }}{{ divider(index) }}</gl-link
>
<gl-popover
v-if="shouldShowPopover(contact)"
diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_formatted_date.vue b/app/assets/javascripts/sidebar/components/date/sidebar_formatted_date.vue
index 87cf1c29fb0..627b8452508 100644
--- a/app/assets/javascripts/sidebar/components/date/sidebar_formatted_date.vue
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_formatted_date.vue
@@ -35,7 +35,7 @@ export default {
<template>
<div class="gl-display-flex gl-align-items-center hide-collapsed">
<span
- :class="hasDate ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'"
+ :class="hasDate ? 'gl-text-gray-900' : 'gl-text-gray-500'"
data-testid="sidebar-date-value"
>
{{ formattedDate }}
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 cb49f329f7e..699d1bebea1 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -1,7 +1,9 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { mapGetters } from 'vuex';
-import { __ } from '~/locale';
+import { mapGetters, mapActions } from 'vuex';
+import { __, sprintf } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import createFlash from '~/flash';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
@@ -23,11 +25,11 @@ export default {
editForm,
GlIcon,
},
-
directives: {
GlTooltip: GlTooltipDirective,
},
-
+ mixins: [glFeatureFlagMixin()],
+ inject: ['fullPath'],
props: {
isEditable: {
required: true,
@@ -41,6 +43,9 @@ export default {
},
computed: {
...mapGetters(['getNoteableData']),
+ isMergeRequest() {
+ return this.getNoteableData.targetType === 'merge_request' && this.glFeatures.movedMrSidebar;
+ },
issuableDisplayName() {
const isInIssuePage = this.getNoteableData.targetType === this.$options.issue;
return isInIssuePage ? __('issue') : __('merge request');
@@ -66,17 +71,49 @@ export default {
},
methods: {
+ ...mapActions(['updateLockedAttribute']),
toggleForm() {
if (this.isEditable) {
this.isLockDialogOpen = !this.isLockDialogOpen;
}
},
+ toggleLocked() {
+ this.isLoading = true;
+
+ this.updateLockedAttribute({
+ locked: !this.isLocked,
+ fullPath: this.fullPath,
+ })
+ .catch(() => {
+ const flashMessage = __(
+ 'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
+ );
+ createFlash({
+ message: sprintf(flashMessage, { issuableDisplayName: this.issuableDisplayName }),
+ });
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ },
},
};
</script>
<template>
- <div class="block issuable-sidebar-item lock">
+ <li v-if="isMergeRequest" class="gl-new-dropdown-item">
+ <button type="button" class="dropdown-item" @click="toggleLocked">
+ <span class="gl-new-dropdown-item-text-wrapper">
+ <template v-if="isLocked">
+ {{ __('Unlock merge request') }}
+ </template>
+ <template v-else>
+ {{ __('Lock merge request') }}
+ </template>
+ </span>
+ </button>
+ </li>
+ <div v-else class="block issuable-sidebar-item lock">
<div
v-gl-tooltip.left.viewport="{ title: tooltipLabel }"
class="sidebar-collapsed-icon"
@@ -86,7 +123,7 @@ export default {
<gl-icon :name="lockStatus.icon" class="sidebar-item-icon is-active" />
</div>
- <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900">
+ <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-font-weight-bold">
{{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }}
<a
v-if="isEditable"
@@ -111,12 +148,6 @@ export default {
/>
<div data-testid="lock-status" class="sidebar-item-value" :class="lockStatus.class">
- <gl-icon
- :size="16"
- :name="lockStatus.icon"
- class="sidebar-item-icon"
- :class="lockStatus.iconClass"
- />
{{ lockStatus.displayText }}
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 3fd35de2132..77e41648e9b 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -101,7 +101,10 @@ export default {
<gl-loading-icon v-if="loading" size="sm" />
<span v-else data-testid="collapsed-count"> {{ participantCount }} </span>
</div>
- <div v-if="showParticipantLabel" class="title hide-collapsed gl-mb-2 gl-line-height-20">
+ <div
+ v-if="showParticipantLabel"
+ class="title hide-collapsed gl-mb-2! gl-line-height-20 gl-font-weight-bold"
+ >
<gl-loading-icon v-if="loading" size="sm" :inline="true" />
{{ participantLabel }}
</div>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue
index 60d8fb4d408..e09b5d913f7 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue
@@ -96,7 +96,11 @@ export default {
<gl-icon v-if="hasNoUsers" name="user" :aria-label="__('None')" />
<collapsed-reviewer v-for="user in collapsedUsers" :key="user.id" :user="user" />
<button v-if="hasMoreThanTwoReviewers" class="btn-link" type="button">
- <span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span>
+ <span
+ class="avatar-counter sidebar-avatar-counter gl-display-flex gl-align-items-center gl-pl-3"
+ >
+ {{ sidebarAvatarCounter }}
+ </span>
<gl-icon
v-if="!allReviewersCanMerge"
name="warning-solid"
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue
index a11468c8761..36a08482e69 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue
@@ -47,8 +47,6 @@ export default {
return sprintf(__('%{userName} (cannot merge)'), { userName: this.user.name });
} else if (this.cannotMerge) {
return __('Cannot merge');
- } else if (this.tooltipHasName) {
- return this.user.name;
}
return '';
@@ -70,14 +68,15 @@ export default {
<template>
<!-- must be `d-inline-block` or parent flex-basis causes width issues -->
<gl-link
- v-gl-tooltip="tooltipOption"
:href="reviewerUrl"
:title="tooltipTitle"
- class="gl-display-inline-block"
+ :data-user-id="user.id"
+ data-placement="left"
+ class="gl-display-inline-block js-user-link"
>
<!-- use d-flex so that slot can be appropriately styled -->
<span class="gl-display-flex">
- <reviewer-avatar :user="user" :img-size="32" :issuable-type="issuableType" />
+ <reviewer-avatar :user="user" :img-size="24" :issuable-type="issuableType" />
<slot :user="user"></slot>
</span>
</gl-link>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
index 367dcdb961b..933b9b11b40 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
@@ -33,7 +33,7 @@ export default {
};
</script>
<template>
- <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900">
+ <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-font-weight-bold">
{{ reviewerTitle }}
<gl-loading-icon v-if="loading" size="sm" inline class="align-bottom" />
<a
diff --git a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
index 3e6be3487b1..2f58e11c00f 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
@@ -98,7 +98,7 @@ export default {
'gl-mb-3': index !== users.length - 1,
'attention-requests': glFeatures.mrAttentionRequests,
}"
- class="gl-display-grid gl-align-items-center reviewer-grid"
+ class="gl-display-grid gl-align-items-center reviewer-grid gl-mr-2"
data-testid="reviewer"
>
<reviewer-avatar-link
@@ -108,9 +108,8 @@ export default {
class="gl-word-break-word gl-mr-2"
data-css-area="user"
>
- <div class="gl-ml-3 gl-line-height-normal gl-display-grid">
- <span>{{ user.name }}</span>
- <span>@{{ user.username }}</span>
+ <div class="gl-ml-3 gl-line-height-normal gl-display-grid gl-align-items-center">
+ {{ user.name }}
</div>
</reviewer-avatar-link>
<attention-requested-toggle
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index ec23e817127..897cab45fe4 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -331,20 +331,19 @@ export default {
:data-testid="`select-${formatIssuableAttribute.kebab}`"
:class="isClassicSidebar ? 'hide-collapsed' : 'gl-mt-3'"
>
- <span v-if="updating" class="gl-font-weight-bold">{{ selectedTitle }}</span>
+ <span v-if="updating">{{ selectedTitle }}</span>
<span v-else-if="!currentAttribute" class="gl-text-gray-500">
{{ $options.i18n.none }}
</span>
<slot
v-else
name="value"
- :attributeTitle="attributeTitle"
- :attributeUrl="attributeUrl"
- :currentAttribute="currentAttribute"
+ :attribute-title="attributeTitle"
+ :attribute-url="attributeUrl"
+ :current-attribute="currentAttribute"
>
<gl-link
v-gl-tooltip="tooltipText"
- class="gl-text-gray-900! gl-font-weight-bold"
:href="attributeUrl"
:data-qa-selector="`${formatIssuableAttribute.snake}_link`"
>
@@ -389,9 +388,9 @@ export default {
<slot
v-else
name="list"
- :attributesList="attributesList"
- :isAttributeChecked="isAttributeChecked"
- :updateAttribute="updateAttribute"
+ :attributes-list="attributesList"
+ :is-attribute-checked="isAttributeChecked"
+ :update-attribute="updateAttribute"
>
<gl-dropdown-item
v-for="attrItem in attributesList"
diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
index 056b3e98a1c..7551b181a58 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
@@ -110,7 +110,7 @@ export default {
<template>
<div>
<div
- class="gl-display-flex gl-align-items-center gl-line-height-20 gl-mb-2 gl-text-gray-900"
+ class="gl-display-flex gl-align-items-center gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-font-weight-bold"
@click.self="collapse"
>
<span class="hide-collapsed" data-testid="title" @click="collapse">
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
index 7a10a9f3a4c..1bafa845665 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
@@ -5,6 +5,7 @@ import { IssuableType } from '~/issues/constants';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { subscribedQueries, Tracking } from '~/sidebar/constants';
const ICON_ON = 'notifications';
@@ -25,6 +26,7 @@ export default {
GlToggle,
SidebarEditableItem,
},
+ mixins: [glFeatureFlagMixin()],
props: {
iid: {
type: String,
@@ -82,6 +84,9 @@ export default {
},
},
computed: {
+ isMergeRequest() {
+ return this.issuableType === IssuableType.MergeRequest && this.glFeatures.movedMrSidebar;
+ },
isLoading() {
return this.$apollo.queries?.subscribed?.loading || this.loading;
},
@@ -171,7 +176,20 @@ export default {
</script>
<template>
+ <li v-if="isMergeRequest" class="gl-new-dropdown-item">
+ <button type="button" class="dropdown-item" @click="toggleSubscribed">
+ <span class="gl-new-dropdown-item-text-wrapper">
+ <template v-if="subscribed">
+ {{ __('Turn off notifications') }}
+ </template>
+ <template v-else>
+ {{ __('Turn on notifications') }}
+ </template>
+ </span>
+ </button>
+ </li>
<sidebar-editable-item
+ v-else
ref="editable"
:title="$options.i18n.notifications"
:tracking="$options.tracking"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
index 5d4031ac68b..d9797961d40 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
@@ -78,9 +78,9 @@ export default {
},
},
fields: [
- { key: 'spentAt', label: __('Spent At'), sortable: true },
+ { key: 'spentAt', label: __('Spent At'), sortable: true, tdClass: 'gl-w-quarter' },
{ key: 'user', label: __('User'), sortable: true },
- { key: 'timeSpent', label: __('Time Spent'), sortable: true },
+ { key: 'timeSpent', label: __('Time Spent'), sortable: true, tdClass: 'gl-w-15' },
{ key: 'summary', label: __('Summary / Note'), sortable: true },
],
};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index fdbcef22bba..057bb9f0100 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -204,7 +204,7 @@ export default {
:time-estimate-human-readable="humanTimeEstimate"
/>
<div
- class="hide-collapsed gl-line-height-20 gl-text-gray-900 gl-display-flex gl-align-items-center"
+ class="hide-collapsed gl-line-height-20 gl-text-gray-900 gl-display-flex gl-align-items-center gl-font-weight-bold gl-mr-3"
>
{{ __('Time tracking') }}
<gl-loading-icon v-if="isTimeTrackingInfoLoading" size="sm" class="gl-ml-2" inline />
@@ -221,8 +221,7 @@ export default {
</div>
<div v-if="!isTimeTrackingInfoLoading" class="hide-collapsed">
<div v-if="showEstimateOnlyState" data-testid="estimateOnlyPane">
- <span class="gl-font-weight-bold">{{ $options.i18n.estimatedOnlyText }} </span
- >{{ humanTimeEstimate }}
+ <span>{{ $options.i18n.estimatedOnlyText }} </span>{{ humanTimeEstimate }}
</div>
<time-tracking-spent-only-pane
v-if="showSpentOnlyState"
@@ -250,6 +249,7 @@ export default {
</gl-link>
<gl-modal
modal-id="time-tracking-report"
+ size="lg"
:title="__('Time tracking report')"
:hide-footer="true"
>
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
index eabba619af5..482b9343e70 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
@@ -6,6 +6,9 @@ import { __, sprintf } from '~/locale';
import { todoQueries, TodoMutationTypes, todoMutations } from '~/sidebar/constants';
import { todoLabel } from '~/vue_shared/components/sidebar/todo_toggle//utils';
import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
+import Tracking from '~/tracking';
+
+const trackingMixin = Tracking.mixin();
export default {
components: {
@@ -16,6 +19,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [trackingMixin],
inject: {
isClassicSidebar: {
default: false,
@@ -151,6 +155,10 @@ export default {
message: errors[0],
});
}
+ this.track('click_todo', {
+ label: 'right_sidebar',
+ property: this.hasTodo,
+ });
},
)
.catch(() => {
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 2a7d967cb61..351bb50d941 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -218,7 +218,7 @@ function mountCrmContactsComponent() {
if (!el) return;
- const { issueId } = el.dataset;
+ const { issueId, groupIssuesPath } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
@@ -231,6 +231,7 @@ function mountCrmContactsComponent() {
createElement('crm-contacts', {
props: {
issueId,
+ groupIssuesPath,
},
}),
});
@@ -430,10 +431,7 @@ function mountLockComponent(store) {
return;
}
- const { fullPath } = getSidebarOptions();
-
- const dataNode = document.getElementById('js-lock-issue-data');
- const initialData = JSON.parse(dataNode.innerHTML);
+ const { fullPath, editable } = getSidebarOptions();
// eslint-disable-next-line no-new
new Vue({
@@ -446,7 +444,7 @@ function mountLockComponent(store) {
render: (createElement) =>
createElement(IssuableLockForm, {
props: {
- isEditable: initialData.is_editable,
+ isEditable: editable,
},
}),
});
diff --git a/app/assets/javascripts/sidebar/queries/remove_attention_request.mutation.graphql b/app/assets/javascripts/sidebar/queries/remove_attention_request.mutation.graphql
new file mode 100644
index 00000000000..d9b9c04fd63
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/remove_attention_request.mutation.graphql
@@ -0,0 +1,7 @@
+mutation mergeRequestRemoveAttentionRequest($projectPath: ID!, $iid: String!, $userId: UserID!) {
+ mergeRequestRemoveAttentionRequest(
+ input: { projectPath: $projectPath, iid: $iid, userId: $userId }
+ ) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/request_attention.mutation.graphql b/app/assets/javascripts/sidebar/queries/request_attention.mutation.graphql
new file mode 100644
index 00000000000..99a86e4fe5c
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/request_attention.mutation.graphql
@@ -0,0 +1,5 @@
+mutation mergeRequestRequestAttention($projectPath: ID!, $iid: String!, $userId: UserID!) {
+ mergeRequestRequestAttention(input: { projectPath: $projectPath, iid: $iid, userId: $userId }) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/reviewer_rereview.mutation.graphql b/app/assets/javascripts/sidebar/queries/reviewer_rereview.mutation.graphql
index 73765e7d77b..0d66ee0d6e5 100644
--- a/app/assets/javascripts/sidebar/queries/reviewer_rereview.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/reviewer_rereview.mutation.graphql
@@ -1,4 +1,4 @@
-mutation mergeRequestRequestRereview($projectPath: ID!, $iid: String!, $userId: ID!) {
+mutation mergeRequestRequestRereview($projectPath: ID!, $iid: String!, $userId: UserID!) {
mergeRequestReviewerRereview(input: { projectPath: $projectPath, iid: $iid, userId: $userId }) {
errors
}
diff --git a/app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql b/app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql
deleted file mode 100644
index a9f4af6e1b9..00000000000
--- a/app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql
+++ /dev/null
@@ -1,7 +0,0 @@
-mutation mergeRequestToggleAttentionRequested($projectPath: ID!, $iid: String!, $userId: ID!) {
- mergeRequestToggleAttentionRequested(
- input: { projectPath: $projectPath, iid: $iid, userId: $userId }
- ) {
- errors
- }
-}
diff --git a/app/assets/javascripts/sidebar/queries/update_merge_request_milestone.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_merge_request_milestone.mutation.graphql
index 368f06fac7f..938953ccfb2 100644
--- a/app/assets/javascripts/sidebar/queries/update_merge_request_milestone.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_merge_request_milestone.mutation.graphql
@@ -1,4 +1,4 @@
-mutation mergeRequestSetMilestone($fullPath: ID!, $iid: String!, $attributeId: ID) {
+mutation mergeRequestSetMilestone($fullPath: ID!, $iid: String!, $attributeId: MilestoneID) {
issuableSetAttribute: mergeRequestSetMilestone(
input: { projectPath: $fullPath, iid: $iid, milestoneId: $attributeId }
) {
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index 90d8f2098bb..ea170203576 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -5,7 +5,8 @@ import createGqClient, { fetchPolicies } from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql';
import sidebarDetailsMRQuery from '../queries/sidebar_details_mr.query.graphql';
-import toggleAttentionRequestedMutation from '../queries/toggle_attention_requested.mutation.graphql';
+import requestAttentionMutation from '../queries/request_attention.mutation.graphql';
+import removeAttentionRequestMutation from '../queries/remove_attention_request.mutation.graphql';
const queries = {
merge_request: sidebarDetailsMRQuery,
@@ -92,9 +93,19 @@ export default class SidebarService {
});
}
- toggleAttentionRequested(userId) {
+ requestAttention(userId) {
return gqClient.mutate({
- mutation: toggleAttentionRequestedMutation,
+ mutation: requestAttentionMutation,
+ variables: {
+ userId: convertToGraphQLId(TYPE_USER, `${userId}`),
+ projectPath: this.fullPath,
+ iid: this.iid.toString(),
+ },
+ });
+ }
+ removeAttentionRequest(userId) {
+ return gqClient.mutate({
+ mutation: removeAttentionRequestMutation,
variables: {
userId: convertToGraphQLId(TYPE_USER, `${userId}`),
projectPath: this.fullPath,
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 83fb8f31dfb..7df901577b8 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -40,6 +40,7 @@ export default class SidebarMediator {
const data = { assignee_ids: assignees };
try {
+ const { currentUserHasAttention } = this.store;
const res = await this.service.update(field, data);
this.store.overwrite('assignees', res.data.assignees);
@@ -48,6 +49,10 @@ export default class SidebarMediator {
this.store.overwrite('reviewers', res.data.reviewers);
}
+ if (currentUserHasAttention && this.store.isAddingAssignee) {
+ toast(__('Assigned user(s). Your attention request was removed.'));
+ }
+
return Promise.resolve(res);
} catch (e) {
return Promise.reject(e);
@@ -63,11 +68,16 @@ export default class SidebarMediator {
const data = { reviewer_ids: reviewers };
try {
+ const { currentUserHasAttention } = this.store;
const res = await this.service.update(field, data);
this.store.overwrite('reviewers', res.data.reviewers);
this.store.overwrite('assignees', res.data.assignees);
+ if (currentUserHasAttention && this.store.isAddingAssignee) {
+ toast(__('Requested review. Your attention request was removed.'));
+ }
+
return Promise.resolve(res);
} catch (e) {
return Promise.reject();
@@ -98,14 +108,19 @@ export default class SidebarMediator {
}
}
- async toggleAttentionRequested(type, { user, callback }) {
+ async toggleAttentionRequested(type, { user, callback, direction }) {
+ const mutations = {
+ add: (id) => this.service.requestAttention(id),
+ remove: (id) => this.service.removeAttentionRequest(id),
+ };
+
try {
const isReviewer = type === 'reviewer';
const reviewerOrAssignee = isReviewer
? this.store.findReviewer(user)
: this.store.findAssignee(user);
- await this.service.toggleAttentionRequested(user.id);
+ await mutations[direction]?.(user.id);
if (reviewerOrAssignee.attention_requested) {
toast(
@@ -115,12 +130,22 @@ export default class SidebarMediator {
);
} else {
const currentUserId = gon.current_user_id;
+ const { currentUserHasAttention } = this.store;
if (currentUserId !== user.id) {
this.removeCurrentUserAttentionRequested();
}
- toast(sprintf(__('Requested attention from @%{username}'), { username: user.username }));
+ toast(
+ currentUserHasAttention && currentUserId !== user.id
+ ? sprintf(
+ __(
+ 'Requested attention from @%{username}. Your own attention request was removed.',
+ ),
+ { username: user.username },
+ )
+ : sprintf(__('Requested attention from @%{username}'), { username: user.username }),
+ );
}
this.store.updateReviewer(user.id, 'attention_requested');
@@ -138,7 +163,7 @@ export default class SidebarMediator {
captureError: true,
actionConfig: {
title: __('Try again'),
- clickHandler: () => this.toggleAttentionRequired(type, { user, callback }),
+ clickHandler: () => this.toggleAttentionRequired(type, { user, callback, direction }),
},
});
}
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index 2caa6f4f0a0..ca85ee7fd94 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -18,7 +18,9 @@ export default class SidebarStore {
this.humanTimeSpent = '';
this.timeTrackingLimitToHours = timeTrackingLimitToHours;
this.assignees = [];
+ this.addingAssignees = [];
this.reviewers = [];
+ this.addingReviewers = [];
this.isFetching = {
assignees: true,
reviewers: true,
@@ -32,6 +34,7 @@ export default class SidebarStore {
this.subscribeDisabledDescription = '';
this.subscribed = null;
this.changing = false;
+ this.issuableType = options.issuableType;
SidebarStore.singleton = this;
}
@@ -73,12 +76,20 @@ export default class SidebarStore {
if (!this.findAssignee(assignee)) {
this.changing = true;
this.assignees.push(assignee);
+
+ if (assignee.id !== this.currentUser.id) {
+ this.addingAssignees.push(assignee.id);
+ }
}
}
addReviewer(reviewer) {
if (!this.findReviewer(reviewer)) {
this.reviewers.push(reviewer);
+
+ if (reviewer.id !== this.currentUser.id) {
+ this.addingReviewers.push(reviewer.id);
+ }
}
}
@@ -114,12 +125,14 @@ export default class SidebarStore {
if (assignee) {
this.changing = true;
this.assignees = this.assignees.filter(({ id }) => id !== assignee.id);
+ this.addingAssignees = this.addingAssignees.filter(({ id }) => id !== assignee.id);
}
}
removeReviewer(reviewer) {
if (reviewer) {
this.reviewers = this.reviewers.filter(({ id }) => id !== reviewer.id);
+ this.addingReviewers = this.addingReviewers.filter(({ id }) => id !== reviewer.id);
}
}
@@ -147,4 +160,26 @@ export default class SidebarStore {
setMoveToProjectId(moveToProjectId) {
this.moveToProjectId = moveToProjectId;
}
+
+ get currentUserHasAttention() {
+ if (!window.gon?.features?.mrAttentionRequests || !this.isMergeRequest) return false;
+
+ const currentUserId = this.currentUser.id;
+ const currentUserReviewer = this.findReviewer({ id: currentUserId });
+ const currentUserAssignee = this.findAssignee({ id: currentUserId });
+
+ return currentUserReviewer?.attention_requested || currentUserAssignee?.attention_requested;
+ }
+
+ get isAddingAssignee() {
+ return this.addingAssignees.length > 0;
+ }
+
+ get isAddingReviewer() {
+ return this.addingReviewers.length > 0;
+ }
+
+ get isMergeRequest() {
+ return this.issuableType === 'merge_request';
+ }
}
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index e4a97f08c8d..2537ec78850 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlFormInput, GlFormGroup } from '@gitlab/ui';
import eventHub from '~/blob/components/eventhub';
import createFlash from '~/flash';
@@ -11,7 +11,6 @@ import {
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
-import TitleField from '~/vue_shared/components/form/title.vue';
import { SNIPPET_CREATE_MUTATION_ERROR, SNIPPET_UPDATE_MUTATION_ERROR } from '../constants';
import { getSnippetMixin } from '../mixins/snippets';
@@ -31,10 +30,11 @@ export default {
SnippetDescriptionEdit,
SnippetVisibilityEdit,
SnippetBlobActionsEdit,
- TitleField,
FormFooterActions,
GlButton,
GlLoadingIcon,
+ GlFormInput,
+ GlFormGroup,
},
mixins: [getSnippetMixin],
inject: ['selectedLevel'],
@@ -67,6 +67,7 @@ export default {
description: '',
visibilityLevel: this.selectedLevel,
},
+ showValidation: false,
};
},
computed: {
@@ -85,8 +86,11 @@ export default {
hasValidBlobs() {
return this.actions.every((x) => x.content);
},
- updatePrevented() {
- return this.snippet.title === '' || !this.hasValidBlobs || this.isUpdating;
+ isTitleValid() {
+ return this.snippet.title !== '';
+ },
+ isFormValid() {
+ return this.isTitleValid && this.hasValidBlobs;
},
isProjectSnippet() {
return Boolean(this.projectPath);
@@ -112,6 +116,12 @@ export default {
}
return this.snippet.webUrl;
},
+ shouldShowBlobsErrors() {
+ return this.showValidation && !this.hasValidBlobs;
+ },
+ shouldShowTitleErrors() {
+ return this.showValidation && !this.isTitleValid;
+ },
},
beforeCreate() {
performanceMarkAndMeasure({ mark: SNIPPET_MARK_EDIT_APP_START });
@@ -165,6 +175,12 @@ export default {
};
},
handleFormSubmit() {
+ this.showValidation = true;
+
+ if (!this.isFormValid) {
+ return;
+ }
+
this.isUpdating = true;
this.$apollo
@@ -206,19 +222,31 @@ export default {
class="loading-animation prepend-top-20 gl-mb-6"
/>
<template v-else>
- <title-field
- id="snippet-title"
- v-model="snippet.title"
- data-qa-selector="snippet_title_field"
- required
- :autofocus="true"
- />
+ <gl-form-group
+ :label="__('Title')"
+ label-for="snippet-title"
+ :invalid-feedback="__('This field is required.')"
+ :state="!shouldShowTitleErrors"
+ >
+ <gl-form-input
+ id="snippet-title"
+ v-model="snippet.title"
+ data-testid="snippet-title-input"
+ data-qa-selector="snippet_title_field"
+ :autofocus="true"
+ />
+ </gl-form-group>
+
<snippet-description-edit
v-model="snippet.description"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
/>
- <snippet-blob-actions-edit :init-blobs="blobs" @actions="updateActions" />
+ <snippet-blob-actions-edit
+ :init-blobs="blobs"
+ :is-valid="!shouldShowBlobsErrors"
+ @actions="updateActions"
+ />
<snippet-visibility-edit
v-model="snippet.visibilityLevel"
@@ -228,12 +256,13 @@ export default {
<form-footer-actions>
<template #prepend>
<gl-button
+ class="js-no-auto-disable"
category="primary"
type="submit"
variant="confirm"
- :disabled="updatePrevented"
data-qa-selector="submit_button"
data-testid="snippet-submit-btn"
+ :disabled="isUpdating"
>{{ saveButtonLabel }}</gl-button
>
</template>
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
index d221195ddc7..260ee496df0 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlFormGroup } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import { s__, sprintf } from '~/locale';
import { SNIPPET_MAX_BLOBS } from '../constants';
@@ -10,12 +10,18 @@ export default {
components: {
SnippetBlobEdit,
GlButton,
+ GlFormGroup,
},
props: {
initBlobs: {
type: Array,
required: true,
},
+ isValid: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -124,16 +130,26 @@ export default {
</script>
<template>
<div class="form-group">
- <label :for="firstInputId">{{ s__('Snippets|Files') }}</label>
- <snippet-blob-edit
- v-for="(blobId, index) in blobIds"
- :key="blobId"
- :class="{ 'gl-mt-3': index > 0 }"
- :blob="blobs[blobId]"
- :can-delete="canDelete"
- @blob-updated="updateBlob(blobId, $event)"
- @delete="deleteBlob(blobId)"
- />
+ <gl-form-group
+ :label="s__('Snippets|Files')"
+ :label-for="firstInputId"
+ :invalid-feedback="
+ s__(
+ 'Snippets|Snippets can\'t contain empty files. Ensure all files have content, or delete them.',
+ )
+ "
+ :state="isValid"
+ >
+ <snippet-blob-edit
+ v-for="(blobId, index) in blobIds"
+ :key="blobId"
+ :class="{ 'gl-mt-3': index > 0 }"
+ :blob="blobs[blobId]"
+ :can-delete="canDelete"
+ @blob-updated="updateBlob(blobId, $event)"
+ @delete="deleteBlob(blobId)"
+ />
+ </gl-form-group>
<gl-button
:disabled="!canAdd"
data-testid="add_button"
diff --git a/app/assets/javascripts/snippets/mutations/delete_snippet.mutation.graphql b/app/assets/javascripts/snippets/mutations/delete_snippet.mutation.graphql
index f43d53661f4..a13c143f775 100644
--- a/app/assets/javascripts/snippets/mutations/delete_snippet.mutation.graphql
+++ b/app/assets/javascripts/snippets/mutations/delete_snippet.mutation.graphql
@@ -1,4 +1,4 @@
-mutation DeleteSnippet($id: ID!) {
+mutation DeleteSnippet($id: SnippetID!) {
destroySnippet(input: { id: $id }) {
errors
}
diff --git a/app/assets/javascripts/sortable/constants.js b/app/assets/javascripts/sortable/constants.js
index 7fddac00ab2..f5bb0a3b11f 100644
--- a/app/assets/javascripts/sortable/constants.js
+++ b/app/assets/javascripts/sortable/constants.js
@@ -1,3 +1,5 @@
+export const DRAG_CLASS = 'is-dragging';
+
/**
* Default config options for sortablejs.
* @type {object}
@@ -12,7 +14,7 @@
export const defaultSortableOptions = {
animation: 200,
forceFallback: true,
- fallbackClass: 'is-dragging',
+ fallbackClass: DRAG_CLASS,
fallbackOnBody: true,
ghostClass: 'is-ghost',
fallbackTolerance: 1,
diff --git a/app/assets/javascripts/sortable/utils.js b/app/assets/javascripts/sortable/utils.js
index c2c8fb03b58..88ac1295a39 100644
--- a/app/assets/javascripts/sortable/utils.js
+++ b/app/assets/javascripts/sortable/utils.js
@@ -1,13 +1,17 @@
/* global DocumentTouch */
-import { defaultSortableOptions } from './constants';
+import { defaultSortableOptions, DRAG_CLASS } from './constants';
export function sortableStart() {
- document.body.classList.add('is-dragging');
+ document.body.classList.add(DRAG_CLASS);
}
export function sortableEnd() {
- document.body.classList.remove('is-dragging');
+ document.body.classList.remove(DRAG_CLASS);
+}
+
+export function isDragging() {
+ return document.body.classList.contains(DRAG_CLASS);
}
export function getSortableDefaultOptions(options) {
diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue
index ea775eff358..2f2efe290ec 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_area.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue
@@ -167,7 +167,9 @@ export default {
:content="editableContent"
:initial-edit-type="editorMode"
:image-root="imageRoot"
- :options="{ customRenderers }"
+ :options="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ customRenderers,
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
class="mb-9 pb-6 h-100"
@modeChange="onModeChange"
@input="onInputChange"
diff --git a/app/assets/javascripts/tracking/dispatch_snowplow_event.js b/app/assets/javascripts/tracking/dispatch_snowplow_event.js
index 7e596f5f36f..5daeaf1d85b 100644
--- a/app/assets/javascripts/tracking/dispatch_snowplow_event.js
+++ b/app/assets/javascripts/tracking/dispatch_snowplow_event.js
@@ -1,3 +1,4 @@
+import * as Sentry from '@sentry/browser';
import getStandardContext from './get_standard_context';
export function dispatchSnowplowEvent(
@@ -24,5 +25,11 @@ export function dispatchSnowplowEvent(
value = Number(value);
}
- return window.snowplow('trackStructEvent', category, action, label, property, value, contexts);
+ try {
+ window.snowplow('trackStructEvent', category, action, label, property, value, contexts);
+ return true;
+ } catch (error) {
+ Sentry.captureException(error);
+ return false;
+ }
}
diff --git a/app/assets/javascripts/tracking/tracker.js b/app/assets/javascripts/tracking/tracker.js
new file mode 100644
index 00000000000..9ad86e76b6e
--- /dev/null
+++ b/app/assets/javascripts/tracking/tracker.js
@@ -0,0 +1,267 @@
+import { LOAD_ACTION_ATTR_SELECTOR } from './constants';
+import { dispatchSnowplowEvent } from './dispatch_snowplow_event';
+import getStandardContext from './get_standard_context';
+import {
+ getEventHandlers,
+ createEventPayload,
+ renameKey,
+ getReferrersCache,
+ addReferrersCacheEntry,
+} from './utils';
+
+export const Tracker = {
+ nonInitializedQueue: [],
+ initialized: false,
+ definitionsLoaded: false,
+ definitionsManifest: {},
+ definitionsEventsQueue: [],
+ definitions: [],
+ ALLOWED_URL_HASHES: ['#diff', '#note'],
+ /**
+ * (Legacy) Determines if tracking is enabled at the user level.
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/DNT.
+ *
+ * @returns {Boolean}
+ */
+ trackable() {
+ return !['1', 'yes'].includes(
+ window.doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack,
+ );
+ },
+
+ /**
+ * Determines if Snowplow is available/enabled.
+ *
+ * @returns {Boolean}
+ */
+ enabled() {
+ return typeof window.snowplow === 'function' && Tracker.trackable();
+ },
+
+ /**
+ * Dispatches a structured event per our taxonomy:
+ * https://docs.gitlab.com/ee/development/snowplow/index.html#structured-event-taxonomy.
+ *
+ * If the library is not initialized and events are trying to be
+ * dispatched (data-attributes, load-events), they will be added
+ * to a queue to be flushed afterwards.
+ *
+ * If there is an error when using the library, it will return ´false´
+ * and ´true´ otherwise.
+ *
+ * @param {...any} eventData defined event taxonomy
+ * @returns {Boolean}
+ */
+ event(...eventData) {
+ if (!Tracker.enabled()) {
+ return false;
+ }
+
+ if (!Tracker.initialized) {
+ Tracker.nonInitializedQueue.push(eventData);
+ return false;
+ }
+
+ return dispatchSnowplowEvent(...eventData);
+ },
+
+ /**
+ * Preloads event definitions.
+ *
+ * @returns {undefined}
+ */
+ loadDefinitions() {
+ // TODO: fetch definitions from the server and flush the queue
+ // See https://gitlab.com/gitlab-org/gitlab/-/issues/358256
+ Tracker.definitionsLoaded = true;
+
+ while (Tracker.definitionsEventsQueue.length) {
+ Tracker.dispatchFromDefinition(...Tracker.definitionsEventsQueue.shift());
+ }
+ },
+
+ /**
+ * Dispatches a structured event with data from its event definition.
+ *
+ * @param {String} basename
+ * @param {Object} eventData
+ * @returns {Boolean}
+ */
+ definition(basename, eventData = {}) {
+ if (!Tracker.enabled()) {
+ return false;
+ }
+
+ if (!(basename in Tracker.definitionsManifest)) {
+ throw new Error(`Missing Snowplow event definition "${basename}"`);
+ }
+
+ return Tracker.dispatchFromDefinition(basename, eventData);
+ },
+
+ /**
+ * Builds an event with data from a valid definition and sends it to
+ * Snowplow. If the definitions are not loaded, it pushes the data to a queue.
+ *
+ * @param {String} basename
+ * @param {Object} eventData
+ * @returns {Boolean}
+ */
+ dispatchFromDefinition(basename, eventData) {
+ if (!Tracker.definitionsLoaded) {
+ Tracker.definitionsEventsQueue.push([basename, eventData]);
+
+ return false;
+ }
+
+ const eventDefinition = Tracker.definitions.find((definition) => definition.key === basename);
+
+ return Tracker.event(
+ eventData.category ?? eventDefinition.category,
+ eventData.action ?? eventDefinition.action,
+ eventData,
+ );
+ },
+
+ /**
+ * Dispatches any event emitted before initialization.
+ *
+ * @returns {undefined}
+ */
+ flushPendingEvents() {
+ Tracker.initialized = true;
+
+ while (Tracker.nonInitializedQueue.length) {
+ dispatchSnowplowEvent(...Tracker.nonInitializedQueue.shift());
+ }
+ },
+
+ /**
+ * Attaches event handlers for data-attributes powered events.
+ *
+ * @param {String} category - the default category for all events
+ * @param {HTMLElement} parent - element containing data-attributes
+ * @returns {Array}
+ */
+ bindDocument(category = document.body.dataset.page, parent = document) {
+ if (!Tracker.enabled() || parent.trackingBound) {
+ return [];
+ }
+
+ // eslint-disable-next-line no-param-reassign
+ parent.trackingBound = true;
+
+ const handlers = getEventHandlers(category, (...args) => Tracker.event(...args));
+ handlers.forEach((event) => parent.addEventListener(event.name, event.func));
+
+ return handlers;
+ },
+
+ /**
+ * Attaches event handlers for load-events (on render).
+ *
+ * @param {String} category - the default category for all events
+ * @param {HTMLElement} parent - element containing event targets
+ * @returns {Array}
+ */
+ trackLoadEvents(category = document.body.dataset.page, parent = document) {
+ if (!Tracker.enabled()) {
+ return [];
+ }
+
+ const loadEvents = parent.querySelectorAll(LOAD_ACTION_ATTR_SELECTOR);
+
+ loadEvents.forEach((element) => {
+ const { action, data } = createEventPayload(element);
+ Tracker.event(category, action, data);
+ });
+
+ return loadEvents;
+ },
+
+ /**
+ * Enable Snowplow automatic form tracking.
+ * The config param requires at least one array of either forms
+ * class names, or field name attributes.
+ * https://docs.gitlab.com/ee/development/snowplow/index.html#form-tracking.
+ *
+ * @param {Object} config
+ * @param {Array} contexts
+ * @returns {undefined}
+ */
+ enableFormTracking(config, contexts = []) {
+ if (!Tracker.enabled()) {
+ return;
+ }
+
+ if (!Array.isArray(config?.forms?.allow) && !Array.isArray(config?.fields?.allow)) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Unable to enable form event tracking without allow rules.');
+ }
+
+ // Ignore default/standard schema
+ const standardContext = getStandardContext();
+ const userProvidedContexts = contexts.filter(
+ (context) => context.schema !== standardContext.schema,
+ );
+
+ const mappedConfig = {};
+ if (config.forms) {
+ mappedConfig.forms = renameKey(config.forms, 'allow', 'whitelist');
+ }
+
+ if (config.fields) {
+ mappedConfig.fields = renameKey(config.fields, 'allow', 'whitelist');
+ }
+
+ const enabler = () => window.snowplow('enableFormTracking', mappedConfig, userProvidedContexts);
+
+ if (document.readyState === 'complete') {
+ enabler();
+ } else {
+ document.addEventListener('readystatechange', () => {
+ if (document.readyState === 'complete') {
+ enabler();
+ }
+ });
+ }
+ },
+
+ /**
+ * Replaces the URL and referrer for the default web context
+ * if the replacements are available.
+ *
+ * @returns {undefined}
+ */
+ setAnonymousUrls() {
+ const { snowplowPseudonymizedPageUrl: pageUrl } = window.gl;
+
+ if (!pageUrl) {
+ return;
+ }
+
+ const referrers = getReferrersCache();
+ const pageLinks = Object.seal({
+ url: pageUrl,
+ referrer: '',
+ originalUrl: window.location.href,
+ });
+
+ const appendHash = Tracker.ALLOWED_URL_HASHES.some((prefix) =>
+ window.location.hash.startsWith(prefix),
+ );
+ const customUrl = `${pageUrl}${appendHash ? window.location.hash : ''}`;
+ window.snowplow('setCustomUrl', customUrl);
+
+ if (document.referrer) {
+ const node = referrers.find((links) => links.originalUrl === document.referrer);
+
+ if (node) {
+ pageLinks.referrer = node.url;
+ window.snowplow('setReferrerUrl', pageLinks.referrer);
+ }
+ }
+
+ addReferrersCacheEntry(referrers, pageLinks);
+ },
+};
diff --git a/app/assets/javascripts/tracking/tracking.js b/app/assets/javascripts/tracking/tracking.js
index f299c57b33f..923aea433f1 100644
--- a/app/assets/javascripts/tracking/tracking.js
+++ b/app/assets/javascripts/tracking/tracking.js
@@ -1,268 +1,7 @@
-import { LOAD_ACTION_ATTR_SELECTOR } from './constants';
-import { dispatchSnowplowEvent } from './dispatch_snowplow_event';
-import getStandardContext from './get_standard_context';
-import {
- getEventHandlers,
- createEventPayload,
- renameKey,
- addExperimentContext,
- getReferrersCache,
- addReferrersCacheEntry,
-} from './utils';
-
-const ALLOWED_URL_HASHES = ['#diff', '#note'];
-
-export default class Tracking {
- static nonInitializedQueue = [];
- static initialized = false;
- static definitionsLoaded = false;
- static definitionsManifest = {};
- static definitionsEventsQueue = [];
- static definitions = [];
-
- /**
- * (Legacy) Determines if tracking is enabled at the user level.
- * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/DNT.
- *
- * @returns {Boolean}
- */
- static trackable() {
- return !['1', 'yes'].includes(
- window.doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack,
- );
- }
-
- /**
- * Determines if Snowplow is available/enabled.
- *
- * @returns {Boolean}
- */
- static enabled() {
- return typeof window.snowplow === 'function' && this.trackable();
- }
-
- /**
- * Dispatches a structured event per our taxonomy:
- * https://docs.gitlab.com/ee/development/snowplow/index.html#structured-event-taxonomy.
- *
- * If the library is not initialized and events are trying to be
- * dispatched (data-attributes, load-events), they will be added
- * to a queue to be flushed afterwards.
- *
- * @param {...any} eventData defined event taxonomy
- * @returns {undefined|Boolean}
- */
- static event(...eventData) {
- if (!this.enabled()) {
- return false;
- }
-
- if (!this.initialized) {
- this.nonInitializedQueue.push(eventData);
- return false;
- }
-
- return dispatchSnowplowEvent(...eventData);
- }
-
- /**
- * Preloads event definitions.
- *
- * @returns {undefined}
- */
- static loadDefinitions() {
- // TODO: fetch definitions from the server and flush the queue
- // See https://gitlab.com/gitlab-org/gitlab/-/issues/358256
- this.definitionsLoaded = true;
-
- while (this.definitionsEventsQueue.length) {
- this.dispatchFromDefinition(...this.definitionsEventsQueue.shift());
- }
- }
-
- /**
- * Dispatches a structured event with data from its event definition.
- *
- * @param {String} basename
- * @param {Object} eventData
- * @returns {undefined|Boolean}
- */
- static definition(basename, eventData = {}) {
- if (!this.enabled()) {
- return false;
- }
-
- if (!(basename in this.definitionsManifest)) {
- throw new Error(`Missing Snowplow event definition "${basename}"`);
- }
-
- return this.dispatchFromDefinition(basename, eventData);
- }
-
- /**
- * Builds an event with data from a valid definition and sends it to
- * Snowplow. If the definitions are not loaded, it pushes the data to a queue.
- *
- * @param {String} basename
- * @param {Object} eventData
- * @returns {undefined|Boolean}
- */
- static dispatchFromDefinition(basename, eventData) {
- if (!this.definitionsLoaded) {
- this.definitionsEventsQueue.push([basename, eventData]);
-
- return false;
- }
-
- const eventDefinition = this.definitions.find((definition) => definition.key === basename);
-
- return this.event(
- eventData.category ?? eventDefinition.category,
- eventData.action ?? eventDefinition.action,
- eventData,
- );
- }
-
- /**
- * Dispatches any event emitted before initialization.
- *
- * @returns {undefined}
- */
- static flushPendingEvents() {
- this.initialized = true;
-
- while (this.nonInitializedQueue.length) {
- dispatchSnowplowEvent(...this.nonInitializedQueue.shift());
- }
- }
-
- /**
- * Attaches event handlers for data-attributes powered events.
- *
- * @param {String} category - the default category for all events
- * @param {HTMLElement} parent - element containing data-attributes
- * @returns {Array}
- */
- static bindDocument(category = document.body.dataset.page, parent = document) {
- if (!this.enabled() || parent.trackingBound) {
- return [];
- }
-
- // eslint-disable-next-line no-param-reassign
- parent.trackingBound = true;
-
- const handlers = getEventHandlers(category, (...args) => this.event(...args));
- handlers.forEach((event) => parent.addEventListener(event.name, event.func));
-
- return handlers;
- }
-
- /**
- * Attaches event handlers for load-events (on render).
- *
- * @param {String} category - the default category for all events
- * @param {HTMLElement} parent - element containing event targets
- * @returns {Array}
- */
- static trackLoadEvents(category = document.body.dataset.page, parent = document) {
- if (!this.enabled()) {
- return [];
- }
-
- const loadEvents = parent.querySelectorAll(LOAD_ACTION_ATTR_SELECTOR);
-
- loadEvents.forEach((element) => {
- const { action, data } = createEventPayload(element);
- this.event(category, action, data);
- });
-
- return loadEvents;
- }
-
- /**
- * Enable Snowplow automatic form tracking.
- * The config param requires at least one array of either forms
- * class names, or field name attributes.
- * https://docs.gitlab.com/ee/development/snowplow/index.html#form-tracking.
- *
- * @param {Object} config
- * @param {Array} contexts
- * @returns {undefined}
- */
- static enableFormTracking(config, contexts = []) {
- if (!this.enabled()) {
- return;
- }
-
- if (!Array.isArray(config?.forms?.allow) && !Array.isArray(config?.fields?.allow)) {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- throw new Error('Unable to enable form event tracking without allow rules.');
- }
-
- // Ignore default/standard schema
- const standardContext = getStandardContext();
- const userProvidedContexts = contexts.filter(
- (context) => context.schema !== standardContext.schema,
- );
-
- const mappedConfig = {};
- if (config.forms) {
- mappedConfig.forms = renameKey(config.forms, 'allow', 'whitelist');
- }
-
- if (config.fields) {
- mappedConfig.fields = renameKey(config.fields, 'allow', 'whitelist');
- }
-
- const enabler = () => window.snowplow('enableFormTracking', mappedConfig, userProvidedContexts);
-
- if (document.readyState === 'complete') {
- enabler();
- } else {
- document.addEventListener('readystatechange', () => {
- if (document.readyState === 'complete') {
- enabler();
- }
- });
- }
- }
-
- /**
- * Replaces the URL and referrer for the default web context
- * if the replacements are available.
- *
- * @returns {undefined}
- */
- static setAnonymousUrls() {
- const { snowplowPseudonymizedPageUrl: pageUrl } = window.gl;
-
- if (!pageUrl) {
- return;
- }
-
- const referrers = getReferrersCache();
- const pageLinks = Object.seal({
- url: pageUrl,
- referrer: '',
- originalUrl: window.location.href,
- });
-
- const appendHash = ALLOWED_URL_HASHES.some((prefix) => window.location.hash.startsWith(prefix));
- const customUrl = `${pageUrl}${appendHash ? window.location.hash : ''}`;
- window.snowplow('setCustomUrl', customUrl);
-
- if (document.referrer) {
- const node = referrers.find((links) => links.originalUrl === document.referrer);
-
- if (node) {
- pageLinks.referrer = node.url;
- window.snowplow('setReferrerUrl', pageLinks.referrer);
- }
- }
-
- addReferrersCacheEntry(referrers, pageLinks);
- }
+import { Tracker } from 'jh_else_ce/tracking/tracker';
+import { addExperimentContext } from './utils';
+const Tracking = Object.assign(Tracker, {
/**
* Returns an implementation of this class in the form of
* a Vue mixin.
@@ -270,7 +9,7 @@ export default class Tracking {
* @param {Object} opts - default options for all events
* @returns {Object}
*/
- static mixin(opts = {}) {
+ mixin(opts = {}) {
return {
computed: {
trackingCategory() {
@@ -294,5 +33,7 @@ export default class Tracking {
},
},
};
- }
-}
+ },
+});
+
+export default Tracking;
diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js
index 4544373d8aa..438ae2bc1bc 100644
--- a/app/assets/javascripts/user_popovers.js
+++ b/app/assets/javascripts/user_popovers.js
@@ -32,6 +32,7 @@ const populateUserInfo = (user) => {
([userData, status]) => {
if (userData) {
Object.assign(user, {
+ id: userId,
avatarUrl: userData.avatar_url,
bot: userData.bot,
username: userData.username,
@@ -42,6 +43,7 @@ const populateUserInfo = (user) => {
websiteUrl: userData.website_url,
pronouns: userData.pronouns,
localTime: userData.local_time,
+ isFollowed: userData.is_followed,
loaded: true,
});
}
@@ -97,15 +99,29 @@ export default function addPopovers(elements = document.querySelectorAll('.js-us
bio: null,
workInformation: null,
status: null,
+ isFollowed: false,
loaded: false,
};
const renderedPopover = new UserPopoverComponent({
propsData: {
target: el,
user,
+ placement: el.dataset.placement || 'top',
},
});
+ const { userId } = el.dataset;
+
+ renderedPopover.$on('follow', () => {
+ UsersCache.updateById(userId, { is_followed: true });
+ user.isFollowed = true;
+ });
+
+ renderedPopover.$on('unfollow', () => {
+ UsersCache.updateById(userId, { is_followed: false });
+ user.isFollowed = false;
+ });
+
initializedPopovers.set(el, renderedPopover);
renderedPopover.$mount();
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index f7a5589af90..e1e5cc565c6 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -35,7 +35,7 @@ function UsersSelect(currentUser, els, options = {}) {
}
}
- const { handleClick } = options;
+ const { handleClick, autoAssignToMe } = options;
const userSelect = this;
$els.each((i, dropdown) => {
@@ -172,10 +172,7 @@ function UsersSelect(currentUser, els, options = {}) {
});
};
- $assignToMeLink.on('click', (e) => {
- e.preventDefault();
- $(e.currentTarget).hide();
-
+ const onAssignToMeClick = () => {
if ($dropdown.data('multiSelect')) {
assignYourself();
checkMaxSelect();
@@ -194,8 +191,19 @@ function UsersSelect(currentUser, els, options = {}) {
.text(gon.current_user_fullname)
.removeClass('is-default');
}
+ };
+
+ $assignToMeLink.on('click', (e) => {
+ e.preventDefault();
+ $(e.currentTarget).hide();
+ onAssignToMeClick();
});
+ if (autoAssignToMe) {
+ $assignToMeLink.hide();
+ onAssignToMeClick();
+ }
+
$block.on('click', '.js-assign-yourself', (e) => {
e.preventDefault();
return assignTo(userSelect.currentUser.id);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue b/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue
index 492e68b636f..437d035fbf5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue
@@ -12,6 +12,11 @@ export default {
},
mixins: [glFeatureFlagMixin()],
props: {
+ state: {
+ type: String,
+ required: false,
+ default: '',
+ },
isSquashEnabled: {
type: Boolean,
required: false,
@@ -30,8 +35,16 @@ export default {
type: String,
required: true,
},
+ mergeCommitSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
+ isMerged() {
+ return this.state === 'merged';
+ },
targetBranchEscaped() {
return escape(this.targetBranch);
},
@@ -39,6 +52,22 @@ export default {
return n__('%d commit', '%d commits', this.isSquashEnabled ? 1 : this.commitsCount);
},
message() {
+ if (this.glFeatures.restructuredMrWidget) {
+ if (this.state === 'closed') {
+ return s__('mrWidgetCommitsAdded|The changes were not merged into %{targetBranch}.');
+ } else if (this.isMerged) {
+ return s__(
+ 'mrWidgetCommitsAdded|Changes merged into %{targetBranch} with %{mergeCommitSha}%{squashedCommits}.',
+ );
+ }
+
+ return this.isFastForwardEnabled
+ ? s__('mrWidgetCommitsAdded|%{commitCount} will be added to %{targetBranch}.')
+ : s__(
+ 'mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}%{squashedCommits}.',
+ );
+ }
+
return this.isFastForwardEnabled
? s__('mrWidgetCommitsAdded|Adds %{commitCount} to %{targetBranch}.')
: s__(
@@ -48,6 +77,13 @@ export default {
textDecorativeComponent() {
return this.glFeatures.restructuredMrWidget ? 'span' : 'strong';
},
+ squashCommitMessage() {
+ if (this.isMerged) {
+ return s__('mergedCommitsAdded|(commits were squashed)');
+ }
+
+ return n__('(squashes %d commit)', '(squashes %d commits)', this.commitsCount);
+ },
},
mergeCommitCount,
};
@@ -69,9 +105,14 @@ export default {
</template>
<template #squashedCommits>
<template v-if="glFeatures.restructuredMrWidget && isSquashEnabled">
- {{ n__('(squashes %d commit)', '(squashes %d commits)', commitsCount) }}</template
+ {{ squashCommitMessage }}</template
></template
>
+ <template #mergeCommitSha>
+ <template v-if="glFeatures.restructuredMrWidget"
+ ><span class="label-branch">{{ mergeCommitSha }}</span></template
+ >
+ </template>
</gl-sprintf>
</span>
</template>
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 24cefd63ce3..e7d5e4086bc 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
@@ -2,8 +2,11 @@
import { GlButton } from '@gitlab/ui';
import createFlash from '~/flash';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
-import { s__ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { s__, __ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
+import showToast from '~/vue_shared/plugins/global_toast';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
import eventHub from '../../event_hub';
import approvalsMixin from '../../mixins/approvals';
import MrWidgetContainer from '../mr_widget_container.vue';
@@ -21,7 +24,7 @@ export default {
ApprovalsSummaryOptional,
GlButton,
},
- mixins: [approvalsMixin],
+ mixins: [approvalsMixin, glFeatureFlagsMixin()],
props: {
mr: {
type: Object,
@@ -171,6 +174,14 @@ export default {
return serviceFn()
.then((data) => {
this.mr.setApprovals(data);
+
+ if (
+ this.glFeatures.mrAttentionRequests &&
+ SidebarMediator.singleton?.store.currentUserHasAttention
+ ) {
+ showToast(__('Approved. Your attention request was removed.'));
+ }
+
eventHub.$emit('MRWidgetUpdateRequested');
eventHub.$emit('ApprovalUpdated');
sidebarEventHub.$emit('removeCurrentUserAttentionRequested');
@@ -217,7 +228,7 @@ export default {
<slot
:is-approving="isApproving"
:approve-with-auth="approveWithAuth"
- :hasApproval-auth-error="hasApprovalAuthError"
+ :has-approval-auth-error="hasApprovalAuthError"
></slot>
</template>
</div>
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 f1b89c42fb5..0bc17de638b 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
@@ -47,6 +47,8 @@ export default {
fullData: [],
isCollapsed: true,
showFade: false,
+ modalData: undefined,
+ modalName: undefined,
};
},
computed: {
@@ -116,6 +118,9 @@ export default {
return summary;
},
+ modalId() {
+ return this.modalName || `modal${this.$options.name}`;
+ },
},
watch: {
isCollapsed(newVal) {
@@ -249,7 +254,7 @@ export default {
class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center"
data-testid="widget-extension-top-level"
>
- <div class="gl-flex-grow-1">
+ <div class="gl-flex-grow-1" data-testid="widget-extension-top-level-summary">
<template v-if="isLoadingSummary">{{ widgetLoadingText }}</template>
<template v-else-if="hasFetchError">{{ widgetErrorText }}</template>
<div v-else>
@@ -306,12 +311,20 @@ export default {
data-testid="extension-list-item"
>
<gl-intersection-observer
- :options="{ rootMargin: '100px', thresholds: 0.1 }"
+ :options="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ rootMargin: '100px',
+ thresholds: 0.1,
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
class="gl-w-full"
@appear="appear(index)"
@disappear="disappear(index)"
>
- <child-content :data="item" :widget-label="widgetLabel" :level="2" />
+ <child-content
+ :data="item"
+ :widget-label="widgetLabel"
+ :modal-id="modalId"
+ :level="2"
+ />
</gl-intersection-observer>
</div>
</dynamic-scroller-item>
@@ -322,5 +335,8 @@ export default {
class="fade mr-extenson-scrim gl-absolute gl-left-0 gl-bottom-0 gl-w-full gl-h-7 gl-pointer-events-none"
></div>
</div>
+ <div v-if="$options.modalComponent && modalData">
+ <component :is="$options.modalComponent" :modal-id="modalId" v-bind="modalData" />
+ </div>
</section>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
index 5cfee21dd5e..0ca4c92a5ae 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
@@ -1,5 +1,5 @@
<script>
-import { GlBadge, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlBadge, GlLink, GlSafeHtmlDirective, GlModalDirective } from '@gitlab/ui';
import StatusIcon from './status_icon.vue';
import Actions from './actions.vue';
import { generateText } from './utils';
@@ -14,6 +14,7 @@ export default {
},
directives: {
SafeHtml: GlSafeHtmlDirective,
+ GlModal: GlModalDirective,
},
props: {
data: {
@@ -24,6 +25,11 @@ export default {
type: String,
required: true,
},
+ modalId: {
+ type: String,
+ required: false,
+ default: null,
+ },
level: {
type: Number,
required: true,
@@ -63,6 +69,11 @@ export default {
<div v-if="data.link">
<gl-link :href="data.link.href">{{ data.link.text }}</gl-link>
</div>
+ <div v-if="data.modal">
+ <gl-link v-gl-modal="modalId" @click="data.modal.onClick">
+ {{ data.modal.text }}
+ </gl-link>
+ </div>
<div v-if="data.supportingText">
<p v-safe-html="generateText(data.supportingText)" class="gl-m-0"></p>
</div>
@@ -87,6 +98,7 @@ export default {
:key="childData.id"
:data="childData"
:widget-label="widgetLabel"
+ :modal-id="modalId"
:level="3"
data-testid="child-content"
/>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
index 8438f3492b2..65273678fb9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
@@ -14,6 +14,7 @@ export const registerExtension = (extension) => {
i18n: extension.i18n,
expandEvent: extension.expandEvent,
enablePolling: extension.enablePolling,
+ modalComponent: extension.modalComponent,
computed: {
...Object.keys(extension.computed).reduce(
(acc, computedKey) => ({
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue
index 01d8de132e7..456a1f17aae 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue
@@ -49,7 +49,7 @@ export default {
]"
class="gl-rounded-full gl-mr-3 gl-relative gl-p-2"
>
- <gl-loading-icon v-if="isLoading" size="md" inline class="gl-display-block" />
+ <gl-loading-icon v-if="isLoading" size="lg" inline class="gl-display-block" />
<gl-icon
v-else
:name="$options.EXTENSION_ICON_NAMES[iconName]"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js
index 5fba070f79c..cba12507eba 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js
@@ -35,9 +35,7 @@ const textStyleTags = {
[getStartTag('small')]: '<span class="gl-font-sm gl-text-gray-700">',
};
-export const generateText = (text) => {
- if (typeof text !== 'string') return null;
-
+const createText = (text) => {
return text
.replace(
new RegExp(
@@ -60,3 +58,21 @@ export const generateText = (text) => {
)
.replace(/%{([a-z]|_)+}/g, ''); // Filter out any tags we don't know about
};
+
+export const generateText = (text) => {
+ if (typeof text === 'string') {
+ return createText(text);
+ } else if (
+ typeof text === 'object' &&
+ typeof text.text === 'string' &&
+ typeof text.href === 'string'
+ ) {
+ return createText(
+ `${
+ text.prependText ? `${text.prependText} ` : ''
+ }<a class="gl-text-decoration-underline" href="${text.href}">${text.text}</a>`,
+ );
+ }
+
+ return null;
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 8cdaa3316ee..e1d88099580 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -1,9 +1,5 @@
<script>
import {
- GlButton,
- GlDropdown,
- GlDropdownSectionHeader,
- GlDropdownItem,
GlLink,
GlTooltipDirective,
GlModalDirective,
@@ -14,8 +10,6 @@ import { constructWebIDEPath } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
-import MrWidgetHowToMergeModal from './mr_widget_how_to_merge_modal.vue';
import MrWidgetIcon from './mr_widget_icon.vue';
export default {
@@ -24,14 +18,8 @@ export default {
clipboardButton,
TooltipOnTruncate,
MrWidgetIcon,
- MrWidgetHowToMergeModal,
- GlButton,
- GlDropdown,
- GlDropdownSectionHeader,
- GlDropdownItem,
GlLink,
GlSprintf,
- WebIdeLink,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -107,71 +95,6 @@ export default {
</gl-sprintf>
</div>
</div>
-
- <div class="branch-actions d-flex">
- <template v-if="mr.isOpen">
- <web-ide-link
- v-if="!mr.sourceBranchRemoved"
- :show-edit-button="false"
- :show-web-ide-button="true"
- :web-ide-url="webIdePath"
- :web-ide-text="$options.i18n.webIdeText"
- :show-gitpod-button="mr.showGitpodButton"
- :gitpod-url="mr.gitpodUrl"
- :gitpod-enabled="mr.gitpodEnabled"
- :user-preferences-gitpod-path="mr.userPreferencesGitpodPath"
- :user-profile-enable-gitpod-path="mr.userProfileEnableGitpodPath"
- :gitpod-text="$options.i18n.gitpodText"
- class="gl-display-none gl-md-display-inline-block gl-mr-3"
- data-placement="bottom"
- tabindex="0"
- data-qa-selector="open_in_web_ide_button"
- />
- <gl-button
- v-gl-modal-directive="'modal-merge-info'"
- :disabled="mr.sourceBranchRemoved"
- class="js-check-out-branch gl-mr-3"
- >
- {{ s__('mrWidget|Check out branch') }}
- </gl-button>
- <mr-widget-how-to-merge-modal
- :is-fork="isFork"
- :can-merge="mr.canMerge"
- :source-branch="mr.sourceBranch"
- :source-project="mr.sourceProject"
- :source-project-path="mr.sourceProjectFullPath"
- :target-branch="mr.targetBranch"
- :source-project-default-url="mr.sourceProjectDefaultUrl"
- :reviewing-docs-path="mr.reviewingDocsPath"
- />
- </template>
- <gl-dropdown
- v-gl-tooltip
- :title="__('Download as')"
- :aria-label="__('Download as')"
- icon="download"
- right
- data-qa-selector="download_dropdown"
- >
- <gl-dropdown-section-header>{{ __('Download as') }}</gl-dropdown-section-header>
- <gl-dropdown-item
- :href="mr.emailPatchesPath"
- class="js-download-email-patches"
- download
- data-qa-selector="download_email_patches_menu_item"
- >
- {{ s__('mrWidget|Email patches') }}
- </gl-dropdown-item>
- <gl-dropdown-item
- :href="mr.plainDiffPath"
- class="js-download-plain-diff"
- download
- data-qa-selector="download_plain_diff_menu_item"
- >
- {{ s__('mrWidget|Plain diff') }}
- </gl-dropdown-item>
- </gl-dropdown>
- </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
index e906b8c3b59..c2a3ae361ca 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
@@ -112,11 +112,19 @@ export default {
return escapeShellString(this.sourceBranch);
},
},
+ mounted() {
+ document.addEventListener('click', (e) => {
+ if (e.target.closest('.js-check-out-modal-trigger')) {
+ this.$refs.modal.show();
+ }
+ });
+ },
};
</script>
<template>
<gl-modal
+ ref="modal"
modal-id="modal-merge-info"
:no-enforce-focus="true"
:title="$options.i18n.title"
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 c0b80eef082..3b3b46e9772 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
@@ -276,12 +276,11 @@ export default {
</div>
</div>
<div>
- <span class="mr-widget-pipeline-graph">
- <span class="stage-cell">
+ <span class="gl-align-items-center gl-display-inline-flex mr-widget-pipeline-graph">
+ <span class="gl-align-items-center gl-display-inline-flex gl-flex-wrap stage-cell">
<linked-pipelines-mini-list v-if="triggeredBy.length" :triggered-by="triggeredBy" />
<pipeline-mini-graph
v-if="hasStages"
- class="gl-display-inline-block"
stages-class="mr-widget-pipeline-stages"
:stages="pipeline.details.stages"
:is-merge-train="isMergeTrain"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
index 2cef37d5c2e..b8a1f89d232 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml, GlLink } from '@gitlab/ui';
+import { GlSafeHtmlDirective as SafeHtml, GlLink, GlSprintf } from '@gitlab/ui';
import { s__, n__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -10,6 +10,7 @@ export default {
},
components: {
GlLink,
+ GlSprintf,
},
mixins: [glFeatureFlagMixin()],
props: {
@@ -28,6 +29,16 @@ export default {
required: false,
default: true,
},
+ divergedCommitsCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ targetBranchPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
closesText() {
@@ -81,5 +92,19 @@ export default {
}}</gl-link>
</span>
</p>
+ <div
+ v-if="
+ divergedCommitsCount > 0 && glFeatures.updatedMrHeader && !glFeatures.restructuredMrWidget
+ "
+ class="diverged-commits-count"
+ >
+ <gl-sprintf :message="s__('mrWidget|The source branch is %{link} the target branch')">
+ <template #link>
+ <gl-link :href="targetBranchPath">{{
+ n__('%d commit behind', '%d commits behind', divergedCommitsCount)
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
</section>
</template>
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 9499603163b..7ff1eb6e73a 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
@@ -46,7 +46,7 @@ export default {
<gl-button
v-if="!glFeatures.restructuredMrWidget && showDisabledButton"
category="primary"
- variant="success"
+ variant="confirm"
data-testid="disabled-merge-button"
:disabled="true"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
index a44caf886a4..aabbeac564a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
@@ -175,7 +175,7 @@ export default {
{{ cancelButtonText }}
</gl-button>
</h4>
- <section class="mr-info-list">
+ <section v-if="!glFeatures.restructuredMrWidget" class="mr-info-list">
<p v-if="shouldRemoveSourceBranch">
{{ s__('mrWidget|Deletes the source branch') }}
</p>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
index 6d5ca58aa20..d50e52f5ac1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
@@ -1,4 +1,5 @@
<script>
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MrWidgetAuthorTime from '../mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
@@ -8,6 +9,7 @@ export default {
MrWidgetAuthorTime,
statusIcon,
},
+ mixins: [glFeatureFlagMixin()],
props: {
/* TODO: This is providing all store and service down when it
only needs metrics and targetBranch */
@@ -29,7 +31,7 @@ export default {
:date-readable="mr.metrics.readableClosedAt"
/>
- <section class="mr-info-list">
+ <section v-if="!glFeatures.restructuredMrWidget" class="mr-info-list">
<p>
{{ s__('mrWidget|The changes were not merged into') }}
<a :href="mr.targetBranchPath" class="label-branch"> {{ mr.targetBranch }} </a>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index 7435f578852..def30dacf8a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlModalDirective, GlSkeletonLoader } from '@gitlab/ui';
+import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import userPermissionsQuery from '../../queries/permissions.query.graphql';
@@ -13,9 +13,6 @@ export default {
StatusIcon,
GlButton,
},
- directives: {
- GlModalDirective,
- },
mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
apollo: {
userPermissions: {
@@ -131,9 +128,9 @@ export default {
</gl-button>
<gl-button
v-if="canMerge"
- v-gl-modal-directive="'modal-merge-info'"
:size="glFeatures.restructuredMrWidget ? 'small' : 'medium'"
data-testid="merge-locally-button"
+ class="js-check-out-modal-trigger"
>
{{ s__('mrWidget|Resolve locally') }}
</gl-button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index 84dac95ce74..bf036f562ed 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -1,6 +1,7 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlLoadingIcon, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import api from '~/api';
import createFlash from '~/flash';
import { s__, __ } from '~/locale';
@@ -22,6 +23,7 @@ export default {
GlLoadingIcon,
GlButton,
},
+ mixins: [glFeatureFlagMixin()],
props: {
mr: {
type: Object,
@@ -181,7 +183,11 @@ export default {
{{ s__('mrWidget|Delete source branch') }}
</gl-button>
</div>
- <section class="mr-info-list" data-qa-selector="merged_status_content">
+ <section
+ v-if="!glFeatures.restructuredMrWidget"
+ class="mr-info-list"
+ data-qa-selector="merged_status_content"
+ >
<p>
{{ s__('mrWidget|The changes were merged into') }}
<span class="label-branch">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue
index 887d1aab524..b86ab69af3f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue
@@ -1,5 +1,6 @@
<script>
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import simplePoll from '~/lib/utils/simple_poll';
import MergeRequest from '~/merge_request';
import eventHub from '../../event_hub';
@@ -14,6 +15,7 @@ export default {
components: {
statusIcon,
},
+ mixins: [glFeatureFlagMixin()],
props: {
mr: {
type: Object,
@@ -88,7 +90,7 @@ export default {
{{ mergeStatus.message }}
<gl-emoji :data-name="mergeStatus.emoji" />
</h4>
- <section class="mr-info-list">
+ <section v-if="!glFeatures.restructuredMrWidget" class="mr-info-list">
<p>
{{ s__('mrWidget|Merges changes into') }}
<span class="label-branch">
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 4f8faeb877f..4fb95fe635c 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
@@ -322,11 +322,33 @@ export default {
},
restructuredWidgetShowMergeButtons() {
if (this.glFeatures.restructuredMrWidget) {
- return this.isMergeAllowed && this.state.userPermissions.canMerge;
+ return (
+ (this.isMergeAllowed || this.isAutoMergeAvailable) &&
+ this.state.userPermissions.canMerge &&
+ !this.mr.mergeOngoing &&
+ !this.mr.autoMergeEnabled
+ );
}
return true;
},
+ sourceBranchDeletedText() {
+ if (this.glFeatures.restructuredMrWidget) {
+ if (this.removeSourceBranch) {
+ return this.mr.state === 'merged'
+ ? __('Deleted the source branch.')
+ : __('Source branch will be deleted.');
+ }
+
+ return this.mr.state === 'merged'
+ ? __('Did not delete the source branch.')
+ : __('Source branch will not be deleted.');
+ }
+
+ return this.removeSourceBranch
+ ? __('Deletes the source branch.')
+ : __('Does not delete the source branch.');
+ },
},
mounted() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
@@ -421,6 +443,8 @@ export default {
if (this.glFeatures.mergeRequestWidgetGraphql) {
this.updateGraphqlState();
}
+
+ this.isMakingRequest = false;
})
.catch(() => {
this.isMakingRequest = false;
@@ -499,6 +523,7 @@ export default {
<template>
<div
+ data-testid="ready_to_merge_state"
:class="{
'gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-pl-7 gl-rounded-bottom-left-base gl-rounded-bottom-right-base':
glFeatures.restructuredMrWidget,
@@ -611,6 +636,7 @@ export default {
glFeatures.restructuredMrWidget && (shouldShowSquashEdit || shouldShowMergeEdit)
"
v-model="editCommitMessage"
+ data-testid="widget_edit_commit_message"
class="gl-display-flex gl-align-items-center"
>
{{ __('Edit commit message') }}
@@ -686,25 +712,36 @@ export default {
v-if="!restructuredWidgetShowMergeButtons"
class="gl-w-full gl-order-n1 gl-text-gray-500"
>
- <strong>
+ <strong v-if="mr.state !== 'closed'">
{{ __('Merge details') }}
</strong>
<ul class="gl-pl-4 gl-m-0">
+ <li
+ v-if="mr.divergedCommitsCount > 0 && glFeatures.updatedMrHeader"
+ class="gl-line-height-normal"
+ >
+ <gl-sprintf
+ :message="s__('mrWidget|The source branch is %{link} the target branch')"
+ >
+ <template #link>
+ <gl-link :href="mr.targetBranchPath">{{
+ n__('%d commit behind', '%d commits behind', mr.divergedCommitsCount)
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </li>
<li class="gl-line-height-normal">
<added-commit-message
+ :state="mr.state"
+ :merge-commit-sha="mr.shortMergeCommitSha"
:is-squash-enabled="squashBeforeMerge"
:is-fast-forward-enabled="!shouldShowMergeEdit"
:commits-count="commitsCount"
:target-branch="stateData.targetBranch"
/>
</li>
- <li class="gl-line-height-normal">
- <template v-if="removeSourceBranch">
- {{ __('Deletes the source branch.') }}
- </template>
- <template v-else>
- {{ __('Does not delete the source branch.') }}
- </template>
+ <li v-if="mr.state !== 'closed'" class="gl-line-height-normal">
+ {{ sourceBranchDeletedText }}
</li>
<li v-if="mr.relatedLinks" class="gl-line-height-normal">
<related-links
@@ -733,6 +770,8 @@ export default {
:state="mr.state"
:related-links="mr.relatedLinks"
:show-assign-to-me="false"
+ :diverged-commits-count="mr.divergedCommitsCount"
+ :target-branch-path="mr.targetBranchPath"
class="mr-ready-merge-related-links gl-display-inline"
/>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
index d32db50874c..cea8df2484b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
@@ -83,14 +83,11 @@ export default {
this.collapsedData.newErrors.map((e) => {
return fullData.push({
text: `${capitalizeFirstCharacter(e.severity)} - ${e.description}`,
- subtext: sprintf(
- s__(`ciReport|in %{open_link}${e.file_path}:${e.line}%{close_link}`),
- {
- open_link: `<a class="gl-text-decoration-underline" href="${e.urlPath}">`,
- close_link: '</a>',
- },
- false,
- ),
+ subtext: {
+ prependText: s__(`ciReport|in`),
+ text: `${e.file_path}:${e.line}`,
+ href: e.urlPath,
+ },
icon: {
name: SEVERITY_ICONS_EXTENSION[e.severity],
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js
index cd5cfb6837c..23f14bea4e1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js
@@ -10,6 +10,8 @@ export const i18n = {
label: s__('Reports|Test summary'),
loading: s__('Reports|Test summary results are loading'),
error: s__('Reports|Test summary failed to load results'),
+ newHeader: s__('Reports|New'),
+ fixedHeader: s__('Reports|Fixed'),
fullReport: s__('Reports|Full report'),
noChanges: (bold) => s__(`Reports|${noText(bold)} changed test results`),
@@ -36,4 +38,32 @@ export const i18n = {
sprintf(s__('Reports|An error occurred while loading %{name} results'), { name }),
headReportParsingError: s__('Reports|Head report parsing error:'),
baseReportParsingError: s__('Reports|Base report parsing error:'),
+
+ recentFailureSummary: (recentlyFailed, failed) => {
+ if (failed < 2) {
+ return sprintf(
+ s__(
+ 'Reports|%{recentlyFailed} out of %{failed} failed test has failed more than once in the last 14 days',
+ ),
+ { recentlyFailed, failed },
+ );
+ }
+ return sprintf(
+ n__(
+ 'Reports|%{recentlyFailed} out of %{failed} failed tests has failed more than once in the last 14 days',
+ 'Reports|%{recentlyFailed} out of %{failed} failed tests have failed more than once in the last 14 days',
+ recentlyFailed,
+ ),
+ { recentlyFailed, failed },
+ );
+ },
+ recentFailureCount: (recentFailures) =>
+ sprintf(
+ n__(
+ 'Reports|Failed %{count} time in %{base_branch} in the last 14 days',
+ 'Reports|Failed %{count} times in %{base_branch} in the last 14 days',
+ recentFailures.count,
+ ),
+ recentFailures,
+ ),
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js
index 65d9257903f..577b2cbfc5c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js
@@ -1,7 +1,13 @@
import { uniqueId } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { EXTENSION_ICONS } from '../../constants';
-import { summaryTextBuilder, reportTextBuilder, reportSubTextBuilder } from './utils';
+import {
+ summaryTextBuilder,
+ reportTextBuilder,
+ reportSubTextBuilder,
+ countRecentlyFailedTests,
+ recentFailuresTextBuilder,
+} from './utils';
import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants';
export default {
@@ -18,7 +24,10 @@ export default {
if (data.hasSuiteError) {
return this.$options.i18n.error;
}
- return summaryTextBuilder(this.$options.i18n.label, data.summary);
+ return {
+ subject: summaryTextBuilder(this.$options.i18n.label, data.summary),
+ meta: recentFailuresTextBuilder(data.summary),
+ };
},
statusIcon(data) {
if (data.parsingInProgress) {
@@ -50,6 +59,10 @@ export default {
hasSuiteError: data.suites?.some((suite) => suite.status === ERROR_STATUS),
parsingInProgress: status === 204,
...data,
+ summary: {
+ recentlyFailed: countRecentlyFailedTests(data.suites),
+ ...data.summary,
+ },
},
};
});
@@ -66,17 +79,66 @@ export default {
}
return EXTENSION_ICONS.success;
},
- prepareReports() {
- return this.collapsedData.suites.map((suite) => {
+ testHeader(test, sectionHeader, index) {
+ const headers = [];
+ if (index === 0) {
+ headers.push(sectionHeader);
+ }
+ if (test.recent_failures?.count && test.recent_failures?.base_branch) {
+ headers.push(i18n.recentFailureCount(test.recent_failures));
+ }
+ return headers;
+ },
+ mapTestAsChild({ iconName, sectionHeader }) {
+ return (test, index) => {
return {
- id: uniqueId('suite-'),
- text: reportTextBuilder(suite),
- subtext: reportSubTextBuilder(suite),
- icon: {
- name: this.suiteIcon(suite),
- },
+ id: uniqueId('test-'),
+ header: this.testHeader(test, sectionHeader, index),
+ icon: { name: iconName },
+ text: test.name,
};
- });
+ };
+ },
+ prepareReports() {
+ return this.collapsedData.suites
+ .map((suite) => {
+ return {
+ ...suite,
+ summary: {
+ recentlyFailed: countRecentlyFailedTests(suite),
+ ...suite.summary,
+ },
+ };
+ })
+ .map((suite) => {
+ return {
+ id: uniqueId('suite-'),
+ text: reportTextBuilder(suite),
+ subtext: reportSubTextBuilder(suite),
+ icon: {
+ name: this.suiteIcon(suite),
+ },
+ children: [
+ ...[...suite.new_failures, ...suite.new_errors].map(
+ this.mapTestAsChild({
+ sectionHeader: i18n.newHeader,
+ iconName: EXTENSION_ICONS.failed,
+ }),
+ ),
+ ...[...suite.existing_failures, ...suite.existing_errors].map(
+ this.mapTestAsChild({
+ iconName: EXTENSION_ICONS.failed,
+ }),
+ ),
+ ...[...suite.resolved_failures, ...suite.resolved_errors].map(
+ this.mapTestAsChild({
+ sectionHeader: i18n.fixedHeader,
+ iconName: EXTENSION_ICONS.success,
+ }),
+ ),
+ ],
+ };
+ });
},
},
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
index a74ed20362f..9e4b0ac581c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
@@ -43,13 +43,42 @@ export const reportTextBuilder = ({ name = '', summary = {}, status }) => {
return i18n.summaryText(name, resultsString);
};
-export const reportSubTextBuilder = ({ suite_errors }) => {
- const errors = [];
- if (suite_errors?.head) {
- errors.push(`${i18n.headReportParsingError} ${suite_errors.head}`);
- }
- if (suite_errors?.base) {
- errors.push(`${i18n.baseReportParsingError} ${suite_errors.base}`);
+export const recentFailuresTextBuilder = (summary = {}) => {
+ const { failed, recentlyFailed } = summary;
+ if (!failed || !recentlyFailed) return '';
+
+ return i18n.recentFailureSummary(recentlyFailed, failed);
+};
+
+export const reportSubTextBuilder = ({ suite_errors, summary }) => {
+ if (suite_errors?.head || suite_errors?.base) {
+ const errors = [];
+ if (suite_errors?.head) {
+ errors.push(`${i18n.headReportParsingError} ${suite_errors.head}`);
+ }
+ if (suite_errors?.base) {
+ errors.push(`${i18n.baseReportParsingError} ${suite_errors.base}`);
+ }
+ return errors.join('<br />');
}
- return errors.join('<br />');
+ return recentFailuresTextBuilder(summary);
+};
+
+export const countRecentlyFailedTests = (subject) => {
+ // handle either a single report or an array of reports
+ const reports = !subject.length ? [subject] : subject;
+
+ return reports
+ .map((report) => {
+ return (
+ [report.new_failures, report.existing_failures, report.resolved_failures]
+ // only count tests which have failed more than once
+ .map(
+ (failureArray) =>
+ failureArray.filter((failure) => failure.recent_failures?.count > 1).length,
+ )
+ .reduce((total, count) => total + count, 0)
+ );
+ })
+ .reduce((total, count) => total + count, 0);
};
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 4b3ad288768..8ebb7f6f159 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
@@ -165,7 +165,10 @@ export default {
return this.mr?.codequalityReportsPath;
},
shouldRenderRelatedLinks() {
- return Boolean(this.mr.relatedLinks) && !this.mr.isNothingToMergeState;
+ return (
+ (Boolean(this.mr.relatedLinks) || this.mr.divergedCommitsCount > 0) &&
+ !this.mr.isNothingToMergeState
+ );
},
shouldRenderSourceBranchRemovalStatus() {
return (
@@ -195,6 +198,9 @@ export default {
shouldRenderTestReport() {
return Boolean(this.mr?.testResultsPath);
},
+ shouldRenderRefactoredTestReport() {
+ return window.gon?.features?.refactorMrWidgetTestSummary;
+ },
mergeError() {
let { mergeError } = this.mr;
@@ -228,6 +234,9 @@ export default {
isRestructuredMrWidgetEnabled() {
return window.gon?.features?.restructuredMrWidget;
},
+ isUpdatedHeaderEnabled() {
+ return window.gon?.features?.updatedMrHeader;
+ },
},
watch: {
'mr.machineValue': {
@@ -512,7 +521,7 @@ export default {
}
},
registerTestReportExtension() {
- if (this.shouldRenderTestReport && this.shouldShowExtension) {
+ if (this.shouldRenderTestReport && this.shouldRenderRefactoredTestReport) {
registerExtension(testReportExtension);
}
},
@@ -521,11 +530,15 @@ export default {
</script>
<template>
<div v-if="isLoaded" class="mr-state-widget gl-mt-3">
- <header class="gl-rounded-base gl-border-solid gl-border-1 gl-border-gray-100">
+ <header
+ v-if="shouldRenderCollaborationStatus || !isUpdatedHeaderEnabled"
+ :class="{ 'mr-widget-workflow gl-mt-0!': isUpdatedHeaderEnabled }"
+ class="gl-rounded-base gl-border-solid gl-border-1 gl-border-gray-100 gl-overflow-hidden"
+ >
<mr-widget-alert-message v-if="shouldRenderCollaborationStatus" type="info">
{{ s__('mrWidget|Members who can merge are allowed to add commits.') }}
</mr-widget-alert-message>
- <mr-widget-header :mr="mr" />
+ <mr-widget-header v-if="!isUpdatedHeaderEnabled" :mr="mr" />
</header>
<mr-widget-suggest-pipeline
v-if="shouldSuggestPipelines"
@@ -588,7 +601,7 @@ export default {
/>
<grouped-test-reports-app
- v-if="mr.testResultsPath && !shouldShowExtension"
+ v-if="shouldRenderTestReport && !shouldRenderRefactoredTestReport"
class="js-reports-container"
:endpoint="mr.testResultsPath"
:head-blob-path="mr.headBlobPath"
@@ -617,6 +630,8 @@ export default {
v-if="shouldRenderRelatedLinks"
:state="mr.state"
:related-links="mr.relatedLinks"
+ :diverged-commits-count="mr.divergedCommitsCount"
+ :target-branch-path="mr.targetBranchPath"
class="mr-info-list gl-ml-7 gl-pb-5"
/>
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
index 99e6f4e9beb..efc0673bc26 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
@@ -11,7 +11,6 @@ fragment ReadyToMerge on Project {
shouldRemoveSourceBranch
forceRemoveSourceBranch
defaultMergeCommitMessage
- defaultMergeCommitMessageWithDescription
defaultSquashCommitMessage
squash
squashOnMerge
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index eb07609d5d6..146cf7e11a7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -1,5 +1,5 @@
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
-import { statusBoxState } from '~/issuable/components/status_box.vue';
+import { badgeState } from '~/issuable/components/status_box.vue';
import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
import { machine } from '~/lib/utils/finite_state_machine';
import {
@@ -221,8 +221,8 @@ export default class MergeRequestStore {
}
updateStatusState(state) {
- if (this.mergeRequestState !== state && statusBoxState.updateStatus) {
- statusBoxState.updateStatus();
+ if (this.mergeRequestState !== state && badgeState.updateStatus) {
+ badgeState.updateStatus();
}
}
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 948d2505966..c93f620995f 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
@@ -278,7 +278,7 @@ export default {
data-testid="viewIncidentBtn"
:href="incidentPath(alert.issue.iid)"
category="primary"
- variant="success"
+ variant="confirm"
>
{{ s__('AlertManagement|View incident') }}
</gl-button>
@@ -288,7 +288,7 @@ export default {
data-testid="createIncidentBtn"
:loading="incidentCreationInProgress"
category="primary"
- variant="success"
+ variant="confirm"
@click="createIncident()"
>
{{ s__('AlertManagement|Create incident') }}
diff --git a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
index 8b76af05ffe..6a03e38a31d 100644
--- a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
@@ -1,12 +1,12 @@
<script>
-import { GlSegmentedControl } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
+import SegmentedControlButtonGroup from '~/vue_shared/components/segmented_control_button_group.vue';
import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
export default {
components: {
- GlSegmentedControl,
CiCdAnalyticsAreaChart,
+ SegmentedControlButtonGroup,
},
props: {
charts: {
@@ -38,7 +38,11 @@ export default {
</script>
<template>
<div>
- <gl-segmented-control v-model="selectedChart" :options="chartRanges" class="gl-mb-4" />
+ <segmented-control-button-group
+ v-model="selectedChart"
+ :options="chartRanges"
+ class="gl-mb-4"
+ />
<ci-cd-analytics-area-chart
v-if="chart"
v-bind="$attrs"
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index 07bd6019b80..9bccc49e894 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -11,17 +11,22 @@ import { GlIcon } from '@gitlab/ui';
* }
*
* Used in:
- * - Pipelines table Badge
- * - Pipelines table mini graph
- * - Pipeline graph
- * - Pipeline show view badge
- * - Jobs table
+ * - Extended MR Popover
* - Jobs show view header
* - Jobs show view sidebar
+ * - Jobs table
* - Linked pipelines
- * - Extended MR Popover
+ * - 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, 16, 18, 24, 32, 48, 72];
+const validSizes = [8, 12, 14, 16, 24, 32, 48, 72];
export default {
components: {
@@ -45,6 +50,11 @@ export default {
required: false,
default: false,
},
+ isInteractive: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
cssClasses: {
type: String,
required: false,
@@ -52,9 +62,9 @@ export default {
},
},
computed: {
- cssClass() {
+ wrapperStyleClasses() {
const status = this.status.group;
- return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
+ return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status} gl-rounded-full gl-justify-content-center`;
},
icon() {
return this.borderless ? `${this.status.icon}_borderless` : this.status.icon;
@@ -63,7 +73,10 @@ export default {
};
</script>
<template>
- <span :class="cssClass">
+ <span
+ :class="[wrapperStyleClasses, { interactive: isInteractive }]"
+ :style="{ height: `${size}px`, width: `${size}px` }"
+ >
<gl-icon :name="icon" :size="size" :class="cssClasses" :aria-label="status.icon" />
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue b/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
index 7a166f9a3e4..78db2bf15b0 100644
--- a/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
@@ -4,6 +4,7 @@
*
* @example
* <color-picker
+ :id="example-id"
:invalid-feedback="__('Please enter a valid hex (#RRGGBB or #RGB) color value')"
:label="__('Background color')"
:value="#FF0000"
@@ -12,6 +13,7 @@
/>
*/
import { GlFormGroup, GlFormInput, GlFormInputGroup, GlLink, GlTooltipDirective } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
import { __ } from '~/locale';
const PREVIEW_COLOR_DEFAULT_CLASSES =
@@ -29,6 +31,11 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
+ id: {
+ type: String,
+ required: false,
+ default: () => uniqueId('color-picker-'),
+ },
invalidFeedback: {
type: String,
required: false,
@@ -94,14 +101,13 @@ export default {
<div>
<gl-form-group
:label="label"
- label-for="color-picker"
+ :label-for="id"
:description="description"
:invalid-feedback="invalidFeedback"
:state="state"
:class="{ 'gl-mb-3!': hasSuggestedColors }"
>
<gl-form-input-group
- id="color-picker"
max-length="7"
type="text"
class="gl-align-center gl-rounded-0 gl-rounded-top-right-base gl-rounded-bottom-right-base"
@@ -112,6 +118,7 @@ export default {
<template #prepend>
<div :class="previewColorClasses" :style="previewColor" data-testid="color-preview">
<gl-form-input
+ :id="id"
type="color"
class="gl-absolute gl-top-0 gl-left-0 gl-h-full! gl-p-0! gl-m-0! gl-opacity-0"
tabindex="-1"
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index ebbc1bfb037..388353bc35b 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -172,7 +172,9 @@ export default {
:img-src="author.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
+ :img-size="16"
class="avatar-image-container text-decoration-none"
+ img-css-classes="gl-mr-3"
/>
<tooltip-on-truncate :title="title" class="flex-truncate-child">
<gl-link :href="commitUrl" class="commit-row-message cgray">{{ title }}</gl-link>
diff --git a/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue b/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue
new file mode 100644
index 00000000000..298c7bc50cc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue
@@ -0,0 +1,39 @@
+<script>
+import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
+import { confidentialityInfoText } from '../constants';
+
+export default {
+ components: {
+ GlBadge,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ workspaceType: {
+ type: String,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ confidentialTooltip() {
+ return confidentialityInfoText(this.workspaceType, this.issuableType);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-badge
+ v-gl-tooltip.bottom
+ :title="confidentialTooltip"
+ icon="eye-slash"
+ variant="warning"
+ class="gl-display-inline gl-mr-2"
+ >{{ __('Confidential') }}</gl-badge
+ >
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js
index 6629b293eb9..8481280f25f 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js
@@ -14,6 +14,8 @@ const Template = (args, { argTypes }) => ({
additionalInformation: args.additionalInformation || null,
confirmDangerMessage: args.confirmDangerMessage || 'You require more Vespene Gas',
htmlConfirmationMessage: args.confirmDangerMessage || false,
+ confirmButtonText: args.confirmButtonText || 'Cancel',
+ cancelButtonText: args.cancelButtonText || 'Confirm',
},
});
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
index 88890b3332d..37e480f7e41 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
@@ -12,7 +12,7 @@ import {
CONFIRM_DANGER_MODAL_TITLE,
CONFIRM_DANGER_PHRASE_TEXT,
CONFIRM_DANGER_WARNING,
- CONFIRM_DANGER_MODAL_ERROR,
+ CONFIRM_DANGER_MODAL_CANCEL,
} from './constants';
export default {
@@ -40,6 +40,9 @@ export default {
additionalInformation: {
default: CONFIRM_DANGER_WARNING,
},
+ cancelButtonText: {
+ default: CONFIRM_DANGER_MODAL_CANCEL,
+ },
},
props: {
modalId: {
@@ -66,6 +69,11 @@ export default {
attributes: [{ variant: 'danger', disabled: !this.isValid, class: 'qa-confirm-button' }],
};
},
+ actionCancel() {
+ return {
+ text: this.cancelButtonText,
+ };
+ },
},
methods: {
equalString(a, b) {
@@ -77,7 +85,6 @@ export default {
CONFIRM_DANGER_MODAL_TITLE,
CONFIRM_DANGER_WARNING,
CONFIRM_DANGER_PHRASE_TEXT,
- CONFIRM_DANGER_MODAL_ERROR,
},
};
</script>
@@ -88,6 +95,7 @@ export default {
:data-testid="modalId"
:title="$options.i18n.CONFIRM_DANGER_MODAL_TITLE"
:action-primary="actionPrimary"
+ :action-cancel="actionCancel"
@primary="$emit('confirm')"
>
<gl-alert
@@ -110,7 +118,7 @@ export default {
</template>
</gl-sprintf>
</p>
- <gl-form-group :state="isValid" :invalid-feedback="$options.i18n.CONFIRM_DANGER_MODAL_ERROR">
+ <gl-form-group :state="isValid">
<gl-form-input
id="confirm_name_input"
v-model="confirmationPhrase"
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/constants.js b/app/assets/javascripts/vue_shared/components/confirm_danger/constants.js
index fa44a9be411..90d55d0f93f 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/constants.js
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/constants.js
@@ -2,7 +2,6 @@ import { __ } from '~/locale';
export const CONFIRM_DANGER_MODAL_ID = 'confirm-danger-modal';
export const CONFIRM_DANGER_MODAL_TITLE = __('Confirmation required');
-export const CONFIRM_DANGER_MODAL_ERROR = __('Confirmation required');
export const CONFIRM_DANGER_MODAL_BUTTON = __('Confirm');
export const CONFIRM_DANGER_WARNING = __(
'This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.',
@@ -10,3 +9,4 @@ export const CONFIRM_DANGER_WARNING = __(
export const CONFIRM_DANGER_PHRASE_TEXT = __(
'Please type %{phrase_code} to proceed or close this modal to cancel.',
);
+export const CONFIRM_DANGER_MODAL_CANCEL = __('Cancel');
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
index e546ca57c5e..181c1b89e31 100644
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
@@ -248,7 +248,7 @@ export default {
__('Cancel')
}}</gl-button>
<gl-button
- variant="success"
+ variant="confirm"
category="primary"
:disabled="!isValid"
@click="setFixedRange()"
diff --git a/app/assets/javascripts/vue_shared/components/deployment_instance.vue b/app/assets/javascripts/vue_shared/components/deployment_instance.vue
index 41b783aa011..4aae86fc82b 100644
--- a/app/assets/javascripts/vue_shared/components/deployment_instance.vue
+++ b/app/assets/javascripts/vue_shared/components/deployment_instance.vue
@@ -14,6 +14,7 @@
*/
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import { mergeUrlParams } from '~/lib/utils/url_utility';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
@@ -22,7 +23,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
-
+ mixins: [glFeatureFlagsMixin()],
props: {
/**
* Represents the status of the pod. Each state is represented with a different
@@ -75,7 +76,9 @@ export default {
},
computedLogPath() {
- return this.isLink ? mergeUrlParams({ pod_name: this.podName }, this.logsPath) : null;
+ return this.isLink && this.glFeatures.monitorLogging
+ ? mergeUrlParams({ pod_name: this.podName }, this.logsPath)
+ : null;
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue
deleted file mode 100644
index afde0c81580..00000000000
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-<script>
-export default {
- props: {
- name: {
- type: String,
- required: true,
- },
- value: {
- type: [Number, String],
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <input :name="name" :value="value" type="hidden" />
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
deleted file mode 100644
index edb5ffdc39c..00000000000
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
-
-export default {
- components: {
- GlIcon,
- },
- props: {
- placeholderText: {
- type: String,
- required: true,
- default: __('Search'),
- },
- focused: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return { searchQuery: this.value };
- },
- watch: {
- searchQuery(query) {
- this.$emit('input', query);
- },
- focused(val) {
- if (val) {
- this.$refs.searchInput.focus();
- }
- },
- },
-};
-</script>
-
-<template>
- <div class="dropdown-input">
- <input
- ref="searchInput"
- v-model="searchQuery"
- :placeholder="placeholderText"
- class="dropdown-input-field"
- type="search"
- autocomplete="off"
- />
- <gl-icon name="search" class="dropdown-input-search" data-hidden="true" />
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
index 2a79ccc2648..840911dc99c 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
@@ -1,28 +1,22 @@
<script>
import {
- GlIcon,
GlLoadingIcon,
GlDropdown,
GlDropdownForm,
GlDropdownDivider,
GlDropdownItem,
- GlDropdownSectionHeader,
GlSearchBoxByType,
} from '@gitlab/ui';
import { __ } from '~/locale';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
export default {
components: {
- GlIcon,
GlLoadingIcon,
GlDropdown,
GlDropdownForm,
GlDropdownDivider,
GlDropdownItem,
- GlDropdownSectionHeader,
GlSearchBoxByType,
- TooltipOnTruncate,
},
props: {
selectText: {
@@ -45,11 +39,6 @@ export default {
required: false,
default: () => [],
},
- groupedOptions: {
- type: Array,
- required: false,
- default: () => [],
- },
isLoading: {
type: Boolean,
required: false,
@@ -70,6 +59,11 @@ export default {
required: false,
default: false,
},
+ customIsSelectedOption: {
+ type: Function,
+ required: false,
+ default: undefined,
+ },
},
computed: {
isSearchEmpty() {
@@ -87,6 +81,9 @@ export default {
}
},
isSelected(option) {
+ if (this.customIsSelectedOption !== undefined) {
+ return this.customIsSelectedOption(option);
+ }
if (Array.isArray(this.selected)) {
return this.selected.some((label) => label.title === option.title);
}
@@ -143,7 +140,7 @@ export default {
<gl-dropdown-form class="gl-relative gl-min-h-7" data-qa-selector="labels_dropdown_content">
<gl-loading-icon
v-if="isLoading"
- size="md"
+ size="lg"
class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
/>
<template v-else>
@@ -177,36 +174,7 @@ export default {
{{ option.title }}
</slot>
</gl-dropdown-item>
- <template v-for="(optionGroup, index) in groupedOptions">
- <gl-dropdown-divider v-if="index !== 0" :key="index" />
- <gl-dropdown-section-header :key="optionGroup.id">
- <div class="gl-display-flex gl-max-w-full">
- <tooltip-on-truncate
- :title="optionGroup.title"
- class="gl-text-truncate gl-flex-grow-1"
- >
- {{ optionGroup.title }}
- </tooltip-on-truncate>
- <span v-if="optionGroup.secondaryText" class="gl-float-right gl-font-weight-normal">
- <gl-icon name="clock" class="gl-mr-2" />
- {{ optionGroup.secondaryText }}
- </span>
- </div>
- </gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="option in optionGroup.options"
- :key="optionKey(option)"
- :is-checked="isSelected(option)"
- is-check-centered
- is-check-item
- data-testid="unselected-option"
- @click="selectOption(option)"
- >
- <slot name="item" :item="option">
- {{ option.title }}
- </slot>
- </gl-dropdown-item>
- </template>
+ <slot v-bind="{ isSelected }" name="grouped-options"></slot>
<gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
{{ $options.i18n.noMatchingResults }}
</gl-dropdown-item>
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
index b0c1c1531aa..680f229d5e8 100644
--- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
@@ -1,5 +1,5 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import Mousetrap from 'mousetrap';
import VirtualList from 'vue-virtual-scroll-list';
@@ -9,13 +9,13 @@ import Item from './item.vue';
export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
-export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
const originalStopCallback = Mousetrap.prototype.stopCallback;
export default {
components: {
GlIcon,
+ GlLoadingIcon,
Item,
VirtualList,
},
@@ -71,7 +71,7 @@ export default {
return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1;
},
listHeight() {
- return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT;
+ return FILE_FINDER_ROW_HEIGHT;
},
showClearInputButton() {
return this.searchText.trim() !== '';
@@ -265,9 +265,9 @@ export default {
</li>
</template>
<li v-else class="dropdown-menu-empty-item">
- <div class="gl-mr-3 gl-ml-3 gl-mt-3 gl-mb-3">
+ <div class="gl-mr-3 gl-ml-3 gl-mt-5 gl-mb-3">
<template v-if="loading">
- {{ __('Loading...') }}
+ <gl-loading-icon />
</template>
<template v-else>
{{ __('No files found.') }}
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index 3d48c74b40b..d7a84798e47 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -1,6 +1,6 @@
import { __ } from '~/locale';
-export const DEBOUNCE_DELAY = 200;
+export const DEBOUNCE_DELAY = 500;
export const MAX_RECENT_TOKENS_SIZE = 3;
export const FILTER_NONE = 'None';
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 6638a5de62f..33d507dad57 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
@@ -89,32 +89,20 @@ export default {
required: false,
default: () => ({}),
},
+ syncFilterAndSort: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
- let selectedSortOption = this.sortOptions[0]?.sortDirection?.descending;
- let selectedSortDirection = SortDirection.descending;
-
- // Extract correct sortBy value based on initialSortBy
- if (this.initialSortBy) {
- selectedSortOption = this.sortOptions
- .filter(
- (sortBy) =>
- sortBy.sortDirection.ascending === this.initialSortBy ||
- sortBy.sortDirection.descending === this.initialSortBy,
- )
- .pop();
- selectedSortDirection = Object.keys(selectedSortOption.sortDirection).find(
- (key) => selectedSortOption.sortDirection[key] === this.initialSortBy,
- );
- }
-
return {
initialRender: true,
recentSearchesPromise: null,
recentSearches: [],
filterValue: this.initialFilterValue,
- selectedSortOption,
- selectedSortDirection,
+ selectedSortOption: this.sortOptions[0],
+ selectedSortDirection: SortDirection.descending,
};
},
computed: {
@@ -173,7 +161,20 @@ export default {
return undefined;
},
},
+ watch: {
+ initialFilterValue(newValue) {
+ if (this.syncFilterAndSort) {
+ this.filterValue = newValue;
+ }
+ },
+ initialSortBy(newValue) {
+ if (this.syncFilterAndSort) {
+ this.updateSelectedSortValues(newValue);
+ }
+ },
+ },
created() {
+ this.updateSelectedSortValues(this.initialSortBy);
if (this.recentSearchesStorageKey) this.setupRecentSearch();
},
methods: {
@@ -309,12 +310,25 @@ export default {
const cleared = true;
this.$emit('onFilter', [], cleared);
},
+ updateSelectedSortValues(sort) {
+ if (!sort) {
+ return;
+ }
+
+ this.selectedSortOption = this.sortOptions.find(
+ (sortBy) =>
+ sortBy.sortDirection.ascending === sort || sortBy.sortDirection.descending === sort,
+ );
+ this.selectedSortDirection = Object.keys(this.selectedSortOption.sortDirection).find(
+ (key) => this.selectedSortOption.sortDirection[key] === sort,
+ );
+ },
},
};
</script>
<template>
- <div class="vue-filtered-search-bar-container d-md-flex">
+ <div class="vue-filtered-search-bar-container gl-md-display-flex">
<gl-form-checkbox
v-if="showCheckbox"
class="gl-align-self-center"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
index 696456be990..848c49c48c7 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
@@ -87,6 +87,7 @@ export default {
:get-active-token-value="getActiveAuthor"
:default-suggestions="defaultAuthors"
:preloaded-suggestions="preloadedAuthors"
+ v-bind="$attrs"
@fetch-suggestions="fetchAuthors"
v-on="$listeners"
>
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 e7923e0b55e..c3a0a97a7ba 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
@@ -211,10 +211,22 @@ export default {
@select="handleTokenValueSelected"
>
<template #view-token="viewTokenProps">
- <slot name="view-token" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot>
+ <slot
+ name="view-token"
+ :view-token-props="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ ...viewTokenProps,
+ activeTokenValue,
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ ></slot>
</template>
<template #view="viewTokenProps">
- <slot name="view" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot>
+ <slot
+ name="view"
+ :view-token-props="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ ...viewTokenProps,
+ activeTokenValue,
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ ></slot>
</template>
<template v-if="suggestionsEnabled" #suggestions>
<template v-if="showDefaultSuggestions">
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
index 4ecfc1cf40c..aa5161ca93c 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
@@ -65,6 +65,7 @@ export default {
:suggestions="branches"
:suggestions-loading="loading"
:get-active-token-value="getActiveBranch"
+ v-bind="$attrs"
@fetch-suggestions="fetchBranches"
v-on="$listeners"
>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
index 5a69751a2cc..210d814d22a 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
@@ -67,6 +67,7 @@ export default {
:suggestions="emojis"
:suggestions-loading="loading"
:get-active-token-value="getActiveEmoji"
+ v-bind="$attrs"
@fetch-suggestions="fetchEmojis"
v-on="$listeners"
>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
index 3f7a8920f48..6f24955814c 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -104,6 +104,7 @@ export default {
:suggestions="labels"
:get-active-token-value="getActiveLabel"
:default-suggestions="defaultLabels"
+ v-bind="$attrs"
@fetch-suggestions="fetchLabels"
v-on="$listeners"
>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
index 11c081ab4f8..69265d0fdc9 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
@@ -84,6 +84,7 @@ export default {
:suggestions="milestones"
:suggestions-loading="loading"
:get-active-token-value="getActiveMilestone"
+ v-bind="$attrs"
@fetch-suggestions="fetchMilestones"
v-on="$listeners"
>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
index f353cc3a765..9e68c92af5d 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
@@ -66,6 +66,7 @@ export default {
:suggestions="releases"
:suggestions-loading="loading"
:get-active-token-value="getActiveRelease"
+ v-bind="$attrs"
@fetch-suggestions="fetchReleases"
v-on="$listeners"
>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 722df3cc58b..1f309a19b14 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -111,6 +111,16 @@ export default {
required: false,
default: false,
},
+ showCommentToolBar: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ restrictedToolBarItems: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
@@ -331,7 +341,7 @@ export default {
:enable-preview="enablePreview"
:show-suggest-popover="showSuggestPopover"
:suggestion-start-index="suggestionsStartIndex"
- data-testid="markdownHeader"
+ :restricted-tool-bar-items="restrictedToolBarItems"
@preview-markdown="showPreviewTab"
@write-markdown="showWriteTab"
@handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
@@ -350,6 +360,7 @@ export default {
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
:can-attach-file="canAttachFile"
+ :show-comment-tool-bar="showCommentToolBar"
/>
</div>
</div>
@@ -362,8 +373,6 @@ export default {
<suggestions
v-if="hasSuggestion"
:note-html="markdownPreview"
- :from-line="lineNumber"
- :from-content="lineContent"
:line-type="lineType"
:disabled="true"
:suggestions="suggestions"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index d0bd5046bf0..ba2b5eaa4f9 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -54,6 +54,11 @@ export default {
required: false,
default: true,
},
+ restrictedToolBarItems: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
@@ -193,7 +198,10 @@ export default {
<toolbar-button
tag="**"
:button-title="
- sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey })
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), {
+ modifierKey,
+ }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
"
:shortcuts="$options.shortcuts.bold"
icon="bold"
@@ -201,22 +209,28 @@ export default {
<toolbar-button
tag="_"
:button-title="
- sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey })
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), {
+ modifierKey,
+ }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
"
:shortcuts="$options.shortcuts.italic"
icon="italic"
/>
<toolbar-button
+ v-if="!restrictedToolBarItems.includes('strikethrough')"
tag="~~"
:button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}⇧X)'), {
- modifierKey,
+ modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
})
"
:shortcuts="$options.shortcuts.strikethrough"
icon="strikethrough"
/>
<toolbar-button
+ v-if="!restrictedToolBarItems.includes('quote')"
:prepend="true"
:tag="tag"
:button-title="__('Insert a quote')"
@@ -266,30 +280,37 @@ export default {
tag="[{text}](url)"
tag-select="url"
:button-title="
- sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey })
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), {
+ modifierKey,
+ }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
"
:shortcuts="$options.shortcuts.link"
icon="link"
/>
<toolbar-button
+ v-if="!restrictedToolBarItems.includes('bullet-list')"
:prepend="true"
tag="- "
:button-title="__('Add a bullet list')"
icon="list-bulleted"
/>
<toolbar-button
+ v-if="!restrictedToolBarItems.includes('numbered-list')"
:prepend="true"
tag="1. "
:button-title="__('Add a numbered list')"
icon="list-numbered"
/>
<toolbar-button
+ v-if="!restrictedToolBarItems.includes('task-list')"
:prepend="true"
tag="- [ ] "
:button-title="__('Add a task list')"
icon="list-task"
/>
<toolbar-button
+ v-if="!restrictedToolBarItems.includes('collapsible-section')"
:tag="mdCollapsibleSection"
:prepend="true"
tag-select="Click to expand"
@@ -297,12 +318,14 @@ export default {
icon="details-block"
/>
<toolbar-button
+ v-if="!restrictedToolBarItems.includes('table')"
:tag="mdTable"
:prepend="true"
:button-title="__('Add a table')"
icon="table"
/>
<toolbar-button
+ v-if="!restrictedToolBarItems.includes('full-screen')"
class="js-zen-enter"
:prepend="true"
:button-title="__('Go full screen')"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index f1c293c87f4..6c99a749edc 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -24,6 +24,11 @@ export default {
required: false,
default: true,
},
+ showCommentToolBar: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
hasQuickActionsDocsPath() {
@@ -34,24 +39,33 @@ export default {
</script>
<template>
- <div class="comment-toolbar clearfix">
+ <div v-if="showCommentToolBar" class="comment-toolbar clearfix">
<div class="toolbar-text">
<template v-if="!hasQuickActionsDocsPath && markdownDocsPath">
- <gl-link :href="markdownDocsPath" target="_blank">
- {{ __('Markdown is supported') }}
- </gl-link>
+ <gl-sprintf
+ :message="
+ s__('MarkdownToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}')
+ "
+ >
+ <template #markdownDocsLink="{ content }">
+ <gl-link :href="markdownDocsPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
</template>
<template v-if="hasQuickActionsDocsPath && markdownDocsPath">
<gl-sprintf
:message="
- __(
- '%{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd} and %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd} are supported',
+ s__(
+ 'NoteToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}. For %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd}, type %{keyboardStart}/%{keyboardEnd}.',
)
"
>
<template #markdownDocsLink="{ content }">
<gl-link :href="markdownDocsPath" target="_blank">{{ content }}</gl-link>
</template>
+ <template #keyboard="{ content }">
+ <kbd>{{ content }}</kbd>
+ </template>
<template #quickActionsDocsLink="{ content }">
<gl-link :href="quickActionsDocsPath" target="_blank">{{ content }}</gl-link>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue b/app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue
index 3e796a73f72..e23721da223 100644
--- a/app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue
+++ b/app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue
@@ -90,7 +90,9 @@ export default {
modal-id="upload-metric-modal"
size="sm"
:action-primary="actionPrimaryProps"
- :action-cancel="{ text: $options.i18n.modalCancel }"
+ :action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ text: $options.i18n.modalCancel,
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:title="$options.i18n.modalTitle"
:visible="modalVisible"
@hidden="clearInputs"
diff --git a/app/assets/javascripts/vue_shared/components/metric_images/metric_images_table.vue b/app/assets/javascripts/vue_shared/components/metric_images/metric_images_table.vue
index 8eb8e52728d..bbbaaeb8a9e 100644
--- a/app/assets/javascripts/vue_shared/components/metric_images/metric_images_table.vue
+++ b/app/assets/javascripts/vue_shared/components/metric_images/metric_images_table.vue
@@ -159,7 +159,9 @@ export default {
size="sm"
:visible="modalVisible"
:action-primary="deleteActionPrimaryProps"
- :action-cancel="{ text: $options.i18n.modalCancel }"
+ :action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ text: $options.i18n.modalCancel,
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@primary.prevent="onDelete"
@hidden="resetEditFields"
>
@@ -177,7 +179,9 @@ export default {
modal-id="edit-metric-modal"
size="sm"
:action-primary="updateActionPrimaryProps"
- :action-cancel="{ text: $options.i18n.modalCancel }"
+ :action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ text: $options.i18n.modalCancel,
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:visible="editModalVisible"
data-testid="metric-image-edit-modal"
@hidden="resetEditFields"
diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
index a069d1cd756..21212e82de4 100644
--- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
@@ -61,7 +61,9 @@ export default {
v-for="(tab, i) in tabs"
:key="i"
:title-link-class="`js-${scope}-tab-${tab.scope} gl-display-inline-flex`"
- :title-link-attributes="{ 'data-testid': `${scope}-tab-${tab.scope}` }"
+ :title-link-attributes="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ 'data-testid': `${scope}-tab-${tab.scope}`,
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:active="tab.isActive"
@click="onTabClick(tab)"
>
diff --git a/app/assets/javascripts/vue_shared/components/paginated_list.vue b/app/assets/javascripts/vue_shared/components/paginated_list.vue
index e19b8510399..ddc7a457b98 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_list.vue
+++ b/app/assets/javascripts/vue_shared/components/paginated_list.vue
@@ -29,7 +29,7 @@ export default {
</template>
<template #default="{ listItem, query }">
- <slot :listItem="listItem" :query="query"></slot>
+ <slot :list-item="listItem" :query="query"></slot>
</template>
</gl-paginated-list>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/registry/list_item.vue b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
index 6bb321713d5..a8b250f2041 100644
--- a/app/assets/javascripts/vue_shared/components/registry/list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
@@ -32,7 +32,6 @@ export default {
return {
'gl-border-t-transparent': !this.first && !this.selected,
'gl-border-t-gray-100': this.first && !this.selected,
- 'gl-opacity-5': this.disabled,
'gl-border-b-gray-100': !this.selected,
'gl-bg-blue-50 gl-border-blue-200': this.selected,
};
diff --git a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
index 767a108dde5..da68fe961a6 100644
--- a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
@@ -116,6 +116,7 @@ export default {
@clear="clearSearch"
/>
<gl-sorting
+ data-testid="registry-sort-dropdown"
:text="sortText"
:is-ascending="isSortAscending"
@sortDirectionChange="onDirectionChange"
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
index d5493aa5a66..9eaaf7d1c18 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
@@ -50,6 +50,11 @@ export default {
required: false,
default: null,
},
+ defaultPlatformName: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
apollo: {
platforms: {
@@ -64,9 +69,10 @@ export default {
});
},
result() {
- // Select first platform by default
- if (this.platforms?.[0]) {
- this.selectPlatform(this.platforms[0]);
+ if (this.platforms.length) {
+ // If it is set and available, select the defaultSelectedPlatform.
+ // Otherwise, select the first available platform
+ this.selectPlatform(this.defaultPlatform() || this.platforms[0]);
}
},
error() {
@@ -138,6 +144,14 @@ export default {
show() {
this.$refs.modal.show();
},
+ focusSelected() {
+ // By default the first platform always gets the focus, but when the `defaultPlatformName`
+ // property is present, any other platform might actually be selected.
+ this.$refs[this.selectedPlatformName]?.[0].$el.focus();
+ },
+ defaultPlatform() {
+ return this.platforms.find((platform) => platform.name === this.defaultPlatformName);
+ },
selectPlatform(platform) {
this.selectedPlatform = platform;
@@ -182,6 +196,8 @@ export default {
:title="$options.i18n.installARunner"
:action-secondary="$options.closeButton"
v-bind="$attrs"
+ v-on="$listeners"
+ @shown="focusSelected"
>
<gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)">
{{ $options.i18n.fetchError }}
@@ -203,6 +219,7 @@ export default {
<gl-button
v-for="platform in platforms"
:key="platform.name"
+ :ref="platform.name"
:selected="selectedPlatform && selectedPlatform.name === platform.name"
@click="selectPlatform(platform)"
>
diff --git a/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue b/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue
new file mode 100644
index 00000000000..f50706b6de8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlButtonGroup, GlButton } from '@gitlab/ui';
+
+// TODO: We're planning to move this component to GitLab UI
+// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1787
+export default {
+ components: {
+ GlButtonGroup,
+ GlButton,
+ },
+ props: {
+ options: {
+ type: Array,
+ required: true,
+ },
+ value: {
+ type: [String, Number, Boolean],
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <gl-button-group>
+ <gl-button
+ v-for="opt in options"
+ :key="opt.value"
+ :disabled="!!opt.disabled"
+ :selected="value === opt.value"
+ @click="$emit('input', opt.value)"
+ >
+ <slot name="button-content" v-bind="opt">{{ opt.text }}</slot>
+ </gl-button>
+ </gl-button-group>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue
index 12daaea8758..dfa2ca2d20c 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue
@@ -175,7 +175,7 @@ export default {
:debounce="300"
/>
<div data-testid="content" class="dropdown-content">
- <gl-loading-icon v-if="projectsListLoading" size="md" class="gl-p-5" />
+ <gl-loading-icon v-if="projectsListLoading" size="lg" class="gl-p-5" />
<ul v-else>
<gl-dropdown-item
v-for="project in projects"
@@ -199,7 +199,7 @@ export default {
>
<gl-button
category="primary"
- variant="success"
+ variant="confirm"
:disabled="!Boolean(selectedProject)"
class="gl-text-center! issuable-move-button"
@click="handleMoveClick"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
index 3ec33a653b8..2cccb8325f4 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
@@ -104,7 +104,7 @@ export default {
<gl-button
:disabled="disableCreate"
category="primary"
- variant="success"
+ variant="confirm"
class="float-left d-flex align-items-center"
@click="handleCreateClick"
>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
index 623e7799493..134575b7a27 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
@@ -185,7 +185,7 @@ export default {
<gl-loading-icon
v-if="labelsFetchInProgress"
class="labels-fetch-loading gl-align-items-center w-100 h-100"
- size="md"
+ size="lg"
/>
<ul v-else class="list-unstyled gl-mb-0 gl-word-break-word">
<label-item
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
index 7989ad40b5a..e91a0489ef1 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
@@ -23,7 +23,7 @@ export default {
</script>
<template>
- <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900">
+ <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-font-weight-bold">
{{ __('Labels') }}
<template v-if="allowLabelEdit">
<gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline />
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
index 88977652556..090bf9493bf 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
@@ -179,7 +179,7 @@ export default {
<gl-button
:disabled="disableCreate"
category="primary"
- variant="success"
+ variant="confirm"
class="gl-display-flex gl-align-items-center"
data-testid="create-button"
@click="createLabel"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
index ae179ef93c7..f595e635f2c 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
@@ -147,7 +147,7 @@ export default {
<gl-loading-icon
v-if="labelsFetchInProgress"
class="labels-fetch-loading gl-align-items-center gl-w-full gl-h-full gl-mb-3"
- size="md"
+ size="lg"
/>
<template v-else>
<gl-dropdown-item
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
index 1b8e4bcfec6..c30ca5369ee 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
@@ -36,6 +36,9 @@ export default {
return content;
},
+ firstLineClass() {
+ return { 'gl-mt-3!': this.number === 1 };
+ },
},
methods: {
wrapBidiChar(bidiChar) {
@@ -56,10 +59,11 @@ export default {
</script>
<template>
<div class="gl-display-flex">
- <div class="line-numbers gl-pt-0! gl-pb-0! gl-absolute gl-z-index-3">
+ <div class="gl-p-0! gl-absolute gl-z-index-3 gl-border-r diff-line-num line-numbers">
<gl-link
:id="`L${number}`"
- class="file-line-num diff-line-num gl-user-select-none"
+ class="gl-user-select-none gl-ml-5 gl-pr-3 gl-shadow-none! file-line-num diff-line-num"
+ :class="firstLineClass"
:to="`#L${number}`"
:data-line-number="number"
>
@@ -68,7 +72,8 @@ export default {
</div>
<pre
- class="code highlight gl-p-0! gl-w-full gl-overflow-visible! gl-ml-11!"
+ class="gl-p-0! gl-w-full gl-overflow-visible! gl-ml-11! gl-border-none! code highlight"
+ :class="firstLineClass"
><code><span :id="`LC${number}`" v-safe-html="formattedContent" :lang="language" class="line" data-testid="content"></span></code></pre>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
index edf2229a9a1..ed87a202b15 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
@@ -2,6 +2,7 @@
import { GlSafeHtmlDirective, GlLoadingIcon } from '@gitlab/ui';
import LineHighlighter from '~/blob/line_highlighter';
import eventHub from '~/notes/event_hub';
+import languageLoader from '~/content_editor/services/highlight_js_language_loader';
import { ROUGE_TO_HLJS_LANGUAGE_MAP, LINES_PER_CHUNK } from './constants';
import Chunk from './components/chunk.vue';
@@ -129,7 +130,7 @@ export default {
let languageDefinition;
try {
- languageDefinition = await import(`highlight.js/lib/languages/${this.language}`);
+ languageDefinition = await languageLoader[this.language]();
this.hljs.registerLanguage(this.language, languageDefinition.default);
} catch (message) {
this.$emit('error', message);
diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
index e784bba6698..0d466df1b7f 100644
--- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
+++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
@@ -42,6 +42,6 @@ export default {
:class="cssClass"
:title="tooltipTitle(time)"
:datetime="time"
- ><slot :timeAgo="timeAgo">{{ timeAgo }}</slot></time
+ ><slot :time-ago="timeAgo">{{ timeAgo }}</slot></time
>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
index 62de76e46b5..f62bf686f85 100644
--- a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
+++ b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
@@ -160,7 +160,7 @@ export default {
>
<gl-icon name="upload" :size="iconStyles.size" :class="iconStyles.class" />
<p class="gl-mb-0" data-testid="upload-text">
- <slot name="upload-text" :openFileUpload="openFileUpload">
+ <slot name="upload-text" :open-file-upload="openFileUpload">
<gl-sprintf
:message="
singleFileSelection
diff --git a/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue b/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue
new file mode 100644
index 00000000000..bc5e0cf10dd
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue
@@ -0,0 +1,68 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { s__ } from '~/locale';
+
+export default {
+ name: 'UsageBanner',
+ components: {
+ GlSkeletonLoader,
+ },
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ i18n: {
+ dependencyProxy: s__('UsageQuota|Dependency proxy'),
+ storageUsed: s__('UsageQuota|Storage used'),
+ dependencyProxyMessage: s__(
+ 'UsageQuota|Local proxy used for frequently-accessed upstream Docker images. %{linkStart}More information%{linkEnd}',
+ ),
+ },
+ storageUsageQuotaHelpPage: helpPagePath('user/usage_quotas'),
+};
+</script>
+<template>
+ <div class="gl-display-flex gl-flex-direction-column">
+ <div class="gl-display-flex gl-align-items-center gl-py-3">
+ <div
+ class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1"
+ >
+ <div class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1">
+ <div
+ v-if="$slots['left-primary-text']"
+ class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0 gl-mb-4"
+ >
+ <slot name="left-primary-text"></slot>
+ </div>
+ <div
+ v-if="$slots['left-secondary-text']"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-grow-1 gl-w-70p gl-md-max-w-70p"
+ >
+ <slot name="left-secondary-text"></slot>
+ </div>
+ </div>
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500 gl-flex-shrink-0"
+ >
+ <div
+ v-if="$slots['right-primary-text']"
+ class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
+ >
+ <slot name="right-primary-text"></slot>
+ </div>
+ <div
+ v-if="$slots['right-secondary-text']"
+ class="gl-display-flex gl-align-items-center gl-min-h-6"
+ >
+ <slot v-if="!loading" name="right-secondary-text"></slot>
+ <gl-skeleton-loader v-else :width="60" :lines="1" />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index cac8f0a9aa5..ec7a7cd72ae 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -6,9 +6,13 @@ import {
GlIcon,
GlSafeHtmlDirective,
GlSprintf,
+ GlButton,
} from '@gitlab/ui';
+import { __ } from '~/locale';
import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
import { glEmojiTag } from '~/emoji';
+import createFlash from '~/flash';
+import { followUser, unfollowUser } from '~/rest_api';
import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
const MAX_SKELETON_LINES = 4;
@@ -24,6 +28,7 @@ export default {
UserAvatarImage,
UserNameWithStatus,
GlSprintf,
+ GlButton,
},
directives: {
SafeHtml: GlSafeHtmlDirective,
@@ -38,6 +43,16 @@ export default {
required: true,
default: null,
},
+ placement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+ },
+ data() {
+ return {
+ toggleFollowLoading: false,
+ };
},
computed: {
statusHtml() {
@@ -59,6 +74,59 @@ export default {
availabilityStatus() {
return this.user?.status?.availability || '';
},
+ isNotCurrentUser() {
+ return !this.userIsLoading && this.user.username !== gon.current_username;
+ },
+ shouldRenderToggleFollowButton() {
+ return this.isNotCurrentUser && typeof this.user?.isFollowed !== 'undefined';
+ },
+ toggleFollowButtonText() {
+ if (this.toggleFollowLoading) return null;
+
+ return this.user?.isFollowed ? __('Unfollow') : __('Follow');
+ },
+ toggleFollowButtonVariant() {
+ return this.user?.isFollowed ? 'default' : 'confirm';
+ },
+ },
+ methods: {
+ async toggleFollow() {
+ if (this.user.isFollowed) {
+ this.unfollow();
+ } else {
+ this.follow();
+ }
+ },
+ async follow() {
+ this.toggleFollowLoading = true;
+ try {
+ await followUser(this.user.id);
+ this.$emit('follow');
+ } catch (error) {
+ createFlash({
+ message: __('An error occurred while trying to follow this user, please try again.'),
+ error,
+ captureError: true,
+ });
+ } finally {
+ this.toggleFollowLoading = false;
+ }
+ },
+ async unfollow() {
+ this.toggleFollowLoading = true;
+ try {
+ await unfollowUser(this.user.id);
+ this.$emit('unfollow');
+ } catch (error) {
+ createFlash({
+ message: __('An error occurred while trying to unfollow this user, please try again.'),
+ error,
+ captureError: true,
+ });
+ } finally {
+ this.toggleFollowLoading = false;
+ }
+ },
},
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
};
@@ -66,12 +134,24 @@ export default {
<template>
<!-- 200ms delay so not every mouseover triggers Popover -->
- <gl-popover :target="target" :delay="200" boundary="viewport" placement="top">
+ <gl-popover :target="target" :delay="200" :placement="placement" boundary="viewport">
<div class="gl-p-3 gl-line-height-normal gl-display-flex" data-testid="user-popover">
- <div class="gl-p-2 flex-shrink-1">
- <user-avatar-image :img-src="user.avatarUrl" :size="64" css-classes="gl-mr-3!" />
+ <div
+ class="gl-p-2 flex-shrink-1 gl-display-flex gl-flex-direction-column align-items-center gl-w-70p"
+ >
+ <user-avatar-image :img-src="user.avatarUrl" :size="64" css-classes="gl-m-0!" />
+ <div v-if="shouldRenderToggleFollowButton" class="gl-mt-3">
+ <gl-button
+ :variant="toggleFollowButtonVariant"
+ :loading="toggleFollowLoading"
+ size="small"
+ data-testid="toggle-follow-button"
+ @click="toggleFollow"
+ >{{ toggleFollowButtonText }}</gl-button
+ >
+ </div>
</div>
- <div class="gl-p-2 gl-w-full gl-min-w-0">
+ <div class="gl-w-full gl-min-w-0 gl-word-break-word">
<template v-if="userIsLoading">
<gl-skeleton-loader
:lines="$options.maxSkeletonLines"
@@ -94,7 +174,7 @@ export default {
<div class="gl-text-gray-500">
<div v-if="user.bio" class="gl-display-flex gl-mb-2">
<gl-icon name="profile" class="gl-flex-shrink-0" />
- <span ref="bio" class="gl-ml-2 gl-overflow-hidden">{{ user.bio }}</span>
+ <span ref="bio" class="gl-ml-2">{{ user.bio }}</span>
</div>
<div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
<gl-icon name="work" class="gl-flex-shrink-0" />
diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
index 9df5254155e..91f20863089 100644
--- a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
+++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
@@ -298,7 +298,7 @@ export default {
<gl-loading-icon
v-if="isLoading"
data-testid="loading-participants"
- size="md"
+ size="lg"
class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
/>
<template v-else>
diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js
index 9cb66f6e65f..3ebeec4a50b 100644
--- a/app/assets/javascripts/vue_shared/constants.js
+++ b/app/assets/javascripts/vue_shared/constants.js
@@ -1,4 +1,5 @@
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
+import { IssuableType, WorkspaceType } from '~/issues/constants';
const INTERVALS = {
minute: 'minute',
@@ -66,3 +67,14 @@ export const getTimeWindow = (timeWindowName) =>
export const AVATAR_SHAPE_OPTION_CIRCLE = 'circle';
export const AVATAR_SHAPE_OPTION_RECT = 'rect';
+
+export const confidentialityInfoText = (workspaceType, issuableType) =>
+ sprintf(
+ __(
+ 'Only %{workspaceType} members with at least Reporter role can view or be notified about this %{issuableType}.',
+ ),
+ {
+ workspaceType: workspaceType === WorkspaceType.project ? __('project') : __('group'),
+ issuableType: issuableType === IssuableType.Issue ? __('issue') : __('epic'),
+ },
+ );
diff --git a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
index c216a05bdb0..0758cb507e9 100644
--- a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
@@ -1,5 +1,5 @@
<script>
-import { GlForm, GlFormInput } from '@gitlab/ui';
+import { GlForm, GlFormInput, GlFormGroup } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
@@ -9,6 +9,7 @@ export default {
components: {
GlForm,
GlFormInput,
+ GlFormGroup,
MarkdownField,
LabelsSelect,
},
@@ -37,6 +38,7 @@ export default {
selectedLabels: [],
};
},
+ computed: {},
methods: {
handleUpdateSelectedLabels(labels) {
if (labels.length) {
@@ -52,12 +54,15 @@ export default {
<div data-testid="issuable-title" class="form-group row">
<label for="issuable-title" class="col-form-label col-sm-2">{{ __('Title') }}</label>
<div class="col-sm-10">
- <gl-form-input
- id="issuable-title"
- v-model="issuableTitle"
- :autofocus="true"
- :placeholder="__('Title')"
- />
+ <gl-form-group :description="__('Maximum of 255 characters')">
+ <gl-form-input
+ id="issuable-title"
+ v-model="issuableTitle"
+ maxlength="255"
+ :autofocus="true"
+ :placeholder="__('Title')"
+ />
+ </gl-form-group>
</div>
</div>
<div data-testid="issuable-description" class="form-group row">
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
index 8008b85bbdb..6453290f6ea 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
@@ -55,7 +55,7 @@ export default {
return createdSecondsAgo < SECONDS_IN_DAY;
},
author() {
- return this.issuable.author;
+ return this.issuable.author || {};
},
webUrl() {
return this.issuable.gitlabWebUrl || this.issuable.webUrl;
@@ -215,7 +215,7 @@ export default {
<span class="gl-display-none gl-sm-display-inline">
<span aria-hidden="true">&middot;</span>
<span class="issuable-authored gl-mr-3">
- <gl-sprintf :message="__('created %{timeAgo} by %{author}')">
+ <gl-sprintf v-if="author.name" :message="__('created %{timeAgo} by %{author}')">
<template #timeAgo>
<span
v-gl-tooltip.bottom
@@ -241,6 +241,17 @@ export default {
</gl-link>
</template>
</gl-sprintf>
+ <gl-sprintf v-else :message="__('created %{timeAgo}')">
+ <template #timeAgo>
+ <span
+ v-gl-tooltip.bottom
+ :title="tooltipTitle(issuable.createdAt)"
+ data-testid="issuable-created-at"
+ >
+ {{ createdAt }}
+ </span>
+ </template>
+ </gl-sprintf>
</span>
<slot name="timeframe"></slot>
</span>
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
index 20f178dfb7d..8b293b2e9f6 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
@@ -168,6 +168,11 @@ export default {
required: false,
default: '',
},
+ syncFilterAndSort: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -282,6 +287,7 @@ export default {
:sort-options="sortOptions"
:initial-filter-value="initialFilterValue"
:initial-sort-by="initialSortBy"
+ :sync-filter-and-sort="syncFilterAndSort"
:show-checkbox="showBulkEditSidebar"
:checkbox-checked="allIssuablesChecked"
class="gl-flex-grow-1 gl-border-t-none row-content-block"
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 ee7e113af72..649dbd6576b 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
@@ -1,14 +1,23 @@
<script>
-import { GlIcon, GlButton, GlTooltipDirective, GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
+import {
+ GlIcon,
+ GlBadge,
+ GlButton,
+ GlTooltipDirective,
+ GlAvatarLink,
+ GlAvatarLabeled,
+} from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isExternal } from '~/lib/utils/url_utility';
import { n__, sprintf } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { IssuableStates } from '~/vue_shared/issuable/list/constants';
export default {
components: {
GlIcon,
+ GlBadge,
GlButton,
GlAvatarLink,
GlAvatarLabeled,
@@ -26,6 +35,11 @@ export default {
type: Object,
required: true,
},
+ issuableState: {
+ type: String,
+ required: false,
+ default: '',
+ },
statusBadgeClass: {
type: String,
required: false,
@@ -36,6 +50,11 @@ export default {
required: false,
default: '',
},
+ statusIconClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
blocked: {
type: Boolean,
required: false,
@@ -53,6 +72,9 @@ export default {
},
},
computed: {
+ badgeVariant() {
+ return this.issuableState === IssuableStates.Opened ? 'success' : 'info';
+ },
authorId() {
return getIdFromGraphQLId(`${this.author.id}`);
},
@@ -71,6 +93,9 @@ export default {
{ completedCount, count },
);
},
+ hasTasks() {
+ return this.taskCompletionStatus.count > 0;
+ },
},
mounted() {
this.toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button');
@@ -88,10 +113,15 @@ export default {
<template>
<div class="detail-page-header">
<div class="detail-page-header-body">
- <div data-testid="status" class="issuable-status-box status-box" :class="statusBadgeClass">
- <gl-icon v-if="statusIcon" :name="statusIcon" class="d-block d-sm-none" />
- <span class="d-none d-sm-block"><slot name="status-badge"></slot></span>
- </div>
+ <gl-badge
+ data-testid="status"
+ class="issuable-status-badge gl-mr-3"
+ :class="statusBadgeClass"
+ :variant="badgeVariant"
+ >
+ <gl-icon v-if="statusIcon" :name="statusIcon" :class="statusIconClass" />
+ <span class="gl-display-none gl-sm-display-block"><slot name="status-badge"></slot></span>
+ </gl-badge>
<div class="issuable-meta gl-display-flex gl-align-items-center d-md-inline-block">
<div v-if="blocked || confidential" class="gl-display-inline-block">
<div v-if="blocked" data-testid="blocked" class="issuable-warning-icon inline">
@@ -128,7 +158,7 @@ export default {
<strong class="author d-sm-none d-inline">@{{ author.username }}</strong>
</gl-avatar-link>
<span
- v-if="taskCompletionStatus"
+ v-if="taskCompletionStatus && hasTasks"
data-testid="task-status"
class="gl-display-none gl-md-display-block gl-lg-display-inline-block"
>{{ taskStatusString }}</span
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 8849af2a52e..c165ee91c59 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
@@ -27,6 +27,11 @@ export default {
required: false,
default: '',
},
+ statusIconClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
enableEdit: {
type: Boolean,
required: false,
@@ -102,8 +107,10 @@ export default {
<template>
<div class="issuable-show-container" data-qa-selector="issuable_show_container">
<issuable-header
+ :issuable-state="issuable.state"
:status-badge-class="statusBadgeClass"
:status-icon="statusIcon"
+ :status-icon-class="statusIconClass"
:blocked="issuable.blocked"
:confidential="issuable.confidential"
:created-at="issuable.createdAt"
@@ -122,6 +129,7 @@ export default {
:issuable="issuable"
:status-badge-class="statusBadgeClass"
:status-icon="statusIcon"
+ :status-icon-class="statusIconClass"
:enable-edit="enableEdit"
:enable-autocomplete="enableAutocomplete"
:enable-autosave="enableAutosave"
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 45941174a62..47f05a2cee2 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
@@ -86,7 +86,7 @@ export default {
>
<p
data-testid="status"
- class="issuable-status-box status-box gl-my-0"
+ class="issuable-status-box status-box gl-white-space-nowrap gl-my-0"
:class="statusBadgeClass"
>
<gl-icon :name="statusIcon" class="gl-display-block d-sm-none gl-h-6!" />
diff --git a/app/assets/javascripts/vue_shared/mixins/timeago.js b/app/assets/javascripts/vue_shared/mixins/timeago.js
index c5f41d81167..2a0256548a8 100644
--- a/app/assets/javascripts/vue_shared/mixins/timeago.js
+++ b/app/assets/javascripts/vue_shared/mixins/timeago.js
@@ -1,4 +1,4 @@
-import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
+import { formatDate, getTimeago, timeagoLanguageCode } from '~/lib/utils/datetime_utility';
/**
* Mixin with time ago methods used in some vue components
@@ -8,7 +8,7 @@ export default {
timeFormatted(time) {
const timeago = getTimeago();
- return timeago.format(time);
+ return timeago.format(time, timeagoLanguageCode);
},
tooltipTitle(time) {
diff --git a/app/assets/javascripts/vue_shared/security_configuration/components/section_layout.vue b/app/assets/javascripts/vue_shared/security_configuration/components/section_layout.vue
new file mode 100644
index 00000000000..6045d75ac11
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_configuration/components/section_layout.vue
@@ -0,0 +1,34 @@
+<script>
+import SectionLoader from './section_loader.vue';
+
+export default {
+ name: 'SectionLayout',
+ components: {
+ SectionLoader,
+ },
+ props: {
+ heading: {
+ type: String,
+ required: true,
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="row gl-m-0 gl-border-b gl-line-height-20 gl-py-6">
+ <div class="col-lg-4 gl-pl-0 gl-pr-9">
+ <h2 class="gl-font-size-h2 gl-mt-0">{{ heading }}</h2>
+ <slot name="description"></slot>
+ </div>
+ <div class="col-lg-8 gl-pr-0 gl-pl-0">
+ <section-loader v-if="isLoading" />
+ <slot v-else name="features"></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/security_configuration/components/section_loader.vue b/app/assets/javascripts/vue_shared/security_configuration/components/section_loader.vue
new file mode 100644
index 00000000000..b15e25b0943
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_configuration/components/section_loader.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlCard, GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ name: 'SectionLoader',
+ components: {
+ GlCard,
+ GlSkeletonLoader,
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-skeleton-loader :width="1248" :height="180">
+ <rect x="0" y="0" width="100" height="15" rx="4" />
+ <rect x="0" y="24" width="460" height="32" rx="4" />
+ <rect x="0" y="71" width="100" height="15" rx="4" />
+ <rect x="0" y="95" width="460" height="72" rx="4" />
+ </gl-skeleton-loader>
+ <gl-card v-for="i in 2" :key="i" class="gl-mb-5">
+ <template #header>
+ <gl-skeleton-loader :width="1248" :height="15">
+ <rect x="0" y="0" width="300" height="15" rx="4" />
+ </gl-skeleton-loader>
+ </template>
+ <gl-skeleton-loader :width="1248" :height="15">
+ <rect x="0" y="0" width="600" height="15" rx="4" />
+ </gl-skeleton-loader>
+ <gl-skeleton-loader :width="1248" :height="15">
+ <rect x="0" y="0" width="300" height="15" rx="4" />
+ </gl-skeleton-loader>
+ </gl-card>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
index 458bacce915..6a4f671abb9 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/utils.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
@@ -90,7 +90,7 @@ const createStatusMessage = ({ reportType, status, total }) => {
if (status) {
message = __('%{reportType} %{status}');
} else if (!total) {
- message = __('%{reportType} detected %{totalStart}no%{totalEnd} vulnerabilities.');
+ message = __('%{reportType} detected no %{totalStart}new%{totalEnd} vulnerabilities.');
} else {
message = __(
'%{reportType} detected %{totalStart}%{total}%{totalEnd} potential %{vulnMessage}',
diff --git a/app/assets/javascripts/whats_new/components/feature.vue b/app/assets/javascripts/whats_new/components/feature.vue
index a93bda326de..90f6230ef72 100644
--- a/app/assets/javascripts/whats_new/components/feature.vue
+++ b/app/assets/javascripts/whats_new/components/feature.vue
@@ -37,9 +37,11 @@ export default {
<template>
<div class="gl-py-6 gl-px-6 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
<gl-link
+ v-if="feature.image_url"
:href="feature.url"
target="_blank"
class="gl-display-block"
+ data-testid="whats-new-image-link"
data-track-action="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
@@ -67,9 +69,11 @@ export default {
v-for="packageName in feature.packages"
:key="packageName"
size="md"
- class="whats-new-item-badge gl-mr-2"
+ variant="tier"
+ icon="license"
+ class="gl-mr-2"
>
- <gl-icon name="license" />{{ packageName }}
+ {{ packageName }}
</gl-badge>
</div>
<div
diff --git a/app/assets/javascripts/work_items/components/item_state.vue b/app/assets/javascripts/work_items/components/item_state.vue
new file mode 100644
index 00000000000..0b6c1a75bb2
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/item_state.vue
@@ -0,0 +1,62 @@
+<script>
+import { GlFormGroup, GlFormSelect } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { STATE_OPEN, STATE_CLOSED } from '../constants';
+
+export default {
+ i18n: {
+ status: __('Status'),
+ },
+ states: [
+ {
+ value: STATE_OPEN,
+ text: __('Open'),
+ },
+ {
+ value: STATE_CLOSED,
+ text: __('Closed'),
+ },
+ ],
+ components: {
+ GlFormGroup,
+ GlFormSelect,
+ },
+ props: {
+ state: {
+ type: String,
+ required: true,
+ },
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ currentState() {
+ return this.$options.states[this.state];
+ },
+ },
+ methods: {
+ setState(newState) {
+ if (newState !== this.state) {
+ this.$emit('changed', newState);
+ }
+ },
+ },
+ labelId: 'work-item-state-select',
+};
+</script>
+
+<template>
+ <gl-form-group :label="$options.i18n.status" :label-for="$options.labelId">
+ <gl-form-select
+ :id="$options.labelId"
+ :value="state"
+ :options="$options.states"
+ :disabled="loading"
+ class="gl-w-auto"
+ @change="setState"
+ />
+ </gl-form-group>
+</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 40b6fcdd204..31e4a932c5a 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -1,7 +1,7 @@
<script>
import { GlDropdown, GlDropdownItem, GlModal, GlModalDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
-import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql';
+import Tracking from '~/tracking';
export default {
i18n: {
@@ -15,55 +15,36 @@ export default {
directives: {
GlModal: GlModalDirective,
},
+ mixins: [Tracking.mixin({ label: 'actions_menu' })],
props: {
workItemId: {
type: String,
required: false,
default: null,
},
- canUpdate: {
+ canDelete: {
type: Boolean,
required: false,
default: false,
},
},
- emits: ['workItemDeleted', 'error'],
+ emits: ['deleteWorkItem'],
methods: {
- deleteWorkItem() {
- this.$apollo
- .mutate({
- mutation: deleteWorkItemMutation,
- variables: {
- input: {
- id: this.workItemId,
- },
- },
- })
- .then(({ data: { workItemDelete, errors } }) => {
- if (errors?.length) {
- throw new Error(errors[0].message);
- }
-
- if (workItemDelete?.errors.length) {
- throw new Error(workItemDelete.errors[0]);
- }
-
- this.$emit('workItemDeleted');
- })
- .catch((e) => {
- this.$emit(
- 'error',
- e.message ||
- s__('WorkItem|Something went wrong when deleting the work item. Please try again.'),
- );
- });
+ handleDeleteWorkItem() {
+ this.track('click_delete_work_item');
+ this.$emit('deleteWorkItem');
+ },
+ handleCancelDeleteWorkItem({ trigger }) {
+ if (trigger !== 'ok') {
+ this.track('cancel_delete_work_item');
+ }
},
},
};
</script>
<template>
- <div v-if="canUpdate">
+ <div v-if="canDelete">
<gl-dropdown
icon="ellipsis_v"
text-sr-only
@@ -81,7 +62,8 @@ export default {
:title="$options.i18n.deleteWorkItem"
:ok-title="$options.i18n.deleteWorkItem"
ok-variant="danger"
- @ok="deleteWorkItem"
+ @ok="handleDeleteWorkItem"
+ @hide="handleCancelDeleteWorkItem"
>
{{
s__(
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 f2fb1e3ccbc..4222ffe42fe 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -1,15 +1,20 @@
<script>
-import { GlAlert } from '@gitlab/ui';
+import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
import { i18n } from '../constants';
import workItemQuery from '../graphql/work_item.query.graphql';
import workItemTitleSubscription from '../graphql/work_item_title.subscription.graphql';
+import WorkItemActions from './work_item_actions.vue';
+import WorkItemState from './work_item_state.vue';
import WorkItemTitle from './work_item_title.vue';
export default {
i18n,
components: {
GlAlert,
+ GlSkeletonLoader,
+ WorkItemActions,
WorkItemTitle,
+ WorkItemState,
},
props: {
workItemId: {
@@ -49,9 +54,18 @@ export default {
},
},
computed: {
+ workItemLoading() {
+ return this.$apollo.queries.workItem.loading;
+ },
workItemType() {
return this.workItem.workItemType?.name;
},
+ canUpdate() {
+ return this.workItem?.userPermissions?.updateWorkItem;
+ },
+ canDelete() {
+ return this.workItem?.userPermissions?.deleteWorkItem;
+ },
},
};
</script>
@@ -62,12 +76,35 @@ export default {
{{ error }}
</gl-alert>
- <work-item-title
- :loading="$apollo.queries.workItem.loading"
- :work-item-id="workItem.id"
- :work-item-title="workItem.title"
- :work-item-type="workItemType"
- @error="error = $event"
- />
+ <div v-if="workItemLoading" class="gl-max-w-26 gl-py-5">
+ <gl-skeleton-loader :height="65" :width="240">
+ <rect width="240" height="20" x="5" y="0" rx="4" />
+ <rect width="100" height="20" x="5" y="45" rx="4" />
+ </gl-skeleton-loader>
+ </div>
+ <template v-else>
+ <div class="gl-display-flex">
+ <work-item-title
+ :work-item-id="workItem.id"
+ :work-item-title="workItem.title"
+ :work-item-type="workItemType"
+ class="gl-mr-5"
+ @error="error = $event"
+ @updated="$emit('workItemUpdated')"
+ />
+ <work-item-actions
+ :work-item-id="workItem.id"
+ :can-delete="canDelete"
+ class="gl-ml-auto gl-mt-5"
+ @deleteWorkItem="$emit('deleteWorkItem')"
+ @error="error = $event"
+ />
+ </div>
+ <work-item-state
+ :work-item="workItem"
+ @error="error = $event"
+ @updated="$emit('workItemUpdated')"
+ />
+ </template>
</section>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
index a79091fb8b2..172a40a6e56 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
@@ -1,42 +1,87 @@
<script>
-import { GlAlert, GlButton, GlModal } from '@gitlab/ui';
-import WorkItemActions from './work_item_actions.vue';
+import { GlAlert, GlModal } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import deleteWorkItemFromTaskMutation from '../graphql/delete_task_from_work_item.mutation.graphql';
import WorkItemDetail from './work_item_detail.vue';
export default {
components: {
GlAlert,
- GlButton,
GlModal,
WorkItemDetail,
- WorkItemActions,
},
props: {
- canUpdate: {
- type: Boolean,
+ workItemId: {
+ type: String,
required: false,
- default: false,
+ default: null,
},
- visible: {
- type: Boolean,
- required: true,
+ issueGid: {
+ type: String,
+ required: false,
+ default: '',
},
- workItemId: {
+ lockVersion: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ lineNumberStart: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ lineNumberEnd: {
type: String,
required: false,
default: null,
},
},
- emits: ['workItemDeleted', 'close'],
+ emits: ['workItemDeleted', 'workItemUpdated', 'close'],
data() {
return {
error: undefined,
};
},
methods: {
- handleWorkItemDeleted() {
- this.$emit('workItemDeleted');
- this.closeModal();
+ deleteWorkItem() {
+ this.$apollo
+ .mutate({
+ mutation: deleteWorkItemFromTaskMutation,
+ variables: {
+ input: {
+ id: this.issueGid,
+ lockVersion: this.lockVersion,
+ taskData: {
+ id: this.workItemId,
+ lineNumberStart: Number(this.lineNumberStart),
+ lineNumberEnd: Number(this.lineNumberEnd),
+ },
+ },
+ },
+ })
+ .then(
+ ({
+ data: {
+ workItemDeleteTask: {
+ workItem: { descriptionHtml },
+ errors,
+ },
+ },
+ }) => {
+ if (errors?.length) {
+ throw new Error(errors[0].message);
+ }
+
+ this.$emit('workItemDeleted', descriptionHtml);
+ this.$refs.modal.hide();
+ },
+ )
+ .catch((e) => {
+ this.error =
+ e.message ||
+ s__('WorkItem|Something went wrong when deleting the work item. Please try again.');
+ });
},
closeModal() {
this.error = '';
@@ -45,37 +90,31 @@ export default {
setErrorMessage(message) {
this.error = message;
},
+ show() {
+ this.$refs.modal.show();
+ },
},
};
</script>
<template>
- <gl-modal hide-footer modal-id="work-item-detail-modal" :visible="visible" @hide="closeModal">
- <template #modal-header>
- <div class="gl-w-full gl-display-flex gl-align-items-center gl-justify-content-end">
- <h2 class="modal-title gl-mr-auto">{{ s__('WorkItem|Work Item') }}</h2>
- <work-item-actions
- :work-item-id="workItemId"
- :can-update="canUpdate"
- @workItemDeleted="handleWorkItemDeleted"
- @error="setErrorMessage"
- />
- <gl-button category="tertiary" icon="close" :aria-label="__('Close')" @click="closeModal" />
- </div>
- </template>
+ <gl-modal ref="modal" hide-footer size="lg" modal-id="work-item-detail-modal" @hide="closeModal">
<gl-alert v-if="error" variant="danger" @dismiss="error = false">
{{ error }}
</gl-alert>
- <work-item-detail :work-item-id="workItemId" />
+ <work-item-detail
+ :work-item-id="workItemId"
+ @deleteWorkItem="deleteWorkItem"
+ @workItemUpdated="$emit('workItemUpdated')"
+ />
</gl-modal>
</template>
<style>
-/* hide the existing close button until we can do it
- * with https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2710
+/* hide the existing modal header
*/
-#work-item-detail-modal .modal-header > .gl-button {
+#work-item-detail-modal .modal-header {
display: none;
}
</style>
diff --git a/app/assets/javascripts/work_items/components/work_item_state.vue b/app/assets/javascripts/work_items/components/work_item_state.vue
new file mode 100644
index 00000000000..51db4c804eb
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_state.vue
@@ -0,0 +1,98 @@
+<script>
+import * as Sentry from '@sentry/browser';
+import Tracking from '~/tracking';
+import {
+ i18n,
+ STATE_OPEN,
+ STATE_CLOSED,
+ STATE_EVENT_CLOSE,
+ STATE_EVENT_REOPEN,
+} from '../constants';
+import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
+import ItemState from './item_state.vue';
+
+export default {
+ components: {
+ ItemState,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ workItem: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ updateInProgress: false,
+ };
+ },
+ computed: {
+ workItemType() {
+ return this.workItem.workItemType?.name;
+ },
+ tracking() {
+ return {
+ category: 'workItems:show',
+ label: 'item_state',
+ property: `type_${this.workItemType}`,
+ };
+ },
+ },
+ methods: {
+ async updateWorkItemState(newState) {
+ const stateEventMap = {
+ [STATE_OPEN]: STATE_EVENT_REOPEN,
+ [STATE_CLOSED]: STATE_EVENT_CLOSE,
+ };
+
+ const stateEvent = stateEventMap[newState];
+
+ await this.updateWorkItem(stateEvent);
+ },
+ async updateWorkItem(updatedState) {
+ if (!updatedState) {
+ return;
+ }
+
+ this.updateInProgress = true;
+
+ try {
+ this.track('updated_state');
+
+ const {
+ data: { workItemUpdate },
+ } = await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItem.id,
+ stateEvent: updatedState,
+ },
+ },
+ });
+
+ if (workItemUpdate?.errors?.length) {
+ throw new Error(workItemUpdate.errors[0]);
+ }
+
+ this.$emit('updated');
+ } catch (error) {
+ this.$emit('error', i18n.updateError);
+ Sentry.captureException(error);
+ }
+
+ this.updateInProgress = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <item-state
+ v-if="workItem.state"
+ :state="workItem.state"
+ :loading="updateInProgress"
+ @changed="updateWorkItemState"
+ />
+</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 88a825853cc..d2e6d3c0bbf 100644
--- a/app/assets/javascripts/work_items/components/work_item_title.vue
+++ b/app/assets/javascripts/work_items/components/work_item_title.vue
@@ -1,5 +1,4 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
import Tracking from '~/tracking';
import { i18n } from '../constants';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
@@ -7,16 +6,10 @@ import ItemTitle from './item_title.vue';
export default {
components: {
- GlLoadingIcon,
ItemTitle,
},
mixins: [Tracking.mixin()],
props: {
- loading: {
- type: Boolean,
- required: false,
- default: false,
- },
workItemId: {
type: String,
required: false,
@@ -59,6 +52,7 @@ export default {
},
});
this.track('updated_title');
+ this.$emit('updated');
} catch {
this.$emit('error', i18n.updateError);
}
@@ -68,6 +62,5 @@ export default {
</script>
<template>
- <gl-loading-icon v-if="loading" class="gl-mt-3" size="md" />
- <item-title v-else :title="workItemTitle" @title-changed="updateTitle" />
+ <item-title :title="workItemTitle" @title-changed="updateTitle" />
</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index d3bcaf0f95f..e914500108f 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -1,6 +1,14 @@
import { s__ } from '~/locale';
+export const STATE_OPEN = 'OPEN';
+export const STATE_CLOSED = 'CLOSED';
+
+export const STATE_EVENT_REOPEN = 'REOPEN';
+export const STATE_EVENT_CLOSE = 'CLOSE';
+
export const i18n = {
fetchError: s__('WorkItem|Something went wrong when fetching the work item. Please try again.'),
updateError: s__('WorkItem|Something went wrong while updating the work item. Please try again.'),
};
+
+export const DEFAULT_MODAL_TYPE = 'Task';
diff --git a/app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql b/app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql
new file mode 100644
index 00000000000..32c07ed48c7
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql
@@ -0,0 +1,9 @@
+mutation workItemDeleteTask($input: WorkItemDeleteTaskInput!) {
+ workItemDeleteTask(input: $input) {
+ workItem {
+ id
+ descriptionHtml
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
index 2707d6bb790..e25fd102699 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -1,8 +1,14 @@
fragment WorkItem on WorkItem {
id
title
+ state
+ description
workItemType {
id
name
}
+ userPermissions {
+ deleteWorkItem
+ updateWorkItem
+ }
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item.query.graphql b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
index 1d3dae0649d..3b46fed97ec 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
@@ -1,6 +1,6 @@
#import "./work_item.fragment.graphql"
-query workItem($id: ID!) {
+query workItem($id: WorkItemID!) {
workItem(id: $id) {
...WorkItem
}
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index 10fae9b9cc0..e39b0d6a353 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -5,7 +5,7 @@ import { createApolloProvider } from './graphql/provider';
export const initWorkItemsRoot = () => {
const el = document.querySelector('#js-work-items');
- const { fullPath } = el.dataset;
+ const { fullPath, issuesListPath } = el.dataset;
return new Vue({
el,
@@ -13,6 +13,7 @@ export const initWorkItemsRoot = () => {
apolloProvider: createApolloProvider(),
provide: {
fullPath,
+ issuesListPath,
},
render(createElement) {
return createElement(App);
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 a95da80ac95..04c6a61689c 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -6,6 +6,7 @@ import workItemQuery from '../graphql/work_item.query.graphql';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import createWorkItemFromTaskMutation from '../graphql/create_work_item_from_task.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
+import { DEFAULT_MODAL_TYPE } from '../constants';
import ItemTitle from '../components/item_title.vue';
@@ -77,6 +78,13 @@ export default {
text: node.name,
}));
},
+ result() {
+ if (!this.selectedWorkItemType && this.isModal) {
+ this.selectedWorkItemType = this.formOptions.find(
+ (options) => options.text === DEFAULT_MODAL_TYPE,
+ )?.value;
+ }
+ },
error() {
this.error = this.$options.fetchTypesErrorText;
},
@@ -115,20 +123,15 @@ export default {
},
},
update(store, { data: { workItemCreate } }) {
- const { id, title, workItemType } = workItemCreate.workItem;
+ const { workItem } = workItemCreate;
store.writeQuery({
query: workItemQuery,
variables: {
- id,
+ id: workItem.id,
},
data: {
- workItem: {
- __typename: 'WorkItem',
- id,
- title,
- workItemType,
- },
+ workItem,
},
});
},
@@ -185,11 +188,11 @@ export default {
<form @submit.prevent="createWorkItem">
<gl-alert v-if="error" variant="danger" @dismiss="error = null">{{ error }}</gl-alert>
<div :class="{ 'gl-px-5': isModal }" data-testid="content">
- <item-title :title="title" data-testid="title-input" @title-input="handleTitleInput" />
+ <item-title :title="initialTitle" data-testid="title-input" @title-input="handleTitleInput" />
<div>
<gl-loading-icon
v-if="$apollo.queries.workItemTypes.loading"
- size="md"
+ size="lg"
data-testid="loading-types"
/>
<gl-form-select
diff --git a/app/assets/javascripts/work_items/pages/work_item_root.vue b/app/assets/javascripts/work_items/pages/work_item_root.vue
index b8f2bcff25d..6dc3dc3b3c9 100644
--- a/app/assets/javascripts/work_items/pages/work_item_root.vue
+++ b/app/assets/javascripts/work_items/pages/work_item_root.vue
@@ -1,26 +1,70 @@
<script>
+import { GlAlert } from '@gitlab/ui';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
import WorkItemDetail from '../components/work_item_detail.vue';
+import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql';
export default {
components: {
+ GlAlert,
WorkItemDetail,
},
+ inject: ['issuesListPath'],
props: {
id: {
type: String,
required: true,
},
},
+ data() {
+ return {
+ error: '',
+ };
+ },
computed: {
gid() {
return convertToGraphQLId(TYPE_WORK_ITEM, this.id);
},
},
+ methods: {
+ deleteWorkItem() {
+ this.$apollo
+ .mutate({
+ mutation: deleteWorkItemMutation,
+ variables: {
+ input: {
+ id: this.gid,
+ },
+ },
+ })
+ .then(({ data: { workItemDelete, errors } }) => {
+ if (errors?.length) {
+ throw new Error(errors[0].message);
+ }
+
+ if (workItemDelete?.errors.length) {
+ throw new Error(workItemDelete.errors[0]);
+ }
+
+ this.$toast.show(s__('WorkItem|Work item deleted'));
+ visitUrl(this.issuesListPath);
+ })
+ .catch((e) => {
+ this.error =
+ e.message ||
+ s__('WorkItem|Something went wrong when deleting the work item. Please try again.');
+ });
+ },
+ },
};
</script>
<template>
- <work-item-detail :work-item-id="gid" />
+ <div>
+ <gl-alert v-if="error" variant="danger" @dismiss="error = ''">{{ error }}</gl-alert>
+ <work-item-detail :work-item-id="gid" @deleteWorkItem="deleteWorkItem" />
+ </div>
</template>
diff --git a/app/assets/javascripts/work_items/router/index.js b/app/assets/javascripts/work_items/router/index.js
index 142fab8cfa6..2b39a298720 100644
--- a/app/assets/javascripts/work_items/router/index.js
+++ b/app/assets/javascripts/work_items/router/index.js
@@ -1,8 +1,10 @@
+import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueRouter from 'vue-router';
import { joinPaths } from '~/lib/utils/url_utility';
import { routes } from './routes';
+Vue.use(GlToast);
Vue.use(VueRouter);
export function createRouter(fullPath) {
diff --git a/app/assets/javascripts/work_items_hierarchy/components/app.vue b/app/assets/javascripts/work_items_hierarchy/components/app.vue
index 621cfe5bace..779bd27516a 100644
--- a/app/assets/javascripts/work_items_hierarchy/components/app.vue
+++ b/app/assets/javascripts/work_items_hierarchy/components/app.vue
@@ -1,6 +1,6 @@
<script>
import { GlBanner } from '@gitlab/ui';
-import Cookies from 'js-cookie';
+import Cookies from '~/lib/utils/cookies';
import { parseBoolean } from '~/lib/utils/common_utils';
import RESPONSE from '../static_response';
import { WORK_ITEMS_SURVEY_COOKIE_NAME, workItemTypes } from '../constants';
diff --git a/app/assets/stylesheets/bootstrap_migration_components.scss b/app/assets/stylesheets/bootstrap_migration_components.scss
index b6cecbe5806..676e69707c7 100644
--- a/app/assets/stylesheets/bootstrap_migration_components.scss
+++ b/app/assets/stylesheets/bootstrap_migration_components.scss
@@ -26,7 +26,7 @@ input[type='file'] {
line-height: 1;
}
-.form-group.row .col-form-label {
+.form-group.row > .col-form-label {
// Bootstrap 4 aligns labels to the left
// for horizontal forms
@include media-breakpoint-up(md) {
@@ -118,7 +118,14 @@ input[type='file'] {
margin-bottom: 16px;
.well-segment {
- padding: 16px;
+ padding: 1rem;
+
+ &.pipeline-info {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+ }
&:not(:last-of-type) {
border-bottom: 1px solid $well-inner-border;
diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss
index 7f498b79d33..870ed50c6eb 100644
--- a/app/assets/stylesheets/components/content_editor.scss
+++ b/app/assets/stylesheets/components/content_editor.scss
@@ -13,6 +13,11 @@
}
}
+ img.ProseMirror-selectednode {
+ outline: 3px solid rgba($blue-400, 0.48);
+ outline-offset: -3px;
+ }
+
ul[data-type='taskList'] {
list-style: none;
padding: 0;
@@ -121,3 +126,7 @@
border: 1px solid $black-transparent;
background-color: var(--gl-color-chip-color);
}
+
+.bubble-menu-form {
+ width: 320px;
+}
diff --git a/app/assets/stylesheets/components/dashboard_skeleton.scss b/app/assets/stylesheets/components/dashboard_skeleton.scss
index 09ba89c0782..1dcaa47470b 100644
--- a/app/assets/stylesheets/components/dashboard_skeleton.scss
+++ b/app/assets/stylesheets/components/dashboard_skeleton.scss
@@ -48,8 +48,7 @@
}
}
- &-header,
- &-footer {
+ &-header {
&-failed {
background-color: $red-100;
}
diff --git a/app/assets/stylesheets/components/design_management/design.scss b/app/assets/stylesheets/components/design_management/design.scss
index a9be1d89495..b8bd1000bfd 100644
--- a/app/assets/stylesheets/components/design_management/design.scss
+++ b/app/assets/stylesheets/components/design_management/design.scss
@@ -7,7 +7,7 @@ $t-gray-a-16-design-pin: rgba($black, 0.16);
}
.design-detail {
- background-color: rgba($black, 0.9);
+ background-color: rgba($modal-backdrop-bg, $modal-backdrop-opacity);
.with-performance-bar & {
top: 35px;
diff --git a/app/assets/stylesheets/components/whats_new.scss b/app/assets/stylesheets/components/whats_new.scss
index 98a7ea5792b..c1c68f64d86 100644
--- a/app/assets/stylesheets/components/whats_new.scss
+++ b/app/assets/stylesheets/components/whats_new.scss
@@ -47,12 +47,6 @@
margin-top: calc(#{$performance-bar-height} + #{$system-header-height} + #{$header-height});
}
-.gl-badge.whats-new-item-badge {
- background-color: $purple-light;
- color: $purple;
- @include gl-font-weight-bold;
-}
-
.whats-new-item-title-link {
&:hover,
&:focus,
diff --git a/app/assets/stylesheets/errors.scss b/app/assets/stylesheets/errors.scss
index dc08c816d7d..00cc3409fa7 100644
--- a/app/assets/stylesheets/errors.scss
+++ b/app/assets/stylesheets/errors.scss
@@ -118,3 +118,11 @@ a {
}
}
}
+
+.tanuki-logo {
+ width: 210px;
+ height: 210px;
+ max-width: 40vw;
+ display: block;
+ margin: map-get($spacing-scale, 4) auto;
+}
diff --git a/app/assets/stylesheets/framework/broadcast_messages.scss b/app/assets/stylesheets/framework/broadcast_messages.scss
index 58f986ec0ae..a0bfca79dc3 100644
--- a/app/assets/stylesheets/framework/broadcast_messages.scss
+++ b/app/assets/stylesheets/framework/broadcast_messages.scss
@@ -14,11 +14,14 @@
}
.broadcast-banner-message {
- text-align: center;
-
p {
margin-bottom: 0;
}
+
+ a {
+ color: inherit;
+ text-decoration: underline;
+ }
}
.broadcast-notification-message {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 9cebd4f49a4..33522c66024 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -121,6 +121,10 @@
@include btn-color($white, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-normal, $gl-text-color);
}
+@mixin btn-purple {
+ @include btn-color($purple-700, $purple-800, $purple-800, $purple-900, $purple-900, $purple-950, $white);
+}
+
@mixin btn-with-margin {
margin-left: $btn-side-margin;
float: left;
@@ -194,6 +198,10 @@
@include btn-red;
}
+ &.btn-purple {
+ @include btn-purple;
+ }
+
&.btn-grouped {
@include btn-with-margin;
}
@@ -261,7 +269,7 @@
.btn-block {
width: 100%;
margin: 0;
- margin-bottom: 15px;
+ @include gl-mb-5;
&.btn {
padding: 6px 0;
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index f0495fdc94e..7a77256398e 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -34,7 +34,7 @@
@media (min-width: map-get($grid-breakpoints, md)) {
// The `+11` is to ensure the file header border shows when scrolled -
// the bottom of the compare-versions header and the top of the file header
- $mr-file-header-top: calc(#{$mr-version-controls-height} + #{$header-height} + #{$mr-tabs-height} + 11px);
+ $mr-file-header-top: calc(#{$header-height} + #{$mr-tabs-height});
position: -webkit-sticky;
position: sticky;
@@ -582,25 +582,6 @@ table.code {
}
}
-.diff-expansion-cell {
- flex: 1 1;
- min-width: max-content;
-}
-
-.diff-expansion-cell-middle {
- flex: 0 1 max-content;
-}
-
-@media only screen and (min-width: $breakpoint-xl) {
- .diff-expansion-cell-start {
- text-align: right;
- }
-
- .diff-expansion-cell-end {
- text-align: left;
- }
-}
-
// Merge request diff grid layout
.diff-grid {
.diff-td {
@@ -612,6 +593,10 @@ table.code {
.diff-grid-row {
display: grid;
grid-template-columns: 1fr 1fr;
+
+ &.diff-grid-row-full {
+ grid-template-columns: 1fr;
+ }
}
.diff-grid-left,
@@ -626,6 +611,14 @@ table.code {
grid-template-columns: 50px 1fr !important;
}
+ .diff-grid-2-col {
+ grid-template-columns: 100px 1fr !important;
+
+ &.parallel {
+ grid-template-columns: 50px 1fr !important;
+ }
+ }
+
&.inline-diff-view .diff-grid-3-col {
grid-template-columns: 50px 50px 1fr !important;
}
@@ -1209,3 +1202,10 @@ table.code {
position: absolute;
bottom: 100vh;
}
+
+.diff-line-expand-button {
+ &:hover,
+ &:focus {
+ @include gl-bg-gray-200;
+ }
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index e378fcb6129..f322c6c8929 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -376,14 +376,12 @@ span.idiff {
a {
color: $gl-text-color;
}
+}
- @include media-breakpoint-down(md) {
- .file-actions {
- margin-top: $gl-padding-8;
-
- .btn {
- margin-bottom: $gl-padding-8;
- }
+.blob-content-holder .file-actions {
+ @include media-breakpoint-down(sm) {
+ .btn {
+ margin-bottom: $gl-padding-8;
}
}
}
@@ -452,7 +450,6 @@ span.idiff {
.note-container {
.user-avatar-link.new-comment {
position: absolute;
- margin: 40px $gl-padding 0 116px;
}
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 5dd71cec8d1..37b61d36911 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -49,7 +49,7 @@
margin: 0 0 10px;
}
- .dropdown-menu-toggle,
+ .dropdown-menu-toggle.dropdown-menu-toggle,
.update-issues-btn .btn {
width: 100%;
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 4d0d64ae723..50783433c3d 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -83,7 +83,6 @@ label {
margin-right: 0;
.form-control {
- height: 29px;
background: $white;
font-family: $monospace-font;
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index f44123fc2ed..f76a0cbbae8 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -10,17 +10,6 @@
right: 0;
border-radius: 0;
- .logo-text {
- line-height: initial;
-
- svg {
- width: 55px;
- height: 14px;
- margin: 0;
- fill: $white;
- }
- }
-
.close-icon {
display: none;
}
@@ -33,14 +22,6 @@
min-height: $header-height;
padding-left: 0;
- .title-container {
- display: flex;
- align-items: stretch;
- flex: 1 1 auto;
- padding-top: 0;
- overflow: visible;
- }
-
.title {
padding-right: 0;
color: currentColor;
@@ -71,10 +52,14 @@
display: flex;
align-items: center;
padding: 2px 8px;
- margin: 5px 2px 5px -8px;
+ margin: 4px 2px 4px -12px;
border-radius: $border-radius-default;
}
+ .canary-badge {
+ margin-left: -8px;
+ }
+
.project-item-select {
right: auto;
left: 0;
@@ -127,7 +112,7 @@
border-radius: 0;
min-width: 45px;
padding: 0;
- margin: $gl-padding-8 -7px $gl-padding-8 0;
+ margin: $gl-padding-8 $gl-padding-8 $gl-padding-8 0;
font-size: 14px;
text-align: center;
color: currentColor;
@@ -353,6 +338,14 @@
}
}
+ &:last-child {
+ > a {
+ font-weight: 600;
+ line-height: 16px;
+ color: $gl-text-color;
+ }
+ }
+
> a {
font-size: 12px;
color: currentColor;
@@ -390,17 +383,6 @@
margin-left: auto;
}
-.breadcrumbs-sub-title {
- margin: 0;
- font-size: 12px;
- font-weight: 600;
- line-height: 16px;
-
- a {
- color: $gl-text-color;
- }
-}
-
.btn-sign-in {
background-color: $indigo-100;
color: $indigo-900;
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 0aeb7208c59..ca0240b6a65 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -3,6 +3,17 @@
svg {
fill: $green-500;
}
+
+ &.interactive {
+ &:hover {
+ background: $green-500;
+
+ svg {
+ --svg-status-bg: #{$green-100};
+ box-shadow: 0 0 0 1px $green-500;
+ }
+ }
+ }
}
.ci-status-icon-error,
@@ -10,6 +21,17 @@
svg {
fill: $red-500;
}
+
+ &.interactive {
+ &:hover {
+ background: $red-500;
+
+ svg {
+ --svg-status-bg: #{$red-100};
+ box-shadow: 0 0 0 1px $red-500;
+ }
+ }
+ }
}
.ci-status-icon-pending,
@@ -19,11 +41,33 @@
svg {
fill: $orange-500;
}
+
+ &.interactive {
+ &:hover {
+ background: $orange-500;
+
+ svg {
+ --svg-status-bg: #{$orange-100};
+ box-shadow: 0 0 0 1px $orange-500;
+ }
+ }
+ }
}
.ci-status-icon-running {
svg {
- fill: $blue-400;
+ fill: $blue-500;
+ }
+
+ &.interactive {
+ &:hover {
+ background: $blue-500;
+
+ svg {
+ --svg-status-bg: #{$blue-100};
+ box-shadow: 0 0 0 1px $blue-500;
+ }
+ }
}
}
@@ -32,7 +76,18 @@
.ci-status-icon-scheduled,
.ci-status-icon-manual {
svg {
- fill: $gl-text-color;
+ fill: $gray-900;
+ }
+
+ &.interactive {
+ &:hover {
+ background: $gray-900;
+
+ svg {
+ --svg-status-bg: #{$gray-100};
+ box-shadow: 0 0 0 1px $gray-900;
+ }
+ }
}
}
@@ -42,7 +97,18 @@
.ci-status-icon-skipped,
.ci-status-icon-notfound {
svg {
- fill: var(--gray-400, $gray-400);
+ fill: $gray-500;
+ }
+
+ &.interactive {
+ &:hover {
+ background: $gray-500;
+
+ svg {
+ --svg-status-bg: #{$gray-100};
+ box-shadow: 0 0 0 1px $gray-500;
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/framework/logo.scss b/app/assets/stylesheets/framework/logo.scss
index c5feefb8c54..1845438eedb 100644
--- a/app/assets/stylesheets/framework/logo.scss
+++ b/app/assets/stylesheets/framework/logo.scss
@@ -9,19 +9,16 @@
}
.tanuki-logo {
- .tanuki-left-ear,
- .tanuki-right-ear,
- .tanuki-nose {
+ .tanuki {
@include tanuki-logo-colors($tanuki-red);
}
- .tanuki-left-eye,
- .tanuki-right-eye {
+ .left-cheek,
+ .right-cheek {
@include tanuki-logo-colors($tanuki-orange);
}
- .tanuki-left-cheek,
- .tanuki-right-cheek {
+ .chin {
@include tanuki-logo-colors($tanuki-yellow);
}
@@ -31,98 +28,54 @@
@include webkit-prefix(animation-iteration-count, infinite);
}
- .tanuki-left-cheek {
- @include include-keyframes(animate-tanuki-left-cheek) {
+ .tanuki {
+ @include include-keyframes(animate-tanuki-base) {
0%,
- 10%,
- 100% {
- fill: lighten($tanuki-yellow, 25%);
- }
-
- 90% {
- fill: $tanuki-yellow;
- }
- }
- }
-
- .tanuki-left-eye {
- @include include-keyframes(animate-tanuki-left-eye) {
- 10%,
- 80% {
- fill: $tanuki-orange;
- }
-
- 20%,
- 90% {
- fill: lighten($tanuki-orange, 25%);
- }
- }
- }
-
- .tanuki-left-ear {
- @include include-keyframes(animate-tanuki-left-ear) {
- 10%,
- 80% {
+ 50% {
fill: $tanuki-red;
}
- 20%,
- 90% {
+ 25% {
fill: lighten($tanuki-red, 25%);
}
}
}
- .tanuki-nose {
- @include include-keyframes(animate-tanuki-nose) {
- 20%,
- 70% {
- fill: $tanuki-red;
- }
-
- 30%,
- 80% {
- fill: lighten($tanuki-red, 25%);
- }
- }
- }
-
- .tanuki-right-eye {
- @include include-keyframes(animate-tanuki-right-eye) {
- 30%,
- 60% {
+ .right-cheek {
+ @include include-keyframes(animate-tanuki-right-cheek) {
+ 25%,
+ 75% {
fill: $tanuki-orange;
}
- 40%,
- 70% {
+ 50% {
fill: lighten($tanuki-orange, 25%);
}
}
}
- .tanuki-right-ear {
- @include include-keyframes(animate-tanuki-right-ear) {
- 30%,
- 60% {
- fill: $tanuki-red;
+ .chin {
+ @include include-keyframes(animate-tanuki-chin) {
+ 50%,
+ 100% {
+ fill: $tanuki-yellow;
}
- 40%,
- 70% {
- fill: lighten($tanuki-red, 25%);
+ 75% {
+ fill: lighten($tanuki-yellow, 25%);
}
}
}
- .tanuki-right-cheek {
- @include include-keyframes(animate-tanuki-right-cheek) {
- 40% {
- fill: $tanuki-yellow;
+ .left-cheek {
+ @include include-keyframes(animate-tanuki-left-cheek) {
+ 25%,
+ 75% {
+ fill: $tanuki-orange;
}
- 60% {
- fill: lighten($tanuki-yellow, 25%);
+ 100% {
+ fill: lighten($tanuki-orange, 25%);
}
}
}
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index 8cad55f414a..549b61aedae 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -174,6 +174,10 @@
width: 100%;
}
+ .btn.dropdown-toggle-split {
+ margin-left: 1px;
+ }
+
/* This resets the width of the control so that the search button doesn't wrap */
.gl-search-box-by-click .form-control {
width: 1%;
@@ -368,7 +372,7 @@
.project-item-select-holder.btn-group {
.new-project-item-select-button {
- max-width: 44px;
+ max-width: 32px;
}
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index d270f802c56..dd9581c4692 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -19,13 +19,23 @@
.right-sidebar-collapsed {
padding-right: 0;
- @include media-breakpoint-up(sm) {
- &:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
- padding-right: $gutter-collapsed-width;
+ &:not(.is-merge-request) {
+ @include media-breakpoint-up(sm) {
+ &:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
+ padding-right: $gutter-collapsed-width;
+ }
+
+ .merge-request-tabs-holder.affix {
+ right: $gutter-collapsed-width;
+ }
}
+ }
- .merge-request-tabs-holder.affix {
- right: $gutter-collapsed-width;
+ &.is-merge-request {
+ @include media-breakpoint-up(md) {
+ .content-wrapper {
+ padding-right: $gutter-collapsed-width;
+ }
}
}
@@ -49,6 +59,18 @@
padding-right: 0;
z-index: $zindex-dropdown-menu;
+ &.right-sidebar-merge-requests {
+ width: 270px;
+
+ @include media-breakpoint-up(md) {
+ z-index: auto;
+ }
+
+ .shortcut-sidebar-dropdown-toggle {
+ margin-right: 0 !important;
+ }
+ }
+
@include media-breakpoint-only(sm) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter-collapsed-width;
@@ -71,12 +93,20 @@
}
.right-sidebar {
- border-left: 1px solid $border-color;
+ border-left: 1px solid $gray-100;
- .sidebar-container,
- .issuable-sidebar {
- // Add 100px so that potentially visible vertical scroll bar is hidden
- width: calc(100% + 100px);
+ &.right-sidebar-merge-requests {
+ @include media-breakpoint-up(md) {
+ border-left: 0;
+ }
+ }
+
+ &:not(.right-sidebar-merge-requests) {
+ .sidebar-container,
+ .issuable-sidebar {
+ // Add 100px so that potentially visible vertical scroll bar is hidden
+ width: calc(100% + 100px);
+ }
}
}
@@ -135,7 +165,11 @@
// rest of the sidebar, and could be removed once the sidebar has been fully converted to use
// gitlab-ui components.
.title .gl-button {
- color: $gl-text-color;
+ font-weight: $gl-font-weight-bold;
+
+ .gl-button {
+ color: $gl-text-color;
+ }
}
}
@@ -227,6 +261,10 @@
margin-right: -$gl-spacing-scale-2;
}
+.issuable-sidebar.is-merge-request .edit-link {
+ margin-right: 0;
+}
+
.assignee-grid {
grid-template-areas: ' attention user';
grid-template-columns: min-content 1fr;
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 6348703e9e1..f39d53c5b1c 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -10,9 +10,6 @@ table {
* color palette in https://gitlab.com/gitlab-org/gitlab/-/issues/213570
*
* The overwrites here affected the following areas:
- * - The security dashboard tables. When removing
- * this code, table-th-transparent and original-text-color classes should
- * be removed there.
* - The subscription seats table. When removing this code, the .seats-table
* <th> and margin overrides should be removed there.
*
@@ -23,8 +20,16 @@ table {
@include gl-text-gray-500;
}
- .md &:not(.code),
&.table {
+ .thead-white {
+ th {
+ background-color: $white;
+ }
+ }
+ }
+
+ .md &:not(.code),
+ &.table:not(.gl-table) {
margin-bottom: $gl-padding;
.dropdown-menu a {
@@ -58,23 +63,12 @@ table {
&.wide {
width: 55%;
}
-
- &.table-th-transparent {
- background: none;
- color: $gl-text-color-secondary;
- }
-
- &.original-gl-th {
- @include gl-text-gray-500;
- border-bottom: 1px solid $cycle-analytics-light-gray;
- }
}
}
.thead-white {
th {
- background-color: $white;
color: $gl-text-color-secondary;
border-top: 0;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 8e3b34e4eaf..bc649b6407d 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -453,7 +453,7 @@ $border-radius-small: 2px;
$border-radius-large: 8px;
$default-icon-size: 16px;
$layout-link-gray: #7e7c7c;
-$btn-side-margin: 10px;
+$btn-side-margin: $grid-size;
$btn-sm-side-margin: 7px;
$btn-margin-5: 5px;
$count-arrow-border: #dce0e5;
@@ -658,7 +658,6 @@ $calendar-user-contrib-text: #959494;
$cycle-analytics-box-padding: 30px;
$cycle-analytics-box-text-color: #8c8c8c;
$cycle-analytics-big-font: 19px;
-$cycle-analytics-light-gray: #bfbfbf;
$cycle-analytics-dismiss-icon-color: #b2b2b2;
/*
@@ -934,7 +933,6 @@ $issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15);
Merge requests
*/
$mr-tabs-height: 48px;
-$mr-version-controls-height: 56px;
/*
Compare Branches
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index b796f04750b..cfd215b81b8 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -39,7 +39,7 @@
.icon-container {
display: inline-block;
- margin-right: 8px;
+ margin: 0 0.5rem 0 0.25rem;
svg {
position: relative;
diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss
index bd327082e20..433141ae690 100644
--- a/app/assets/stylesheets/highlight/common.scss
+++ b/app/assets/stylesheets/highlight/common.scss
@@ -19,7 +19,17 @@
background: $dark-diff-match-color;
}
-@mixin diff-expansion($background, $border, $link) {
+@mixin diff-expansion($background, $color, $hover-background, $hover-color) {
+ background-color: $background;
+ color: $color;
+
+ &:hover {
+ background-color: $hover-background;
+ color: $hover-color;
+ }
+}
+
+@mixin old-diff-expansion($background, $border, $link) {
background-color: $background;
.diff-td,
@@ -49,6 +59,13 @@
}
}
+
+@mixin dark-diff-expansion-line {
+ &.expansion .diff-td {
+ background-color: $dark-diff-match-color;
+ }
+}
+
@mixin line-coverage-border-color($coverage, $no-coverage) {
transition: border-left 0.1s ease-out;
@@ -121,7 +138,8 @@
@include gl-mr-2;
@include gl-w-4;
@include gl-h-4;
- @include gl-float-left;
+ @include gl-absolute;
+ @include gl-left-3;
background-color: $color;
mask-image: asset_url('icons-stacked.svg#link');
mask-repeat: no-repeat;
diff --git a/app/assets/stylesheets/highlight/themes/dark.scss b/app/assets/stylesheets/highlight/themes/dark.scss
index c51b1f04757..0eeebdb2e7a 100644
--- a/app/assets/stylesheets/highlight/themes/dark.scss
+++ b/app/assets/stylesheets/highlight/themes/dark.scss
@@ -154,8 +154,12 @@ $dark-il: #de935f;
color: $dark-line-color;
}
- .line_expansion {
- @include diff-expansion($dark-main-bg, $dark-border, $dark-na);
+ .old-line_expansion {
+ @include old-diff-expansion($dark-main-bg, $dark-border, $dark-na);
+ }
+
+ .diff-line-expand-button {
+ @include diff-expansion($gray-600, $gray-200, $gray-300, $white);
}
// Diff line
@@ -166,6 +170,10 @@ $dark-il: #de935f;
@include dark-diff-match-line;
}
+ &.diff-grid-row {
+ @include dark-diff-expansion-line;
+ }
+
.diff-td.diff-line-num.hll:not(.empty-cell),
.diff-td.line-coverage.hll:not(.empty-cell),
.diff-td.line-codequality.hll:not(.empty-cell),
diff --git a/app/assets/stylesheets/highlight/themes/monokai.scss b/app/assets/stylesheets/highlight/themes/monokai.scss
index 226bb44f0e7..b8cd97d6504 100644
--- a/app/assets/stylesheets/highlight/themes/monokai.scss
+++ b/app/assets/stylesheets/highlight/themes/monokai.scss
@@ -1,6 +1,7 @@
/* https://github.com/richleland/pygments-css/blob/master/monokai.css */
@import '../common';
+@import 'highlight.js/styles/base16/monokai.css';
/*
* Monokai Colors
@@ -124,8 +125,12 @@ $monokai-gh: #75715e;
color: $monokai-text-color;
}
- .line_expansion {
- @include diff-expansion($monokai-bg, $monokai-border, $monokai-k);
+ .old-line_expansion {
+ @include old-diff-expansion($monokai-bg, $monokai-border, $monokai-k);
+ }
+
+ .diff-line-expand-button {
+ @include diff-expansion($gray-600, $gray-200, $gray-300, $white);
}
// Diff line
@@ -136,6 +141,10 @@ $monokai-gh: #75715e;
@include dark-diff-match-line;
}
+ &.diff-grid-row {
+ @include dark-diff-expansion-line;
+ }
+
.diff-td.diff-line-num.hll:not(.empty-cell),
.diff-td.line-coverage.hll:not(.empty-cell),
.diff-td.line-codequality.hll:not(.empty-cell),
diff --git a/app/assets/stylesheets/highlight/themes/none.scss b/app/assets/stylesheets/highlight/themes/none.scss
index 7a36aba8be7..99a3de23c26 100644
--- a/app/assets/stylesheets/highlight/themes/none.scss
+++ b/app/assets/stylesheets/highlight/themes/none.scss
@@ -44,10 +44,15 @@
color: $gl-text-color;
}
- .line_expansion {
- @include diff-expansion($gray-light, $white-normal, $gl-text-color);
+ .old-line_expansion {
+ @include old-diff-expansion($gray-light, $white-normal, $gl-text-color);
}
+ .diff-line-expand-button {
+ @include diff-expansion($gray-100, $gray-700, $gray-200, $gray-800);
+ }
+
+
// Diff line
$none-expanded-border: #e0e0e0;
$none-expanded-bg: #e0e0e0;
diff --git a/app/assets/stylesheets/highlight/themes/solarized-dark.scss b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
index acd401e1694..55d17b8f1d2 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-dark.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
@@ -1,6 +1,7 @@
/* https://gist.github.com/qguv/7936275 */
@import '../common';
+@import 'highlight.js/styles/base16/solarized-dark.css';
/*
* Solarized dark colors
@@ -127,8 +128,12 @@ $solarized-dark-il: #2aa198;
color: $solarized-dark-pre-color;
}
- .line_expansion {
- @include diff-expansion($solarized-dark-line-bg, $solarized-dark-border, $solarized-dark-kd);
+ .old-line_expansion {
+ @include old-diff-expansion($solarized-dark-line-bg, $solarized-dark-border, $solarized-dark-kd);
+ }
+
+ .diff-line-expand-button {
+ @include diff-expansion(lighten($solarized-dark-pre-bg, 10%), $gray-200, lighten($solarized-dark-pre-bg, 20%), $white);
}
// Diff line
@@ -139,6 +144,10 @@ $solarized-dark-il: #2aa198;
@include dark-diff-match-line;
}
+ &.diff-grid-row {
+ @include dark-diff-expansion-line;
+ }
+
.diff-td.diff-line-num.hll:not(.empty-cell),
.diff-td.line-coverage.hll:not(.empty-cell),
.diff-td.line-codequality.hll:not(.empty-cell),
diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss
index ddcecc4cbcf..72b961097e4 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-light.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss
@@ -1,6 +1,7 @@
/* https://gist.github.com/qguv/7936275 */
@import '../common';
+@import 'highlight.js/styles/base16/solarized-light.css';
/*
* Solarized light syntax colors
@@ -133,9 +134,13 @@ $solarized-light-il: #2aa198;
background-color: $solarized-light-pre-bg;
color: $solarized-light-pre-color;
}
+
+ .old-line_expansion {
+ @include old-diff-expansion($solarized-light-line-bg, $solarized-light-border, $solarized-light-kd);
+ }
- .line_expansion {
- @include diff-expansion($solarized-light-line-bg, $solarized-light-border, $solarized-light-kd);
+ .diff-line-expand-button {
+ @include diff-expansion($gray-100, $gray-700, $gray-200, $gray-800);
}
// Diff line
@@ -146,6 +151,10 @@ $solarized-light-il: #2aa198;
@include match-line;
}
+ &.diff-grid-row.expansion .diff-td {
+ background-color: $solarized-light-matchline-bg;
+ }
+
.diff-td.diff-line-num.hll:not(.empty-cell),
.diff-td.line-coverage.hll:not(.empty-cell),
.diff-td.line-codequality.hll:not(.empty-cell),
diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss
index 20a36d2e8b1..b984c194033 100644
--- a/app/assets/stylesheets/highlight/white_base.scss
+++ b/app/assets/stylesheets/highlight/white_base.scss
@@ -77,6 +77,22 @@ $white-gc-bg: #eaf2f5;
background-color: $gray-light;
}
+@mixin diff-match-line {
+ &.expansion {
+ &.match .diff-td {
+ color: $gray-400;
+ }
+
+ .diff-td {
+ background-color: $gray-50;
+
+ &:first-child {
+ border-color: $gray-100;
+ }
+ }
+ }
+}
+
// Line numbers
.file-line-num {
@include line-number-link($black-transparent);
@@ -117,8 +133,8 @@ pre.code,
color: $white-code-color;
}
-.line_expansion {
- @include diff-expansion($gray-light, $border-color, $blue-600);
+.old-line_expansion {
+ @include old-diff-expansion($gray-light, $border-color, $blue-600);
&.diff-tr:last-child {
border-bottom-right-radius: 4px;
@@ -130,6 +146,10 @@ pre.code,
}
}
+.diff-line-expand-button {
+ @include diff-expansion($gray-100, $gray-700, $gray-200, $gray-800);
+}
+
// Diff line
.line_holder {
&.match .line_content,
@@ -138,6 +158,10 @@ pre.code,
@include match-line;
}
+ &.diff-grid-row {
+ @include diff-match-line;
+ }
+
&:not(.match) .diff-grid-left:hover,
&:not(.match) .diff-grid-right:hover,
&.code-search-line:hover {
diff --git a/app/assets/stylesheets/mailer.scss b/app/assets/stylesheets/mailer.scss
index 5f50489555b..b8cbe64df38 100644
--- a/app/assets/stylesheets/mailer.scss
+++ b/app/assets/stylesheets/mailer.scss
@@ -197,6 +197,5 @@ tr.footer td {
.footer-logo {
width: 90px;
- height: 33px;
}
}
diff --git a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
index ee777820b81..3327f8da632 100644
--- a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
+++ b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
@@ -13,98 +13,6 @@
}
}
-@mixin mini-pipeline-graph-color(
- $color-background-default,
- $color-background-hover-focus,
- $color-background-active,
- $color-foreground-default,
- $color-foreground-hover-focus,
- $color-foreground-active
-) {
- background-color: $color-background-default;
- border-color: $color-foreground-default;
-
- svg {
- fill: $color-foreground-default;
- }
-
- &:hover,
- &:focus {
- background-color: $color-background-hover-focus;
- border-color: $color-foreground-hover-focus;
-
- svg {
- fill: $color-foreground-hover-focus;
- }
- }
-
- &:active {
- background-color: $color-background-active;
- border-color: $color-foreground-active;
-
- svg {
- fill: $color-foreground-active;
- }
- }
-
- &:focus {
- box-shadow: 0 0 4px 1px $blue-300;
- }
-}
-
-@mixin mini-pipeline-item() {
- border-radius: 100px;
- background-color: var(--white, $white);
- border-width: 1px;
- border-style: solid;
- width: $ci-action-icon-size;
- height: $ci-action-icon-size;
- margin: 0;
- padding: 0;
- position: relative;
- vertical-align: middle;
-
- &:hover,
- &:active,
- &:focus {
- outline: none;
- border-width: 2px;
- }
-
- // Dropdown button animation in mini pipeline graph
- &.ci-status-icon-success {
- @include mini-pipeline-graph-color(var(--white, $white), $green-100, $green-200, $green-500, $green-600, $green-700);
- }
-
- &.ci-status-icon-failed {
- @include mini-pipeline-graph-color(var(--white, $white), $red-100, $red-200, $red-500, $red-600, $red-700);
- }
-
- &.ci-status-icon-pending,
- &.ci-status-icon-waiting-for-resource,
- &.ci-status-icon-success-with-warnings {
- @include mini-pipeline-graph-color(var(--white, $white), $orange-50, $orange-100, $orange-500, $orange-600, $orange-700);
- }
-
- &.ci-status-icon-running {
- @include mini-pipeline-graph-color(var(--white, $white), $blue-100, $blue-200, $blue-500, $blue-600, $blue-700);
- }
-
- &.ci-status-icon-canceled,
- &.ci-status-icon-scheduled,
- &.ci-status-icon-disabled,
- &.ci-status-icon-manual {
- @include mini-pipeline-graph-color(var(--white, $white), $gray-500, $gray-700, $gray-900, $gray-950, $black);
- }
-
- &.ci-status-icon-preparing,
- &.ci-status-icon-created,
- &.ci-status-icon-not-found,
- &.ci-status-icon-skipped {
- @include mini-pipeline-graph-color(var(--white, $white), var(--gray-100, $gray-100), var(--gray-200, $gray-200), var(--gray-400, $gray-400), var(--gray-500, $gray-500), var(--gray-600, $gray-600));
- }
-}
-
/**
Action icons inside dropdowns:
- mini graph in pipelines table
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index eecd4954e39..81d35b8bc7b 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -49,6 +49,7 @@
height: calc(100vh - #{$issue-board-list-difference-xs});
overflow-x: scroll;
min-height: 200px;
+ border-left: 8px solid var(--gray-10, $white);
@include media-breakpoint-only(sm) {
height: calc(100vh - #{$issue-board-list-difference-sm});
@@ -131,8 +132,7 @@
.board-inner {
font-size: $issue-boards-font-size;
- background: var(--gray-10, $gray-10);
- border: 1px solid var(--gray-100, $gray-100);
+ background: var(--gray-50, $gray-50);
}
// to highlight columns we have animated pulse of box-shadow
@@ -169,33 +169,7 @@
}
}
-.board-header {
- &.has-border::before {
- border-top: 3px solid;
- border-color: inherit;
- border-top-left-radius: $border-radius-default;
- border-top-right-radius: $border-radius-default;
- content: '';
- position: absolute;
- width: calc(100% + 2px);
- top: 0;
- left: 0;
- margin-top: -1px;
- margin-right: -1px;
- margin-left: -1px;
- padding-top: 1px;
- padding-right: 1px;
- padding-left: 1px;
-
- .board-title {
- padding-top: ($gl-padding - 3px);
- padding-bottom: $gl-padding;
- }
- }
-}
-
.board-title {
- border-bottom: 1px solid var(--gray-100, $gray-100);
height: 3rem;
.max-issue-size::before {
@@ -218,8 +192,7 @@
}
.board-card {
- background: var(--white, $white);
- border: 1px solid var(--gray-100, $gray-100);
+ background: var(--gray-10, $white);
box-shadow: 0 1px 2px rgba(var(--black, $black), 0.1);
line-height: $gl-padding;
list-style: none;
@@ -239,6 +212,10 @@
background-color: var(--blue-50, $blue-50);
}
+ &.sortable-chosen {
+ box-shadow: 0 2px 4px 0 rgba($black, 0.16);
+ }
+
.gl-label {
margin-top: 4px;
margin-right: 4px;
diff --git a/app/assets/stylesheets/page_bundles/dashboard_projects.scss b/app/assets/stylesheets/page_bundles/dashboard_projects.scss
index eb0e1701b7f..5eced37bed3 100644
--- a/app/assets/stylesheets/page_bundles/dashboard_projects.scss
+++ b/app/assets/stylesheets/page_bundles/dashboard_projects.scss
@@ -15,9 +15,9 @@
.blank-state-link {
&:hover {
- background-color: $gray-light;
+ background-color: var(--gray-50, $gray-10);
+ color: var(--gl-text-color, $gl-text-color);
text-decoration: none;
- color: $gl-text-color;
}
}
diff --git a/app/assets/stylesheets/page_bundles/issues_show.scss b/app/assets/stylesheets/page_bundles/issues_show.scss
new file mode 100644
index 00000000000..9873a0121c0
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/issues_show.scss
@@ -0,0 +1,67 @@
+@import 'mixins_and_variables_and_functions';
+
+.description {
+ ul,
+ ol {
+ /* We're changing list-style-position to inside because the default of outside
+ * doesn't move the negative margin to the left of the bullet. */
+ list-style-position: inside;
+ }
+
+ li {
+ position: relative;
+ /* In the browser, the li element comes after (to the right of) the bullet point, so hovering
+ * over the left of the bullet point doesn't trigger a row hover. To trigger hovering on the
+ * left, we're applying negative margin here to shift the li element left. */
+ margin-inline-start: -1rem;
+ padding-inline-start: 2.5rem;
+
+ .drag-icon {
+ position: absolute;
+ inset-block-start: 0.3rem;
+ inset-inline-start: 1rem;
+ }
+ }
+
+ ul.task-list {
+ > li.task-list-item {
+ /* We're using !important to override the same selector in typography.scss */
+ margin-inline-start: -1rem !important;
+ padding-inline-start: 2.5rem;
+
+ > input.task-list-item-checkbox {
+ position: static;
+ vertical-align: middle;
+ margin-block-start: -2px;
+ }
+ }
+ }
+}
+
+.description.work-items-enabled {
+ ul.task-list {
+ > li.task-list-item {
+ .js-add-task {
+ svg {
+ visibility: hidden;
+ }
+
+ &:focus svg {
+ visibility: visible;
+ }
+ }
+
+ &:hover,
+ &:focus-within {
+ .js-add-task svg {
+ visibility: visible;
+ }
+ }
+ }
+ }
+}
+
+.is-ghost {
+ opacity: 0.3;
+ pointer-events: none;
+}
diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss
index 1c8fd7e2590..f153569f99b 100644
--- a/app/assets/stylesheets/page_bundles/jira_connect.scss
+++ b/app/assets/stylesheets/page_bundles/jira_connect.scss
@@ -17,6 +17,11 @@
@import '@gitlab/ui/src/components/base/table/table';
@import '@gitlab/ui/src/components/base/tooltip/tooltip';
@import '@gitlab/ui/src/components/base/search_box_by_type/search_box_by_type';
+@import '@gitlab/ui/src/components/base/form/form_input/form_input';
+@import '@gitlab/ui/src/components/base/form/form_radio/form_radio';
+@import '@gitlab/ui/src/components/base/form/form_radio_group/form_radio_group';
+@import '@gitlab/ui/src/components/base/form/form_checkbox/form_checkbox';
+@import '@gitlab/ui/src/components/base/form/form_group/form_group';
$header-height: 40px;
@@ -39,9 +44,3 @@ $header-height: 40px;
height: calc(100% - #{$header-height});
max-width: 1000px;
}
-
-// needed for external_link
-svg.s16 {
- width: 16px;
- height: 16px;
-}
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index 34a3d936a67..f04cdfba0e4 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -41,7 +41,7 @@ $tabs-holder-z-index: 250;
// If they don't match, the file tree and the diff files stick
// to the top at different heights, which is a bad-looking defect
$diff-file-header-top: 11px;
- $top-pos: calc(#{$header-height} + #{$mr-tabs-height} + #{$mr-version-controls-height} + #{$diff-file-header-top});
+ $top-pos: calc(#{$header-height} + #{$mr-tabs-height} + #{$diff-file-header-top});
position: -webkit-sticky;
position: sticky;
@@ -121,10 +121,6 @@ $tabs-holder-z-index: 250;
@include media-breakpoint-down(sm) {
flex-direction: column;
- .stage-cell .stage-container {
- margin-top: 16px;
- }
-
.dropdown .mini-pipeline-graph-dropdown-menu.dropdown-menu {
transform: initial;
}
@@ -666,22 +662,19 @@ $tabs-holder-z-index: 250;
margin-top: $gl-padding;
position: relative;
- &::before {
+ &:not(:last-child)::before {
content: '';
border-left: 1px solid var(--gray-100, $gray-100);
position: absolute;
left: 28px;
- top: -17px;
+ bottom: -17px;
height: 16px;
}
}
.mr-version-controls {
- position: relative;
- z-index: $tabs-holder-z-index + 10;
background: var(--white, $white);
color: var(--gl-text-color, $gl-text-color);
- margin-top: -1px;
.mr-version-menus-container {
display: flex;
@@ -703,45 +696,26 @@ $tabs-holder-z-index: 250;
}
.content-block {
- padding: $gl-padding;
+ padding: $gl-padding-8 $gl-padding;
border-bottom: 0;
}
.mr-version-dropdown,
.mr-version-compare-dropdown {
- margin: 0 0.5rem;
- }
-
- .dropdown-title {
- color: var(--gl-text-color, $gl-text-color);
- }
-
- // Shortening button height by 1px to make compare-versions
- // header 56px and fit into our 8px design grid
- .btn {
- height: 34px;
- }
-
- @include media-breakpoint-up(md) {
- position: -webkit-sticky;
- position: sticky;
- top: calc(#{$header-height} + #{$mr-tabs-height});
+ margin: 0 $gl-spacing-scale-1;
- .with-system-header & {
- top: calc(#{$header-height} + #{$mr-tabs-height} + #{$system-header-height});
- }
-
- .with-system-header.with-performance-bar & {
- top: calc(#{$header-height} + #{$mr-tabs-height} + #{$system-header-height} + #{$performance-bar-height});
- }
+ .dropdown-toggle.gl-button {
+ padding: $gl-spacing-scale-2 2px $gl-spacing-scale-2 $gl-spacing-scale-2;
+ font-weight: $gl-font-weight-bold;
- .mr-version-menus-container {
- flex-wrap: nowrap;
+ .gl-button-icon {
+ margin-left: $gl-spacing-scale-1;
+ }
}
+ }
- .with-performance-bar & {
- top: calc(#{$header-height} + #{$performance-bar-height} + #{$mr-tabs-height});
- }
+ .dropdown-title {
+ color: var(--gl-text-color, $gl-text-color);
}
}
@@ -761,3 +735,11 @@ $tabs-holder-z-index: 250;
.attention-request-sidebar-popover {
z-index: 999;
}
+
+.merge-request-overview {
+ @include media-breakpoint-up(md) {
+ display: grid;
+ grid-template-columns: 1fr 270px;
+ grid-gap: 5%;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/milestone.scss b/app/assets/stylesheets/page_bundles/milestone.scss
index aa582db10d2..c401f1a4902 100644
--- a/app/assets/stylesheets/page_bundles/milestone.scss
+++ b/app/assets/stylesheets/page_bundles/milestone.scss
@@ -167,7 +167,7 @@ $status-box-line-height: 26px;
border-bottom: 1px solid var(--border-color, $border-color);
}
-@include media-breakpoint-down(xs) {
+@include media-breakpoint-down(md) {
.milestone-actions {
@include clearfix();
padding-top: $gl-vert-padding;
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index cbb6d68bf35..e6afc70acbb 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -139,7 +139,7 @@
}
.gl-downstream-pipeline-job-width {
- width: 170px;
+ width: 8rem;
}
.gl-linked-pipeline-padding {
diff --git a/app/assets/stylesheets/page_bundles/pipeline_editor.scss b/app/assets/stylesheets/page_bundles/pipeline_editor.scss
new file mode 100644
index 00000000000..e167052a3e1
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/pipeline_editor.scss
@@ -0,0 +1,22 @@
+@import 'mixins_and_variables_and_functions';
+
+.file-tree-container {
+ @include gl-w-full;
+
+ @media (min-width: $breakpoint-md) {
+ width: 300px;
+ }
+}
+
+.file-tree-container > div.gl-overflow-y-auto {
+ max-height: 220px;
+
+ @media (min-width: $breakpoint-md) {
+ max-height: 700px;
+ }
+}
+
+.file-tree-includes-link:hover > svg {
+ @include gl-display-block;
+ top: 2px;
+}
diff --git a/app/assets/stylesheets/page_bundles/pipelines.scss b/app/assets/stylesheets/page_bundles/pipelines.scss
index 7b54be5c91f..a225a0f0061 100644
--- a/app/assets/stylesheets/page_bundles/pipelines.scss
+++ b/app/assets/stylesheets/page_bundles/pipelines.scss
@@ -73,36 +73,12 @@
// Mini Pipelines
.stage-cell {
- .mini-pipeline-graph-dropdown-toggle {
- svg {
- height: $ci-action-icon-size;
- width: $ci-action-icon-size;
- position: absolute;
- top: -1px;
- left: -1px;
- z-index: 2;
- overflow: visible;
- }
-
- &:hover,
- &:active,
- &:focus {
- svg {
- top: -2px;
- left: -2px;
- }
- }
- }
-
.stage-container {
- display: inline-block;
- position: relative;
- vertical-align: middle;
- height: $ci-action-icon-size;
- margin: 3px 0;
+ align-items: center;
+ display: inline-flex;
+ .stage-container {
- margin-left: 6px;
+ margin-left: 4px;
}
// Hack to show a button tooltip inline
@@ -118,44 +94,15 @@
&:not(:last-child) {
&::after {
content: '';
- width: 7px;
+ width: 4px;
position: absolute;
- right: -7px;
- top: 11px;
- border-bottom: 2px solid var(--border-color, $border-color);
- }
- }
-
- //delete when all pipelines are updated to new size
- &.mr-widget-pipeline-stages {
- + .stage-container {
- margin-left: 4px;
- }
-
- &:not(:last-child) {
- &::after {
- width: 4px;
- right: -4px;
- top: 11px;
- }
+ right: -4px;
+ border-bottom: 2px solid $gray-200;
}
}
}
}
-// Commit mini pipeline (HAML)
-button.mini-pipeline-graph-dropdown-toggle,
-// GlDropdown mini pipeline (Vue)
-// As the `mini-pipeline-item` mixin specificity is lower
-// than the toggle of dropdown with 'variant="link"' we add
-// classes ".gl-button.btn-link" to make it more specific
-// and avoid having the size overriden
-//
-// See https://gitlab.com/gitlab-org/gitlab/-/issues/320737
-button.gl-button.btn-link.mini-pipeline-graph-dropdown-toggle {
- @include mini-pipeline-item();
-}
-
// Action icons inside dropdowns:
// mini graph in pipelines table
// mini graph in MR widget pipeline
diff --git a/app/assets/stylesheets/page_bundles/terms.scss b/app/assets/stylesheets/page_bundles/terms.scss
index 8eb66e58aed..9dff3e9c99c 100644
--- a/app/assets/stylesheets/page_bundles/terms.scss
+++ b/app/assets/stylesheets/page_bundles/terms.scss
@@ -22,14 +22,6 @@
justify-content: space-between;
line-height: $line-height-base;
- .logo-text {
- width: 55px;
- height: 24px;
- display: flex;
- flex-direction: column;
- justify-content: center;
- }
-
.navbar-collapse {
padding-right: 0;
flex-grow: 0;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index cc8ea1493fc..afe57bb26e6 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -35,9 +35,6 @@
}
.mr-widget-pipeline-graph {
- display: inline-block;
- vertical-align: middle;
-
.dropdown-menu {
margin-top: 11px;
}
@@ -45,8 +42,6 @@
}
.branch-info .commit-icon {
- margin-right: 8px;
-
svg {
top: 3px;
}
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index f237d57aa88..e0319952adb 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -8,7 +8,8 @@
a {
color: $gl-text-color;
- &.link {
+ &.link,
+ &.gl-link {
color: $blue-600;
}
}
@@ -25,6 +26,7 @@
.detail-page-header-body {
position: relative;
display: flex;
+ align-items: center;
flex: 1 1;
min-width: 0;
@@ -38,9 +40,18 @@
align-self: center;
flex: 0 0 auto;
- @include media-breakpoint-down(xs) {
- width: 100%;
- margin-top: 10px;
+ &:not(.is-merge-request) {
+ @include media-breakpoint-down(xs) {
+ width: 100%;
+ margin-top: 10px;
+ }
+ }
+
+ &.is-merge-request {
+ @include media-breakpoint-down(sm) {
+ width: 100%;
+ margin-top: 10px;
+ }
}
}
@@ -56,4 +67,8 @@
.description {
margin-top: 6px;
}
+
+ .author-link {
+ color: $gl-text-color;
+ }
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index c00af802c06..4093ef087dc 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -108,21 +108,13 @@
.merge-icon {
color: $orange-400;
position: absolute;
+ bottom: -3px;
+ right: -3px;
filter: drop-shadow(0 0 0.5px $white) drop-shadow(0 0 1px $white) drop-shadow(0 0 2px $white);
}
}
-.assignee .merge-icon {
- top: calc(50% + 0.25rem);
- left: 1.275rem;
-}
-
-.reviewer .merge-icon {
- bottom: -3px;
- right: -3px;
-}
-
-.right-sidebar {
+@mixin right-sidebar {
position: fixed;
top: $header-height;
// Default value for CSS var must contain a unit
@@ -130,10 +122,23 @@
bottom: var(--review-bar-height, 0px);
right: 0;
transition: width $sidebar-transition-duration;
- background: $gray-light;
+ background-color: $white;
z-index: 200;
overflow: hidden;
+}
+
+.right-sidebar {
+ &:not(.right-sidebar-merge-requests) {
+ @include right-sidebar;
+ }
+
+ &.right-sidebar-merge-requests {
+ @include media-breakpoint-down(sm) {
+ @include right-sidebar;
+ }
+ }
+
@include media-breakpoint-down(sm) {
z-index: 251;
}
@@ -143,10 +148,6 @@
&:hover {
color: $blue-800;
-
- .avatar {
- border-color: rgba($gray-normal, 0.2);
- }
}
}
@@ -176,12 +177,26 @@
}
}
+ &.right-sidebar-merge-requests {
+ .block,
+ .sidebar-contained-width,
+ .issuable-sidebar-header {
+ width: 100%;
+ border-bottom: 0;
+ }
+
+ .block {
+ @include media-breakpoint-up(md) {
+ padding: $gl-spacing-scale-5 0;
+ }
+ }
+ }
+
.block,
.sidebar-contained-width,
.issuable-sidebar-header {
@include clearfix;
padding: $gl-padding 0;
- border-bottom: 1px solid $border-gray-normal;
// This prevents the mess when resizing the sidebar
// of elements repositioning themselves..
width: $gutter-inner-width;
@@ -218,16 +233,6 @@
}
}
- .cross-project-reference {
- span {
- width: 85%;
- }
-
- button {
- padding: 1px 5px;
- }
- }
-
.selectbox {
display: none;
@@ -242,9 +247,20 @@
.issuable-sidebar {
height: 100%;
- overflow-y: scroll;
- overflow-x: hidden;
- -webkit-overflow-scrolling: touch;
+
+ &:not(.is-merge-request) {
+ overflow-y: scroll;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ &.is-merge-request {
+ @include media-breakpoint-down(sm) {
+ overflow-y: scroll;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+ }
+ }
}
&.affix-top .issuable-sidebar {
@@ -252,7 +268,9 @@
}
&.right-sidebar-expanded {
- width: $gutter-width;
+ &:not(.right-sidebar-merge-requests) {
+ width: $gutter-width;
+ }
.value {
line-height: 1;
@@ -260,6 +278,12 @@
.issuable-sidebar {
padding: 0 20px;
+
+ &.is-merge-request {
+ @include media-breakpoint-up(md) {
+ padding: 0;
+ }
+ }
}
&:not(.boards-sidebar):not([data-signed-in]):not([data-always-show-toggle]) {
@@ -302,8 +326,17 @@
/* Extra small devices (phones, less than 768px) */
display: none;
/* Small devices (tablets, 768px and up) */
- @include media-breakpoint-up(sm) {
- display: block;
+
+ &:not(.right-sidebar-merge-requests) {
+ @include media-breakpoint-up(sm) {
+ display: block;
+ }
+ }
+
+ &.right-sidebar-merge-requests {
+ @include media-breakpoint-up(md) {
+ display: block;
+ }
}
width: $gutter-collapsed-width;
@@ -380,10 +413,6 @@
}
}
- .sidebar-avatar-counter {
- padding-top: 2px;
- }
-
.todo-undone {
color: $blue-600;
fill: $blue-600;
@@ -428,6 +457,10 @@
width: 24px;
height: 24px;
border-radius: 12px;
+
+ ~.merge-icon {
+ bottom: 0;
+ }
}
.sidebar-collapsed-user {
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index f127b0dc66c..04e0ef6631e 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -307,32 +307,3 @@ ul.related-merge-requests > li gl-emoji {
.issuable-header-slide-leave-to {
transform: translateY(-100%);
}
-
-.description.work-items-enabled {
- ul.task-list {
- > li.task-list-item {
- padding-inline-start: 2.5rem;
-
- .js-add-task {
- svg {
- visibility: hidden;
- }
-
- &:focus svg {
- visibility: visible;
- }
- }
-
- > input.task-list-item-checkbox {
- left: 1.25rem;
- }
-
- &:hover,
- &:focus-within {
- .js-add-task svg {
- visibility: visible;
- }
- }
- }
- }
-}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 4a3ec5992a5..7f0bdadd2bc 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -45,7 +45,6 @@
.omniauth-container {
box-shadow: 0 0 0 1px $border-color;
border-radius: $border-radius;
- padding: 15px;
.login-heading h3 {
font-weight: $gl-font-weight-normal;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index f95cff012d0..0d3ed0e7c71 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -233,8 +233,7 @@ $tabs-holder-z-index: 250;
top: calc(#{$header-height} + #{$system-header-height} + #{$performance-bar-height});
}
- @include media-breakpoint-up(sm) {
- position: -webkit-sticky;
+ @include media-breakpoint-up(md) {
position: sticky;
}
@@ -294,8 +293,7 @@ $tabs-holder-z-index: 250;
justify-content: space-between;
@include media-breakpoint-down(xs) {
- .discussion-filter-container,
- .line-resolve-all-container {
+ .discussion-filter-container {
margin-bottom: $gl-padding-4;
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index d8c3851748d..1949603b416 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -840,35 +840,6 @@ $system-note-svg-size: 16px;
}
}
-.line-resolve-all-container {
- > div {
- white-space: nowrap;
- }
-
- .btn-group .btn:first-child {
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
- }
-}
-
-.line-resolve-all {
- vertical-align: middle;
- display: inline-block;
- padding: $gl-padding-8 $gl-padding-12;
- background-color: $gray-light;
- border: 1px solid $border-color;
- border-right: 0;
- border-radius: $border-radius-default;
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- font-size: $gl-font-size;
- line-height: 1rem;
-
- @include media-breakpoint-down(xs) {
- flex: 1;
- }
-}
-
.line-resolve-btn {
position: relative;
top: 0;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 8034389adc8..3b76130dd1a 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -548,15 +548,6 @@ pre.light-well {
}
}
-.git-clone-holder,
-.mobile-git-clone {
- .btn {
- .icon {
- fill: $white;
- }
- }
-}
-
.new-protected-branch,
.new-protected-tag {
label {
@@ -713,22 +704,6 @@ pre.light-well {
margin-bottom: 10px;
}
-.service-installation {
- padding: 32px;
- margin: 32px;
- border-radius: 3px;
- background-color: $white;
-
- h3 {
- margin-top: 0;
- }
-
- hr {
- margin: 32px 0;
- border-color: $border-color;
- }
-}
-
.project-badge {
opacity: 0.9;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 18a0f119edf..8755db83d35 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -2,6 +2,11 @@ $search-dropdown-max-height: 400px;
$search-avatar-size: 16px;
$search-sidebar-min-width: 240px;
$search-sidebar-max-width: 300px;
+$search-input-field-x-min-width: 200px;
+$search-input-field-min-width: 320px;
+$search-input-field-max-width: 600px;
+
+$border-radius-medium: 3px;
.search-results {
.search-result-row {
@@ -37,8 +42,44 @@ input[type='checkbox']:hover {
0 0 0 1px lighten($dropdown-input-focus-shadow, 20%);
}
+.header-content {
+ .header-search-new {
+ max-width: $search-input-field-max-width;
+ }
+
+ &.header-search-is-active {
+ .navbar-collapse {
+ flex-grow: 1;
+ }
+
+ @include media-breakpoint-down(lg) {
+ .title-container {
+ flex: 0;
+ overflow: hidden;
+ }
+ }
+
+ @include media-breakpoint-up(xl) {
+ .navbar-nav {
+ padding-left: 1rem;
+ }
+ }
+ }
+}
+
+// This is a temporary workaround!
+// the button in GitLab UI Search components need to be updated to not be the small size
+// see in Figma: https://www.figma.com/file/qEddyqCrI7kPSBjGmwkZzQ/Component-library?node-id=43905%3A45540
+.header-search .gl-search-box-by-type-clear.btn-sm {
+ padding: 0.5rem !important;
+}
+
.header-search {
- width: 320px;
+ min-width: $search-input-field-min-width;
+
+ @include media-breakpoint-between(md, lg) {
+ min-width: $search-input-field-x-min-width;
+ }
input,
svg {
@@ -66,9 +107,10 @@ input[type='checkbox']:hover {
border-width: 0;
border-style: solid;
border-image: none;
- border-radius: 3px;
+ border-radius: $border-radius-medium;
box-shadow: none;
white-space: pre-wrap;
+ box-sizing: border-box;
// Safari
word-wrap: break-word;
overflow-wrap: break-word;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 33c66648718..a9fbff8958d 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -6,10 +6,6 @@
.nav-block {
margin: 16px 0;
- .btn svg {
- color: $gl-text-color-secondary;
- }
-
.tree-ref-holder {
margin-right: 15px;
}
@@ -100,7 +96,7 @@
}
}
- .tree-table {
+ table.tree-table {
margin-bottom: 0;
tr {
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index 62d45332204..001431e517b 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -12,6 +12,7 @@ body.gl-dark {
--gl-text-color: #fafafa;
--border-color: #4f4f4f;
--black: #fff;
+ --nav-active-bg: rgba(255, 255, 255, 0.08);
}
:root {
--white: #333;
@@ -350,9 +351,6 @@ h1 {
.d-lg-none {
display: none !important;
}
- .d-lg-block {
- display: block !important;
- }
}
.sr-only {
position: absolute;
@@ -393,8 +391,7 @@ a.gl-badge.badge-info:active {
background-color: #0b5cad;
}
a.gl-badge.badge-info:active {
- box-shadow: inset 0 0 0 1px rgba(51, 51, 51, 0.8),
- 0 0 0 1px rgba(51, 51, 51, 0.4), 0 0 0 4px rgba(66, 143, 220, 0.48);
+ box-shadow: 0 0 0 1px #333, 0 0 0 3px #1f75cb;
outline: none;
}
.gl-badge.badge-success {
@@ -407,8 +404,7 @@ a.gl-badge.badge-success:active {
background-color: #24663b;
}
a.gl-badge.badge-success:active {
- box-shadow: inset 0 0 0 1px rgba(51, 51, 51, 0.8),
- 0 0 0 1px rgba(51, 51, 51, 0.4), 0 0 0 4px rgba(66, 143, 220, 0.48);
+ box-shadow: 0 0 0 1px #333, 0 0 0 3px #1f75cb;
outline: none;
}
.gl-badge.badge-warning {
@@ -421,8 +417,7 @@ a.gl-badge.badge-warning:active {
background-color: #8f4700;
}
a.gl-badge.badge-warning:active {
- box-shadow: inset 0 0 0 1px rgba(51, 51, 51, 0.8),
- 0 0 0 1px rgba(51, 51, 51, 0.4), 0 0 0 4px rgba(66, 143, 220, 0.48);
+ box-shadow: 0 0 0 1px #333, 0 0 0 3px #1f75cb;
outline: none;
}
.gl-button .gl-badge {
@@ -487,9 +482,8 @@ a.gl-badge.badge-warning:active {
}
.gl-link:active {
text-decoration: underline;
- box-shadow: 0 0 0 1px rgba(51, 51, 51, 0.4),
- 0 0 0 4px rgba(66, 143, 220, 0.48);
- outline: none;
+ outline: 2px solid #1f75cb;
+ outline-offset: 2px;
}
.gl-button {
display: inline-flex;
@@ -518,8 +512,7 @@ a.gl-badge.badge-warning:active {
}
.gl-button.gl-button.btn-default:active,
.gl-button.gl-button.btn-default.active {
- box-shadow: inset 0 0 0 2px #bfbfbf, 0 0 0 1px rgba(51, 51, 51, 0.4),
- 0 0 0 4px rgba(66, 143, 220, 0.48);
+ box-shadow: inset 0 0 0 1px #bfbfbf, 0 0 0 1px #333, 0 0 0 3px #1f75cb;
outline: none;
background-color: #404040;
}
@@ -742,7 +735,7 @@ body {
}
}
@media (max-width: 767.98px) {
- .dropdown-menu-toggle {
+ .dropdown-menu-toggle.dropdown-menu-toggle {
width: 100%;
}
}
@@ -770,15 +763,6 @@ input {
right: 0;
border-radius: 0;
}
-.navbar-gitlab .logo-text {
- line-height: initial;
-}
-.navbar-gitlab .logo-text svg {
- width: 55px;
- height: 14px;
- margin: 0;
- fill: #333;
-}
.navbar-gitlab .close-icon {
display: none;
}
@@ -790,13 +774,6 @@ input {
min-height: var(--header-height, 40px);
padding-left: 0;
}
-.navbar-gitlab .header-content .title-container {
- display: flex;
- align-items: stretch;
- flex: 1 1 auto;
- padding-top: 0;
- overflow: visible;
-}
.navbar-gitlab .header-content .title {
padding-right: 0;
color: currentColor;
@@ -817,9 +794,12 @@ input {
display: flex;
align-items: center;
padding: 2px 8px;
- margin: 5px 2px 5px -8px;
+ margin: 4px 2px 4px -12px;
border-radius: 4px;
}
+.navbar-gitlab .header-content .title .canary-badge {
+ margin-left: -8px;
+}
.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
margin: 0 2px;
}
@@ -853,7 +833,7 @@ input {
border-radius: 0;
min-width: 45px;
padding: 0;
- margin: 8px -7px 8px 0;
+ margin: 8px 8px 8px 0;
font-size: 14px;
text-align: center;
color: currentColor;
@@ -997,6 +977,16 @@ input {
.top-nav-toggle .dropdown-icon {
margin-right: 0.5rem;
}
+.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;
@@ -1513,8 +1503,16 @@ svg.s12 {
svg.s16 {
vertical-align: -3px;
}
+.header-content .header-search-new {
+ max-width: 600px;
+}
.header-search {
- width: 320px;
+ min-width: 320px;
+}
+@media (min-width: 768px) and (max-width: 1199.98px) {
+ .header-search {
+ min-width: 200px;
+ }
}
.header-search.is-not-active::after {
content: "/";
@@ -1535,6 +1533,7 @@ svg.s16 {
border-radius: 3px;
box-shadow: none;
white-space: pre-wrap;
+ box-sizing: border-box;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: keep-all;
@@ -1787,6 +1786,7 @@ body.gl-dark {
--white: #333;
--black: #fff;
--svg-status-bg: #333;
+ --nav-active-bg: rgba(255, 255, 255, 0.08);
}
.nav-sidebar li a {
color: var(--gray-600);
@@ -1862,6 +1862,7 @@ body.gl-dark
}
body.gl-dark .header-search {
background-color: rgba(250, 250, 250, 0.2) !important;
+ border-radius: 4px;
}
body.gl-dark .header-search svg.gl-search-box-by-type-search-icon {
color: rgba(250, 250, 250, 0.8);
@@ -1900,9 +1901,6 @@ body.gl-dark .nav-sidebar .fly-out-top-item .fly-out-top-item-container {
background-color: var(--gray-100, #303030);
color: var(--gray-900, #fafafa);
}
-body.gl-dark .logo-text svg {
- fill: var(--gl-text-color);
-}
body.gl-dark .navbar-gitlab {
background-color: var(--gray-50);
box-shadow: 0 1px 0 0 var(--gray-100);
@@ -2023,6 +2021,7 @@ body.gl-dark {
--white: #333;
--black: #fff;
--svg-status-bg: #333;
+ --nav-active-bg: rgba(255, 255, 255, 0.08);
}
.tab-width-8 {
-moz-tab-size: 8;
@@ -2045,17 +2044,47 @@ body.gl-dark {
.gl-display-none {
display: none;
}
+@media (min-width: 992px) {
+ .gl-lg-display-none\! {
+ display: none !important;
+ }
+}
+.gl-display-flex {
+ display: flex;
+}
+@media (min-width: 992px) {
+ .gl-lg-display-flex {
+ display: flex;
+ }
+}
@media (min-width: 576px) {
.gl-sm-display-block {
display: block;
}
}
+@media (min-width: 992px) {
+ .gl-lg-display-block {
+ display: block;
+ }
+}
+.gl-display-inline-block\! {
+ display: inline-block !important;
+}
+.gl-align-items-stretch {
+ align-items: stretch;
+}
+.gl-flex-grow-1 {
+ flex-grow: 1;
+}
.gl-relative {
position: relative;
}
.gl-absolute {
position: absolute;
}
+.gl-w-full {
+ width: 100%;
+}
.gl-px-3 {
padding-left: 0.5rem;
padding-right: 0.5rem;
@@ -2063,6 +2092,9 @@ body.gl-dark {
.gl-pr-2 {
padding-right: 0.25rem;
}
+.gl-pt-0 {
+ padding-top: 0;
+}
.gl-ml-n2 {
margin-left: -0.25rem;
}
@@ -2073,6 +2105,12 @@ body.gl-dark {
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;
}
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
index a8b7299b935..c42b5554d8d 100644
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -336,9 +336,6 @@ h1 {
.d-lg-none {
display: none !important;
}
- .d-lg-block {
- display: block !important;
- }
}
.sr-only {
position: absolute;
@@ -379,8 +376,7 @@ a.gl-badge.badge-info:active {
background-color: #9dc7f1;
}
a.gl-badge.badge-info:active {
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.8),
- 0 0 0 1px rgba(255, 255, 255, 0.4), 0 0 0 4px rgba(31, 117, 203, 0.48);
+ box-shadow: 0 0 0 1px #fff, 0 0 0 3px #428fdc;
outline: none;
}
.gl-badge.badge-success {
@@ -393,8 +389,7 @@ a.gl-badge.badge-success:active {
background-color: #91d4a8;
}
a.gl-badge.badge-success:active {
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.8),
- 0 0 0 1px rgba(255, 255, 255, 0.4), 0 0 0 4px rgba(31, 117, 203, 0.48);
+ box-shadow: 0 0 0 1px #fff, 0 0 0 3px #428fdc;
outline: none;
}
.gl-badge.badge-warning {
@@ -407,8 +402,7 @@ a.gl-badge.badge-warning:active {
background-color: #e9be74;
}
a.gl-badge.badge-warning:active {
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.8),
- 0 0 0 1px rgba(255, 255, 255, 0.4), 0 0 0 4px rgba(31, 117, 203, 0.48);
+ box-shadow: 0 0 0 1px #fff, 0 0 0 3px #428fdc;
outline: none;
}
.gl-button .gl-badge {
@@ -473,9 +467,8 @@ a.gl-badge.badge-warning:active {
}
.gl-link:active {
text-decoration: underline;
- box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.4),
- 0 0 0 4px rgba(31, 117, 203, 0.48);
- outline: none;
+ outline: 2px solid #428fdc;
+ outline-offset: 2px;
}
.gl-button {
display: inline-flex;
@@ -504,8 +497,7 @@ a.gl-badge.badge-warning:active {
}
.gl-button.gl-button.btn-default:active,
.gl-button.gl-button.btn-default.active {
- box-shadow: inset 0 0 0 2px #5e5e5e, 0 0 0 1px rgba(255, 255, 255, 0.4),
- 0 0 0 4px rgba(31, 117, 203, 0.48);
+ box-shadow: inset 0 0 0 1px #5e5e5e, 0 0 0 1px #fff, 0 0 0 3px #428fdc;
outline: none;
background-color: #dbdbdb;
}
@@ -728,7 +720,7 @@ body {
}
}
@media (max-width: 767.98px) {
- .dropdown-menu-toggle {
+ .dropdown-menu-toggle.dropdown-menu-toggle {
width: 100%;
}
}
@@ -756,15 +748,6 @@ input {
right: 0;
border-radius: 0;
}
-.navbar-gitlab .logo-text {
- line-height: initial;
-}
-.navbar-gitlab .logo-text svg {
- width: 55px;
- height: 14px;
- margin: 0;
- fill: #fff;
-}
.navbar-gitlab .close-icon {
display: none;
}
@@ -776,13 +759,6 @@ input {
min-height: var(--header-height, 40px);
padding-left: 0;
}
-.navbar-gitlab .header-content .title-container {
- display: flex;
- align-items: stretch;
- flex: 1 1 auto;
- padding-top: 0;
- overflow: visible;
-}
.navbar-gitlab .header-content .title {
padding-right: 0;
color: currentColor;
@@ -803,9 +779,12 @@ input {
display: flex;
align-items: center;
padding: 2px 8px;
- margin: 5px 2px 5px -8px;
+ margin: 4px 2px 4px -12px;
border-radius: 4px;
}
+.navbar-gitlab .header-content .title .canary-badge {
+ margin-left: -8px;
+}
.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
margin: 0 2px;
}
@@ -839,7 +818,7 @@ input {
border-radius: 0;
min-width: 45px;
padding: 0;
- margin: 8px -7px 8px 0;
+ margin: 8px 8px 8px 0;
font-size: 14px;
text-align: center;
color: currentColor;
@@ -983,6 +962,16 @@ input {
.top-nav-toggle .dropdown-icon {
margin-right: 0.5rem;
}
+.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;
@@ -1499,8 +1488,16 @@ svg.s12 {
svg.s16 {
vertical-align: -3px;
}
+.header-content .header-search-new {
+ max-width: 600px;
+}
.header-search {
- width: 320px;
+ min-width: 320px;
+}
+@media (min-width: 768px) and (max-width: 1199.98px) {
+ .header-search {
+ min-width: 200px;
+ }
}
.header-search.is-not-active::after {
content: "/";
@@ -1521,6 +1518,7 @@ svg.s16 {
border-radius: 3px;
box-shadow: none;
white-space: pre-wrap;
+ box-sizing: border-box;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: keep-all;
@@ -1706,17 +1704,47 @@ svg.s16 {
.gl-display-none {
display: none;
}
+@media (min-width: 992px) {
+ .gl-lg-display-none\! {
+ display: none !important;
+ }
+}
+.gl-display-flex {
+ display: flex;
+}
+@media (min-width: 992px) {
+ .gl-lg-display-flex {
+ display: flex;
+ }
+}
@media (min-width: 576px) {
.gl-sm-display-block {
display: block;
}
}
+@media (min-width: 992px) {
+ .gl-lg-display-block {
+ display: block;
+ }
+}
+.gl-display-inline-block\! {
+ display: inline-block !important;
+}
+.gl-align-items-stretch {
+ align-items: stretch;
+}
+.gl-flex-grow-1 {
+ flex-grow: 1;
+}
.gl-relative {
position: relative;
}
.gl-absolute {
position: absolute;
}
+.gl-w-full {
+ width: 100%;
+}
.gl-px-3 {
padding-left: 0.5rem;
padding-right: 0.5rem;
@@ -1724,6 +1752,9 @@ svg.s16 {
.gl-pr-2 {
padding-right: 0.25rem;
}
+.gl-pt-0 {
+ padding-top: 0;
+}
.gl-ml-n2 {
margin-left: -0.25rem;
}
@@ -1734,6 +1765,12 @@ svg.s16 {
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;
}
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
index 751ad26ca21..020ed9c040b 100644
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -138,9 +138,8 @@ hr {
margin-right: -15px;
margin-left: -15px;
}
+.col-md-6,
.col-sm-12,
-.col-sm-7,
-.col-sm-5,
.col {
position: relative;
width: 100%;
@@ -159,14 +158,6 @@ hr {
order: 12;
}
@media (min-width: 576px) {
- .col-sm-5 {
- flex: 0 0 41.6666666667%;
- max-width: 41.6666666667%;
- }
- .col-sm-7 {
- flex: 0 0 58.3333333333%;
- max-width: 58.3333333333%;
- }
.col-sm-12 {
flex: 0 0 100%;
max-width: 100%;
@@ -178,6 +169,12 @@ hr {
order: 12;
}
}
+@media (min-width: 768px) {
+ .col-md-6 {
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+}
.form-control {
display: block;
width: 100%;
@@ -354,8 +351,7 @@ fieldset:disabled a.btn {
background-color: #fff;
}
.gl-button.gl-button.btn-default:active {
- box-shadow: inset 0 0 0 2px #5e5e5e, 0 0 0 1px rgba(255, 255, 255, 0.4),
- 0 0 0 4px rgba(31, 117, 203, 0.48);
+ box-shadow: inset 0 0 0 1px #5e5e5e, 0 0 0 1px #fff, 0 0 0 3px #428fdc;
outline: none;
background-color: #dbdbdb;
}
@@ -367,8 +363,7 @@ fieldset:disabled a.btn {
box-shadow: inset 0 0 0 1px #1068bf;
}
.gl-button.gl-button.btn-confirm:active {
- box-shadow: inset 0 0 0 2px #033464, 0 0 0 1px rgba(255, 255, 255, 0.4),
- 0 0 0 4px rgba(31, 117, 203, 0.48);
+ box-shadow: inset 0 0 0 1px #033464, 0 0 0 1px #fff, 0 0 0 3px #428fdc;
outline: none;
background-color: #0b5cad;
}
@@ -519,6 +514,16 @@ label.label-bold {
.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: #868686;
opacity: 1;
@@ -568,7 +573,6 @@ svg {
.login-page .omniauth-container {
box-shadow: 0 0 0 1px #dbdbdb;
border-radius: 0.25rem;
- padding: 15px;
}
.login-page .login-box .login-heading h3,
.login-page .omniauth-container .login-heading h3 {
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index 550e3981401..6a9e96c3ac5 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -99,6 +99,7 @@ $white-normal: #333;
$white-dark: #444;
$border-color: #4f4f4f;
+$nav-active-bg: rgba(255, 255, 255, 0.08);
body.gl-dark {
--gray-10: #{$gray-10};
@@ -199,6 +200,7 @@ body.gl-dark {
--black: #{$black};
--svg-status-bg: #{$white};
+ --nav-active-bg: #{$nav-active-bg};
.gl-button.gl-button,
.gl-button.gl-button.btn-block {
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index 83254fe1a52..dbb961fe71f 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -71,10 +71,13 @@
body.gl-dark {
@include gitlab-theme($gray-900, $gray-400, $gray-500, $gray-900, $gray-900, $white);
- .logo-text svg {
- fill: var(--gl-text-color);
+ .terms {
+ .logo-text {
+ fill: var(--black);
+ }
}
+
.navbar-gitlab {
background-color: var(--gray-50);
box-shadow: 0 1px 0 0 var(--gray-100);
diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss
index 07194e2b532..234010074aa 100644
--- a/app/assets/stylesheets/themes/theme_helper.scss
+++ b/app/assets/stylesheets/themes/theme_helper.scss
@@ -150,6 +150,7 @@
.header-search {
background-color: rgba($search-and-nav-links, 0.2) !important;
+ border-radius: $border-radius-default;
&:hover {
background-color: rgba($search-and-nav-links, 0.3) !important;
diff --git a/app/assets/stylesheets/themes/theme_light.scss b/app/assets/stylesheets/themes/theme_light.scss
index f2fdd499781..66b2b3c3437 100644
--- a/app/assets/stylesheets/themes/theme_light.scss
+++ b/app/assets/stylesheets/themes/theme_light.scss
@@ -15,8 +15,8 @@ body {
background-color: $gray-50;
box-shadow: 0 1px 0 0 $border-color;
- .logo-text svg {
- fill: $gray-900;
+ .logo-text {
+ fill: #171321;
}
.navbar-sub-nav,
@@ -48,6 +48,7 @@ body {
.header-search {
background-color: $white !important;
box-shadow: inset 0 0 0 1px $border-color !important;
+ border-radius: $border-radius-default;
&:hover {
background-color: $white !important;
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index d7a5e21e303..fd85ff894a7 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -366,3 +366,30 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
/* stylelint-disable property-no-vendor-prefix */
-webkit-backdrop-filter: blur(2px); // still required by Safari
}
+
+/*
+ * The below style will be moved to @gitlab/ui by
+ * https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1792
+ */
+.gl-text-purple-800 {
+ color: $purple-800;
+}
+
+.gl-bg-theme-indigo-800 {
+ background-color: $theme-indigo-800;
+}
+
+.gl-border-indigo-700 {
+ border-color: $theme-indigo-700;
+}
+
+.gl-border-gray-75 {
+ border-color: $gl-text-color-quaternary;
+}
+
+.gl-min-h-8 {
+ min-height: $gl-spacing-scale-8;
+}
+
+/* End gitlab-ui#1751 */
+
diff --git a/app/components/diffs/overflow_warning_component.html.haml b/app/components/diffs/overflow_warning_component.html.haml
index 907d066e73d..b184fa1d527 100644
--- a/app/components/diffs/overflow_warning_component.html.haml
+++ b/app/components/diffs/overflow_warning_component.html.haml
@@ -1,9 +1,9 @@
= render Pajamas::AlertComponent.new(title: _('Too many changes to show.'),
variant: :warning,
- alert_class: 'gl-mb-5') do
- .gl-alert-body
+ alert_class: 'gl-mb-5') do |c|
+ = c.body do
= message
- .gl-alert-actions
+ = c.actions do
= diff_link
= patch_link
diff --git a/app/components/pajamas/alert_component.html.haml b/app/components/pajamas/alert_component.html.haml
index a1d3c700e57..782ac8b9ca2 100644
--- a/app/components/pajamas/alert_component.html.haml
+++ b/app/components/pajamas/alert_component.html.haml
@@ -1,5 +1,6 @@
-.gl-alert{ role: 'alert', class: ["gl-alert-#{@variant}", @alert_class], data: @alert_data }
- = sprite_icon(icon, css_class: icon_classes)
+.gl-alert{ role: 'alert', class: [base_class, @alert_class], data: @alert_data }
+ - if @show_icon
+ = sprite_icon(icon, css_class: icon_classes)
- if @dismissible
%button.btn.gl-dismiss-btn.btn-default.btn-sm.gl-button.btn-default-tertiary.btn-icon.js-close{ type: 'button',
aria: { label: _('Dismiss') },
@@ -10,4 +11,9 @@
- if @title
%h4.gl-alert-title
= @title
- = content
+ - if body?
+ .gl-alert-body
+ = body
+ - if actions?
+ .gl-alert-actions
+ = actions
diff --git a/app/components/pajamas/alert_component.rb b/app/components/pajamas/alert_component.rb
index 4bb6c41661b..c1b2132da29 100644
--- a/app/components/pajamas/alert_component.rb
+++ b/app/components/pajamas/alert_component.rb
@@ -6,26 +6,39 @@ module Pajamas
# @param [String] title
# @param [Symbol] variant
# @param [Boolean] dismissible
+ # @param [Boolean] show_icon
# @param [String] alert_class
# @param [Hash] alert_data
# @param [String] close_button_class
# @param [Hash] close_button_data
def initialize(
- title: nil, variant: :info, dismissible: true,
+ title: nil, variant: :info, dismissible: true, show_icon: true,
alert_class: nil, alert_data: {}, close_button_class: nil, close_button_data: {})
@title = title
@variant = variant
@dismissible = dismissible
+ @show_icon = show_icon
@alert_class = alert_class
@alert_data = alert_data
@close_button_class = close_button_class
@close_button_data = close_button_data
end
+ def base_class
+ classes = ["gl-alert-#{@variant}"]
+ classes.push('gl-alert-not-dismissible') unless @dismissible
+ classes.push('gl-alert-no-icon') unless @show_icon
+
+ classes.join(' ')
+ end
+
private
delegate :sprite_icon, to: :helpers
+ renders_one :body
+ renders_one :actions
+
ICONS = {
info: 'information-o',
warning: 'warning',
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 75d1e4bf6a0..253fca0a253 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -27,6 +27,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 :integrations, [:integrations]
feature_category :pages, [:lets_encrypt_terms_of_service]
@@ -53,7 +54,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def service_usage_data
- @service_ping_data_present = Rails.cache.exist?('usage_data')
+ @service_ping_data_present = prerecorded_service_ping_data.present?
end
def update
@@ -63,7 +64,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def usage_data
respond_to do |format|
format.html do
- usage_data_json = Gitlab::Json.pretty_generate(Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values, cached: true))
+ usage_data_json = Gitlab::Json.pretty_generate(service_ping_data)
render html: Gitlab::Highlight.highlight('payload.json', usage_data_json, language: 'json')
end
@@ -71,7 +72,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
format.json do
Gitlab::UsageDataCounters::ServiceUsageDataCounter.count(:download_payload_click)
- render json: Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values, cached: true).to_json
+ render json: service_ping_data.to_json
end
end
end
@@ -255,6 +256,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:raw_blob_request_limit,
:issues_create_limit,
:notes_create_limit,
+ :pipeline_limit_per_project_user_sha,
:default_branch_name,
disabled_oauth_sign_in_sources: [],
import_sources: [],
@@ -306,6 +308,14 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def valid_setting_panels
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
+ end
end
Admin::ApplicationSettingsController.prepend_mod_with('Admin::ApplicationSettingsController')
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index 83413afb6b7..a6a21cf3649 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -19,10 +19,7 @@ class Admin::ApplicationsController < Admin::ApplicationController
end
def new
- # Default access tokens to expire. This preserves backward compatibility
- # with existing applications. This will be removed in 15.0.
- # Removal issue: https://gitlab.com/gitlab-org/gitlab/-/issues/340848
- @application = Doorkeeper::Application.new(expire_access_tokens: true)
+ @application = Doorkeeper::Application.new
end
def edit
diff --git a/app/controllers/admin/background_migrations_controller.rb b/app/controllers/admin/background_migrations_controller.rb
index 42b89a3317e..c6c9e0ced22 100644
--- a/app/controllers/admin/background_migrations_controller.rb
+++ b/app/controllers/admin/background_migrations_controller.rb
@@ -2,6 +2,9 @@
class Admin::BackgroundMigrationsController < Admin::ApplicationController
feature_category :database
+ urgency :low
+
+ around_action :support_multiple_databases
def index
@relations_by_tab = {
@@ -13,6 +16,13 @@ class Admin::BackgroundMigrationsController < Admin::ApplicationController
@current_tab = @relations_by_tab.key?(params[:tab]) ? params[:tab] : 'queued'
@migrations = @relations_by_tab[@current_tab].page(params[:page])
@successful_rows_counts = batched_migration_class.successful_rows_counts(@migrations.map(&:id))
+ @databases = Gitlab::Database.db_config_names
+ end
+
+ def show
+ @migration = batched_migration_class.find(params[:id])
+
+ @failed_jobs = @migration.batched_jobs.with_status(:failed).page(params[:page])
end
def pause
@@ -38,6 +48,18 @@ class Admin::BackgroundMigrationsController < Admin::ApplicationController
private
+ def support_multiple_databases
+ Gitlab::Database::SharedModel.using_connection(base_model.connection) do
+ yield
+ end
+ end
+
+ def base_model
+ @selected_database = params[:database] || Gitlab::Database::MAIN_DATABASE_NAME
+
+ Gitlab::Database.database_base_models[@selected_database]
+ end
+
def batched_migration_class
@batched_migration_class ||= Gitlab::Database::BackgroundMigration::BatchedMigration
end
diff --git a/app/controllers/admin/batched_jobs_controller.rb b/app/controllers/admin/batched_jobs_controller.rb
new file mode 100644
index 00000000000..0a00ba13dc8
--- /dev/null
+++ b/app/controllers/admin/batched_jobs_controller.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class Admin::BatchedJobsController < Admin::ApplicationController
+ feature_category :database
+ urgency :low
+
+ around_action :support_multiple_databases
+
+ def show
+ @job = Gitlab::Database::BackgroundMigration::BatchedJob.find(params[:id])
+
+ @transition_logs = @job.batched_job_transition_logs
+ end
+
+ private
+
+ def support_multiple_databases
+ Gitlab::Database::SharedModel.using_connection(base_model.connection) do
+ yield
+ end
+ end
+
+ def base_model
+ @selected_database = params[:database] || Gitlab::Database::MAIN_DATABASE_NAME
+
+ Gitlab::Database.database_base_models[@selected_database]
+ end
+end
diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb
index ed63e65d4df..b24b25446b0 100644
--- a/app/controllers/admin/deploy_keys_controller.rb
+++ b/app/controllers/admin/deploy_keys_controller.rb
@@ -5,6 +5,7 @@ class Admin::DeployKeysController < Admin::ApplicationController
before_action :deploy_key, only: [:destroy, :edit, :update]
feature_category :continuous_delivery
+ urgency :low
def index
end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 9abb244bc92..4d163824ef6 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -61,7 +61,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
def members_update
- member_params = params.permit(:user_ids, :access_level, :expires_at)
+ member_params = params.permit(:user_id, :access_level, :expires_at)
result = Members::CreateService.new(current_user, member_params.merge(limit: -1, source: @group, invite_source: 'admin-group-page')).execute
if result[:status] == :success
diff --git a/app/controllers/admin/jobs_controller.rb b/app/controllers/admin/jobs_controller.rb
index b800ca79d6b..ef9264d1615 100644
--- a/app/controllers/admin/jobs_controller.rb
+++ b/app/controllers/admin/jobs_controller.rb
@@ -4,6 +4,7 @@ class Admin::JobsController < Admin::ApplicationController
BUILDS_PER_PAGE = 30
feature_category :continuous_integration
+ urgency :low
def index
# We need all builds for tabs counters
diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb
index 822b7a93c9c..4747f3c5dea 100644
--- a/app/controllers/admin/labels_controller.rb
+++ b/app/controllers/admin/labels_controller.rb
@@ -4,6 +4,7 @@ class Admin::LabelsController < Admin::ApplicationController
before_action :set_label, only: [:show, :edit, :update, :destroy]
feature_category :team_planning
+ urgency :low
def index
@labels = Label.templates.page(params[:page])
diff --git a/app/controllers/admin/requests_profiles_controller.rb b/app/controllers/admin/requests_profiles_controller.rb
deleted file mode 100644
index b60cb7ff9c2..00000000000
--- a/app/controllers/admin/requests_profiles_controller.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-class Admin::RequestsProfilesController < Admin::ApplicationController
- feature_category :not_owned # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
-
- def index
- @profile_token = Gitlab::RequestProfiler.profile_token
- @profiles = Gitlab::RequestProfiler.all.group_by(&:request_path)
- end
-
- def show
- clean_name = Rack::Utils.clean_path_info(params[:name])
- profile = Gitlab::RequestProfiler.find(clean_name)
-
- unless profile && profile.content_type
- return redirect_to admin_requests_profiles_path, alert: 'Profile not found'
- end
-
- send_file profile.file_path, type: "#{profile.content_type}; charset=utf-8", disposition: 'inline'
- end
-end
diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb
index a4055cbe990..0165c6471db 100644
--- a/app/controllers/admin/runner_projects_controller.rb
+++ b/app/controllers/admin/runner_projects_controller.rb
@@ -4,6 +4,7 @@ class Admin::RunnerProjectsController < Admin::ApplicationController
before_action :project, only: [:create]
feature_category :runner
+ urgency :low
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index 06880ace899..02e33baaf07 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -5,24 +5,20 @@ class Admin::RunnersController < Admin::ApplicationController
before_action :runner, except: [:index, :tag_list, :runner_setup_scripts]
before_action only: [:index] do
- push_frontend_feature_flag(:admin_runners_bulk_delete, default_enabled: :yaml)
+ push_frontend_feature_flag(:admin_runners_bulk_delete)
end
feature_category :runner
+ urgency :low
def index
end
def show
- # We will show runner details in a read-only view in
- # future iterations. For now, this route will have a
- # redirect until this new view is developed. See more:
- # https://gitlab.com/gitlab-org/gitlab/-/issues/347856
- redirect_to edit_admin_runner_path(runner) unless Feature.enabled?(:runner_read_only_admin_view, default_enabled: :yaml)
end
def edit
- assign_builds_and_projects
+ assign_projects
end
def update
@@ -31,7 +27,7 @@ class Admin::RunnersController < Admin::ApplicationController
format.html { redirect_to edit_admin_runner_path(@runner) }
end
else
- assign_builds_and_projects
+ assign_projects
render 'show'
end
end
@@ -87,12 +83,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
# rubocop: disable CodeReuse/ActiveRecord
- def assign_builds_and_projects
- @builds = runner
- .builds
- .order_id_desc
- .preload_project_and_pipeline_project.first(30)
-
+ def assign_projects
@projects =
if params[:search].present?
::Project.search(params[:search])
diff --git a/app/controllers/admin/sessions_controller.rb b/app/controllers/admin/sessions_controller.rb
index 9c378f4c883..63579421573 100644
--- a/app/controllers/admin/sessions_controller.rb
+++ b/app/controllers/admin/sessions_controller.rb
@@ -68,7 +68,7 @@ class Admin::SessionsController < ApplicationController
def valid_otp_attempt?(user)
otp_validation_result =
- ::Users::ValidateOtpService.new(user).execute(user_params[:otp_attempt])
+ ::Users::ValidateManualOtpService.new(user).execute(user_params[:otp_attempt])
valid_otp_attempt = otp_validation_result[:status] == :success
return valid_otp_attempt if Gitlab::Database.read_only?
diff --git a/app/controllers/admin/topics_controller.rb b/app/controllers/admin/topics_controller.rb
index ccc38ba7cd5..908313bdb83 100644
--- a/app/controllers/admin/topics_controller.rb
+++ b/app/controllers/admin/topics_controller.rb
@@ -51,7 +51,8 @@ class Admin::TopicsController < Admin::ApplicationController
[
:avatar,
:description,
- :name
+ :name,
+ :title
]
end
end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index f19333d5d57..6b11b8eda5c 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -372,7 +372,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def check_ban_user_feature_flag
- access_denied! unless Feature.enabled?(:ban_user_feature_flag, default_enabled: :yaml)
+ access_denied! unless Feature.enabled?(:ban_user_feature_flag)
end
def log_impersonation_event
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 572ec40ef16..4fc96752507 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -286,6 +286,13 @@ class ApplicationController < ActionController::Base
end
end
+ def render_409(message = nil)
+ respond_to do |format|
+ format.html { render template: "errors/request_conflict", formats: :html, layout: "errors", status: :conflict, locals: { message: message } }
+ format.any { head :conflict }
+ end
+ end
+
def respond_422
head :unprocessable_entity
end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 663e3cf8648..f84d2ed320d 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -12,8 +12,9 @@ class AutocompleteController < ApplicationController
feature_category :code_review, [:merge_request_target_branches]
feature_category :continuous_delivery, [:deploy_keys_with_owners]
- urgency :low, [:merge_request_target_branches]
- urgency :default, [:users]
+ urgency :low, [:merge_request_target_branches, :deploy_keys_with_owners, :users]
+ urgency :low, [:award_emojis]
+ urgency :medium, [:projects]
def users
group = Autocomplete::GroupFinder
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index e7ae941886d..11377df7a10 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -22,6 +22,7 @@ module Boards
before_action :can_move_issues?, only: [:bulk_move]
feature_category :team_planning
+ urgency :low
def index
list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params)
diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb
index 696b251301f..c3b5a887920 100644
--- a/app/controllers/boards/lists_controller.rb
+++ b/app/controllers/boards/lists_controller.rb
@@ -9,6 +9,7 @@ module Boards
skip_before_action :authenticate_user!, only: [:index]
feature_category :team_planning
+ urgency :low
def index
lists = Boards::Lists::ListService.new(board.resource_parent, current_user).execute(board)
diff --git a/app/controllers/clusters/base_controller.rb b/app/controllers/clusters/base_controller.rb
index f88d381b3bf..2401d8b1044 100644
--- a/app/controllers/clusters/base_controller.rb
+++ b/app/controllers/clusters/base_controller.rb
@@ -9,6 +9,10 @@ class Clusters::BaseController < ApplicationController
helper_method :clusterable
feature_category :kubernetes_management
+ urgency :low, [
+ :index, :show, :environments, :cluster_status, :prometheus_proxy,
+ :destroy, :new_cluster_docs, :connect, :new, :create_user
+ ]
private
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index 939c0ef220c..ae3b6125bde 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -6,12 +6,9 @@ class Clusters::ClustersController < Clusters::BaseController
include MetricsDashboard
before_action :cluster, only: [:cluster_status, :show, :update, :destroy, :clear_cache]
- before_action :generate_gcp_authorize_url, only: [:new]
- before_action :validate_gcp_token, only: [:new]
- before_action :gcp_cluster, only: [:new]
- before_action :user_cluster, only: [:new, :connect]
+ before_action :user_cluster, only: [:connect]
before_action :authorize_read_cluster!, only: [:show, :index]
- before_action :authorize_create_cluster!, only: [:new, :connect, :authorize_aws_role]
+ before_action :authorize_create_cluster!, only: [:connect, :authorize_aws_role]
before_action :authorize_update_cluster!, only: [:update]
before_action :update_applications_status, only: [:cluster_status]
before_action :ensure_feature_enabled!, except: [:index, :new_cluster_docs]
@@ -46,16 +43,6 @@ class Clusters::ClustersController < Clusters::BaseController
end
end
- def new
- if params[:provider] == 'aws'
- @aws_role = Aws::Role.create_or_find_by!(user: current_user)
- @instance_types = load_instance_types.to_json
-
- elsif params[:provider] == 'gcp'
- redirect_to @authorize_url if @authorize_url && !@valid_gcp_token
- end
- end
-
# Overridding ActionController::Metal#status is NOT a good idea
def cluster_status
respond_to do |format|
@@ -108,24 +95,6 @@ class Clusters::ClustersController < Clusters::BaseController
redirect_to clusterable.index_path, status: :found
end
- def create_gcp
- @gcp_cluster = ::Clusters::CreateService
- .new(current_user, create_gcp_cluster_params)
- .execute(access_token: token_in_session)
- .present(current_user: current_user)
-
- if @gcp_cluster.persisted?
- redirect_to @gcp_cluster.show_path
- else
- generate_gcp_authorize_url
- validate_gcp_token
- user_cluster
- params[:provider] = 'gcp'
-
- render :new, locals: { active_tab: 'create' }
- end
- end
-
def create_aws
@aws_cluster = ::Clusters::CreateService
.new(current_user, create_aws_cluster_params)
@@ -173,16 +142,12 @@ class Clusters::ClustersController < Clusters::BaseController
private
- def certificate_based_clusters_enabled?
- Feature.enabled?(:certificate_based_clusters, clusterable, default_enabled: :yaml, type: :ops)
- end
-
def ensure_feature_enabled!
- render_404 unless certificate_based_clusters_enabled?
+ render_404 unless clusterable.certificate_based_clusters_enabled?
end
def cluster_list
- return [] unless certificate_based_clusters_enabled?
+ return [] unless clusterable.certificate_based_clusters_enabled?
finder = ClusterAncestorsFinder.new(clusterable.__subject__, current_user)
clusters = finder.execute
@@ -239,24 +204,6 @@ class Clusters::ClustersController < Clusters::BaseController
end
end
- def create_gcp_cluster_params
- params.require(:cluster).permit(
- *base_permitted_cluster_params,
- :name,
- provider_gcp_attributes: [
- :gcp_project_id,
- :zone,
- :num_nodes,
- :machine_type,
- :cloud_run,
- :legacy_abac
- ]).merge(
- provider_type: :gcp,
- platform_type: :kubernetes,
- clusterable: clusterable.__subject__
- )
- end
-
def create_aws_cluster_params
params.require(:cluster).permit(
*base_permitted_cluster_params,
@@ -300,10 +247,10 @@ class Clusters::ClustersController < Clusters::BaseController
end
def generate_gcp_authorize_url
- new_path = clusterable.new_path(provider: :gcp).to_s
- error_path = @project ? project_clusters_path(@project) : new_path
+ connect_path = clusterable.connect_path().to_s
+ error_path = @project ? project_clusters_path(@project) : connect_path
- state = generate_session_key_redirect(new_path, error_path)
+ state = generate_session_key_redirect(connect_path, error_path)
@authorize_url = GoogleApi::CloudPlatform::Client.new(
nil, callback_google_api_auth_url,
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index 14dcec33545..4228a93d310 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -23,9 +23,9 @@ module AuthenticatesWithTwoFactor
session[:otp_user_id] = user.id
session[:user_password_hash] = Digest::SHA256.hexdigest(user.encrypted_password)
- push_frontend_feature_flag(:webauthn, default_enabled: :yaml)
+ push_frontend_feature_flag(:webauthn)
- if Feature.enabled?(:webauthn, default_enabled: :yaml)
+ if Feature.enabled?(:webauthn)
setup_webauthn_authentication(user)
else
setup_u2f_authentication(user)
diff --git a/app/controllers/concerns/authenticates_with_two_factor_for_admin_mode.rb b/app/controllers/concerns/authenticates_with_two_factor_for_admin_mode.rb
index 05be04059fd..574fc6c0f37 100644
--- a/app/controllers/concerns/authenticates_with_two_factor_for_admin_mode.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor_for_admin_mode.rb
@@ -11,7 +11,7 @@ module AuthenticatesWithTwoFactorForAdminMode
return handle_locked_user(user) unless user.can?(:log_in)
session[:otp_user_id] = user.id
- push_frontend_feature_flag(:webauthn, default_enabled: :yaml)
+ push_frontend_feature_flag(:webauthn)
if user.two_factor_webauthn_enabled?
setup_webauthn_authentication(user)
diff --git a/app/controllers/concerns/dependency_proxy/group_access.rb b/app/controllers/concerns/dependency_proxy/group_access.rb
index 44611641529..45392625e45 100644
--- a/app/controllers/concerns/dependency_proxy/group_access.rb
+++ b/app/controllers/concerns/dependency_proxy/group_access.rb
@@ -18,9 +18,5 @@ module DependencyProxy
def authorize_read_dependency_proxy!
access_denied! unless can?(auth_user, :read_dependency_proxy, group)
end
-
- def authorize_admin_dependency_proxy!
- access_denied! unless can?(auth_user, :admin_dependency_proxy, group)
- end
end
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index ae90bd59d01..4d3eb9cd183 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -184,7 +184,7 @@ module IssuableActions
def paginated_discussions
return if params[:per_page].blank?
- return unless issuable.instance_of?(Issue) && Feature.enabled?(:paginated_issue_discussions, project, default_enabled: :yaml)
+ return unless issuable.instance_of?(Issue) && Feature.enabled?(:paginated_issue_discussions, project)
strong_memoize(:paginated_discussions) do
issuable
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 8410a8779f6..55b6747fcfb 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -65,7 +65,7 @@ module NotesActions
json.merge!(note_json(@note))
end
- if @note.errors.present? && @note.errors.attribute_names != [:commands_only]
+ if @note.errors.present? && @note.errors.attribute_names != [:commands_only, :command_names]
render json: json, status: :unprocessable_entity
else
render json: json
diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb
index 794307ebb0c..8e63cc391ff 100644
--- a/app/controllers/concerns/oauth_applications.rb
+++ b/app/controllers/concerns/oauth_applications.rb
@@ -30,7 +30,7 @@ module OauthApplications
end
def permitted_params
- %i{name redirect_uri scopes confidential expire_access_tokens}
+ %i{name redirect_uri scopes confidential}
end
def application_params
diff --git a/app/controllers/concerns/product_analytics_tracking.rb b/app/controllers/concerns/product_analytics_tracking.rb
index 03296d6b233..4021ff83578 100644
--- a/app/controllers/concerns/product_analytics_tracking.rb
+++ b/app/controllers/concerns/product_analytics_tracking.rb
@@ -20,7 +20,7 @@ module ProductAnalyticsTracking
def route_events_to(destinations, name, &block)
track_unique_redis_hll_event(name, &block) if destinations.include?(:redis_hll)
- if destinations.include?(:snowplow) && Feature.enabled?(:route_hll_to_snowplow, tracking_namespace_source, default_enabled: :yaml)
+ if destinations.include?(:snowplow) && Feature.enabled?(:route_hll_to_snowplow, tracking_namespace_source)
Gitlab::Tracking.event(self.class.to_s, name, namespace: tracking_namespace_source, user: current_user)
end
end
diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb
index 8b053ef7c59..c8369c465b8 100644
--- a/app/controllers/concerns/send_file_upload.rb
+++ b/app/controllers/concerns/send_file_upload.rb
@@ -71,6 +71,6 @@ module SendFileUpload
end
def scaling_allowed_by_feature_flags?(file_upload)
- Feature.enabled?(:dynamic_image_resizing, default_enabled: true, type: :ops)
+ Feature.enabled?(:dynamic_image_resizing, type: :ops)
end
end
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index c9b6e8923fe..f914e804e18 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -143,11 +143,17 @@ module UploadsActions
end
def bypass_auth_checks_on_uploads?
- if ::Feature.enabled?(:enforce_auth_checks_on_uploads, project, default_enabled: :yaml)
- false
- else
- action_name == 'show' && embeddable?
+ if ::Feature.enabled?(:enforce_auth_checks_on_uploads, target_project)
+ if target_project && !target_project.public? && target_project.enforce_auth_checks_on_uploads?
+ return false
+ end
end
+
+ action_name == 'show' && embeddable?
+ end
+
+ def target_project
+ nil
end
def find_model
diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb
index 91de1d8aeae..9fc8886aaee 100644
--- a/app/controllers/concerns/wiki_actions.rb
+++ b/app/controllers/concerns/wiki_actions.rb
@@ -21,6 +21,10 @@ module WikiActions
before_action :load_sidebar, except: [:pages]
before_action :set_content_class
+ before_action do
+ push_frontend_feature_flag(:preserve_unchanged_markdown, @group)
+ end
+
before_action only: [:show, :edit, :update] do
@valid_encoding = valid_encoding?
end
diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
index 23ffcd50369..552d74686d6 100644
--- a/app/controllers/dashboard/groups_controller.rb
+++ b/app/controllers/dashboard/groups_controller.rb
@@ -7,6 +7,8 @@ class Dashboard::GroupsController < Dashboard::ApplicationController
feature_category :subgroups
+ urgency :low, [:index]
+
def index
groups = GroupsFinder.new(current_user, all_available: false).execute
render_group_tree(groups)
diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb
index d2f31258ecd..d23518cf051 100644
--- a/app/controllers/dashboard/labels_controller.rb
+++ b/app/controllers/dashboard/labels_controller.rb
@@ -2,6 +2,7 @@
class Dashboard::LabelsController < Dashboard::ApplicationController
feature_category :team_planning
+ urgency :low
def index
respond_to do |format|
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 34d9739d91c..2cb2d6bbe23 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -5,6 +5,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
before_action :groups, only: :index
feature_category :team_planning
+ urgency :low
def index
respond_to do |format|
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 4d6c7a63516..0e4592259d8 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -15,6 +15,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
skip_cross_project_access_check :index, :starred
feature_category :projects
+ urgency :low, [:starred, :index]
def index
respond_to do |format|
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 2c5e6817427..d2434d4b0ba 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -9,6 +9,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
before_action :find_todos, only: [:index, :destroy_all]
feature_category :team_planning
+ urgency :low
def index
@sort = params[:sort]
@@ -98,6 +99,14 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end
def todo_params
- params.permit(:action_id, :author_id, :project_id, :type, :sort, :state, :group_id)
+ aliased_action_id(
+ params.permit(:action_id, :author_id, :project_id, :type, :sort, :state, :group_id)
+ )
+ end
+
+ def aliased_action_id(original_params)
+ return original_params unless original_params[:action_id].to_i == ::Todo::MENTIONED
+
+ original_params.merge(action_id: [::Todo::MENTIONED, ::Todo::DIRECTLY_ADDRESSED])
end
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index f25cc1bbc32..82e5bb6cd7c 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -18,7 +18,8 @@ class DashboardController < Dashboard::ApplicationController
feature_category :team_planning, [:issues, :issues_calendar]
feature_category :code_review, [:merge_requests]
- urgency :low, [:merge_requests]
+ urgency :low, [:merge_requests, :activity]
+ urgency :low, [:issues, :issues_calendar]
def activity
respond_to do |format|
diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb
index aa4196b1c18..97791b43d41 100644
--- a/app/controllers/explore/groups_controller.rb
+++ b/app/controllers/explore/groups_controller.rb
@@ -4,6 +4,7 @@ class Explore::GroupsController < Explore::ApplicationController
include GroupTree
feature_category :subgroups
+ urgency :low
def index
render_group_tree GroupsFinder.new(current_user).execute
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 23e0143506e..34745815f3d 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -24,9 +24,9 @@ class Explore::ProjectsController < Explore::ApplicationController
rescue_from PageOutOfBoundsError, with: :page_out_of_bounds
feature_category :projects
-
# TODO: Set higher urgency after addressing https://gitlab.com/gitlab-org/gitlab/-/issues/357913
- urgency :low, [:index]
+ # and https://gitlab.com/gitlab-org/gitlab/-/issues/358945
+ urgency :low, [:index, :topics, :trending, :starred, :topic]
def index
show_alert_if_search_is_disabled
diff --git a/app/controllers/google_api/authorizations_controller.rb b/app/controllers/google_api/authorizations_controller.rb
index b9c5e87c69c..5080ee5fbbe 100644
--- a/app/controllers/google_api/authorizations_controller.rb
+++ b/app/controllers/google_api/authorizations_controller.rb
@@ -7,6 +7,7 @@ module GoogleApi
before_action :validate_session_key!
feature_category :kubernetes_management
+ urgency :low
##
# handle the response from google after the user
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index b00d85b6b0f..c71c101b434 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -64,7 +64,7 @@ class GraphqlController < ApplicationController
log_exception(exception)
if Rails.env.test? || Rails.env.development?
- render_error("Internal server error: #{exception.message}")
+ render_error("Internal server error: #{exception.message}", raised_at: exception.backtrace[0..10].join(' <-- '))
else
render_error("Internal server error")
end
@@ -207,8 +207,9 @@ class GraphqlController < ApplicationController
render_error("Not found!", status: :not_found)
end
- def render_error(message, status: 500)
+ def render_error(message, status: 500, raised_at: nil)
error = { errors: [message: message] }
+ error[:errors].first['raisedAt'] = raised_at if raised_at
render json: error, status: status
end
diff --git a/app/controllers/groups/autocomplete_sources_controller.rb b/app/controllers/groups/autocomplete_sources_controller.rb
index 17cdcd9cb9b..a2eb475d360 100644
--- a/app/controllers/groups/autocomplete_sources_controller.rb
+++ b/app/controllers/groups/autocomplete_sources_controller.rb
@@ -5,6 +5,7 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController
feature_category :team_planning, [:issues, :labels, :milestones, :commands]
feature_category :code_review, [:merge_requests]
+ urgency :low, [:issues, :labels, :milestones, :commands]
urgency :low, [:merge_requests]
def members
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index c65232c0fea..0fbceb43be1 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -7,8 +7,8 @@ class Groups::BoardsController < Groups::ApplicationController
before_action :assign_endpoint_vars
before_action do
- push_frontend_feature_flag(:board_multi_select, group, default_enabled: :yaml)
- push_frontend_feature_flag(:realtime_labels, group, default_enabled: :yaml)
+ push_frontend_feature_flag(:board_multi_select, group)
+ push_frontend_feature_flag(:realtime_labels, group)
experiment(:prominent_create_board_btn, subject: current_user) do |e|
e.control { }
e.candidate { }
@@ -16,6 +16,7 @@ class Groups::BoardsController < Groups::ApplicationController
end
feature_category :team_planning
+ urgency :low
private
diff --git a/app/controllers/groups/crm/contacts_controller.rb b/app/controllers/groups/crm/contacts_controller.rb
index b59e20d9cea..5bc927911c1 100644
--- a/app/controllers/groups/crm/contacts_controller.rb
+++ b/app/controllers/groups/crm/contacts_controller.rb
@@ -2,6 +2,7 @@
class Groups::Crm::ContactsController < Groups::ApplicationController
feature_category :team_planning
+ urgency :low
before_action :validate_root_group!
before_action :authorize_read_crm_contact!
diff --git a/app/controllers/groups/crm/organizations_controller.rb b/app/controllers/groups/crm/organizations_controller.rb
index 846995ecba5..ef5ddcdbca6 100644
--- a/app/controllers/groups/crm/organizations_controller.rb
+++ b/app/controllers/groups/crm/organizations_controller.rb
@@ -2,6 +2,7 @@
class Groups::Crm::OrganizationsController < Groups::ApplicationController
feature_category :team_planning
+ urgency :low
before_action :validate_root_group!
before_action :authorize_read_crm_organization!
diff --git a/app/controllers/groups/dependency_proxies_controller.rb b/app/controllers/groups/dependency_proxies_controller.rb
index 2e120de435e..8e134529c34 100644
--- a/app/controllers/groups/dependency_proxies_controller.rb
+++ b/app/controllers/groups/dependency_proxies_controller.rb
@@ -4,10 +4,10 @@ module Groups
class DependencyProxiesController < Groups::ApplicationController
include ::DependencyProxy::GroupAccess
- before_action :authorize_admin_dependency_proxy!, only: :update
before_action :verify_dependency_proxy_enabled!
- feature_category :package_registry
+ feature_category :dependency_proxy
+ urgency :low
private
diff --git a/app/controllers/groups/dependency_proxy/application_controller.rb b/app/controllers/groups/dependency_proxy/application_controller.rb
index 18a6ff93e15..f7337a3cdb1 100644
--- a/app/controllers/groups/dependency_proxy/application_controller.rb
+++ b/app/controllers/groups/dependency_proxy/application_controller.rb
@@ -16,8 +16,6 @@ module Groups
prepend_before_action :authenticate_user_from_jwt_token!
def authenticate_user_from_jwt_token!
- return unless dependency_proxy_for_private_groups?
-
authenticate_with_http_token do |token, _|
@authentication_result = EMPTY_AUTH_RESULT
@@ -36,10 +34,6 @@ module Groups
private
- def dependency_proxy_for_private_groups?
- Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true)
- end
-
def request_bearer_token!
# unfortunately, we cannot use https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html#method-i-authentication_request
response.headers['WWW-Authenticate'] = ::DependencyProxy::Registry.authenticate_header
diff --git a/app/controllers/groups/dependency_proxy_auth_controller.rb b/app/controllers/groups/dependency_proxy_auth_controller.rb
index 60b2371fa9a..03579d62ba9 100644
--- a/app/controllers/groups/dependency_proxy_auth_controller.rb
+++ b/app/controllers/groups/dependency_proxy_auth_controller.rb
@@ -2,6 +2,7 @@
class Groups::DependencyProxyAuthController < ::Groups::DependencyProxy::ApplicationController
feature_category :dependency_proxy
+ urgency :low
def authenticate
render plain: '', status: :ok
diff --git a/app/controllers/groups/dependency_proxy_for_containers_controller.rb b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
index 8513979c53b..2e9e0b12d2f 100644
--- a/app/controllers/groups/dependency_proxy_for_containers_controller.rb
+++ b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
@@ -17,6 +17,7 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
attr_reader :token
feature_category :dependency_proxy
+ urgency :low
def manifest
result = DependencyProxy::FindCachedManifestService.new(group, image, tag, token).execute
diff --git a/app/controllers/groups/deploy_tokens_controller.rb b/app/controllers/groups/deploy_tokens_controller.rb
index 9ef22aa33dc..5bab6f59a42 100644
--- a/app/controllers/groups/deploy_tokens_controller.rb
+++ b/app/controllers/groups/deploy_tokens_controller.rb
@@ -4,6 +4,7 @@ class Groups::DeployTokensController < Groups::ApplicationController
before_action :authorize_destroy_deploy_token!
feature_category :continuous_delivery
+ urgency :low
def revoke
Groups::DeployTokens::RevokeService.new(@group, current_user, params).execute
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 51778f31f65..d325bb402e7 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -21,6 +21,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
:override
feature_category :subgroups
+ urgency :low
def index
push_frontend_feature_flag(:group_member_inherited_group, @group)
diff --git a/app/controllers/groups/imports_controller.rb b/app/controllers/groups/imports_controller.rb
index 7cf39e378db..a35237a706d 100644
--- a/app/controllers/groups/imports_controller.rb
+++ b/app/controllers/groups/imports_controller.rb
@@ -4,6 +4,7 @@ class Groups::ImportsController < Groups::ApplicationController
include ContinueParams
feature_category :importers
+ urgency :low
def show
if @group.import_state.nil? || @group.import_state.finished?
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index 7bcc8182bd6..2d821676677 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -10,6 +10,7 @@ class Groups::LabelsController < Groups::ApplicationController
respond_to :html
feature_category :team_planning
+ urgency :low
def index
respond_to do |format|
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 75877cdef9c..494b8c5621d 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -7,6 +7,7 @@ class Groups::MilestonesController < Groups::ApplicationController
before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update, :destroy]
feature_category :team_planning
+ urgency :low
def index
respond_to do |format|
diff --git a/app/controllers/groups/packages_controller.rb b/app/controllers/groups/packages_controller.rb
index 1f3d80260ed..5d808d4c1c6 100644
--- a/app/controllers/groups/packages_controller.rb
+++ b/app/controllers/groups/packages_controller.rb
@@ -5,6 +5,7 @@ module Groups
before_action :verify_packages_enabled!
feature_category :package_registry
+ urgency :low
# The show action renders index to allow frontend routing to work on page refresh
def show
diff --git a/app/controllers/groups/registry/repositories_controller.rb b/app/controllers/groups/registry/repositories_controller.rb
index 549a148bfb8..cb7bf001918 100644
--- a/app/controllers/groups/registry/repositories_controller.rb
+++ b/app/controllers/groups/registry/repositories_controller.rb
@@ -8,7 +8,8 @@ module Groups
before_action :verify_container_registry_enabled!
before_action :authorize_read_container_image!
- feature_category :package_registry
+ feature_category :container_registry
+ urgency :low
def index
respond_to do |format|
diff --git a/app/controllers/groups/releases_controller.rb b/app/controllers/groups/releases_controller.rb
index e87135cc104..c97947683dc 100644
--- a/app/controllers/groups/releases_controller.rb
+++ b/app/controllers/groups/releases_controller.rb
@@ -3,6 +3,7 @@
module Groups
class ReleasesController < Groups::ApplicationController
feature_category :release_evidence
+ urgency :low
def index
respond_to do |format|
@@ -15,19 +16,11 @@ module Groups
private
def releases
- if Feature.enabled?(:group_releases_finder_inoperator)
- Releases::GroupReleasesFinder
- .new(@group, current_user)
- .execute(preload: false)
- .page(params[:page])
- .per(30)
- else
- ReleasesFinder
- .new(@group, current_user, { include_subgroups: true })
- .execute(preload: false)
- .page(params[:page])
- .per(30)
- end
+ Releases::GroupReleasesFinder
+ .new(@group, current_user)
+ .execute(preload: false)
+ .page(params[:page])
+ .per(30)
end
end
end
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
index a2be4d9d7e1..8d687bf3c2c 100644
--- a/app/controllers/groups/runners_controller.rb
+++ b/app/controllers/groups/runners_controller.rb
@@ -3,10 +3,10 @@
class Groups::RunnersController < Groups::ApplicationController
before_action :authorize_read_group_runners!, only: [:index, :show]
before_action :authorize_admin_group_runners!, only: [:edit, :update, :destroy, :pause, :resume]
- before_action :runner_list_group_view_vue_ui_enabled, only: [:index]
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
feature_category :runner
+ urgency :low
def index
finder = Ci::RunnersFinder.new(current_user: current_user, params: { group: @group })
@@ -15,10 +15,6 @@ class Groups::RunnersController < Groups::ApplicationController
Gitlab::Tracking.event(self.class.name, 'index', user: current_user, namespace: @group)
end
- def runner_list_group_view_vue_ui_enabled
- render_404 unless Feature.enabled?(:runner_list_group_view_vue_ui, group, default_enabled: :yaml)
- end
-
def show
end
@@ -33,32 +29,6 @@ class Groups::RunnersController < Groups::ApplicationController
end
end
- def destroy
- if can?(current_user, :delete_runner, @runner)
- Ci::Runners::UnregisterRunnerService.new(@runner, current_user).execute
-
- redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), status: :found
- else
- redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), status: :found, alert: _('Runner cannot be deleted, please contact your administrator.')
- end
- end
-
- def resume
- if Ci::Runners::UpdateRunnerService.new(@runner).update(active: true)
- redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), notice: _('Runner was successfully updated.')
- else
- redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), alert: _('Runner was not updated.')
- end
- end
-
- def pause
- if Ci::Runners::UpdateRunnerService.new(@runner).update(active: false)
- redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), notice: _('Runner was successfully updated.')
- else
- redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), alert: _('Runner was not updated.')
- end
- end
-
private
def runner
diff --git a/app/controllers/groups/settings/applications_controller.rb b/app/controllers/groups/settings/applications_controller.rb
index 6388277e4dc..bfe61696e0f 100644
--- a/app/controllers/groups/settings/applications_controller.rb
+++ b/app/controllers/groups/settings/applications_controller.rb
@@ -57,10 +57,8 @@ module Groups
# https://gitlab.com/gitlab-org/gitlab/-/issues/324187
@applications = @group.oauth_applications.limit(100)
- # Default access tokens to expire. This preserves backward compatibility
- # with existing applications. This will be removed in 15.0.
- # Removal issue: https://gitlab.com/gitlab-org/gitlab/-/issues/340848
- @application ||= Doorkeeper::Application.new(expire_access_tokens: true)
+ # Don't overwrite a value possibly set by `create`
+ @application ||= Doorkeeper::Application.new
end
def set_application
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index 9b9e3f7b0bc..4b75cec19f7 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -3,8 +3,6 @@
module Groups
module Settings
class CiCdController < Groups::ApplicationController
- include RunnerSetupScripts
-
layout 'group_settings'
skip_cross_project_access_check :show
before_action :authorize_admin_group!
@@ -13,16 +11,9 @@ module Groups
before_action :push_licensed_features, only: [:show]
feature_category :continuous_integration
-
- NUMBER_OF_RUNNERS_PER_PAGE = 4
+ urgency :low
def show
- runners_finder = Ci::RunnersFinder.new(current_user: current_user, params: params.merge({ group: @group }))
- # We need all runners for count
- @all_group_runners = runners_finder.execute.except(:limit, :offset)
- @group_runners = runners_finder.execute.page(params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE)
-
- @sort = runners_finder.sort_key
end
def update
@@ -35,13 +26,6 @@ module Groups
redirect_to group_settings_ci_cd_path
end
- def reset_registration_token
- ::Ci::Runners::ResetRegistrationTokenService.new(@group, current_user).execute
-
- flash[:notice] = _('GroupSettings|New runners registration token has been generated!')
- redirect_to group_settings_ci_cd_path
- end
-
def update_auto_devops
if auto_devops_service.execute
flash[:notice] = s_('GroupSettings|Auto DevOps pipeline was updated for the group')
@@ -52,10 +36,6 @@ module Groups
redirect_to group_settings_ci_cd_path
end
- def runner_setup_scripts
- private_runner_setup_scripts
- end
-
private
def define_variables
diff --git a/app/controllers/groups/settings/packages_and_registries_controller.rb b/app/controllers/groups/settings/packages_and_registries_controller.rb
index c44e0727ff9..411b8577c3f 100644
--- a/app/controllers/groups/settings/packages_and_registries_controller.rb
+++ b/app/controllers/groups/settings/packages_and_registries_controller.rb
@@ -8,6 +8,7 @@ module Groups
before_action :verify_packages_enabled!
feature_category :package_registry
+ urgency :low
def show
end
diff --git a/app/controllers/groups/settings/repository_controller.rb b/app/controllers/groups/settings/repository_controller.rb
index 7404075985b..b0431c31179 100644
--- a/app/controllers/groups/settings/repository_controller.rb
+++ b/app/controllers/groups/settings/repository_controller.rb
@@ -12,6 +12,7 @@ module Groups
end
feature_category :continuous_delivery
+ urgency :low
def create_deploy_token
result = Groups::DeployTokens::CreateService.new(@group, current_user, deploy_token_params).execute
diff --git a/app/controllers/groups/shared_projects_controller.rb b/app/controllers/groups/shared_projects_controller.rb
index 7acdacc2d46..2d2664c02e8 100644
--- a/app/controllers/groups/shared_projects_controller.rb
+++ b/app/controllers/groups/shared_projects_controller.rb
@@ -7,6 +7,7 @@ module Groups
skip_cross_project_access_check :index
feature_category :subgroups
+ urgency :low, [:index]
def index
shared_projects = GroupProjectsFinder.new(
diff --git a/app/controllers/groups/uploads_controller.rb b/app/controllers/groups/uploads_controller.rb
index 49249f87d31..22e6549aa04 100644
--- a/app/controllers/groups/uploads_controller.rb
+++ b/app/controllers/groups/uploads_controller.rb
@@ -10,6 +10,7 @@ class Groups::UploadsController < Groups::ApplicationController
before_action :verify_workhorse_api!, only: [:authorize]
feature_category :subgroups
+ urgency :low, [:show]
private
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 995d5abf045..d46cf899d8c 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -31,7 +31,7 @@ class GroupsController < Groups::ApplicationController
before_action :user_actions, only: [:show]
before_action do
- push_frontend_feature_flag(:vue_issues_list, @group, default_enabled: :yaml)
+ push_frontend_feature_flag(:vue_issues_list, @group)
end
before_action :check_export_rate_limit!, only: [:export, :download_export]
@@ -57,10 +57,13 @@ class GroupsController < Groups::ApplicationController
feature_category :code_review, [:merge_requests, :unfoldered_environment_names]
feature_category :projects, [:projects]
feature_category :importers, [:export, :download_export]
+ urgency :low, [:export, :download_export]
urgency :high, [:unfoldered_environment_names]
+
+ urgency :low, [:issues, :issues_calendar, :preview_markdown]
# TODO: Set #show to higher urgency after resolving https://gitlab.com/gitlab-org/gitlab/-/issues/334795
- urgency :low, [:merge_requests, :show]
+ urgency :low, [:merge_requests, :show, :create, :new, :update, :projects, :destroy, :edit, :activity]
def index
redirect_to(current_user ? dashboard_groups_path : explore_groups_path)
@@ -209,7 +212,7 @@ class GroupsController < Groups::ApplicationController
end
def issues
- return super if !html_request? || Feature.disabled?(:vue_issues_list, group, default_enabled: :yaml)
+ return super if !html_request? || Feature.disabled?(:vue_issues_list, group)
@has_issues = IssuesFinder.new(current_user, group_id: group.id, include_subgroups: true).execute
.non_archived
@@ -227,6 +230,8 @@ class GroupsController < Groups::ApplicationController
protected
def render_show_html
+ Gitlab::Tracking.event('group_overview', 'render', user: current_user, namespace: @group)
+
render 'groups/show', locals: { trial: params[:trial] }
end
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
index 9494a686467..2bcbf88039b 100644
--- a/app/controllers/ide_controller.rb
+++ b/app/controllers/ide_controller.rb
@@ -12,7 +12,7 @@ class IdeController < ApplicationController
before_action do
push_frontend_feature_flag(:build_service_proxy)
push_frontend_feature_flag(:schema_linting)
- push_frontend_feature_flag(:reject_unsigned_commits_by_gitlab, default_enabled: :yaml)
+ push_frontend_feature_flag(:reject_unsigned_commits_by_gitlab)
define_index_vars
end
diff --git a/app/controllers/import/available_namespaces_controller.rb b/app/controllers/import/available_namespaces_controller.rb
index 0c2af13d3f3..c16c40cefea 100644
--- a/app/controllers/import/available_namespaces_controller.rb
+++ b/app/controllers/import/available_namespaces_controller.rb
@@ -2,6 +2,7 @@
class Import::AvailableNamespacesController < ApplicationController
feature_category :importers
+ urgency :low
def index
render json: NamespaceSerializer.new.represent(current_user.manageable_groups_with_routes(include_groups_with_developer_maintainer_access: true))
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index 51ca12370e6..7ef07032913 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -5,6 +5,7 @@ class Import::BaseController < ApplicationController
before_action -> { check_rate_limit!(:project_import, scope: [current_user, :project_import], redirect_back: true) }, only: [:create]
feature_category :importers
+ urgency :low
def status
respond_to do |format|
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 7c9525d1744..55707000cf8 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -29,13 +29,12 @@ class Import::BitbucketController < Import::BaseController
end
end
+ # We need to re-expose controller's internal method 'status' as action.
+ # rubocop:disable Lint/UselessMethodDefinition
def status
super
end
-
- def realtime_changes
- super
- end
+ # rubocop:enable Lint/UselessMethodDefinition
def create
bitbucket_client = Bitbucket::Client.new(credentials)
diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb
index 31e9694ca1d..00f3f0b08b2 100644
--- a/app/controllers/import/bitbucket_server_controller.rb
+++ b/app/controllers/import/bitbucket_server_controller.rb
@@ -52,13 +52,12 @@ class Import::BitbucketServerController < Import::BaseController
redirect_to status_import_bitbucket_server_path
end
+ # We need to re-expose controller's internal method 'status' as action.
+ # rubocop:disable Lint/UselessMethodDefinition
def status
super
end
-
- def realtime_changes
- super
- end
+ # rubocop:enable Lint/UselessMethodDefinition
protected
diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb
index f26c06b7e37..34f12aebb91 100644
--- a/app/controllers/import/bulk_imports_controller.rb
+++ b/app/controllers/import/bulk_imports_controller.rb
@@ -7,6 +7,7 @@ class Import::BulkImportsController < ApplicationController
before_action :verify_blocked_uri, only: :status
feature_category :importers
+ urgency :low
POLLING_INTERVAL = 3_000
@@ -98,7 +99,7 @@ class Import::BulkImportsController < ApplicationController
end
def ensure_group_import_enabled
- render_404 unless Feature.enabled?(:bulk_import, default_enabled: :yaml)
+ render_404 unless Feature.enabled?(:bulk_import)
end
def access_token_key
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index 377292d47d8..c223d9d211e 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -54,10 +54,6 @@ class Import::FogbugzController < Import::BaseController
end
# rubocop: enable CodeReuse/ActiveRecord
- def realtime_changes
- super
- end
-
def create
repo = client.repo(params[:repo_id])
fb_session = { uri: session[:fogbugz_uri], token: session[:fogbugz_token] }
diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb
index 32c9da67e90..4b4ac07b389 100644
--- a/app/controllers/import/gitea_controller.rb
+++ b/app/controllers/import/gitea_controller.rb
@@ -16,10 +16,12 @@ class Import::GiteaController < Import::GithubController
super
end
- # Must be defined or it will 404
+ # We need to re-expose controller's internal method 'status' as action.
+ # rubocop:disable Lint/UselessMethodDefinition
def status
super
end
+ # rubocop:enable Lint/UselessMethodDefinition
protected
@@ -61,7 +63,7 @@ class Import::GiteaController < Import::GithubController
override :client
def client
- @client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options)
+ @client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], **client_options)
end
override :client_options
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index fa9517c3545..c846d9d225a 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -16,9 +16,12 @@ class Import::GitlabController < Import::BaseController
redirect_to status_import_gitlab_url
end
+ # We need to re-expose controller's internal method 'status' as action.
+ # rubocop:disable Lint/UselessMethodDefinition
def status
super
end
+ # rubocop:enable Lint/UselessMethodDefinition
def create
repo = client.project(params[:repo_id].to_i)
diff --git a/app/controllers/import/gitlab_groups_controller.rb b/app/controllers/import/gitlab_groups_controller.rb
index c9d5e9986dc..81f18a18776 100644
--- a/app/controllers/import/gitlab_groups_controller.rb
+++ b/app/controllers/import/gitlab_groups_controller.rb
@@ -6,6 +6,7 @@ class Import::GitlabGroupsController < ApplicationController
before_action :check_import_rate_limit!, only: %i[create]
feature_category :importers
+ urgency :low
def create
unless file_is_valid?(group_params[:file])
diff --git a/app/controllers/import/history_controller.rb b/app/controllers/import/history_controller.rb
index 69e31392f21..9677624d0b7 100644
--- a/app/controllers/import/history_controller.rb
+++ b/app/controllers/import/history_controller.rb
@@ -2,4 +2,5 @@
class Import::HistoryController < ApplicationController
feature_category :importers
+ urgency :low
end
diff --git a/app/controllers/import/manifest_controller.rb b/app/controllers/import/manifest_controller.rb
index 956d0c9a2ae..461ba982969 100644
--- a/app/controllers/import/manifest_controller.rb
+++ b/app/controllers/import/manifest_controller.rb
@@ -10,9 +10,12 @@ class Import::ManifestController < Import::BaseController
def new
end
+ # We need to re-expose controller's internal method 'status' as action.
+ # rubocop:disable Lint/UselessMethodDefinition
def status
super
end
+ # rubocop:enable Lint/UselessMethodDefinition
def upload
group = Group.find(params[:group_id])
@@ -36,10 +39,6 @@ class Import::ManifestController < Import::BaseController
end
end
- def realtime_changes
- super
- end
-
def create
repository = importable_repos.find do |project|
project[:id] == params[:repo_id].to_i
diff --git a/app/controllers/import/url_controller.rb b/app/controllers/import/url_controller.rb
index 4e4b6ad125e..fed3412881a 100644
--- a/app/controllers/import/url_controller.rb
+++ b/app/controllers/import/url_controller.rb
@@ -2,6 +2,7 @@
class Import::UrlController < ApplicationController
feature_category :importers
+ urgency :low
def validate
result = Import::ValidateRemoteGitEndpointService.new(validate_params).execute
diff --git a/app/controllers/jira_connect/application_controller.rb b/app/controllers/jira_connect/application_controller.rb
index 9b3bff062dd..e26d69314cd 100644
--- a/app/controllers/jira_connect/application_controller.rb
+++ b/app/controllers/jira_connect/application_controller.rb
@@ -20,60 +20,44 @@ class JiraConnect::ApplicationController < ApplicationController
end
def verify_qsh_claim!
- payload, _ = decode_auth_token!
-
- return if request.format.json? && payload['qsh'] == 'context-qsh'
+ return if request.format.json? && jwt.verify_context_qsh_claim
# Make sure `qsh` claim matches the current request
- render_403 unless payload['qsh'] == Atlassian::Jwt.create_query_string_hash(request.url, request.method, jira_connect_base_url)
- rescue StandardError
- render_403
+ render_403 unless jwt.verify_qsh_claim(request.url, request.method, jira_connect_base_url)
end
def atlassian_jwt_valid?
return false unless installation_from_jwt
# Verify JWT signature with our stored `shared_secret`
- decode_auth_token!
- rescue JWT::DecodeError
- false
+ jwt.valid?(installation_from_jwt.shared_secret)
end
def installation_from_jwt
strong_memoize(:installation_from_jwt) do
- next unless claims['iss']
+ next unless jwt.iss_claim
- JiraConnectInstallation.find_by_client_key(claims['iss'])
- end
- end
-
- def claims
- strong_memoize(:claims) do
- next {} unless auth_token
-
- # Decode without verification to get `client_key` in `iss`
- payload, _ = Atlassian::Jwt.decode(auth_token, nil, false)
- payload
+ JiraConnectInstallation.find_by_client_key(jwt.iss_claim)
end
end
def jira_user
strong_memoize(:jira_user) do
next unless installation_from_jwt
- next unless claims['sub']
+ next unless jwt.sub_claim
# This only works for Jira Cloud installations.
- installation_from_jwt.client.user_info(claims['sub'])
+ installation_from_jwt.client.user_info(jwt.sub_claim)
end
end
- def decode_auth_token!
- Atlassian::Jwt.decode(auth_token, installation_from_jwt.shared_secret)
+ def jwt
+ strong_memoize(:jwt) do
+ Atlassian::JiraConnect::Jwt::Symmetric.new(auth_token)
+ end
end
def auth_token
- strong_memoize(:auth_token) do
- params[:jwt] || request.headers['Authorization']&.split(' ', 2)&.last
- end
+ params[:jwt] || request.headers['Authorization']&.split(' ', 2)&.last
end
end
diff --git a/app/controllers/jira_connect/events_controller.rb b/app/controllers/jira_connect/events_controller.rb
index 3c78f63e069..394fdc9b2f6 100644
--- a/app/controllers/jira_connect/events_controller.rb
+++ b/app/controllers/jira_connect/events_controller.rb
@@ -47,7 +47,7 @@ class JiraConnect::EventsController < JiraConnect::ApplicationController
end
def verify_asymmetric_atlassian_jwt!
- asymmetric_jwt = Atlassian::JiraConnect::AsymmetricJwt.new(auth_token, jwt_verification_claims)
+ asymmetric_jwt = Atlassian::JiraConnect::Jwt::Asymmetric.new(auth_token, jwt_verification_claims)
return head :unauthorized unless asymmetric_jwt.valid?
diff --git a/app/controllers/jira_connect/subscriptions_controller.rb b/app/controllers/jira_connect/subscriptions_controller.rb
index d8ce67d6267..2ba9f8264e1 100644
--- a/app/controllers/jira_connect/subscriptions_controller.rb
+++ b/app/controllers/jira_connect/subscriptions_controller.rb
@@ -19,7 +19,8 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
end
before_action do
- push_frontend_feature_flag(:jira_connect_oauth, @user, default_enabled: :yaml)
+ push_frontend_feature_flag(:jira_connect_oauth, @user)
+ push_frontend_feature_flag(:jira_connect_oauth_self_managed, @user)
end
before_action :allow_rendering_in_iframe, only: :index
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index 81084ffe38b..3724bb0d925 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -57,10 +57,8 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
@authorized_anonymous_tokens = @authorized_tokens.reject(&:application)
@authorized_apps = @authorized_tokens.map(&:application).uniq.reject(&:nil?)
- # Default access tokens to expire. This preserves backward compatibility
- # with existing applications. This will be removed in 15.0.
- # Removal issue: https://gitlab.com/gitlab-org/gitlab/-/issues/340848
- @application ||= Doorkeeper::Application.new(expire_access_tokens: true)
+ # Don't overwrite a value possibly set by `create`
+ @application ||= Doorkeeper::Application.new
end
# Override Doorkeeper to scope to the current user
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index d1c409d071e..0817813f967 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -5,7 +5,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
include InitializesCurrentUserMode
include Gitlab::Utils::StrongMemoize
- before_action :verify_confirmed_email!, :verify_confidential_application!
+ before_action :verify_confirmed_email!
layout 'profile'
@@ -37,8 +37,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
# limit scopes when signing in with GitLab
def downgrade_scopes!
- return unless Feature.enabled?(:omniauth_login_minimal_scopes, current_user,
- default_enabled: :yaml)
+ return unless Feature.enabled?(:omniauth_login_minimal_scopes, current_user)
auth_type = params.delete('gl_auth_type')
return unless auth_type == 'login'
@@ -78,18 +77,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
doorkeeper_application&.includes_scope?(*::Gitlab::Auth::API_SCOPES)
end
- # Confidential apps require the client_secret to be sent with the request.
- # Doorkeeper allows implicit grant flow requests (response_type=token) to
- # work without client_secret regardless of the confidential setting.
- # This leads to security vulnerabilities and we want to block it.
- def verify_confidential_application!
- render 'doorkeeper/authorizations/error' if authorizable_confidential?
- end
-
- def authorizable_confidential?
- pre_auth.authorizable? && pre_auth.response_type == 'token' && pre_auth.client.application.confidential
- end
-
def verify_confirmed_email!
return if current_user&.confirmed?
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index dc5b22e1606..927b50245a4 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -9,7 +9,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
after_action :verify_known_sign_in
- protect_from_forgery except: [:kerberos, :saml, :cas3, :failure] + AuthHelper.saml_providers, with: :exception, prepend: true
+ protect_from_forgery except: [:cas3, :failure] + AuthHelper.saml_providers, with: :exception, prepend: true
feature_category :authentication_and_authorization
diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb
index bd52ef0b0d4..83eabbb736e 100644
--- a/app/controllers/profiles/accounts_controller.rb
+++ b/app/controllers/profiles/accounts_controller.rb
@@ -4,6 +4,7 @@ class Profiles::AccountsController < Profiles::ApplicationController
include AuthHelper
feature_category :users
+ urgency :low, [:show]
def show
render(locals: show_view_variables)
diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb
index 7a88162f469..7e332d9a498 100644
--- a/app/controllers/profiles/emails_controller.rb
+++ b/app/controllers/profiles/emails_controller.rb
@@ -8,6 +8,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
only: [:resend_confirmation_instructions]
feature_category :users
+ urgency :low, [:index]
def index
@primary_email = current_user.email
diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb
index 9e16d195b00..e31ca87a5d5 100644
--- a/app/controllers/profiles/gpg_keys_controller.rb
+++ b/app/controllers/profiles/gpg_keys_controller.rb
@@ -3,7 +3,7 @@
class Profiles::GpgKeysController < Profiles::ApplicationController
before_action :set_gpg_key, only: [:destroy, :revoke]
- feature_category :users
+ feature_category :source_code_management
def index
@gpg_keys = current_user.gpg_keys.with_subkeys
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index 3a189c900ac..90d5f945d78 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -2,6 +2,7 @@
class Profiles::KeysController < Profiles::ApplicationController
feature_category :users
+ urgency :low, [:create, :index]
def index
@keys = current_user.keys.order_id_desc
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index ccfd360a781..9323d266cd5 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -2,6 +2,7 @@
class Profiles::NotificationsController < Profiles::ApplicationController
feature_category :team_planning
+ urgency :low
def show
@user = current_user
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 8dc9697c56d..ad2e384077a 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -63,5 +63,3 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
finder(state: 'active', sort: 'expires_at_asc').execute
end
end
-
-Profiles::PersonalAccessTokensController.prepend_mod_with('Profiles::PersonalAccessTokensController')
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 820b6520f6c..7aca76c2fb1 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -5,6 +5,9 @@ class Profiles::PreferencesController < Profiles::ApplicationController
feature_category :users
+ urgency :low, [:show]
+ urgency :medium, [:update]
+
def show
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 48b0d313d3c..2e71b4801ed 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -9,7 +9,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
helper_method :current_password_required?
before_action do
- push_frontend_feature_flag(:webauthn, default_enabled: :yaml)
+ push_frontend_feature_flag(:webauthn)
end
feature_category :authentication_and_authorization
@@ -35,7 +35,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
@qr_code = build_qr_code
@account_string = account_string
- if Feature.enabled?(:webauthn, default_enabled: :yaml)
+ if Feature.enabled?(:webauthn)
setup_webauthn_registration
else
setup_u2f_registration
@@ -44,7 +44,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def create
otp_validation_result =
- ::Users::ValidateOtpService.new(current_user).execute(params[:pin_code])
+ ::Users::ValidateManualOtpService.new(current_user).execute(params[:pin_code])
if otp_validation_result[:status] == :success
ActiveSession.destroy_all_but_current(current_user, session)
@@ -61,7 +61,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
@qr_code = build_qr_code
@account_string = account_string
- if Feature.enabled?(:webauthn, default_enabled: :yaml)
+ if Feature.enabled?(:webauthn)
setup_webauthn_registration
else
setup_u2f_registration
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index d57a293ab4d..d5e7195a157 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -11,10 +11,11 @@ class ProfilesController < Profiles::ApplicationController
end
skip_before_action :require_email, only: [:show, :update]
before_action do
- push_frontend_feature_flag(:webauthn, default_enabled: :yaml)
+ push_frontend_feature_flag(:webauthn)
end
feature_category :users
+ urgency :low, [:show, :update]
def show
end
diff --git a/app/controllers/projects/alert_management_controller.rb b/app/controllers/projects/alert_management_controller.rb
index ebe867d915d..ef0c47b0eed 100644
--- a/app/controllers/projects/alert_management_controller.rb
+++ b/app/controllers/projects/alert_management_controller.rb
@@ -4,6 +4,7 @@ class Projects::AlertManagementController < Projects::ApplicationController
before_action :authorize_read_alert_management_alert!
feature_category :incident_management
+ urgency :low
def index
end
diff --git a/app/controllers/projects/alerting/notifications_controller.rb b/app/controllers/projects/alerting/notifications_controller.rb
index ae8498ce65f..82fff287c4a 100644
--- a/app/controllers/projects/alerting/notifications_controller.rb
+++ b/app/controllers/projects/alerting/notifications_controller.rb
@@ -13,6 +13,9 @@ module Projects
prepend_before_action :repository, :project_without_auth
feature_category :incident_management
+ # Goal is to increase the urgency to medium.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/361310.
+ urgency :low, [:create]
def create
token = extract_alert_manager_token(request)
diff --git a/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb b/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb
index 03dcb164d94..60bcd1d7238 100644
--- a/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb
+++ b/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb
@@ -4,6 +4,7 @@ class Projects::Analytics::CycleAnalytics::ValueStreamsController < Projects::Ap
respond_to :json
feature_category :planning_analytics
+ urgency :low
before_action :authorize_read_cycle_analytics!
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index f678e19d05d..9dbf989ca3f 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -9,7 +9,8 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
feature_category :users, [:members]
feature_category :snippets, [:snippets]
- urgency :low, [:merge_requests]
+ urgency :low, [:merge_requests, :members]
+ urgency :low, [:issues, :labels, :milestones, :commands, :contacts]
def members
render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target)
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index fb113df137f..70d9b524e4d 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -9,6 +9,8 @@ class Projects::AvatarsController < Projects::ApplicationController
feature_category :projects
+ urgency :low, [:show]
+
def show
@blob = @repository.blob_at_branch(@repository.root_ref, @project.avatar_in_git)
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index 57a06f26f8c..64ced43311a 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -23,8 +23,11 @@ class Projects::BlameController < Projects::ApplicationController
environment_params[:find_latest] = true
@environment = ::Environments::EnvironmentsByDeploymentsFinder.new(@project, current_user, environment_params).execute.last
- @blame = Gitlab::Blame.new(@blob, @commit)
- @blame = Gitlab::View::Presenter::Factory.new(@blame, project: @project, path: @path).fabricate!
+ blame_service = Projects::BlameService.new(@blob, @commit, params.permit(:page))
+
+ @blame = Gitlab::View::Presenter::Factory.new(blame_service.blame, project: @project, path: @path).fabricate!
+
+ render locals: { blame_pagination: blame_service.pagination }
end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 26a7b5662be..a9561fb9312 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -42,8 +42,8 @@ class Projects::BlobController < Projects::ApplicationController
urgency :low, [:create, :show, :edit, :update, :diff]
before_action do
- push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
- push_frontend_feature_flag(:highlight_js, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:refactor_blob_viewer, @project)
+ push_frontend_feature_flag(:highlight_js, @project)
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 7a30e68d9a2..36986a714fb 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -7,8 +7,8 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :check_issues_available!
before_action :assign_endpoint_vars
before_action do
- push_frontend_feature_flag(:board_multi_select, project, default_enabled: :yaml)
- push_frontend_feature_flag(:realtime_labels, project&.group, default_enabled: :yaml)
+ push_frontend_feature_flag(:board_multi_select, project)
+ push_frontend_feature_flag(:realtime_labels, project&.group)
experiment(:prominent_create_board_btn, subject: current_user) do |e|
e.control { }
e.candidate { }
@@ -16,6 +16,7 @@ class Projects::BoardsController < Projects::ApplicationController
end
feature_category :team_planning
+ urgency :low
private
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 6264f10ce2d..27969cb1a75 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -111,7 +111,7 @@ class Projects::BranchesController < Projects::ApplicationController
flash_type = result.error? ? :alert : :notice
flash[flash_type] = result.message
- redirect_to project_branches_path(@project), status: :see_other
+ redirect_back_or_default(default: project_branches_path(@project), options: { status: :see_other })
end
format.js { head result.http_status }
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 61e8e5b015a..4168612d50f 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -5,8 +5,7 @@ class Projects::BuildsController < Projects::ApplicationController
feature_category :continuous_integration
- urgency :high, [:index, :show]
- urgency :low, [:raw]
+ urgency :low, [:raw, :index, :show]
def index
redirect_to project_jobs_path(project)
diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb
index 8c6e8f0e126..dbf3b2051fb 100644
--- a/app/controllers/projects/ci/pipeline_editor_controller.rb
+++ b/app/controllers/projects/ci/pipeline_editor_controller.rb
@@ -3,7 +3,8 @@
class Projects::Ci::PipelineEditorController < Projects::ApplicationController
before_action :check_can_collaborate!
before_action do
- push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:schema_linting, @project)
+ push_frontend_feature_flag(:pipeline_editor_file_tree, @project)
end
feature_category :pipeline_authoring
diff --git a/app/controllers/projects/ci/secure_files_controller.rb b/app/controllers/projects/ci/secure_files_controller.rb
index 5141d0188b0..59ddca19081 100644
--- a/app/controllers/projects/ci/secure_files_controller.rb
+++ b/app/controllers/projects/ci/secure_files_controller.rb
@@ -6,5 +6,6 @@ class Projects::Ci::SecureFilesController < Projects::ApplicationController
feature_category :pipeline_authoring
def show
+ render_404 unless Feature.enabled?(:ci_secure_files, project)
end
end
diff --git a/app/controllers/projects/cluster_agents_controller.rb b/app/controllers/projects/cluster_agents_controller.rb
index 282b9ef1fb7..3f759e5c18c 100644
--- a/app/controllers/projects/cluster_agents_controller.rb
+++ b/app/controllers/projects/cluster_agents_controller.rb
@@ -4,6 +4,7 @@ class Projects::ClusterAgentsController < Projects::ApplicationController
before_action :authorize_can_read_cluster_agent!
feature_category :kubernetes_management
+ urgency :low
def show
@agent_name = params[:name]
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 440375bf3c9..30d001d0ac5 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -6,7 +6,7 @@ class Projects::ClustersController < Clusters::ClustersController
before_action do
push_frontend_feature_flag(:prometheus_computed_alerts)
- push_frontend_feature_flag(:show_gitlab_agent_feedback, type: :ops, default_enabled: :yaml)
+ push_frontend_feature_flag(:show_gitlab_agent_feedback, type: :ops)
end
layout 'project'
diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb
index a1da8d4e91f..43b4cdbe9a8 100644
--- a/app/controllers/projects/cycle_analytics/events_controller.rb
+++ b/app/controllers/projects/cycle_analytics/events_controller.rb
@@ -12,6 +12,7 @@ module Projects
before_action :authorize_read_merge_request!, only: [:code, :review]
feature_category :planning_analytics
+ urgency :low
def issue
render_events(cycle_analytics[:issue].events)
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index dc6a9a73d9e..6160dafb177 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -14,6 +14,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
track_redis_hll_event :show, name: 'p_analytics_valuestream'
feature_category :planning_analytics
+ urgency :low
before_action do
push_licensed_feature(:cycle_analytics_for_groups) if project.licensed_feature_available?(:cycle_analytics_for_groups)
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index ce25f86d692..96afe9dbb9f 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -11,6 +11,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
layout 'project_settings'
feature_category :continuous_delivery
+ urgency :low
def index
respond_to do |format|
diff --git a/app/controllers/projects/deploy_tokens_controller.rb b/app/controllers/projects/deploy_tokens_controller.rb
index 42c2d8b17f1..ed77fa2fee6 100644
--- a/app/controllers/projects/deploy_tokens_controller.rb
+++ b/app/controllers/projects/deploy_tokens_controller.rb
@@ -4,6 +4,7 @@ class Projects::DeployTokensController < Projects::ApplicationController
before_action :authorize_admin_project!
feature_category :continuous_delivery
+ urgency :low
def revoke
@token = @project.deploy_tokens.find(params[:id])
diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb
index 231684427fb..bebade1b21b 100644
--- a/app/controllers/projects/deployments_controller.rb
+++ b/app/controllers/projects/deployments_controller.rb
@@ -4,6 +4,7 @@ class Projects::DeploymentsController < Projects::ApplicationController
before_action :authorize_read_deployment!
feature_category :continuous_delivery
+ urgency :low
# rubocop: disable CodeReuse/ActiveRecord
def index
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index 9f7d47b95f3..a61930d4b99 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -10,6 +10,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
before_action :authorize_resolve_discussion!, only: [:resolve, :unresolve]
feature_category :team_planning
+ urgency :low
def resolve
Discussions::ResolveService.new(project, current_user, one_or_more_discussions: discussion).execute
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 8e81e75ad13..1a2c0d64d19 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -24,9 +24,13 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics, :cancel_auto_stop]
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? }
+ before_action do
+ push_frontend_feature_flag(:monitor_logging, project)
+ end
after_action :expire_etag_cache, only: [:cancel_auto_stop]
feature_category :continuous_delivery
+ urgency :low
def index
@project = ProjectPresenter.new(project, current_user: current_user)
@@ -73,7 +77,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord
def show
- @deployments = environment.deployments.ordered.page(params[:page])
+ @deployments = deployments
end
def new
@@ -202,6 +206,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController
private
+ def deployments
+ environment.deployments.ordered.page(params[:page])
+ end
+
def verify_api_request!
Gitlab::Workhorse.verify_api_request!(request.headers)
end
diff --git a/app/controllers/projects/feature_flags_clients_controller.rb b/app/controllers/projects/feature_flags_clients_controller.rb
index 9a1f8932a27..2652345fc5a 100644
--- a/app/controllers/projects/feature_flags_clients_controller.rb
+++ b/app/controllers/projects/feature_flags_clients_controller.rb
@@ -5,6 +5,7 @@ class Projects::FeatureFlagsClientsController < Projects::ApplicationController
before_action :feature_flags_client
feature_category :feature_flags
+ urgency :low
def reset_token
feature_flags_client.reset_token!
diff --git a/app/controllers/projects/feature_flags_controller.rb b/app/controllers/projects/feature_flags_controller.rb
index 7c0da8f8a24..1d1fe91ad70 100644
--- a/app/controllers/projects/feature_flags_controller.rb
+++ b/app/controllers/projects/feature_flags_controller.rb
@@ -11,6 +11,7 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
before_action :feature_flag, only: [:edit, :update, :destroy]
feature_category :feature_flags
+ urgency :low
def index
@feature_flags = FeatureFlagsFinder
diff --git a/app/controllers/projects/feature_flags_user_lists_controller.rb b/app/controllers/projects/feature_flags_user_lists_controller.rb
index fd81321924a..023eb51cc94 100644
--- a/app/controllers/projects/feature_flags_user_lists_controller.rb
+++ b/app/controllers/projects/feature_flags_user_lists_controller.rb
@@ -5,6 +5,7 @@ class Projects::FeatureFlagsUserListsController < Projects::ApplicationControlle
before_action :user_list, only: [:edit, :show]
feature_category :feature_flags
+ urgency :low
def index
end
diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb
index 606f6ac7941..63309cce1e5 100644
--- a/app/controllers/projects/graphs_controller.rb
+++ b/app/controllers/projects/graphs_controller.rb
@@ -15,6 +15,7 @@ class Projects::GraphsController < Projects::ApplicationController
urgency :low, [:show]
feature_category :continuous_integration, [:ci]
+ urgency :low, [:ci]
def show
respond_to do |format|
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index 3b3f9bdcf6b..41daeddcf7f 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -12,6 +12,7 @@ class Projects::ImportsController < Projects::ApplicationController
before_action :redirect_if_no_import, only: :show
feature_category :importers
+ urgency :low
def new
end
diff --git a/app/controllers/projects/incident_management/pager_duty_incidents_controller.rb b/app/controllers/projects/incident_management/pager_duty_incidents_controller.rb
index 408652b4b9e..f1e518abf48 100644
--- a/app/controllers/projects/incident_management/pager_duty_incidents_controller.rb
+++ b/app/controllers/projects/incident_management/pager_duty_incidents_controller.rb
@@ -11,6 +11,7 @@ module Projects
prepend_before_action :project_without_auth
feature_category :incident_management
+ urgency :low
def create
result = webhook_processor.execute(params[:token])
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
index dd1e51bb9bd..fd7ba7b5460 100644
--- a/app/controllers/projects/incidents_controller.rb
+++ b/app/controllers/projects/incidents_controller.rb
@@ -7,11 +7,12 @@ class Projects::IncidentsController < Projects::ApplicationController
before_action :authorize_read_issue!
before_action :load_incident, only: [:show]
before_action do
- push_frontend_feature_flag(:incident_escalations, @project, default_enabled: :yaml)
- push_frontend_feature_flag(:incident_timeline, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:incident_escalations, @project)
+ push_frontend_feature_flag(:incident_timeline, @project)
end
feature_category :incident_management
+ urgency :low
def index
end
diff --git a/app/controllers/projects/issue_links_controller.rb b/app/controllers/projects/issue_links_controller.rb
index e8c3110574f..956557457fa 100644
--- a/app/controllers/projects/issue_links_controller.rb
+++ b/app/controllers/projects/issue_links_controller.rb
@@ -8,6 +8,7 @@ module Projects
before_action :authorize_issue_link_association!, only: :destroy
feature_category :team_planning
+ urgency :low
private
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 46943e7214a..b65616fdb3c 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -39,16 +39,16 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_download_code!, only: [:related_branches]
before_action do
- push_frontend_feature_flag(:vue_issues_list, project&.group, default_enabled: :yaml)
- push_frontend_feature_flag(:contacts_autocomplete, project&.group, default_enabled: :yaml)
- push_frontend_feature_flag(:incident_timeline, project, default_enabled: :yaml)
+ push_frontend_feature_flag(:vue_issues_list, project&.group)
+ push_frontend_feature_flag(:contacts_autocomplete, project&.group)
+ push_frontend_feature_flag(:incident_timeline, project)
end
before_action only: :show do
- push_frontend_feature_flag(:confidential_notes, project&.group, default_enabled: :yaml)
- push_frontend_feature_flag(:issue_assignees_widget, project, default_enabled: :yaml)
- push_frontend_feature_flag(:paginated_issue_discussions, project, default_enabled: :yaml)
- push_frontend_feature_flag(:realtime_labels, project, default_enabled: :yaml)
+ push_frontend_feature_flag(:confidential_notes, project&.group)
+ push_frontend_feature_flag(:issue_assignees_widget, project)
+ push_frontend_feature_flag(:paginated_issue_discussions, project)
+ push_frontend_feature_flag(:realtime_labels, project)
push_force_frontend_feature_flag(:work_items, project&.work_items_feature_flag_enabled?)
end
@@ -65,10 +65,18 @@ class Projects::IssuesController < Projects::ApplicationController
:toggle_award_emoji, :mark_as_spam, :related_branches,
:can_create_branch, :create_merge_request
]
+ urgency :low, [
+ :index, :calendar, :show, :new, :create, :edit, :update,
+ :destroy, :move, :reorder, :designs, :toggle_subscription,
+ :discussions, :bulk_update, :realtime_changes,
+ :toggle_award_emoji, :mark_as_spam, :related_branches,
+ :can_create_branch, :create_merge_request
+ ]
feature_category :service_desk, [:service_desk]
urgency :low, [:service_desk]
feature_category :importers, [:import_csv, :export_csv]
+ urgency :low, [:import_csv, :export_csv]
attr_accessor :vulnerability_id
@@ -252,7 +260,7 @@ class Projects::IssuesController < Projects::ApplicationController
def vue_issues_list?
action_name.to_sym == :index &&
html_request? &&
- Feature.enabled?(:vue_issues_list, project&.group, default_enabled: :yaml)
+ Feature.enabled?(:vue_issues_list, project&.group)
end
def sorting_field
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 0f6cf97d69d..8c9f82b9dc1 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -21,13 +21,14 @@ class Projects::JobsController < Projects::ApplicationController
before_action :push_jobs_table_vue_search, only: [:index]
before_action do
- push_frontend_feature_flag(:infinitely_collapsible_sections, @project, default_enabled: :yaml)
- push_frontend_feature_flag(:trigger_job_retry_action, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:infinitely_collapsible_sections, @project)
+ push_frontend_feature_flag(:trigger_job_retry_action, @project)
end
layout 'project'
feature_category :continuous_integration
+ urgency :low
def index
# We need all builds for tabs counters
@@ -140,7 +141,7 @@ class Projects::JobsController < Projects::ApplicationController
end
def raw
- if @build.trace.archived_trace_exist?
+ if @build.trace.archived?
workhorse_set_content_type!
send_upload(@build.job_artifacts_trace.file,
send_params: raw_send_params,
@@ -261,10 +262,10 @@ class Projects::JobsController < Projects::ApplicationController
end
def push_jobs_table_vue
- push_frontend_feature_flag(:jobs_table_vue, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:jobs_table_vue, @project)
end
def push_jobs_table_vue_search
- push_frontend_feature_flag(:jobs_table_vue_search, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:jobs_table_vue_search, @project)
end
end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 814081194d6..8ec2cbb41e9 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -15,6 +15,7 @@ class Projects::LabelsController < Projects::ApplicationController
respond_to :js, :html
feature_category :team_planning
+ urgency :low
def index
respond_to do |format|
diff --git a/app/controllers/projects/learn_gitlab_controller.rb b/app/controllers/projects/learn_gitlab_controller.rb
index b9f9a1810b7..61e4a1812ba 100644
--- a/app/controllers/projects/learn_gitlab_controller.rb
+++ b/app/controllers/projects/learn_gitlab_controller.rb
@@ -7,6 +7,7 @@ class Projects::LearnGitlabController < Projects::ApplicationController
before_action :enable_video_tutorials_continuous_onboarding_experiment
feature_category :users
+ urgency :low, [:index]
def index
end
diff --git a/app/controllers/projects/logs_controller.rb b/app/controllers/projects/logs_controller.rb
index a4bdbc827e0..63d8981ef38 100644
--- a/app/controllers/projects/logs_controller.rb
+++ b/app/controllers/projects/logs_controller.rb
@@ -10,6 +10,8 @@ module Projects
feature_category :logging
def index
+ return render_404 unless Feature.enabled?(:monitor_logging, project)
+
if environment || cluster
render :index
else
@@ -28,7 +30,6 @@ module Projects
private
def render_logs(service, permitted_params)
- ::Gitlab::UsageCounters::PodLogs.increment(project.id)
::Gitlab::PollingInterval.set_header(response, interval: 3_000)
result = service.new(cluster, namespace, params: permitted_params).execute
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 03bb132fe47..458df40ece1 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -33,20 +33,25 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action only: [:show] do
push_frontend_feature_flag(:file_identifier_hash)
- push_frontend_feature_flag(:merge_request_widget_graphql, project, default_enabled: :yaml)
+ push_frontend_feature_flag(:merge_request_widget_graphql, project)
push_frontend_feature_flag(:core_security_mr_widget_counts, project)
- push_frontend_feature_flag(:paginated_notes, project, default_enabled: :yaml)
- push_frontend_feature_flag(:confidential_notes, project, default_enabled: :yaml)
- push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml)
- push_frontend_feature_flag(:refactor_mr_widgets_extensions, project, default_enabled: :yaml)
- push_frontend_feature_flag(:rebase_without_ci_ui, project, default_enabled: :yaml)
- push_frontend_feature_flag(:secure_vulnerability_training, project, default_enabled: :yaml)
- push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml)
- push_frontend_feature_flag(:realtime_labels, project, default_enabled: :yaml)
+ push_frontend_feature_flag(:paginated_notes, project)
+ push_frontend_feature_flag(:confidential_notes, project)
+ push_frontend_feature_flag(:restructured_mr_widget, project)
+ push_frontend_feature_flag(:refactor_mr_widgets_extensions, project)
+ push_frontend_feature_flag(:refactor_mr_widget_test_summary, project)
+ push_frontend_feature_flag(:rebase_without_ci_ui, project)
+ push_frontend_feature_flag(:issue_assignees_widget, @project)
+ push_frontend_feature_flag(:realtime_labels, project)
+ push_frontend_feature_flag(:updated_diff_expansion_buttons, project)
+ push_frontend_feature_flag(:mr_attention_requests, current_user)
+ push_frontend_feature_flag(:updated_mr_header, project)
+ push_frontend_feature_flag(:remove_diff_header_icons, project)
+ push_frontend_feature_flag(:moved_mr_sidebar, project)
end
before_action do
- push_frontend_feature_flag(:permit_all_shared_groups_for_approval, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:permit_all_shared_groups_for_approval, @project)
end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
@@ -81,12 +86,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
:rebase,
:discussions,
:pipelines,
- :test_reports
- ]
- urgency :low, [
+ :test_reports,
:codequality_mr_diff_reports,
- :codequality_reports
+ :codequality_reports,
+ :terraform_reports
]
+ urgency :low, [:pipeline_status, :pipelines, :exposed_artifacts]
def index
@merge_requests = @issuables
diff --git a/app/controllers/projects/metrics_dashboard_controller.rb b/app/controllers/projects/metrics_dashboard_controller.rb
index 3f10749602e..e305b018293 100644
--- a/app/controllers/projects/metrics_dashboard_controller.rb
+++ b/app/controllers/projects/metrics_dashboard_controller.rb
@@ -12,6 +12,7 @@ module Projects
before_action do
push_frontend_feature_flag(:prometheus_computed_alerts)
push_frontend_feature_flag(:disable_metric_dashboard_refresh_rate)
+ push_frontend_feature_flag(:monitor_logging, project)
end
feature_category :metrics
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index b896e2543ff..744e45a0f9c 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -19,6 +19,7 @@ class Projects::MilestonesController < Projects::ApplicationController
respond_to :html
feature_category :team_planning
+ urgency :low
def index
@sort = params[:sort] || 'due_date_asc'
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 7322e08e62e..d24b232293b 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -12,6 +12,7 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
feature_category :team_planning
+ urgency :low
def delete_attachment
note.remove_attachment!
diff --git a/app/controllers/projects/packages/infrastructure_registry_controller.rb b/app/controllers/projects/packages/infrastructure_registry_controller.rb
index 99d75afc63a..f1410bf6043 100644
--- a/app/controllers/projects/packages/infrastructure_registry_controller.rb
+++ b/app/controllers/projects/packages/infrastructure_registry_controller.rb
@@ -6,6 +6,7 @@ module Projects
include PackagesAccess
feature_category :infrastructure_as_code
+ urgency :low
def show
@package = project.packages.find(params[:id])
diff --git a/app/controllers/projects/packages/packages_controller.rb b/app/controllers/projects/packages/packages_controller.rb
index 969922266fa..f045bae5c96 100644
--- a/app/controllers/projects/packages/packages_controller.rb
+++ b/app/controllers/projects/packages/packages_controller.rb
@@ -6,6 +6,7 @@ module Projects
include PackagesAccess
feature_category :package_registry
+ urgency :low
# The show action renders index to allow frontend routing to work on page refresh
def show
diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb
index f6171403667..fa38fb209f0 100644
--- a/app/controllers/projects/pipeline_schedules_controller.rb
+++ b/app/controllers/projects/pipeline_schedules_controller.rb
@@ -12,6 +12,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
before_action :authorize_admin_pipeline_schedule!, only: [:destroy]
feature_category :continuous_integration
+ urgency :low
# rubocop: disable CodeReuse/ActiveRecord
def index
diff --git a/app/controllers/projects/pipelines/application_controller.rb b/app/controllers/projects/pipelines/application_controller.rb
index c147d697888..e9dc71a0f4a 100644
--- a/app/controllers/projects/pipelines/application_controller.rb
+++ b/app/controllers/projects/pipelines/application_controller.rb
@@ -11,6 +11,7 @@ module Projects
before_action :authorize_read_pipeline!
feature_category :continuous_integration
+ urgency :low
private
diff --git a/app/controllers/projects/pipelines/tests_controller.rb b/app/controllers/projects/pipelines/tests_controller.rb
index 4daf700a8bd..8f0e20290fe 100644
--- a/app/controllers/projects/pipelines/tests_controller.rb
+++ b/app/controllers/projects/pipelines/tests_controller.rb
@@ -23,7 +23,7 @@ module Projects
def show
respond_to do |format|
format.json do
- if Feature.enabled?(:ci_test_report_artifacts_expired, project, default_enabled: :yaml) && pipeline.has_expired_test_reports?
+ if Feature.enabled?(:ci_test_report_artifacts_expired, project) && pipeline.has_expired_test_reports?
render json: { errors: 'Test report artifacts have expired' }, status: :not_found
else
render json: TestSuiteSerializer
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 02f041637ba..94865024688 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -4,8 +4,11 @@ class Projects::PipelinesController < Projects::ApplicationController
include ::Gitlab::Utils::StrongMemoize
include RedisTracking
- urgency :default, [:status]
- urgency :low, [:index, :new, :builds, :show, :failures, :create, :stage, :retry, :dag, :cancel, :test_report]
+ urgency :low, [
+ :index, :new, :builds, :show, :failures, :create,
+ :stage, :retry, :dag, :cancel, :test_report,
+ :charts, :config_variables, :destroy, :status
+ ]
before_action :disable_query_limiting, only: [:create, :retry]
before_action :pipeline, except: [:index, :new, :create, :charts, :config_variables]
@@ -18,7 +21,9 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :ensure_pipeline, only: [:show, :downloadable_artifacts]
before_action do
- push_frontend_feature_flag(:pipeline_tabs_vue, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:pipeline_tabs_vue, @project)
+ push_frontend_feature_flag(:downstream_retry_action, @project)
+ push_frontend_feature_flag(:failed_jobs_tab_vue, @project)
end
# Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596
@@ -37,6 +42,23 @@ class Projects::PipelinesController < Projects::ApplicationController
POLLING_INTERVAL = 10_000
+ content_security_policy do |policy|
+ next if policy.directives.blank?
+
+ default_script_src = policy.directives['script-src'] || policy.directives['default-src']
+ script_src_values = Array.wrap(default_script_src) | ["'self'", "'unsafe-eval'", 'https://*.zuora.com']
+
+ default_frame_src = policy.directives['frame-src'] || policy.directives['default-src']
+ frame_src_values = Array.wrap(default_frame_src) | ["'self'", 'https://*.zuora.com']
+
+ default_child_src = policy.directives['child-src'] || policy.directives['default-src']
+ child_src_values = Array.wrap(default_child_src) | ["'self'", 'https://*.zuora.com']
+
+ policy.script_src(*script_src_values)
+ policy.frame_src(*frame_src_values)
+ policy.child_src(*child_src_values)
+ end
+
feature_category :continuous_integration, [
:charts, :show, :config_variables, :stage, :cancel, :retry,
:builds, :dag, :failures, :status,
@@ -127,12 +149,22 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def builds
- render_show
+ if Feature.enabled?(:pipeline_tabs_vue, project)
+ redirect_to pipeline_path(@pipeline, tab: 'builds')
+ else
+ render_show
+ end
end
def dag
respond_to do |format|
- format.html { render_show }
+ format.html do
+ if Feature.enabled?(:pipeline_tabs_vue, project)
+ redirect_to pipeline_path(@pipeline, tab: 'dag')
+ else
+ render_show
+ end
+ end
format.json do
render json: Ci::DagPipelineSerializer
.new(project: @project, current_user: @current_user)
@@ -142,7 +174,9 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def failures
- if @pipeline.failed_builds.present?
+ if Feature.enabled?(:pipeline_tabs_vue, project)
+ redirect_to pipeline_path(@pipeline, tab: 'failures')
+ elsif @pipeline.failed_builds.present?
render_show
else
redirect_to pipeline_path(@pipeline)
@@ -196,7 +230,13 @@ class Projects::PipelinesController < Projects::ApplicationController
def test_report
respond_to do |format|
- format.html { render_show }
+ format.html do
+ if Feature.enabled?(:pipeline_tabs_vue, project)
+ redirect_to pipeline_path(@pipeline, tab: 'test_report')
+ else
+ render_show
+ end
+ end
format.json do
render json: TestReportSerializer
.new(current_user: @current_user)
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index 6e08a889520..9adec4dcf00 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -4,6 +4,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
before_action :authorize_admin_pipeline!
feature_category :continuous_integration
+ urgency :low
def show
redirect_to project_settings_ci_cd_path(@project, params: params.to_unsafe_h)
diff --git a/app/controllers/projects/product_analytics_controller.rb b/app/controllers/projects/product_analytics_controller.rb
index 5db7585d8e0..c89cd52530a 100644
--- a/app/controllers/projects/product_analytics_controller.rb
+++ b/app/controllers/projects/product_analytics_controller.rb
@@ -54,6 +54,6 @@ class Projects::ProductAnalyticsController < Projects::ApplicationController
end
def feature_enabled!
- render_404 unless Feature.enabled?(:product_analytics, @project, default_enabled: false)
+ render_404 unless Feature.enabled?(:product_analytics, @project)
end
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 49618c89672..168e703c87d 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -9,6 +9,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
feature_category :projects
+ urgency :low
def index
@sort = params[:sort].presence || sort_value_name
diff --git a/app/controllers/projects/prometheus/alerts_controller.rb b/app/controllers/projects/prometheus/alerts_controller.rb
index 7aebff13278..5e1b9570fa0 100644
--- a/app/controllers/projects/prometheus/alerts_controller.rb
+++ b/app/controllers/projects/prometheus/alerts_controller.rb
@@ -14,9 +14,10 @@ module Projects
prepend_before_action :repository, :project_without_auth, only: [:notify]
before_action :authorize_read_prometheus_alerts!, except: [:notify]
- before_action :alert, only: [:update, :show, :destroy, :metrics_dashboard]
+ before_action :alert, only: [:show, :metrics_dashboard]
feature_category :incident_management
+ urgency :low
def index
render json: serialize_as_json(alerts)
@@ -37,68 +38,13 @@ module Projects
end
end
- def create
- @alert = create_service.execute
-
- if @alert.persisted?
- schedule_prometheus_update!
-
- render json: serialize_as_json(@alert)
- else
- head :bad_request
- end
- end
-
- def update
- if update_service.execute(alert)
- schedule_prometheus_update!
-
- render json: serialize_as_json(alert)
- else
- head :bad_request
- end
- end
-
- def destroy
- if destroy_service.execute(alert)
- schedule_prometheus_update!
-
- head :ok
- else
- head :bad_request
- end
- end
-
private
- def alerts_params
- params.permit(:operator, :threshold, :environment_id, :prometheus_metric_id, :runbook_url)
- end
-
def notify_service
Projects::Prometheus::Alerts::NotifyService
.new(project, params.permit!)
end
- def create_service
- Projects::Prometheus::Alerts::CreateService
- .new(project: project, current_user: current_user, params: alerts_params)
- end
-
- def update_service
- Projects::Prometheus::Alerts::UpdateService
- .new(project: project, current_user: current_user, params: alerts_params)
- end
-
- def destroy_service
- Projects::Prometheus::Alerts::DestroyService
- .new(project: project, current_user: current_user, params: nil)
- end
-
- def schedule_prometheus_update!
- ::Clusters::Applications::ScheduleUpdateService.new(application, project).execute
- end
-
def serialize_as_json(alert_obj)
serializer.represent(alert_obj)
end
@@ -123,10 +69,6 @@ module Projects
}.reverse_merge(opts))
end
- def application
- @application ||= alert.environment.cluster_prometheus_adapter
- end
-
def extract_alert_manager_token(request)
Doorkeeper::OAuth::Token.from_bearer_authorization(request)
end
@@ -136,10 +78,6 @@ module Projects
.find_by_full_path("#{params[:namespace_id]}/#{params[:project_id]}")
end
- def prometheus_alerts
- project.prometheus_alerts.for_environment(params[:environment_id])
- end
-
def metrics_dashboard_params
{
embedded: true,
diff --git a/app/controllers/projects/prometheus/metrics_controller.rb b/app/controllers/projects/prometheus/metrics_controller.rb
index e61d357ce4e..c5778ba15f2 100644
--- a/app/controllers/projects/prometheus/metrics_controller.rb
+++ b/app/controllers/projects/prometheus/metrics_controller.rb
@@ -74,9 +74,9 @@ module Projects
end
def update
- @metric = update_metrics_service(prometheus_metric).execute
+ @metric = prometheus_metric
- if @metric.persisted?
+ if @metric.update(metrics_params)
redirect_to edit_project_integration_path(project, ::Integrations::Prometheus),
notice: _('Metric was successfully updated.')
else
diff --git a/app/controllers/projects/registry/application_controller.rb b/app/controllers/projects/registry/application_controller.rb
index e7bf8c8e757..9a94de3859a 100644
--- a/app/controllers/projects/registry/application_controller.rb
+++ b/app/controllers/projects/registry/application_controller.rb
@@ -9,6 +9,7 @@ module Projects
before_action :authorize_read_container_image!
feature_category :container_registry
+ urgency :low
private
diff --git a/app/controllers/projects/releases/evidences_controller.rb b/app/controllers/projects/releases/evidences_controller.rb
index 41e2ce81eb8..9f59898878c 100644
--- a/app/controllers/projects/releases/evidences_controller.rb
+++ b/app/controllers/projects/releases/evidences_controller.rb
@@ -8,6 +8,7 @@ module Projects
before_action :authorize_read_release_evidence!
feature_category :release_evidence
+ urgency :low
def show
respond_to do |format|
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index 19413d97d9d..1dfb71842bd 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -10,13 +10,20 @@ class Projects::ReleasesController < Projects::ApplicationController
before_action :validate_suffix_path, :fetch_latest_tag, only: :latest_permalink
feature_category :release_orchestration
+ urgency :low
def index
respond_to do |format|
format.html do
require_non_empty_project
end
- format.json { render json: releases }
+ format.json do
+ if Feature.enabled?(:remove_sha_from_releases_json, project)
+ render json: ReleaseSerializer.new.represent(releases)
+ else
+ render json: releases
+ end
+ end
end
end
diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb
index b77ce070492..34ce8df202b 100644
--- a/app/controllers/projects/runner_projects_controller.rb
+++ b/app/controllers/projects/runner_projects_controller.rb
@@ -6,6 +6,7 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
layout 'project_settings'
feature_category :runner
+ urgency :low
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index 0eda8e3352d..ba9576795ec 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -5,6 +5,7 @@ class Projects::RunnersController < Projects::ApplicationController
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
feature_category :runner
+ urgency :low
def index
redirect_to project_settings_ci_cd_path(@project, anchor: 'js-runners-settings')
diff --git a/app/controllers/projects/security/configuration_controller.rb b/app/controllers/projects/security/configuration_controller.rb
index cdb02047215..00a2a5d1193 100644
--- a/app/controllers/projects/security/configuration_controller.rb
+++ b/app/controllers/projects/security/configuration_controller.rb
@@ -42,7 +42,7 @@ module Projects
end
def unify_configuration_enabled?
- Feature.enabled?(:unify_security_configuration, project, default_enabled: :yaml)
+ Feature.enabled?(:unify_security_configuration, project)
end
end
end
diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb
deleted file mode 100644
index 7352edaaab2..00000000000
--- a/app/controllers/projects/serverless/functions_controller.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module Serverless
- class FunctionsController < Projects::ApplicationController
- before_action :ensure_feature_enabled!
- before_action :authorize_read_cluster!
-
- feature_category :not_owned # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
-
- def index
- respond_to do |format|
- format.json do
- functions = finder.execute.select do |function|
- can?(@current_user, :read_cluster, function.cluster)
- end
-
- serialized_functions = serialize_function(functions)
-
- render json: {
- knative_installed: finder.knative_installed,
- functions: serialized_functions
- }.to_json
- end
-
- format.html do
- render
- end
- end
- end
-
- def show
- function = finder.service(params[:environment_id], params[:id])
- return not_found unless function && can?(@current_user, :read_cluster, function.cluster)
-
- @service = serialize_function(function)
- return not_found if @service.nil?
-
- @prometheus = finder.has_prometheus?(params[:environment_id])
-
- respond_to do |format|
- format.json do
- render json: @service
- end
-
- format.html
- end
- end
-
- def metrics
- respond_to do |format|
- format.json do
- metrics = finder.invocation_metrics(params[:environment_id], params[:id])
-
- if metrics.nil?
- head :no_content
- else
- render json: metrics
- end
- end
- end
- end
-
- private
-
- def finder
- Projects::Serverless::FunctionsFinder.new(project)
- end
-
- def serialize_function(function)
- Projects::Serverless::ServiceSerializer.new(current_user: @current_user, project: project).represent(function)
- end
-
- def ensure_feature_enabled!
- render_404 unless Feature.enabled?(:deprecated_serverless, project, default_enabled: :yaml, type: :ops)
- end
- end
- end
-end
diff --git a/app/controllers/projects/service_ping_controller.rb b/app/controllers/projects/service_ping_controller.rb
index 368da8d1ef2..d8f1785d95e 100644
--- a/app/controllers/projects/service_ping_controller.rb
+++ b/app/controllers/projects/service_ping_controller.rb
@@ -3,7 +3,7 @@
class Projects::ServicePingController < Projects::ApplicationController
before_action :authenticate_user!
- feature_category :service_ping
+ feature_category :web_ide
def web_ide_clientside_preview
return render_404 unless Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?
@@ -17,6 +17,7 @@ class Projects::ServicePingController < Projects::ApplicationController
return render_404 unless Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?
Gitlab::UsageDataCounters::WebIdeCounter.increment_previews_success_count
+ Gitlab::UsageDataCounters::EditorUniqueCounter.track_live_preview_edit_action(author: current_user)
head(200)
end
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 1321111faaf..8f83e34411b 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -10,8 +10,6 @@ class Projects::ServicesController < Projects::ApplicationController
before_action :integration
before_action :default_integration, only: [:edit, :update]
before_action :web_hook_logs, only: [:edit, :update]
- before_action :set_deprecation_notice_for_prometheus_integration, only: [:edit, :update]
- before_action :redirect_deprecated_prometheus_integration, only: [:update]
respond_to :html
@@ -118,18 +116,6 @@ class Projects::ServicesController < Projects::ApplicationController
.merge(errors: integration.errors.as_json)
end
- def redirect_deprecated_prometheus_integration
- redirect_to edit_project_integration_path(project, integration) if integration.is_a?(::Integrations::Prometheus) && Feature.enabled?(:settings_operations_prometheus_service, project)
- end
-
- def set_deprecation_notice_for_prometheus_integration
- return if !integration.is_a?(::Integrations::Prometheus) || !Feature.enabled?(:settings_operations_prometheus_service, project)
-
- operations_link_start = "<a href=\"#{project_settings_operations_path(project)}\">"
- message = s_('PrometheusService|You can now manage your Prometheus settings on the %{operations_link_start}Operations%{operations_link_end} page. Fields on this page have been deprecated.') % { operations_link_start: operations_link_start, operations_link_end: "</a>" }
- flash.now[:alert] = message.html_safe
- end
-
def use_inherited_settings?(attributes)
default_integration && attributes[:inherit_from_id] == default_integration.id.to_s
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 3f4d26bb6ec..ee50327be8f 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -18,6 +18,7 @@ module Projects
helper_method :highlight_badge
feature_category :continuous_integration
+ urgency :low
def show
if Feature.enabled?(:ci_pipeline_triggers_settings_vue_ui, @project)
@@ -87,7 +88,7 @@ module Projects
def permitted_project_params
[
:runners_token, :builds_enabled, :build_allow_git_fetch,
- :build_timeout_human_readable, :build_coverage_regex, :public_builds,
+ :build_timeout_human_readable, :public_builds, :ci_separated_caches,
:auto_cancel_pending_pipelines, :ci_config_path, :auto_rollback_enabled,
auto_devops_attributes: [:id, :domain, :enabled, :deploy_strategy],
ci_cd_settings_attributes: [:default_git_depth, :forward_deployment_enabled]
diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
index 43c72b358db..d4126cbd708 100644
--- a/app/controllers/projects/settings/operations_controller.rb
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -17,6 +17,7 @@ module Projects
helper_method :tracing_setting
feature_category :incident_management
+ urgency :low
def update
result = ::Projects::Operations::UpdateService.new(project, current_user, update_params).execute
@@ -133,7 +134,7 @@ module Projects
# overridden in EE
def permitted_project_params
- project_params = {
+ {
incident_management_setting_attributes: ::Gitlab::Tracking::IncidentManagement.tracking_keys.keys,
metrics_setting_attributes: [:external_dashboard_url, :dashboard_timezone],
@@ -149,12 +150,6 @@ module Projects
grafana_integration_attributes: [:token, :grafana_url, :enabled],
tracing_setting_attributes: [:external_url]
}
-
- if Feature.enabled?(:settings_operations_prometheus_service, project)
- project_params[:prometheus_integration_attributes] = [:manual_configuration, :api_url]
- end
-
- project_params
end
end
end
diff --git a/app/controllers/projects/settings/packages_and_registries_controller.rb b/app/controllers/projects/settings/packages_and_registries_controller.rb
index 8f0a19cfac5..0cd2bfa9695 100644
--- a/app/controllers/projects/settings/packages_and_registries_controller.rb
+++ b/app/controllers/projects/settings/packages_and_registries_controller.rb
@@ -9,6 +9,7 @@ module Projects
before_action :packages_and_registries_settings_enabled!
feature_category :package_registry
+ urgency :low
def show
end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index a28c08e87cb..0fd2d56229a 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -12,7 +12,7 @@ module Projects
feature_category :source_code_management, [:show, :cleanup]
feature_category :continuous_delivery, [:create_deploy_token]
- urgency :low, [:show]
+ urgency :low, [:show, :create_deploy_token]
def show
render_show
diff --git a/app/controllers/projects/starrers_controller.rb b/app/controllers/projects/starrers_controller.rb
index 91f49fc4d66..bc857648a06 100644
--- a/app/controllers/projects/starrers_controller.rb
+++ b/app/controllers/projects/starrers_controller.rb
@@ -5,6 +5,8 @@ class Projects::StarrersController < Projects::ApplicationController
feature_category :projects
+ urgency :low, [:index]
+
def index
@starrers = UsersStarProjectsFinder.new(@project, params, current_user: @current_user).execute
@sort = params[:sort].presence || sort_value_name
diff --git a/app/controllers/projects/tags/releases_controller.rb b/app/controllers/projects/tags/releases_controller.rb
index 8e5539f546b..b852673d82a 100644
--- a/app/controllers/projects/tags/releases_controller.rb
+++ b/app/controllers/projects/tags/releases_controller.rb
@@ -9,6 +9,7 @@ class Projects::Tags::ReleasesController < Projects::ApplicationController
before_action :release
feature_category :release_evidence
+ urgency :low
def edit
end
diff --git a/app/controllers/projects/terraform_controller.rb b/app/controllers/projects/terraform_controller.rb
index aef163c98c5..ed1783bb43d 100644
--- a/app/controllers/projects/terraform_controller.rb
+++ b/app/controllers/projects/terraform_controller.rb
@@ -4,6 +4,7 @@ class Projects::TerraformController < Projects::ApplicationController
before_action :authorize_can_read_terraform_state!
feature_category :infrastructure_as_code
+ urgency :low
def index
end
diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb
index dafdeb4c9ef..bba1949a084 100644
--- a/app/controllers/projects/todos_controller.rb
+++ b/app/controllers/projects/todos_controller.rb
@@ -7,6 +7,7 @@ class Projects::TodosController < Projects::ApplicationController
before_action :authenticate_user!, only: [:create]
feature_category :team_planning
+ urgency :low
private
diff --git a/app/controllers/projects/tracings_controller.rb b/app/controllers/projects/tracings_controller.rb
index 2bc0c590e8d..a4aac6aaa32 100644
--- a/app/controllers/projects/tracings_controller.rb
+++ b/app/controllers/projects/tracings_controller.rb
@@ -15,6 +15,7 @@ module Projects
feature_category :tracing
def show
+ render_404 unless Feature.enabled?(:monitor_tracing, @project)
end
private
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index a70795f2065..ed14f66847c 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -17,9 +17,9 @@ class Projects::TreeController < Projects::ApplicationController
before_action :authorize_edit_tree!, only: [:create_dir]
before_action do
- push_frontend_feature_flag(:lazy_load_commits, @project, default_enabled: :yaml)
- push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
- push_frontend_feature_flag(:highlight_js, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:lazy_load_commits, @project)
+ push_frontend_feature_flag(:refactor_blob_viewer, @project)
+ push_frontend_feature_flag(:highlight_js, @project)
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
end
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index eec35fcec8d..f43c7e75fee 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -9,6 +9,7 @@ class Projects::TriggersController < Projects::ApplicationController
layout 'project_settings'
feature_category :continuous_integration
+ urgency :low
def index
redirect_to project_settings_ci_cd_path(@project, anchor: 'js-pipeline-triggers')
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index e6e91231ba2..a364668ea5f 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -23,6 +23,10 @@ class Projects::UploadsController < Projects::ApplicationController
FileUploader
end
+ def target_project
+ model
+ end
+
def find_model
return @project if @project
diff --git a/app/controllers/projects/work_items_controller.rb b/app/controllers/projects/work_items_controller.rb
index d39664e1deb..27857dac2b7 100644
--- a/app/controllers/projects/work_items_controller.rb
+++ b/app/controllers/projects/work_items_controller.rb
@@ -6,6 +6,7 @@ class Projects::WorkItemsController < Projects::ApplicationController
end
feature_category :team_planning
+ urgency :low
def index
render_404 unless project&.work_items_feature_flag_enabled?
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 6cdfdfa9e2f..60d30352ff8 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -35,11 +35,12 @@ class ProjectsController < Projects::ApplicationController
before_action :check_export_rate_limit!, only: [:export, :download_export, :generate_new_export]
before_action do
- push_frontend_feature_flag(:lazy_load_commits, @project, default_enabled: :yaml)
- push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
- push_frontend_feature_flag(:highlight_js, @project, default_enabled: :yaml)
- push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:lazy_load_commits, @project)
+ push_frontend_feature_flag(:refactor_blob_viewer, @project)
+ push_frontend_feature_flag(:highlight_js, @project)
+ push_frontend_feature_flag(:increase_page_size_exponentially, @project)
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?)
end
@@ -56,8 +57,13 @@ class ProjectsController < Projects::ApplicationController
feature_category :code_review, [:unfoldered_environment_names]
feature_category :portfolio_management, [:planning_hierarchy]
+ urgency :low, [:export, :remove_export, :generate_new_export, :download_export]
+ urgency :low, [:preview_markdown, :new_issuable_address]
# TODO: Set high urgency for #show https://gitlab.com/gitlab-org/gitlab/-/issues/334444
- urgency :low, [:refs, :show]
+
+ urgency :low, [:refs, :show, :toggle_star, :transfer, :archive, :destroy, :update, :create,
+ :activity, :edit, :new, :export, :remove_export, :generate_new_export, :download_export]
+
urgency :high, [:unfoldered_environment_names]
def index
@@ -233,6 +239,11 @@ class ProjectsController < Projects::ApplicationController
edit_project_path(@project, anchor: 'js-export-project'),
notice: _("Project export started. A download link will be sent by email and made available on this page.")
)
+ rescue Project::ExportLimitExceeded => ex
+ redirect_to(
+ edit_project_path(@project, anchor: 'js-export-project'),
+ alert: ex.to_s
+ )
end
def download_export
@@ -340,6 +351,8 @@ class ProjectsController < Projects::ApplicationController
#
# pages list order: repository readme, wiki home, issues list, customize workflow
def render_landing_page
+ Gitlab::Tracking.event('project_overview', 'render', user: current_user, project: @project.project)
+
if can?(current_user, :download_code, @project)
return render 'projects/no_repo' unless @project.repository_exists?
@@ -412,6 +425,7 @@ class ProjectsController < Projects::ApplicationController
squash_option
mr_default_target_self
warn_about_potentially_unwanted_characters
+ enforce_auth_checks_on_uploads
]
end
@@ -420,7 +434,6 @@ class ProjectsController < Projects::ApplicationController
:allow_merge_on_skipped_pipeline,
:avatar,
:build_allow_git_fetch,
- :build_coverage_regex,
:build_timeout_human_readable,
:resolve_outdated_diff_discussions,
:container_registry_enabled,
@@ -451,6 +464,7 @@ class ProjectsController < Projects::ApplicationController
:initialize_with_sast,
:initialize_with_readme,
:autoclose_referenced_issues,
+ :ci_separated_caches,
:suggestion_commit_message,
:packages_enabled,
:service_desk_enabled,
diff --git a/app/controllers/pwa_controller.rb b/app/controllers/pwa_controller.rb
new file mode 100644
index 00000000000..ea14dfb27b3
--- /dev/null
+++ b/app/controllers/pwa_controller.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class PwaController < ApplicationController # rubocop:disable Gitlab/NamespacedClass
+ layout 'errors'
+
+ feature_category :navigation
+
+ skip_before_action :authenticate_user!
+
+ def offline
+ end
+end
diff --git a/app/controllers/registrations/welcome_controller.rb b/app/controllers/registrations/welcome_controller.rb
index 41fd1b7a1e6..ea50099120b 100644
--- a/app/controllers/registrations/welcome_controller.rb
+++ b/app/controllers/registrations/welcome_controller.rb
@@ -2,6 +2,8 @@
module Registrations
class WelcomeController < ApplicationController
+ include OneTrustCSP
+
layout 'minimal'
skip_before_action :authenticate_user!, :required_signup_info, :check_two_factor_requirement, only: [:show, :update]
before_action :require_current_user
diff --git a/app/controllers/repositories/lfs_storage_controller.rb b/app/controllers/repositories/lfs_storage_controller.rb
index 252b604dcb0..d54b51b463a 100644
--- a/app/controllers/repositories/lfs_storage_controller.rb
+++ b/app/controllers/repositories/lfs_storage_controller.rb
@@ -6,6 +6,8 @@ module Repositories
include WorkhorseRequest
include SendFileUpload
+ InvalidUploadedFile = Class.new(StandardError)
+
skip_before_action :verify_workhorse_api!, only: :download
# added here as a part of the refactor, will be removed
@@ -44,6 +46,8 @@ module Repositories
end
def upload_finalize
+ validate_uploaded_file!
+
if store_file!(oid, size)
head 200, content_type: LfsRequest::CONTENT_TYPE
else
@@ -55,6 +59,8 @@ module Repositories
render_lfs_forbidden
rescue ObjectStorage::RemoteStoreError
render_lfs_forbidden
+ rescue InvalidUploadedFile
+ render plain: 'SHA256 or size mismatch', status: :bad_request
end
private
@@ -117,5 +123,13 @@ module Repositories
lfs_object: object
)
end
+
+ def validate_uploaded_file!
+ return unless uploaded_file
+
+ if size != uploaded_file.size || oid != uploaded_file.sha256
+ raise InvalidUploadedFile
+ end
+ end
end
end
diff --git a/app/controllers/runner_setup_controller.rb b/app/controllers/runner_setup_controller.rb
index 89b635d5a6f..3926bc553ee 100644
--- a/app/controllers/runner_setup_controller.rb
+++ b/app/controllers/runner_setup_controller.rb
@@ -2,6 +2,7 @@
class RunnerSetupController < ApplicationController
feature_category :runner
+ urgency :low
def platforms
render json: Gitlab::Ci::RunnerInstructions::OS.merge(Gitlab::Ci::RunnerInstructions::OTHER_ENVIRONMENTS)
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index b4e2da0c7b3..aab901c1008 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -25,8 +25,7 @@ class SearchController < ApplicationController
layout 'search'
feature_category :global_search
- urgency :high, [:opensearch]
- urgency :low, [:count]
+ urgency :low
def show
@project = search_service.project
@@ -169,17 +168,17 @@ class SearchController < ApplicationController
search_allowed = case params[:scope]
when 'blobs'
- Feature.enabled?(:global_search_code_tab, current_user, type: :ops, default_enabled: :yaml)
+ Feature.enabled?(:global_search_code_tab, current_user, type: :ops)
when 'commits'
- Feature.enabled?(:global_search_commits_tab, current_user, type: :ops, default_enabled: :yaml)
+ Feature.enabled?(:global_search_commits_tab, current_user, type: :ops)
when 'issues'
- Feature.enabled?(:global_search_issues_tab, current_user, type: :ops, default_enabled: :yaml)
+ Feature.enabled?(:global_search_issues_tab, current_user, type: :ops)
when 'merge_requests'
- Feature.enabled?(:global_search_merge_requests_tab, current_user, type: :ops, default_enabled: :yaml)
+ Feature.enabled?(:global_search_merge_requests_tab, current_user, type: :ops)
when 'wiki_blobs'
- Feature.enabled?(:global_search_wiki_tab, current_user, type: :ops, default_enabled: :yaml)
+ Feature.enabled?(:global_search_wiki_tab, current_user, type: :ops)
when 'users'
- Feature.enabled?(:global_search_users_tab, current_user, type: :ops, default_enabled: :yaml)
+ Feature.enabled?(:global_search_users_tab, current_user, type: :ops)
else
true
end
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
index ebadfd1cdfb..6069924b39a 100644
--- a/app/controllers/sent_notifications_controller.rb
+++ b/app/controllers/sent_notifications_controller.rb
@@ -4,6 +4,7 @@ class SentNotificationsController < ApplicationController
skip_before_action :authenticate_user!
feature_category :team_planning
+ urgency :low
def unsubscribe
@sent_notification = SentNotification.for(params[:id])
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 3e11e0940bf..66a531b0b3b 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -33,7 +33,7 @@ class SessionsController < Devise::SessionsController
before_action :load_recaptcha
before_action :set_invite_params, only: [:new]
before_action do
- push_frontend_feature_flag(:webauthn, default_enabled: :yaml)
+ push_frontend_feature_flag(:webauthn)
end
after_action :log_failed_login, if: :action_new_and_failed_login?
@@ -53,6 +53,7 @@ class SessionsController < Devise::SessionsController
protect_from_forgery with: :exception, prepend: true, except: :destroy
feature_category :authentication_and_authorization
+ urgency :low
CAPTCHA_HEADER = 'X-GitLab-Show-Login-Captcha'
MAX_FAILED_LOGIN_ATTEMPTS = 5
@@ -270,7 +271,7 @@ class SessionsController < Devise::SessionsController
def valid_otp_attempt?(user)
otp_validation_result =
- ::Users::ValidateOtpService.new(user).execute(user_params[:otp_attempt])
+ ::Users::ValidateManualOtpService.new(user).execute(user_params[:otp_attempt])
return true if otp_validation_result[:status] == :success
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
@@ -306,9 +307,9 @@ class SessionsController < Devise::SessionsController
def authentication_method
if user_params[:otp_attempt]
AuthenticationEvent::TWO_FACTOR
- elsif user_params[:device_response] && Feature.enabled?(:webauthn, default_enabled: :yaml)
+ elsif user_params[:device_response] && Feature.enabled?(:webauthn)
AuthenticationEvent::TWO_FACTOR_WEBAUTHN
- elsif user_params[:device_response] && !Feature.enabled?(:webauthn, default_enabled: :yaml)
+ elsif user_params[:device_response] && !Feature.enabled?(:webauthn)
AuthenticationEvent::TWO_FACTOR_U2F
else
AuthenticationEvent::STANDARD
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 228ef710749..794d60e733d 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -34,7 +34,9 @@ class UsersController < ApplicationController
feature_category :snippets, [:snippets]
# TODO: Set higher urgency after resolving https://gitlab.com/gitlab-org/gitlab/-/issues/357914
- urgency :low, [:show]
+ urgency :low, [:show, :calendar_activities, :contributed, :activity, :projects, :groups]
+ urgency :default, [:calendar, :followers, :following, :starred]
+ urgency :high, [:exists]
def show
respond_to do |format|
diff --git a/app/experiments/build_ios_app_guide_email_experiment.rb b/app/experiments/build_ios_app_guide_email_experiment.rb
new file mode 100644
index 00000000000..d334a6a30d9
--- /dev/null
+++ b/app/experiments/build_ios_app_guide_email_experiment.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class BuildIosAppGuideEmailExperiment < ApplicationExperiment
+ control { false }
+ candidate { true }
+end
diff --git a/app/finders/alert_management/alerts_finder.rb b/app/finders/alert_management/alerts_finder.rb
index 1fbc1a4a258..11c0f37a79c 100644
--- a/app/finders/alert_management/alerts_finder.rb
+++ b/app/finders/alert_management/alerts_finder.rb
@@ -66,5 +66,3 @@ module AlertManagement
end
end
end
-
-AlertManagement::AlertsFinder.prepend_mod_with('AlertManagement::AlertsFinder')
diff --git a/app/finders/error_tracking/errors_finder.rb b/app/finders/error_tracking/errors_finder.rb
deleted file mode 100644
index c361d6e2fc2..00000000000
--- a/app/finders/error_tracking/errors_finder.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-module ErrorTracking
- class ErrorsFinder
- def initialize(current_user, project, params)
- @current_user = current_user
- @project = project
- @params = params
- end
-
- def execute
- return ErrorTracking::Error.none unless authorized?
-
- collection = project.error_tracking_errors
- collection = by_status(collection)
- collection = sort(collection)
-
- collection.keyset_paginate(cursor: params[:cursor], per_page: limit)
- end
-
- private
-
- attr_reader :current_user, :project, :params
-
- def by_status(collection)
- if params[:status].present? && ErrorTracking::Error.statuses.key?(params[:status])
- collection.for_status(params[:status])
- else
- collection
- end
- end
-
- def authorized?
- Ability.allowed?(current_user, :read_sentry_issue, project)
- end
-
- def sort(collection)
- params[:sort] ? collection.sort_by_attribute(params[:sort]) : collection.order_id_desc
- end
-
- def limit
- # Restrict the maximum limit at 100 records.
- [(params[:limit] || 20).to_i, 100].min
- end
- end
-end
diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb
index 40d6be03a17..42cd06c8066 100644
--- a/app/finders/group_descendants_finder.rb
+++ b/app/finders/group_descendants_finder.rb
@@ -112,7 +112,7 @@ class GroupDescendantsFinder
group_ids = base_for_ancestors.except(:select, :sort).select(:id)
groups = Group.where(id: group_ids)
- if Feature.enabled?(:linear_group_descendants_finder_upto, current_user, default_enabled: :yaml)
+ if Feature.enabled?(:linear_group_descendants_finder_upto, current_user)
groups.self_and_ancestors(upto: parent_group.id)
else
Gitlab::ObjectHierarchy.new(groups).base_and_ancestors(upto: parent_group.id)
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index 4213a3f1965..048e25046da 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -60,6 +60,10 @@ class GroupMembersFinder < UnionFinder
members = members.filter_by_2fa(params[:two_factor])
end
+ if params[:access_levels].present?
+ members = members.by_access_level(params[:access_levels])
+ end
+
members = apply_additional_filters(members)
by_created_at(members)
diff --git a/app/finders/groups/projects_requiring_authorizations_refresh/base.rb b/app/finders/groups/projects_requiring_authorizations_refresh/base.rb
new file mode 100644
index 00000000000..c719e6ba903
--- /dev/null
+++ b/app/finders/groups/projects_requiring_authorizations_refresh/base.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Groups
+ module ProjectsRequiringAuthorizationsRefresh
+ class Base
+ def initialize(group)
+ @group = group
+ end
+
+ private
+
+ def ids_of_projects_in_hierarchy_and_project_shares(group)
+ project_ids = Set.new
+
+ ids_of_projects_in_hierarchy = group.all_projects.pluck(:id) # rubocop: disable CodeReuse/ActiveRecord
+ ids_of_projects_in_project_shares = ids_of_projects_shared_with_self_and_descendant_groups(group)
+
+ project_ids.merge(ids_of_projects_in_hierarchy)
+ project_ids.merge(ids_of_projects_in_project_shares)
+
+ project_ids
+ end
+
+ def ids_of_projects_shared_with_self_and_descendant_groups(group, batch_size: 50)
+ project_ids = Set.new
+
+ group.self_and_descendants_ids.each_slice(batch_size) do |group_ids|
+ project_ids.merge(ProjectGroupLink.in_group(group_ids).pluck(:project_id)) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ project_ids
+ end
+ end
+ end
+end
diff --git a/app/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder.rb b/app/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder.rb
new file mode 100644
index 00000000000..f6b8b999b99
--- /dev/null
+++ b/app/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+# Groups::ProjectsRequiringAuthorizationsRefresh::OnDirectMembershipFinder
+#
+# Given a group, this finder can be used to obtain a list of Project IDs of projects
+# that requires their `project_authorizations` records to be refreshed in the event where
+# a member has been added/removed/updated in the group.
+
+module Groups
+ module ProjectsRequiringAuthorizationsRefresh
+ class OnDirectMembershipFinder < Base
+ def execute
+ project_ids = Set.new
+
+ project_ids.merge(ids_of_projects_in_hierarchy_and_project_shares(@group))
+ project_ids.merge(ids_of_projects_in_hierarchy_and_project_shares_of_shared_groups(@group))
+
+ project_ids.to_a
+ end
+
+ private
+
+ def ids_of_projects_in_hierarchy_and_project_shares_of_shared_groups(group, batch_size: 10)
+ project_ids = Set.new
+
+ group.shared_groups.each_batch(of: batch_size) do |shared_groups_batch|
+ shared_groups_batch.each do |shared_group|
+ project_ids.merge(ids_of_projects_in_hierarchy_and_project_shares(shared_group))
+ end
+ end
+
+ project_ids
+ end
+ end
+ end
+end
diff --git a/app/finders/groups/projects_requiring_authorizations_refresh/on_transfer_finder.rb b/app/finders/groups/projects_requiring_authorizations_refresh/on_transfer_finder.rb
new file mode 100644
index 00000000000..781e1222287
--- /dev/null
+++ b/app/finders/groups/projects_requiring_authorizations_refresh/on_transfer_finder.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+# Groups::ProjectsRequiringAuthorizationsRefresh::OnTransferFinder
+#
+# Given a group, this finder can be used to obtain a list of Project IDs of projects
+# that requires their `project_authorizations` records to be refreshed in the event where
+# the group has been transferred.
+
+module Groups
+ module ProjectsRequiringAuthorizationsRefresh
+ class OnTransferFinder < Base
+ def execute
+ ids_of_projects_in_hierarchy_and_project_shares(@group).to_a
+ end
+ end
+ end
+end
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index 7cb3e7a5d7f..9a8bc74f435 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -54,7 +54,7 @@ class GroupsFinder < UnionFinder
groups = []
if current_user
- if Feature.enabled?(:use_traversal_ids_groups_finder, current_user, default_enabled: :yaml)
+ if Feature.enabled?(:use_traversal_ids_groups_finder, current_user)
groups << current_user.authorized_groups.self_and_ancestors
groups << current_user.groups.self_and_descendants
else
@@ -81,7 +81,7 @@ class GroupsFinder < UnionFinder
.groups
.where('members.access_level >= ?', params[:min_access_level])
- if Feature.enabled?(:use_traversal_ids_groups_finder, current_user, default_enabled: :yaml)
+ if Feature.enabled?(:use_traversal_ids_groups_finder, current_user)
groups.self_and_descendants
else
Gitlab::ObjectHierarchy
diff --git a/app/finders/incident_management/timeline_events_finder.rb b/app/finders/incident_management/timeline_events_finder.rb
new file mode 100644
index 00000000000..09de46bb79f
--- /dev/null
+++ b/app/finders/incident_management/timeline_events_finder.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ class TimelineEventsFinder
+ def initialize(user, incident, params = {})
+ @user = user
+ @incident = incident
+ @params = params
+ end
+
+ def execute
+ return ::IncidentManagement::TimelineEvent.none unless allowed?
+
+ collection = incident.incident_management_timeline_events
+ collection = by_id(collection)
+ sort(collection)
+ end
+
+ private
+
+ attr_reader :user, :incident, :params
+
+ def allowed?
+ Ability.allowed?(user, :read_incident_management_timeline_event, incident)
+ end
+
+ def by_id(collection)
+ return collection unless params[:id]
+
+ collection.id_in(params[:id])
+ end
+
+ def sort(collection)
+ collection.order_occurred_at_asc
+ end
+ end
+end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index bf7b2265ded..fe07a52cbf0 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -348,7 +348,7 @@ class IssuableFinder
params[:in].blank? &&
klass.try(:pg_full_text_searchable_columns).present? &&
params[:search] =~ FULL_TEXT_SEARCH_TERM_REGEX &&
- Feature.enabled?(:issues_full_text_search, params.project || params.group, default_enabled: :yaml)
+ Feature.enabled?(:issues_full_text_search, params.project || params.group)
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -489,7 +489,7 @@ class IssuableFinder
def or_filters_enabled?
strong_memoize(:or_filters_enabled) do
- Feature.enabled?(:or_issuable_queries, feature_flag_scope, default_enabled: :yaml)
+ Feature.enabled?(:or_issuable_queries, feature_flag_scope)
end
end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index a7eaaddd187..7929c36906d 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -20,7 +20,6 @@
# sort: string
# my_reaction_emoji: string
# public_only: boolean
-# include_hidden: boolean
# due_date: date or '0', '', 'overdue', 'week', or 'month'
# created_after: datetime
# created_before: datetime
@@ -48,6 +47,8 @@ class IssuesFinder < IssuableFinder
# rubocop: disable CodeReuse/ActiveRecord
def with_confidentiality_access_check
+ return Issue.all if params.user_can_see_all_issues?
+
# Only admins can see hidden issues, so for non-admins, we filter out any hidden issues
issues = Issue.without_hidden
@@ -75,9 +76,7 @@ class IssuesFinder < IssuableFinder
private
def init_collection
- if params.include_hidden?
- Issue.all
- elsif params.public_only?
+ if params.public_only?
Issue.public_only
else
with_confidentiality_access_check
diff --git a/app/finders/issues_finder/params.rb b/app/finders/issues_finder/params.rb
index 57bfb35f1b8..7f8acb79ed6 100644
--- a/app/finders/issues_finder/params.rb
+++ b/app/finders/issues_finder/params.rb
@@ -6,10 +6,6 @@ class IssuesFinder
params.fetch(:public_only, false)
end
- def include_hidden?
- user_can_see_all_issues?
- end
-
def filter_by_any_due_date?
due_date? && params[:due_date] == Issue::AnyDueDate.name
end
diff --git a/app/finders/packages/build_infos_finder.rb b/app/finders/packages/build_infos_finder.rb
index 92ad5888eb9..a346c3d754b 100644
--- a/app/finders/packages/build_infos_finder.rb
+++ b/app/finders/packages/build_infos_finder.rb
@@ -2,33 +2,55 @@
module Packages
class BuildInfosFinder
+ include ActiveRecord::ConnectionAdapters::Quoting
+
MAX_PAGE_SIZE = 100
- def initialize(package, params)
- @package = package
+ def initialize(package_ids, params)
+ @package_ids = package_ids
@params = params
end
def execute
- build_infos = @package.build_infos.without_empty_pipelines
- build_infos = apply_order(build_infos)
- build_infos = apply_limit(build_infos)
- apply_cursor(build_infos)
+ return Packages::BuildInfo.none if @package_ids.blank?
+
+ # This is a highly custom query that
+ # will not be re-used elsewhere
+ # rubocop: disable CodeReuse/ActiveRecord
+ query = Packages::Package.id_in(@package_ids)
+ .select('build_infos.*')
+ .from([Packages::Package.arel_table, lateral_query.arel.lateral.as('build_infos')])
+ .order('build_infos.id DESC')
+
+ # We manually select build_infos fields from the lateral query.
+ # Thus, we need to instruct ActiveRecord that returned rows are
+ # actually Packages::BuildInfo objects
+ Packages::BuildInfo.find_by_sql(query.to_sql)
+ # rubocop: enable CodeReuse/ActiveRecord
end
private
- def apply_order(build_infos)
- order_direction = :desc
- order_direction = :asc if last
+ def lateral_query
+ order_direction = last ? :asc : :desc
- build_infos.order_by_pipeline_id(order_direction)
+ # This is a highly custom query that
+ # will not be re-used elsewhere
+ # rubocop: disable CodeReuse/ActiveRecord
+ where_condition = Packages::BuildInfo.arel_table[:package_id]
+ .eq(Arel.sql("#{Packages::Package.table_name}.id"))
+ build_infos = ::Packages::BuildInfo.without_empty_pipelines
+ .where(where_condition)
+ .order(id: order_direction)
+ .limit(max_rows_per_package_id)
+ # rubocop: enable CodeReuse/ActiveRecord
+ apply_cursor(build_infos)
end
- def apply_limit(build_infos)
+ def max_rows_per_package_id
limit = [first, last, max_page_size, MAX_PAGE_SIZE].compact.min
limit += 1 if support_next_page
- build_infos.limit(limit)
+ limit
end
def apply_cursor(build_infos)
diff --git a/app/finders/packages/build_infos_for_many_packages_finder.rb b/app/finders/packages/build_infos_for_many_packages_finder.rb
deleted file mode 100644
index 8f9805f51d0..00000000000
--- a/app/finders/packages/build_infos_for_many_packages_finder.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-# frozen_string_literal: true
-
-module Packages
- # TODO rename to BuildInfosFinder when cleaning up packages_graphql_pipelines_resolver
- # https://gitlab.com/gitlab-org/gitlab/-/issues/358432
- class BuildInfosForManyPackagesFinder
- include ActiveRecord::ConnectionAdapters::Quoting
-
- MAX_PAGE_SIZE = 100
-
- def initialize(package_ids, params)
- @package_ids = package_ids
- @params = params
- end
-
- def execute
- return Packages::BuildInfo.none if @package_ids.blank?
-
- # This is a highly custom query that
- # will not be re-used elsewhere
- # rubocop: disable CodeReuse/ActiveRecord
- query = Packages::Package.id_in(@package_ids)
- .select('build_infos.*')
- .from([Packages::Package.arel_table, lateral_query.arel.lateral.as('build_infos')])
- .order('build_infos.id DESC')
-
- # We manually select build_infos fields from the lateral query.
- # Thus, we need to instruct ActiveRecord that returned rows are
- # actually Packages::BuildInfo objects
- Packages::BuildInfo.find_by_sql(query.to_sql)
- # rubocop: enable CodeReuse/ActiveRecord
- end
-
- private
-
- def lateral_query
- order_direction = last ? :asc : :desc
-
- # This is a highly custom query that
- # will not be re-used elsewhere
- # rubocop: disable CodeReuse/ActiveRecord
- where_condition = Packages::BuildInfo.arel_table[:package_id]
- .eq(Arel.sql("#{Packages::Package.table_name}.id"))
- build_infos = ::Packages::BuildInfo.without_empty_pipelines
- .where(where_condition)
- .order(id: order_direction)
- .limit(max_rows_per_package_id)
- # rubocop: enable CodeReuse/ActiveRecord
- apply_cursor(build_infos)
- end
-
- def max_rows_per_package_id
- limit = [first, last, max_page_size, MAX_PAGE_SIZE].compact.min
- limit += 1 if support_next_page
- limit
- end
-
- def apply_cursor(build_infos)
- if before
- build_infos.with_pipeline_id_greater_than(before)
- elsif after
- build_infos.with_pipeline_id_less_than(after)
- else
- build_infos
- end
- end
-
- def first
- @params[:first]
- end
-
- def last
- @params[:last]
- end
-
- def max_page_size
- @params[:max_page_size]
- end
-
- def before
- @params[:before]
- end
-
- def after
- @params[:after]
- end
-
- def support_next_page
- @params[:support_next_page]
- end
- end
-end
diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb
index be266045951..7d356c1014c 100644
--- a/app/finders/personal_access_tokens_finder.rb
+++ b/app/finders/personal_access_tokens_finder.rb
@@ -79,8 +79,6 @@ class PersonalAccessTokensFinder
tokens.active
when 'inactive'
tokens.inactive
- when 'active_or_expired'
- tokens.not_revoked.expired.or(tokens.active)
else
tokens
end
diff --git a/app/finders/projects/serverless/functions_finder.rb b/app/finders/projects/serverless/functions_finder.rb
deleted file mode 100644
index f8ccea6b820..00000000000
--- a/app/finders/projects/serverless/functions_finder.rb
+++ /dev/null
@@ -1,153 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module Serverless
- class FunctionsFinder
- include Gitlab::Utils::StrongMemoize
- include ReactiveCaching
-
- attr_reader :project
-
- self.reactive_cache_key = ->(finder) { finder.cache_key }
- self.reactive_cache_work_type = :external_dependency
- self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
-
- MAX_CLUSTERS = 10
-
- def initialize(project)
- @project = project
- end
-
- def execute
- knative_services.flatten.compact
- end
-
- def knative_installed
- return knative_installed_from_cluster?(*cache_key) if available_environments.empty?
-
- states = services_finders.map do |finder|
- finder.knative_detected.tap do |state|
- return state if state == ::Clusters::KnativeServicesFinder::KNATIVE_STATES['checking'] # rubocop:disable Cop/AvoidReturnFromBlocks
- end
- end
-
- states.any? { |state| state == ::Clusters::KnativeServicesFinder::KNATIVE_STATES['installed'] }
- end
-
- def service(environment_scope, name)
- knative_service(environment_scope, name)&.first
- end
-
- def invocation_metrics(environment_scope, name)
- environment = finders_for_scope(environment_scope).first&.environment
-
- if environment.present? && environment.prometheus_adapter&.can_query?
- func = ::Serverless::Function.new(project, name, environment.deployment_namespace)
- environment.prometheus_adapter.query(:knative_invocation, func)
- end
- end
-
- def has_prometheus?(environment_scope)
- finders_for_scope(environment_scope).any? do |finder|
- finder.cluster.integration_prometheus_available?
- end
- end
-
- def self.from_cache(project_id)
- project = Project.find(project_id)
-
- new(project)
- end
-
- def cache_key(*args)
- [project.id]
- end
-
- def calculate_reactive_cache(*)
- # rubocop: disable CodeReuse/ActiveRecord
- project.all_clusters.enabled.take(MAX_CLUSTERS).any? do |cluster|
- cluster.kubeclient.knative_client.discover
- rescue Kubeclient::ResourceNotFoundError
- next
- end
- end
-
- private
-
- def knative_installed_from_cluster?(*cache_key)
- cached_data = with_reactive_cache_memoized(*cache_key) { |data| data }
-
- return ::Clusters::KnativeServicesFinder::KNATIVE_STATES['checking'] if cached_data.nil?
-
- cached_data ? true : false
- end
-
- def with_reactive_cache_memoized(*cache_key)
- strong_memoize(:reactive_cache) do
- with_reactive_cache(*cache_key) { |data| data }
- end
- end
-
- def knative_service(environment_scope, name)
- finders_for_scope(environment_scope).map do |finder|
- services = finder
- .services
- .select { |svc| svc["metadata"]["name"] == name }
-
- attributes = add_metadata(finder, services).first
- next unless attributes
-
- Gitlab::Serverless::Service.new(attributes)
- end
- end
-
- def knative_services
- services_finders.map do |finder|
- attributes = add_metadata(finder, finder.services)
-
- attributes&.map do |attributes|
- Gitlab::Serverless::Service.new(attributes)
- end
- end
- end
-
- def add_metadata(finder, services)
- return if services.nil?
-
- add_pod_count = services.one?
-
- services.each do |s|
- s["environment_scope"] = finder.cluster.environment_scope
- s["environment"] = finder.environment
- s["cluster"] = finder.cluster
-
- if add_pod_count
- s["podcount"] = finder
- .service_pod_details(s["metadata"]["name"])
- .length
- end
- end
- end
-
- def services_finders
- strong_memoize(:services_finders) do
- available_environments.map(&:knative_services_finder).compact
- end
- end
-
- def available_environments
- @project.environments.available.preload_cluster
- end
-
- def finders_for_scope(environment_scope)
- services_finders.select do |finder|
- environment_scope == finder.cluster.environment_scope
- end
- end
-
- def id
- nil
- end
- end
- end
-end
diff --git a/app/finders/releases_finder.rb b/app/finders/releases_finder.rb
index 0d72d6ffc6b..78240e0a050 100644
--- a/app/finders/releases_finder.rb
+++ b/app/finders/releases_finder.rb
@@ -29,28 +29,14 @@ class ReleasesFinder
Release.where(project_id: projects).where.not(tag: nil) # rubocop: disable CodeReuse/ActiveRecord
end
- def include_subgroups?
- params.fetch(:include_subgroups, false)
- end
-
def projects
strong_memoize(:projects) do
if parent.is_a?(Project)
Ability.allowed?(current_user, :read_release, parent) ? [parent] : []
- elsif parent.is_a?(Group)
- Ability.allowed?(current_user, :read_release, parent) ? accessible_projects : []
end
end
end
- def accessible_projects
- if include_subgroups?
- Project.for_group_and_its_subgroups(parent)
- else
- parent.projects
- end
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def by_tag(releases)
return releases unless params[:tag].present?
diff --git a/app/finders/tags_finder.rb b/app/finders/tags_finder.rb
index 6bc5419e704..16bba62f766 100644
--- a/app/finders/tags_finder.rb
+++ b/app/finders/tags_finder.rb
@@ -1,10 +1,6 @@
# frozen_string_literal: true
class TagsFinder < GitRefsFinder
- def initialize(repository, params)
- super(repository, params)
- end
-
def execute(gitaly_pagination: false)
tags = if gitaly_pagination
repository.tags_sorted_by(sort, pagination_params)
diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb
index 64903c67573..0f7bf893bb2 100644
--- a/app/finders/user_recent_events_finder.rb
+++ b/app/finders/user_recent_events_finder.rb
@@ -74,7 +74,12 @@ class UserRecentEventsFinder
return Event.none if users.empty?
if Feature.enabled?(:optimized_followed_users_queries, current_user)
- query_builder_params = event_filter.in_operator_query_builder_params(users)
+ array_data = {
+ scope_ids: users,
+ scope_model: User,
+ mapping_column: :author_id
+ }
+ query_builder_params = event_filter.in_operator_query_builder_params(array_data)
Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
.new(**query_builder_params)
diff --git a/app/graphql/mutations/award_emojis/base.rb b/app/graphql/mutations/award_emojis/base.rb
index ce73f2083f2..dc2d46269e6 100644
--- a/app/graphql/mutations/award_emojis/base.rb
+++ b/app/graphql/mutations/award_emojis/base.rb
@@ -26,12 +26,6 @@ module Mutations
private
- # TODO: remove this method when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- def find_object(id:)
- super(id: ::Types::GlobalIDType[::Awardable].coerce_isolated_input(id))
- end
-
def authorize!(object)
super
raise_resource_not_available_error!(NOT_EMOJI_AWARDABLE) unless object.emoji_awardable?
diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb
index 5de042f78d6..d57a097a9e2 100644
--- a/app/graphql/mutations/base_mutation.rb
+++ b/app/graphql/mutations/base_mutation.rb
@@ -4,7 +4,6 @@ module Mutations
class BaseMutation < GraphQL::Schema::RelayClassicMutation
include Gitlab::Graphql::Authorize::AuthorizeResource
prepend Gitlab::Graphql::CopyFieldDescription
- prepend ::Gitlab::Graphql::GlobalIDCompatibility
ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance'
diff --git a/app/graphql/mutations/boards/update.rb b/app/graphql/mutations/boards/update.rb
index 40ab5a5ba7d..7cfce9d2d91 100644
--- a/app/graphql/mutations/boards/update.rb
+++ b/app/graphql/mutations/boards/update.rb
@@ -33,9 +33,6 @@ module Mutations
private
def find_object(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::Board].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/mutations/ci/ci_cd_settings_update.rb b/app/graphql/mutations/ci/ci_cd_settings_update.rb
index dec90ced962..98b8e9567e7 100644
--- a/app/graphql/mutations/ci/ci_cd_settings_update.rb
+++ b/app/graphql/mutations/ci/ci_cd_settings_update.rb
@@ -2,42 +2,9 @@
module Mutations
module Ci
- class CiCdSettingsUpdate < BaseMutation
+ # TODO: Remove in 16.0, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87002
+ class CiCdSettingsUpdate < ProjectCiCdSettingsUpdate
graphql_name 'CiCdSettingsUpdate'
-
- include FindsProject
-
- authorize :admin_project
-
- argument :full_path, GraphQL::Types::ID,
- required: true,
- description: 'Full Path of the project the settings belong to.'
-
- argument :keep_latest_artifact, GraphQL::Types::Boolean,
- required: false,
- description: 'Indicates if the latest artifact should be kept for this project.'
-
- argument :job_token_scope_enabled, GraphQL::Types::Boolean,
- required: false,
- description: 'Indicates CI job tokens generated in this project have restricted access to resources.'
-
- field :ci_cd_settings,
- Types::Ci::CiCdSettingType,
- null: false,
- description: 'CI/CD settings after mutation.'
-
- def resolve(full_path:, **args)
- project = authorized_find!(full_path)
- settings = project.ci_cd_settings
- settings.update(args)
-
- {
- ci_cd_settings: settings,
- errors: errors_on_object(settings)
- }
- end
end
end
end
-
-Mutations::Ci::CiCdSettingsUpdate.prepend_mod_with('Mutations::Ci::CiCdSettingsUpdate')
diff --git a/app/graphql/mutations/ci/job/base.rb b/app/graphql/mutations/ci/job/base.rb
index a9fe26226d9..6ea8e25a58d 100644
--- a/app/graphql/mutations/ci/job/base.rb
+++ b/app/graphql/mutations/ci/job/base.rb
@@ -11,9 +11,6 @@ module Mutations
description: 'ID of the job to mutate.'
def find_object(id: )
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = JobID.coerce_isolated_input(id)
GlobalID::Locator.locate(id)
end
end
diff --git a/app/graphql/mutations/ci/pipeline/base.rb b/app/graphql/mutations/ci/pipeline/base.rb
index aed8035a52a..503cf6c005f 100644
--- a/app/graphql/mutations/ci/pipeline/base.rb
+++ b/app/graphql/mutations/ci/pipeline/base.rb
@@ -13,9 +13,6 @@ module Mutations
private
def find_object(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = PipelineID.coerce_isolated_input(id)
GlobalID::Locator.locate(id)
end
end
diff --git a/app/graphql/mutations/ci/project_ci_cd_settings_update.rb b/app/graphql/mutations/ci/project_ci_cd_settings_update.rb
new file mode 100644
index 00000000000..b0cffa2c088
--- /dev/null
+++ b/app/graphql/mutations/ci/project_ci_cd_settings_update.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ class ProjectCiCdSettingsUpdate < BaseMutation
+ graphql_name 'ProjectCiCdSettingsUpdate'
+
+ include FindsProject
+
+ authorize :admin_project
+
+ argument :full_path, GraphQL::Types::ID,
+ required: true,
+ description: 'Full Path of the project the settings belong to.'
+
+ argument :keep_latest_artifact, GraphQL::Types::Boolean,
+ required: false,
+ description: 'Indicates if the latest artifact should be kept for this project.'
+
+ argument :job_token_scope_enabled, GraphQL::Types::Boolean,
+ required: false,
+ description: 'Indicates CI job tokens generated in this project have restricted access to resources.'
+
+ field :ci_cd_settings,
+ Types::Ci::CiCdSettingType,
+ null: false,
+ description: 'CI/CD settings after mutation.'
+
+ def resolve(full_path:, **args)
+ project = authorized_find!(full_path)
+ settings = project.ci_cd_settings
+ settings.update(args)
+
+ {
+ ci_cd_settings: settings,
+ errors: errors_on_object(settings)
+ }
+ end
+ end
+ end
+end
+
+Mutations::Ci::ProjectCiCdSettingsUpdate.prepend_mod_with('Mutations::Ci::ProjectCiCdSettingsUpdate')
diff --git a/app/graphql/mutations/ci/runner/delete.rb b/app/graphql/mutations/ci/runner/delete.rb
index 1713ec0bf6d..db68914a4eb 100644
--- a/app/graphql/mutations/ci/runner/delete.rb
+++ b/app/graphql/mutations/ci/runner/delete.rb
@@ -23,10 +23,6 @@ module Mutations
end
def find_object(id)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = RunnerID.coerce_isolated_input(id)
-
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/mutations/ci/runner/update.rb b/app/graphql/mutations/ci/runner/update.rb
index 3432840f60f..faccd1273e5 100644
--- a/app/graphql/mutations/ci/runner/update.rb
+++ b/app/graphql/mutations/ci/runner/update.rb
@@ -61,10 +61,6 @@ module Mutations
end
def find_object(id)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = RunnerID.coerce_isolated_input(id)
-
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/mutations/ci/runners_registration_token/reset.rb b/app/graphql/mutations/ci/runners_registration_token/reset.rb
index 29ef7aa2e81..8c49b682ab0 100644
--- a/app/graphql/mutations/ci/runners_registration_token/reset.rb
+++ b/app/graphql/mutations/ci/runners_registration_token/reset.rb
@@ -23,19 +23,24 @@ module Mutations
null: true,
description: 'Runner token after mutation.'
- def resolve(**args)
+ def resolve(type:, id: nil)
+ scope = authorized_find!(type: type, id: id)
+ new_token = reset_token(scope)
+
{
- token: reset_token(**args),
- errors: []
+ token: new_token,
+ errors: errors_on_object(scope)
}
end
private
- def find_object(type:, **args)
- id = args[:id]
-
+ def find_object(type:, id: nil)
case type
+ when 'instance_type'
+ raise Gitlab::Graphql::Errors::ArgumentError, "id must not be specified for '#{type}' scope" if id.present?
+
+ ApplicationSetting.current
when 'group_type'
GitlabSchema.object_from_id(id, expected_type: ::Group)
when 'project_type'
@@ -43,20 +48,7 @@ module Mutations
end
end
- def reset_token(type:, **args)
- id = args[:id]
- scope = nil
-
- case type
- when 'instance_type'
- raise Gitlab::Graphql::Errors::ArgumentError, "id must not be specified for '#{type}' scope" if id.present?
-
- scope = ApplicationSetting.current
- authorize!(scope)
- when 'group_type', 'project_type'
- scope = authorized_find!(type: type, id: id)
- end
-
+ def reset_token(scope)
::Ci::Runners::ResetRegistrationTokenService.new(scope, current_user).execute if scope
end
end
diff --git a/app/graphql/mutations/clusters/agent_tokens/create.rb b/app/graphql/mutations/clusters/agent_tokens/create.rb
index 07bf2536065..a99a54fa5ed 100644
--- a/app/graphql/mutations/clusters/agent_tokens/create.rb
+++ b/app/graphql/mutations/clusters/agent_tokens/create.rb
@@ -58,9 +58,6 @@ module Mutations
private
def find_object(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ClusterAgentID.coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/mutations/clusters/agent_tokens/delete.rb b/app/graphql/mutations/clusters/agent_tokens/delete.rb
deleted file mode 100644
index 603b6b30910..00000000000
--- a/app/graphql/mutations/clusters/agent_tokens/delete.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Mutations
- module Clusters
- module AgentTokens
- class Delete < BaseMutation
- graphql_name 'ClusterAgentTokenDelete'
-
- authorize :admin_cluster
-
- TokenID = ::Types::GlobalIDType[::Clusters::AgentToken]
-
- argument :id, TokenID,
- required: true,
- description: 'Global ID of the cluster agent token that will be deleted.'
-
- def resolve(id:)
- token = authorized_find!(id: id)
- token.destroy
-
- { errors: errors_on_object(token) }
- end
-
- private
-
- def find_object(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = TokenID.coerce_isolated_input(id)
- GitlabSchema.find_by_gid(id)
- end
- end
- end
- end
-end
diff --git a/app/graphql/mutations/clusters/agent_tokens/revoke.rb b/app/graphql/mutations/clusters/agent_tokens/revoke.rb
index ca570792296..974db976f1d 100644
--- a/app/graphql/mutations/clusters/agent_tokens/revoke.rb
+++ b/app/graphql/mutations/clusters/agent_tokens/revoke.rb
@@ -24,9 +24,6 @@ module Mutations
private
def find_object(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = TokenID.coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/mutations/clusters/agents/delete.rb b/app/graphql/mutations/clusters/agents/delete.rb
index 9ada1f31f60..fb482e02794 100644
--- a/app/graphql/mutations/clusters/agents/delete.rb
+++ b/app/graphql/mutations/clusters/agents/delete.rb
@@ -28,9 +28,6 @@ module Mutations
private
def find_object(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = AgentID.coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/mutations/container_expiration_policies/update.rb b/app/graphql/mutations/container_expiration_policies/update.rb
index 762058acf3d..c2208e469c5 100644
--- a/app/graphql/mutations/container_expiration_policies/update.rb
+++ b/app/graphql/mutations/container_expiration_policies/update.rb
@@ -7,7 +7,7 @@ module Mutations
include FindsProject
- authorize :destroy_container_image
+ authorize :admin_container_image
argument :project_path,
GraphQL::Types::ID,
diff --git a/app/graphql/mutations/container_repositories/destroy_base.rb b/app/graphql/mutations/container_repositories/destroy_base.rb
index ddaa6c52121..1c2c4d87a5f 100644
--- a/app/graphql/mutations/container_repositories/destroy_base.rb
+++ b/app/graphql/mutations/container_repositories/destroy_base.rb
@@ -8,9 +8,6 @@ module Mutations
private
def find_object(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::ContainerRepository].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/mutations/customer_relations/contacts/base.rb b/app/graphql/mutations/customer_relations/contacts/base.rb
new file mode 100644
index 00000000000..5d49d48ebe2
--- /dev/null
+++ b/app/graphql/mutations/customer_relations/contacts/base.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Mutations
+ module CustomerRelations
+ module Contacts
+ class Base < BaseMutation
+ include ResolvesIds
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ field :contact,
+ Types::CustomerRelations::ContactType,
+ null: true,
+ description: 'Contact after the mutation.'
+
+ authorize :admin_crm_contact
+
+ def set_organization!(args)
+ return unless args[:organization_id]
+
+ args[:organization_id] = args[:organization_id].model_id
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/customer_relations/contacts/create.rb b/app/graphql/mutations/customer_relations/contacts/create.rb
index 96dc047c3db..5b4063fb89a 100644
--- a/app/graphql/mutations/customer_relations/contacts/create.rb
+++ b/app/graphql/mutations/customer_relations/contacts/create.rb
@@ -3,17 +3,11 @@
module Mutations
module CustomerRelations
module Contacts
- class Create < BaseMutation
+ class Create < Base
graphql_name 'CustomerRelationsContactCreate'
- include ResolvesIds
include Gitlab::Graphql::Authorize::AuthorizeResource
- field :contact,
- Types::CustomerRelations::ContactType,
- null: true,
- description: 'Contact after the mutation.'
-
argument :group_id, ::Types::GlobalIDType[::Group],
required: true,
description: 'Group for the contact.'
@@ -42,8 +36,6 @@ module Mutations
required: false,
description: 'Description of or notes for the contact.'
- authorize :admin_crm_contact
-
def resolve(args)
group = authorized_find!(id: args[:group_id])
@@ -55,12 +47,6 @@ module Mutations
def find_object(id:)
GitlabSchema.object_from_id(id, expected_type: ::Group)
end
-
- def set_organization!(args)
- return unless args[:organization_id]
-
- args[:organization_id] = resolve_ids(args[:organization_id], ::Types::GlobalIDType[::CustomerRelations::Organization])[0]
- end
end
end
end
diff --git a/app/graphql/mutations/customer_relations/contacts/update.rb b/app/graphql/mutations/customer_relations/contacts/update.rb
index a3abf37f21f..1fc4f655464 100644
--- a/app/graphql/mutations/customer_relations/contacts/update.rb
+++ b/app/graphql/mutations/customer_relations/contacts/update.rb
@@ -3,18 +3,9 @@
module Mutations
module CustomerRelations
module Contacts
- class Update < Mutations::BaseMutation
+ class Update < Base
graphql_name 'CustomerRelationsContactUpdate'
- include ResolvesIds
-
- authorize :admin_crm_contact
-
- field :contact,
- Types::CustomerRelations::ContactType,
- null: true,
- description: 'Contact after the mutation.'
-
argument :id, ::Types::GlobalIDType[::CustomerRelations::Contact],
required: true,
description: 'Global ID of the contact.'
@@ -43,6 +34,10 @@ module Mutations
required: false,
description: 'Description of or notes for the contact.'
+ argument :active, GraphQL::Types::Boolean,
+ required: false,
+ description: 'State of the contact.'
+
def resolve(args)
contact = ::Gitlab::Graphql::Lazy.force(GitlabSchema.object_from_id(args.delete(:id), expected_type: ::CustomerRelations::Contact))
raise_resource_not_available_error! unless contact
@@ -50,6 +45,7 @@ module Mutations
group = contact.group
authorize!(group)
+ set_organization!(args)
result = ::CustomerRelations::Contacts::UpdateService.new(group: group, current_user: current_user, params: args).execute(contact)
{ contact: result.payload, errors: result.errors }
end
diff --git a/app/graphql/mutations/customer_relations/organizations/update.rb b/app/graphql/mutations/customer_relations/organizations/update.rb
index 0c05541dbd7..b2153b0b102 100644
--- a/app/graphql/mutations/customer_relations/organizations/update.rb
+++ b/app/graphql/mutations/customer_relations/organizations/update.rb
@@ -34,6 +34,10 @@ module Mutations
required: false,
description: 'Description of or notes for the organization.'
+ argument :active, GraphQL::Types::Boolean,
+ required: false,
+ description: 'State of the organization.'
+
def resolve(args)
organization = ::Gitlab::Graphql::Lazy.force(GitlabSchema.object_from_id(args.delete(:id), expected_type: ::CustomerRelations::Organization))
raise_resource_not_available_error! unless organization
diff --git a/app/graphql/mutations/design_management/move.rb b/app/graphql/mutations/design_management/move.rb
index 1ca03f22880..b19d9b4ce61 100644
--- a/app/graphql/mutations/design_management/move.rb
+++ b/app/graphql/mutations/design_management/move.rb
@@ -35,9 +35,6 @@ module Mutations
end
def find_design(id)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = DesignID.coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
diff --git a/app/graphql/mutations/discussions/toggle_resolve.rb b/app/graphql/mutations/discussions/toggle_resolve.rb
index 2005c9e54e0..fce6e4f416f 100644
--- a/app/graphql/mutations/discussions/toggle_resolve.rb
+++ b/app/graphql/mutations/discussions/toggle_resolve.rb
@@ -54,9 +54,6 @@ module Mutations
end
def find_object(id:)
- # TODO: remove explicit coercion once compatibility layer has been removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = Types::GlobalIDType[Discussion].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
diff --git a/app/graphql/mutations/environments/canary_ingress/update.rb b/app/graphql/mutations/environments/canary_ingress/update.rb
index ce24b8842c6..1cddfdd815b 100644
--- a/app/graphql/mutations/environments/canary_ingress/update.rb
+++ b/app/graphql/mutations/environments/canary_ingress/update.rb
@@ -24,7 +24,7 @@ module Mutations
'https://gitlab.com/groups/gitlab-org/configure/-/epics/8'
def resolve(id:, **kwargs)
- return { errors: [REMOVAL_ERR_MSG] } if cert_based_clusters_ff_disabled?
+ return { errors: [REMOVAL_ERR_MSG] } unless certificate_based_clusters_enabled?
environment = authorized_find!(id: id)
@@ -36,15 +36,14 @@ module Mutations
end
def find_object(id:)
- # TODO: remove as part of https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::Environment].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
private
- def cert_based_clusters_ff_disabled?
- Feature.disabled?(:certificate_based_clusters, default_enabled: :yaml, type: :ops)
+ def certificate_based_clusters_enabled?
+ instance_cluster = ::Clusters::Instance.new
+ instance_cluster.certificate_based_clusters_enabled?
end
end
end
diff --git a/app/graphql/mutations/incident_management/timeline_event/base.rb b/app/graphql/mutations/incident_management/timeline_event/base.rb
new file mode 100644
index 00000000000..742470b4831
--- /dev/null
+++ b/app/graphql/mutations/incident_management/timeline_event/base.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Mutations
+ module IncidentManagement
+ module TimelineEvent
+ class Base < BaseMutation
+ field :timeline_event,
+ ::Types::IncidentManagement::TimelineEventType,
+ null: true,
+ description: 'Timeline event.'
+
+ authorize :admin_incident_management_timeline_event
+
+ private
+
+ def response(result)
+ {
+ timeline_event: result.payload[:timeline_event],
+ errors: result.errors
+ }
+ end
+
+ def find_object(id:)
+ GitlabSchema.object_from_id(id, expected_type: ::IncidentManagement::TimelineEvent).sync
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/incident_management/timeline_event/create.rb b/app/graphql/mutations/incident_management/timeline_event/create.rb
new file mode 100644
index 00000000000..cbc708a2530
--- /dev/null
+++ b/app/graphql/mutations/incident_management/timeline_event/create.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Mutations
+ module IncidentManagement
+ module TimelineEvent
+ class Create < Base
+ graphql_name 'TimelineEventCreate'
+
+ argument :incident_id, Types::GlobalIDType[::Issue],
+ required: true,
+ description: 'Incident ID of the timeline event.'
+
+ argument :note, GraphQL::Types::String,
+ required: true,
+ description: 'Text note of the timeline event.'
+
+ argument :occurred_at, Types::TimeType,
+ required: true,
+ description: 'Timestamp of when the event occurred.'
+
+ def resolve(incident_id:, **args)
+ incident = authorized_find!(id: incident_id)
+
+ authorize!(incident)
+
+ response ::IncidentManagement::TimelineEvents::CreateService.new(incident, current_user, args).execute
+ end
+
+ private
+
+ def find_object(id:)
+ GitlabSchema.object_from_id(id, expected_type: ::Issue).sync
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/incident_management/timeline_event/destroy.rb b/app/graphql/mutations/incident_management/timeline_event/destroy.rb
new file mode 100644
index 00000000000..728a2e7c0d6
--- /dev/null
+++ b/app/graphql/mutations/incident_management/timeline_event/destroy.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Mutations
+ module IncidentManagement
+ module TimelineEvent
+ class Destroy < Base
+ graphql_name 'TimelineEventDestroy'
+
+ argument :id, Types::GlobalIDType[::IncidentManagement::TimelineEvent],
+ required: true,
+ description: 'Timeline event ID to remove.'
+
+ def resolve(id:)
+ timeline_event = authorized_find!(id: id)
+
+ response ::IncidentManagement::TimelineEvents::DestroyService.new(
+ timeline_event,
+ current_user
+ ).execute
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb b/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb
new file mode 100644
index 00000000000..73a20b8a380
--- /dev/null
+++ b/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Mutations
+ module IncidentManagement
+ module TimelineEvent
+ class PromoteFromNote < Base
+ graphql_name 'TimelineEventPromoteFromNote'
+
+ argument :note_id, Types::GlobalIDType[::Note],
+ required: true,
+ description: 'Note ID from which the timeline event promoted.'
+
+ def resolve(note_id:)
+ note = find_object(id: note_id)
+ incident = note&.noteable
+
+ authorize!(incident)
+
+ response ::IncidentManagement::TimelineEvents::CreateService.new(
+ incident,
+ current_user,
+ promoted_from_note: note,
+ note: note.note,
+ occurred_at: note.created_at
+ ).execute
+ end
+
+ private
+
+ def find_object(id:)
+ GitlabSchema.object_from_id(id, expected_type: ::Note).sync
+ end
+
+ def authorize!(object)
+ raise_noteable_not_incident! if object && !object.try(:incident?)
+
+ super
+ end
+
+ def raise_noteable_not_incident!
+ raise_resource_not_available_error! 'Note does not belong to an incident'
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/incident_management/timeline_event/update.rb b/app/graphql/mutations/incident_management/timeline_event/update.rb
new file mode 100644
index 00000000000..1f53bdc19cb
--- /dev/null
+++ b/app/graphql/mutations/incident_management/timeline_event/update.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Mutations
+ module IncidentManagement
+ module TimelineEvent
+ class Update < Base
+ graphql_name 'TimelineEventUpdate'
+
+ argument :id, ::Types::GlobalIDType[::IncidentManagement::TimelineEvent],
+ required: true,
+ description: 'ID of the timeline event to update.'
+
+ argument :note, GraphQL::Types::String,
+ required: false,
+ description: 'Text note of the timeline event.'
+
+ argument :occurred_at, Types::TimeType,
+ required: false,
+ description: 'Timestamp when the event occurred.'
+
+ def resolve(id:, **args)
+ timeline_event = authorized_find!(id: id)
+
+ response ::IncidentManagement::TimelineEvents::UpdateService.new(
+ timeline_event,
+ current_user,
+ args
+ ).execute
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/issues/set_crm_contacts.rb b/app/graphql/mutations/issues/set_crm_contacts.rb
index 62990fc67f1..4df65e4769c 100644
--- a/app/graphql/mutations/issues/set_crm_contacts.rb
+++ b/app/graphql/mutations/issues/set_crm_contacts.rb
@@ -48,7 +48,7 @@ module Mutations
private
def feature_enabled?(project)
- Feature.enabled?(:customer_relations, project.group, default_enabled: :yaml) && project.group&.crm_enabled?
+ Feature.enabled?(:customer_relations, project.group) && project.group&.crm_enabled?
end
end
end
diff --git a/app/graphql/mutations/merge_requests/remove_attention_request.rb b/app/graphql/mutations/merge_requests/remove_attention_request.rb
new file mode 100644
index 00000000000..3b12b09528b
--- /dev/null
+++ b/app/graphql/mutations/merge_requests/remove_attention_request.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Mutations
+ module MergeRequests
+ class RemoveAttentionRequest < Base
+ graphql_name 'MergeRequestRemoveAttentionRequest'
+
+ argument :user_id, ::Types::GlobalIDType[::User],
+ loads: Types::UserType,
+ required: true,
+ description: <<~DESC
+ User ID of the user for attention request removal.
+ DESC
+
+ def resolve(project_path:, iid:, user:)
+ raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless feature_enabled?
+
+ merge_request = authorized_find!(project_path: project_path, iid: iid)
+
+ result = ::MergeRequests::RemoveAttentionRequestedService.new(
+ project: merge_request.project,
+ current_user: current_user,
+ merge_request: merge_request,
+ user: user
+ ).execute
+
+ {
+ merge_request: merge_request,
+ errors: Array(result[:message])
+ }
+ end
+
+ private
+
+ def feature_enabled?
+ current_user&.mr_attention_requests_enabled?
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/merge_requests/request_attention.rb b/app/graphql/mutations/merge_requests/request_attention.rb
new file mode 100644
index 00000000000..5f5565285f6
--- /dev/null
+++ b/app/graphql/mutations/merge_requests/request_attention.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Mutations
+ module MergeRequests
+ class RequestAttention < Base
+ graphql_name 'MergeRequestRequestAttention'
+
+ argument :user_id, ::Types::GlobalIDType[::User],
+ loads: Types::UserType,
+ required: true,
+ description: <<~DESC
+ User ID of the user to request attention.
+ DESC
+
+ def resolve(project_path:, iid:, user:)
+ raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless feature_enabled?
+
+ merge_request = authorized_find!(project_path: project_path, iid: iid)
+
+ result = ::MergeRequests::RequestAttentionService.new(
+ project: merge_request.project,
+ current_user: current_user,
+ merge_request: merge_request,
+ user: user
+ ).execute
+
+ {
+ merge_request: merge_request,
+ errors: Array(result[:message])
+ }
+ end
+
+ private
+
+ def feature_enabled?
+ current_user&.mr_attention_requests_enabled?
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/merge_requests/set_labels.rb b/app/graphql/mutations/merge_requests/set_labels.rb
index 0b40d6c5c5e..f80fcd0f1ac 100644
--- a/app/graphql/mutations/merge_requests/set_labels.rb
+++ b/app/graphql/mutations/merge_requests/set_labels.rb
@@ -23,9 +23,6 @@ module Mutations
merge_request = authorized_find!(project_path: project_path, iid: iid)
project = merge_request.project
- # TODO: remove this line when the compatibility layer is removed:
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- label_ids = label_ids.map { |id| ::Types::GlobalIDType[::Label].coerce_isolated_input(id) }
# MergeRequests::UpdateService expects integers
label_ids = label_ids.compact.map(&:model_id)
diff --git a/app/graphql/mutations/merge_requests/toggle_attention_requested.rb b/app/graphql/mutations/merge_requests/toggle_attention_requested.rb
index f316f23fb85..8913ca48531 100644
--- a/app/graphql/mutations/merge_requests/toggle_attention_requested.rb
+++ b/app/graphql/mutations/merge_requests/toggle_attention_requested.rb
@@ -13,6 +13,8 @@ module Mutations
DESC
def resolve(project_path:, iid:, user:)
+ raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless current_user&.mr_attention_requests_enabled?
+
merge_request = authorized_find!(project_path: project_path, iid: iid)
result = ::MergeRequests::ToggleAttentionRequestedService.new(project: merge_request.project, current_user: current_user, merge_request: merge_request, user: user).execute
diff --git a/app/graphql/mutations/metrics/dashboard/annotations/create.rb b/app/graphql/mutations/metrics/dashboard/annotations/create.rb
index 2eb48c9029d..2e7c0c5a2f9 100644
--- a/app/graphql/mutations/metrics/dashboard/annotations/create.rb
+++ b/app/graphql/mutations/metrics/dashboard/annotations/create.rb
@@ -96,16 +96,7 @@ module Mutations
end
def annotation_source(args)
- # TODO: remove these lines when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- annotation_source_id = if args[:cluster_id]
- ::Types::GlobalIDType[::Clusters::Cluster].coerce_isolated_input(args[:cluster_id])
- else
- ::Types::GlobalIDType[::Environment].coerce_isolated_input(args[:environment_id])
- end
-
- # TODO: uncomment following line once lines above are removed
- # annotation_source_id = args[:cluster_id] || args[:environment_id]
+ annotation_source_id = args[:cluster_id] || args[:environment_id]
authorized_find!(id: annotation_source_id)
end
end
diff --git a/app/graphql/mutations/namespace/package_settings/update.rb b/app/graphql/mutations/namespace/package_settings/update.rb
index 934b75193d7..e499e646781 100644
--- a/app/graphql/mutations/namespace/package_settings/update.rb
+++ b/app/graphql/mutations/namespace/package_settings/update.rb
@@ -8,7 +8,7 @@ module Mutations
include Mutations::ResolvesNamespace
- authorize :create_package_settings
+ authorize :admin_package
argument :namespace_path,
GraphQL::Types::ID,
diff --git a/app/graphql/mutations/notes/base.rb b/app/graphql/mutations/notes/base.rb
index 65bb9e4644c..fb74805db17 100644
--- a/app/graphql/mutations/notes/base.rb
+++ b/app/graphql/mutations/notes/base.rb
@@ -17,9 +17,6 @@ module Mutations
private
def find_object(id:)
- # TODO: remove explicit coercion once compatibility layer has been removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::Note].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/mutations/notes/create/base.rb b/app/graphql/mutations/notes/create/base.rb
index e13a51c6862..1b673204213 100644
--- a/app/graphql/mutations/notes/create/base.rb
+++ b/app/graphql/mutations/notes/create/base.rb
@@ -42,9 +42,6 @@ module Mutations
private
def find_object(id:)
- # TODO: remove explicit coercion once compatibility layer has been removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::Noteable].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
diff --git a/app/graphql/mutations/notes/create/note.rb b/app/graphql/mutations/notes/create/note.rb
index 1cfc11c6b11..4d6f056de09 100644
--- a/app/graphql/mutations/notes/create/note.rb
+++ b/app/graphql/mutations/notes/create/note.rb
@@ -22,11 +22,8 @@ module Mutations
def create_note_params(noteable, args)
discussion_id = nil
- if args[:discussion_id]
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- discussion_gid = ::Types::GlobalIDType[::Discussion].coerce_isolated_input(args[:discussion_id])
- discussion = GitlabSchema.find_by_gid(discussion_gid)
+ if gid = args[:discussion_id]
+ discussion = GitlabSchema.find_by_gid(gid)
authorize_discussion!(discussion)
diff --git a/app/graphql/mutations/notes/reposition_image_diff_note.rb b/app/graphql/mutations/notes/reposition_image_diff_note.rb
index ec68f077c84..9c3377b1f96 100644
--- a/app/graphql/mutations/notes/reposition_image_diff_note.rb
+++ b/app/graphql/mutations/notes/reposition_image_diff_note.rb
@@ -26,6 +26,7 @@ module Mutations
def resolve(note:, position:)
authorize!(note)
+ position = position.to_h.compact
pre_update_checks!(note, position)
updated_note = ::Notes::UpdateService.new(
@@ -46,7 +47,7 @@ module Mutations
# just a `DiffNote` with a particular kind of `Gitlab::Diff::Position`.
# In addition to accepting a `DiffNote` Global ID we also need to
# perform this check.
- def pre_update_checks!(note, position)
+ def pre_update_checks!(note, _position)
unless note.position&.on_image?
raise Gitlab::Graphql::Errors::ResourceNotAvailable,
'Resource is not an ImageDiffNote'
diff --git a/app/graphql/mutations/notes/update/image_diff_note.rb b/app/graphql/mutations/notes/update/image_diff_note.rb
index 284c0f1bb20..8e4dbb06d46 100644
--- a/app/graphql/mutations/notes/update/image_diff_note.rb
+++ b/app/graphql/mutations/notes/update/image_diff_note.rb
@@ -54,7 +54,7 @@ module Mutations
original_position = note.position.to_h
- Gitlab::Diff::Position.new(original_position.merge(args[:position]))
+ Gitlab::Diff::Position.new(original_position.merge(args[:position].to_h))
end
end
end
diff --git a/app/graphql/mutations/packages/destroy.rb b/app/graphql/mutations/packages/destroy.rb
index 81fa53fc116..a398b1ff9dc 100644
--- a/app/graphql/mutations/packages/destroy.rb
+++ b/app/graphql/mutations/packages/destroy.rb
@@ -27,9 +27,6 @@ module Mutations
private
def find_object(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::Packages::Package].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/mutations/packages/destroy_file.rb b/app/graphql/mutations/packages/destroy_file.rb
index 4aa33b3504c..f2a8f2b853a 100644
--- a/app/graphql/mutations/packages/destroy_file.rb
+++ b/app/graphql/mutations/packages/destroy_file.rb
@@ -25,9 +25,6 @@ module Mutations
private
def find_object(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::Packages::PackageFile].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/mutations/release_asset_links/delete.rb b/app/graphql/mutations/release_asset_links/delete.rb
index d8f0946670b..91fa74859f6 100644
--- a/app/graphql/mutations/release_asset_links/delete.rb
+++ b/app/graphql/mutations/release_asset_links/delete.rb
@@ -29,10 +29,6 @@ module Mutations
end
def find_object(id)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ReleaseAssetLinkID.coerce_isolated_input(id)
-
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/mutations/release_asset_links/update.rb b/app/graphql/mutations/release_asset_links/update.rb
index 18d92cd82ae..f9368927371 100644
--- a/app/graphql/mutations/release_asset_links/update.rb
+++ b/app/graphql/mutations/release_asset_links/update.rb
@@ -54,10 +54,6 @@ module Mutations
end
def find_object(id)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ReleaseAssetLinkID.coerce_isolated_input(id)
-
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/mutations/saved_replies/base.rb b/app/graphql/mutations/saved_replies/base.rb
index 59871df687f..4923fcb7851 100644
--- a/app/graphql/mutations/saved_replies/base.rb
+++ b/app/graphql/mutations/saved_replies/base.rb
@@ -24,14 +24,10 @@ module Mutations
end
def feature_enabled?
- Feature.enabled?(:saved_replies, current_user, default_enabled: :yaml)
+ Feature.enabled?(:saved_replies, current_user)
end
def find_object(id)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::Users::SavedReply].coerce_isolated_input(id)
-
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/mutations/timelogs/delete.rb b/app/graphql/mutations/timelogs/delete.rb
new file mode 100644
index 00000000000..8fd41c27b88
--- /dev/null
+++ b/app/graphql/mutations/timelogs/delete.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Timelogs
+ class Delete < Mutations::BaseMutation
+ graphql_name 'TimelogDelete'
+
+ field :timelog,
+ Types::TimelogType,
+ null: true,
+ description: 'Deleted timelog.'
+
+ argument :id,
+ ::Types::GlobalIDType[::Timelog],
+ required: true,
+ description: 'Global ID of the timelog.'
+
+ authorize :admin_timelog
+
+ def resolve(id:)
+ timelog = authorized_find!(id: id)
+ result = ::Timelogs::DeleteService.new(timelog, current_user).execute
+
+ # Return the result payload, not the loaded timelog, so that it returns null in case of unauthorized access
+ { timelog: result.payload, errors: result.errors }
+ end
+
+ def find_object(id:)
+ GitlabSchema.find_by_gid(id)
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/todos/base.rb b/app/graphql/mutations/todos/base.rb
index 4dab3bbc3f4..9a94c5d1e6d 100644
--- a/app/graphql/mutations/todos/base.rb
+++ b/app/graphql/mutations/todos/base.rb
@@ -6,9 +6,6 @@ module Mutations
private
def find_object(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::Todo].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/mutations/todos/create.rb b/app/graphql/mutations/todos/create.rb
index ccc3d0b61e4..489d2f490ff 100644
--- a/app/graphql/mutations/todos/create.rb
+++ b/app/graphql/mutations/todos/create.rb
@@ -17,8 +17,7 @@ module Mutations
description: 'To-do item created.'
def resolve(target_id:)
- id = ::Types::GlobalIDType[Todoable].coerce_isolated_input(target_id)
- target = authorized_find!(id)
+ target = authorized_find!(target_id)
todo = TodoService.new.mark_todo(target, current_user)&.first
errors = errors_on_object(todo) if todo
diff --git a/app/graphql/mutations/todos/mark_all_done.rb b/app/graphql/mutations/todos/mark_all_done.rb
index 67a822c1067..fe4023515a4 100644
--- a/app/graphql/mutations/todos/mark_all_done.rb
+++ b/app/graphql/mutations/todos/mark_all_done.rb
@@ -39,7 +39,7 @@ module Mutations
if args[:target_id].present?
target = Gitlab::Graphql::Lazy.force(
- GitlabSchema.find_by_gid(TodoableID.coerce_isolated_input(args[:target_id]))
+ GitlabSchema.find_by_gid(args[:target_id])
)
raise Gitlab::Graphql::Errors::ResourceNotAvailable, "Resource not available: #{args[:target_id]}" if target.nil?
diff --git a/app/graphql/mutations/todos/restore_many.rb b/app/graphql/mutations/todos/restore_many.rb
index 3453645000b..fe0ad6df65b 100644
--- a/app/graphql/mutations/todos/restore_many.rb
+++ b/app/graphql/mutations/todos/restore_many.rb
@@ -33,9 +33,6 @@ module Mutations
def model_ids_of(ids)
ids.map do |gid|
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- gid = ::Types::GlobalIDType[::Todo].coerce_isolated_input(gid)
gid.model_id.to_i
end.compact
end
diff --git a/app/graphql/mutations/user_preferences/update.rb b/app/graphql/mutations/user_preferences/update.rb
index eface536a87..b71c952b0f2 100644
--- a/app/graphql/mutations/user_preferences/update.rb
+++ b/app/graphql/mutations/user_preferences/update.rb
@@ -38,7 +38,7 @@ module Mutations
def disabled_sort_value?(args)
return false unless [:escalation_status_asc, :escalation_status_desc].include?(args[:issues_sort])
- Feature.disabled?(:incident_escalations, default_enabled: :yaml)
+ Feature.disabled?(:incident_escalations)
end
end
end
diff --git a/app/graphql/mutations/work_items/create.rb b/app/graphql/mutations/work_items/create.rb
index c29dbb899b5..2e136d409ab 100644
--- a/app/graphql/mutations/work_items/create.rb
+++ b/app/graphql/mutations/work_items/create.rb
@@ -53,9 +53,6 @@ module Mutations
private
def global_id_compatibility_params(params)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- params[:work_item_type_id] = ::Types::GlobalIDType[::WorkItems::Type].coerce_isolated_input(params[:work_item_type_id]) if params[:work_item_type_id]
params[:work_item_type_id] = params[:work_item_type_id]&.model_id
params
diff --git a/app/graphql/mutations/work_items/create_from_task.rb b/app/graphql/mutations/work_items/create_from_task.rb
index 278c1bc65a9..4da709401a6 100644
--- a/app/graphql/mutations/work_items/create_from_task.rb
+++ b/app/graphql/mutations/work_items/create_from_task.rb
@@ -55,8 +55,6 @@ module Mutations
private
def find_object(id:)
- # TODO: Remove coercion when working on https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::WorkItem].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/mutations/work_items/delete.rb b/app/graphql/mutations/work_items/delete.rb
index 3d72ebbd95d..1830ab5443c 100644
--- a/app/graphql/mutations/work_items/delete.rb
+++ b/app/graphql/mutations/work_items/delete.rb
@@ -38,8 +38,6 @@ module Mutations
private
def find_object(id:)
- # TODO: Remove coercion when working on https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::WorkItem].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/mutations/work_items/delete_task.rb b/app/graphql/mutations/work_items/delete_task.rb
new file mode 100644
index 00000000000..87620a28fa1
--- /dev/null
+++ b/app/graphql/mutations/work_items/delete_task.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module Mutations
+ module WorkItems
+ class DeleteTask < BaseMutation
+ graphql_name 'WorkItemDeleteTask'
+
+ description "Deletes a task in a work item's description." \
+ ' Available only when feature flag `work_items` is enabled. This feature is experimental and' \
+ ' is subject to change without notice.'
+
+ authorize :update_work_item
+
+ argument :id, ::Types::GlobalIDType[::WorkItem],
+ required: true,
+ description: 'Global ID of the work item.'
+ argument :lock_version, GraphQL::Types::Int,
+ required: true,
+ description: 'Current lock version of the work item containing the task in the description.'
+ argument :task_data, ::Types::WorkItems::DeletedTaskInputType,
+ required: true,
+ description: 'Arguments necessary to delete a task from a work item\'s description.',
+ prepare: ->(attributes, _ctx) { attributes.to_h }
+
+ field :work_item, Types::WorkItemType,
+ null: true,
+ description: 'Updated work item.'
+
+ def resolve(id:, lock_version:, task_data:)
+ work_item = authorized_find!(id: id)
+ task_data[:task] = authorized_find_task!(task_data[:id])
+
+ unless work_item.project.work_items_feature_flag_enabled?
+ return { errors: ['`work_items` feature flag disabled for this project'] }
+ end
+
+ result = ::WorkItems::DeleteTaskService.new(
+ work_item: work_item,
+ current_user: current_user,
+ lock_version: lock_version,
+ task_params: task_data
+ ).execute
+
+ response = { errors: result.errors }
+ response[:work_item] = work_item if result.success?
+
+ response
+ end
+
+ private
+
+ def authorized_find_task!(task_id)
+ task = ::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(task_id))
+
+ if current_user.can?(:delete_work_item, task)
+ task
+ else
+ # Fail early if user cannot delete task
+ raise_resource_not_available_error!
+ end
+ end
+
+ # method used by `authorized_find!(id: id)`
+ def find_object(id:)
+ GitlabSchema.find_by_gid(id)
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/work_items/update.rb b/app/graphql/mutations/work_items/update.rb
index 091237d6fa0..20319301482 100644
--- a/app/graphql/mutations/work_items/update.rb
+++ b/app/graphql/mutations/work_items/update.rb
@@ -52,8 +52,6 @@ module Mutations
private
def find_object(id:)
- # TODO: Remove coercion when working on https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::WorkItem].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/queries/burndown_chart/burnup.iteration.query.graphql b/app/graphql/queries/burndown_chart/burnup.iteration.query.graphql
new file mode 100644
index 00000000000..ff50c34ade3
--- /dev/null
+++ b/app/graphql/queries/burndown_chart/burnup.iteration.query.graphql
@@ -0,0 +1,40 @@
+query BurnupTimesSeriesIterationData(
+ $iterationId: IterationID!
+ $weight: Boolean = false
+ $fullPath: String
+) {
+ iteration(id: $iterationId) {
+ __typename
+ id
+ title
+ report(fullPath: $fullPath) {
+ __typename
+ burnupTimeSeries {
+ __typename
+ date
+ completedCount @skip(if: $weight)
+ scopeCount @skip(if: $weight)
+ completedWeight @include(if: $weight)
+ scopeWeight @include(if: $weight)
+ }
+ stats {
+ __typename
+ total {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ complete {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ incomplete {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ }
+ }
+ }
+}
diff --git a/app/graphql/queries/burndown_chart/burnup.milestone.query.graphql b/app/graphql/queries/burndown_chart/burnup.milestone.query.graphql
new file mode 100644
index 00000000000..18e59249500
--- /dev/null
+++ b/app/graphql/queries/burndown_chart/burnup.milestone.query.graphql
@@ -0,0 +1,36 @@
+query BurnupTimesSeriesMilestoneData($milestoneId: MilestoneID!, $weight: Boolean = false) {
+ milestone(id: $milestoneId) {
+ __typename
+ id
+ title
+ report {
+ __typename
+ burnupTimeSeries {
+ __typename
+ date
+ completedCount @skip(if: $weight)
+ scopeCount @skip(if: $weight)
+ completedWeight @include(if: $weight)
+ scopeWeight @include(if: $weight)
+ }
+ stats {
+ __typename
+ total {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ complete {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ incomplete {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ }
+ }
+ }
+}
diff --git a/app/graphql/queries/burndown_chart/burnup.query.graphql b/app/graphql/queries/burndown_chart/burnup.query.graphql
deleted file mode 100644
index 0795645f8b7..00000000000
--- a/app/graphql/queries/burndown_chart/burnup.query.graphql
+++ /dev/null
@@ -1,75 +0,0 @@
-query BurnupTimesSeriesData(
- $id: ID!
- $isIteration: Boolean = false
- $weight: Boolean = false
- $fullPath: String
-) {
- milestone(id: $id) @skip(if: $isIteration) {
- __typename
- id
- title
- report {
- __typename
- burnupTimeSeries {
- __typename
- date
- completedCount @skip(if: $weight)
- scopeCount @skip(if: $weight)
- completedWeight @include(if: $weight)
- scopeWeight @include(if: $weight)
- }
- stats {
- __typename
- total {
- __typename
- count @skip(if: $weight)
- weight @include(if: $weight)
- }
- complete {
- __typename
- count @skip(if: $weight)
- weight @include(if: $weight)
- }
- incomplete {
- __typename
- count @skip(if: $weight)
- weight @include(if: $weight)
- }
- }
- }
- }
- iteration(id: $id) @include(if: $isIteration) {
- __typename
- id
- title
- report(fullPath: $fullPath) {
- __typename
- burnupTimeSeries {
- __typename
- date
- completedCount @skip(if: $weight)
- scopeCount @skip(if: $weight)
- completedWeight @include(if: $weight)
- scopeWeight @include(if: $weight)
- }
- stats {
- __typename
- total {
- __typename
- count @skip(if: $weight)
- weight @include(if: $weight)
- }
- complete {
- __typename
- count @skip(if: $weight)
- weight @include(if: $weight)
- }
- incomplete {
- __typename
- count @skip(if: $weight)
- weight @include(if: $weight)
- }
- }
- }
- }
-}
diff --git a/app/graphql/queries/design_management/get_design_list.query.graphql b/app/graphql/queries/design_management/get_design_list.query.graphql
index f0caa7c5b4c..c2100b5fa04 100644
--- a/app/graphql/queries/design_management/get_design_list.query.graphql
+++ b/app/graphql/queries/design_management/get_design_list.query.graphql
@@ -1,4 +1,4 @@
-query getDesignList($fullPath: ID!, $iid: String!, $atVersion: ID) {
+query getDesignList($fullPath: ID!, $iid: String!, $atVersion: DesignManagementVersionID) {
project(fullPath: $fullPath) {
__typename
id
diff --git a/app/graphql/queries/pipelines/get_pipeline_details.query.graphql b/app/graphql/queries/pipelines/get_pipeline_details.query.graphql
index f4921706f7e..c8353c738a5 100644
--- a/app/graphql/queries/pipelines/get_pipeline_details.query.graphql
+++ b/app/graphql/queries/pipelines/get_pipeline_details.query.graphql
@@ -18,6 +18,11 @@ fragment LinkedPipelineData on Pipeline {
id
iid
path
+ cancelable
+ retryable
+ userPermissions {
+ updatePipeline
+ }
status: detailedStatus {
__typename
id
@@ -97,6 +102,7 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) {
__typename
id
name
+ kind
scheduledAt
needs {
__typename
diff --git a/app/graphql/queries/snippet/snippet_blob_content.query.graphql b/app/graphql/queries/snippet/snippet_blob_content.query.graphql
index 4459a5e4316..0ac7d4d23a0 100644
--- a/app/graphql/queries/snippet/snippet_blob_content.query.graphql
+++ b/app/graphql/queries/snippet/snippet_blob_content.query.graphql
@@ -1,4 +1,4 @@
-query SnippetBlobContent($ids: [ID!], $rich: Boolean!, $paths: [String!]) {
+query SnippetBlobContent($ids: [SnippetID!], $rich: Boolean!, $paths: [String!]) {
snippets(ids: $ids) {
__typename
nodes {
diff --git a/app/graphql/resolvers/alert_management/http_integrations_resolver.rb b/app/graphql/resolvers/alert_management/http_integrations_resolver.rb
index abc54614a59..225e20bab83 100644
--- a/app/graphql/resolvers/alert_management/http_integrations_resolver.rb
+++ b/app/graphql/resolvers/alert_management/http_integrations_resolver.rb
@@ -26,8 +26,7 @@ module Resolvers
private
def integrations_by(gid:)
- id = Types::GlobalIDType[::AlertManagement::HttpIntegration].coerce_isolated_input(gid)
- object = GitlabSchema.find_by_gid(id)
+ object = GitlabSchema.find_by_gid(gid)
defer { object }.then do |integration|
ret = integration if project == integration&.project
diff --git a/app/graphql/resolvers/base_issues_resolver.rb b/app/graphql/resolvers/base_issues_resolver.rb
index 4cae7866a49..a1fda976876 100644
--- a/app/graphql/resolvers/base_issues_resolver.rb
+++ b/app/graphql/resolvers/base_issues_resolver.rb
@@ -35,7 +35,7 @@ module Resolvers
def prepare_params(args, parent)
return unless [:escalation_status_asc, :escalation_status_desc].include?(args[:sort])
- return if Feature.enabled?(:incident_escalations, parent, default_enabled: :yaml)
+ return if Feature.enabled?(:incident_escalations, parent)
args[:sort] = :created_desc # default for sort argument
end
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
index dbded8f60a0..2b54a3fdd55 100644
--- a/app/graphql/resolvers/base_resolver.rb
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -4,7 +4,6 @@ module Resolvers
class BaseResolver < GraphQL::Schema::Resolver
extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize
- include ::Gitlab::Graphql::GlobalIDCompatibility
argument_class ::Types::BaseArgument
diff --git a/app/graphql/resolvers/boards_resolver.rb b/app/graphql/resolvers/boards_resolver.rb
index 679f2b4cceb..91cd15f615d 100644
--- a/app/graphql/resolvers/boards_resolver.rb
+++ b/app/graphql/resolvers/boards_resolver.rb
@@ -26,9 +26,6 @@ module Resolvers
def extract_board_id(id)
return unless id.present?
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = Types::GlobalIDType[Board].coerce_isolated_input(id)
id.model_id
end
end
diff --git a/app/graphql/resolvers/ci/config_resolver.rb b/app/graphql/resolvers/ci/config_resolver.rb
index f9d60650443..6f861012d83 100644
--- a/app/graphql/resolvers/ci/config_resolver.rb
+++ b/app/graphql/resolvers/ci/config_resolver.rb
@@ -37,7 +37,7 @@ module Resolvers
.new(project: project, current_user: context[:current_user], sha: sha)
.validate(content, dry_run: dry_run)
- response(result).merge(merged_yaml: result.merged_yaml)
+ response(result)
rescue GRPC::InvalidArgument => error
Gitlab::ErrorTracking.track_and_raise_exception(error, sha: sha)
end
@@ -45,20 +45,14 @@ module Resolvers
private
def response(result)
- if result.errors.empty?
- {
- status: :valid,
- errors: [],
- warnings: result.warnings,
- stages: make_stages(result.jobs)
- }
- else
- {
- status: :invalid,
- warnings: result.warnings,
- errors: result.errors
- }
- end
+ {
+ status: result.status,
+ errors: result.errors,
+ warnings: result.warnings,
+ stages: make_stages(result),
+ merged_yaml: result.merged_yaml,
+ includes: result.includes
+ }
end
def make_jobs(config_jobs)
@@ -90,8 +84,10 @@ module Resolvers
end
end
- def make_stages(jobs)
- make_groups(jobs)
+ def make_stages(result)
+ return [] unless result.valid?
+
+ make_groups(result.jobs)
.group_by { |group| group[:stage] }
.map { |name, groups| { name: name, groups: groups } }
end
diff --git a/app/graphql/resolvers/ci/runner_status_resolver.rb b/app/graphql/resolvers/ci/runner_status_resolver.rb
index d916a8a13f0..447ab306ba7 100644
--- a/app/graphql/resolvers/ci/runner_status_resolver.rb
+++ b/app/graphql/resolvers/ci/runner_status_resolver.rb
@@ -3,7 +3,8 @@
module Resolvers
module Ci
# NOTE: This class was introduced to allow modifying the meaning of certain values in RunnerStatusEnum
- # while preserving backward compatibility. It can be removed in 15.0 once the API has stabilized.
+ # while preserving backward compatibility. It can be removed in 17.0 after being deprecated
+ # and made a no-op in %16.0 (legacy_mode will be hard-coded to nil).
class RunnerStatusResolver < BaseResolver
type Types::Ci::RunnerStatusEnum, null: false
@@ -14,7 +15,11 @@ module Resolvers
default_value: '14.5',
required: false,
description: 'Compatibility mode. A null value turns off compatibility mode.',
- deprecated: { reason: 'Will be removed in 15.0. From that release onward, the field will behave as if legacyMode is null', milestone: '14.6' }
+ deprecated: {
+ reason: 'Will be removed in 17.0. In GitLab 16.0 and later, ' \
+ 'the field will act as if `legacyMode` is null',
+ milestone: '15.0'
+ }
def resolve(legacy_mode:, **args)
runner.status(legacy_mode)
diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
index 432d6f48607..de44dbb26d7 100644
--- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
+++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
@@ -68,6 +68,12 @@ module IssueResolverArguments
description: 'Negated arguments.',
prepare: ->(negated_args, ctx) { negated_args.to_h },
required: false
+ argument :crm_contact_id, GraphQL::Types::String,
+ required: false,
+ description: 'ID of a contact assigned to the issues.'
+ argument :crm_organization_id, GraphQL::Types::String,
+ required: false,
+ description: 'ID of an organization assigned to the issues.'
end
def resolve_with_lookahead(**args)
diff --git a/app/graphql/resolvers/concerns/resolves_ids.rb b/app/graphql/resolvers/concerns/resolves_ids.rb
index 8bf2a6b2ac9..3e248c40562 100644
--- a/app/graphql/resolvers/concerns/resolves_ids.rb
+++ b/app/graphql/resolvers/concerns/resolves_ids.rb
@@ -3,13 +3,10 @@
module ResolvesIds
extend ActiveSupport::Concern
- def resolve_ids(ids, type)
+ def resolve_ids(ids)
Array.wrap(ids).map do |id|
next unless id.present?
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = type.coerce_isolated_input(id)
id.model_id
end.compact
end
diff --git a/app/graphql/resolvers/concerns/resolves_snippets.rb b/app/graphql/resolvers/concerns/resolves_snippets.rb
index 46d78a6a8c8..8274b5cc49f 100644
--- a/app/graphql/resolvers/concerns/resolves_snippets.rb
+++ b/app/graphql/resolvers/concerns/resolves_snippets.rb
@@ -28,7 +28,7 @@ module ResolvesSnippets
def snippet_finder_params(args)
{
- ids: resolve_ids(args[:ids], ::Types::GlobalIDType[::Snippet]),
+ ids: resolve_ids(args[:ids]),
scope: args[:visibility]
}.merge(options_by_type(args[:type]))
end
diff --git a/app/graphql/resolvers/concerns/time_frame_arguments.rb b/app/graphql/resolvers/concerns/time_frame_arguments.rb
index 0ec3c642f29..87b7a96045c 100644
--- a/app/graphql/resolvers/concerns/time_frame_arguments.rb
+++ b/app/graphql/resolvers/concerns/time_frame_arguments.rb
@@ -24,10 +24,13 @@ module TimeFrameArguments
# TODO: remove when the start_date and end_date arguments are removed
def validate_timeframe_params!(args)
return unless %i[start_date end_date timeframe].any? { |k| args[k].present? }
- return if args[:timeframe] && %i[start_date end_date].all? { |k| args[k].nil? }
+
+ # the timeframe is passed in as a TimeframeInputType
+ timeframe = args[:timeframe].to_h if args[:timeframe]
+ return if timeframe && %i[start_date end_date].all? { |k| args[k].nil? }
error_message =
- if args[:timeframe].present?
+ if timeframe.present?
"startDate and endDate are deprecated in favor of timeframe. Please use only timeframe."
elsif args[:start_date].nil? || args[:end_date].nil?
"Both startDate and endDate must be present."
@@ -42,7 +45,7 @@ module TimeFrameArguments
def transform_timeframe_parameters(args)
if args[:timeframe]
- args[:timeframe].transform_keys { |k| :"#{k}_date" }
+ args[:timeframe].to_h.transform_keys { |k| :"#{k}_date" }
else
args.slice(:start_date, :end_date)
end
diff --git a/app/graphql/resolvers/design_management/design_at_version_resolver.rb b/app/graphql/resolvers/design_management/design_at_version_resolver.rb
index c87670bc24c..5ba23b5d1ed 100644
--- a/app/graphql/resolvers/design_management/design_at_version_resolver.rb
+++ b/app/graphql/resolvers/design_management/design_at_version_resolver.rb
@@ -18,9 +18,6 @@ module Resolvers
end
def find_object(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::DesignManagement::DesignAtVersion].coerce_isolated_input(id)
dav = GitlabSchema.find_by_gid(id)
return unless consistent?(dav)
diff --git a/app/graphql/resolvers/design_management/design_resolver.rb b/app/graphql/resolvers/design_management/design_resolver.rb
index d9e5203ef0e..eaa11d2fd9d 100644
--- a/app/graphql/resolvers/design_management/design_resolver.rb
+++ b/app/graphql/resolvers/design_management/design_resolver.rb
@@ -54,10 +54,6 @@ module Resolvers
end
def parse_gid(gid)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- gid = ::Types::GlobalIDType[::DesignManagement::Design].coerce_isolated_input(gid)
-
gid.model_id
end
end
diff --git a/app/graphql/resolvers/design_management/designs_resolver.rb b/app/graphql/resolvers/design_management/designs_resolver.rb
index a62ef6d76e5..429bbba74a5 100644
--- a/app/graphql/resolvers/design_management/designs_resolver.rb
+++ b/app/graphql/resolvers/design_management/designs_resolver.rb
@@ -24,13 +24,17 @@ module Resolvers
end
def resolve(ids: nil, filenames: nil, at_version: nil)
- ::DesignManagement::DesignsFinder.new(
- issue,
- current_user,
- ids: design_ids(ids),
- filenames: filenames,
- visible_at_version: version(at_version)
- ).execute
+ context.scoped_set!(:at_version_argument, at_version) if at_version
+
+ ::Gitlab::Graphql::Lazy.with_value(version(at_version)) do |visible_at|
+ ::DesignManagement::DesignsFinder.new(
+ issue,
+ current_user,
+ ids: design_ids(ids),
+ filenames: filenames,
+ visible_at_version: visible_at
+ ).execute
+ end
end
private
@@ -38,19 +42,12 @@ module Resolvers
def version(at_version)
return unless at_version
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- at_version = VersionID.coerce_isolated_input(at_version)
- # TODO: when we get promises use this to make resolve lazy
- Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(at_version))
+ GitlabSchema.find_by_gid(at_version)
end
def design_ids(gids)
return if gids.nil?
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- gids = gids.map { |id| DesignID.coerce_isolated_input(id) }
gids.map(&:model_id)
end
diff --git a/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb b/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb
index 76e365c40b1..30d2865ae85 100644
--- a/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb
+++ b/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb
@@ -34,11 +34,6 @@ module Resolvers
def resolve(design_id: nil, filename: nil, design_at_version_id: nil)
validate_arguments(design_id, filename, design_at_version_id)
- # TODO: remove this when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- design_id &&= DesignID.coerce_isolated_input(design_id)
- design_at_version_id &&= DesignAtVersionID.coerce_isolated_input(design_at_version_id)
-
return unless Ability.allowed?(current_user, :read_design, issue)
return specific_design_at_version(design_at_version_id) if design_at_version_id
diff --git a/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb b/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb
index 97cc7554ba8..9f98762b519 100644
--- a/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb
+++ b/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb
@@ -41,9 +41,6 @@ module Resolvers
def design_ids(gids)
return if gids.nil?
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- gids = gids.map { |id| DesignID.coerce_isolated_input(id) }
gids.map(&:model_id)
end
diff --git a/app/graphql/resolvers/design_management/version_in_collection_resolver.rb b/app/graphql/resolvers/design_management/version_in_collection_resolver.rb
index 2682ce6b3b1..f29cd30c36f 100644
--- a/app/graphql/resolvers/design_management/version_in_collection_resolver.rb
+++ b/app/graphql/resolvers/design_management/version_in_collection_resolver.rb
@@ -24,10 +24,6 @@ module Resolvers
description: "SHA256 of a specific version."
def resolve(version_id: nil, sha: nil)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- version_id &&= VersionID.coerce_isolated_input(version_id)
-
check_args(version_id, sha)
::DesignManagement::VersionsFinder
diff --git a/app/graphql/resolvers/design_management/version_resolver.rb b/app/graphql/resolvers/design_management/version_resolver.rb
index a6403fbd69f..7895981d67c 100644
--- a/app/graphql/resolvers/design_management/version_resolver.rb
+++ b/app/graphql/resolvers/design_management/version_resolver.rb
@@ -18,10 +18,6 @@ module Resolvers
end
def find_object(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::DesignManagement::Version].coerce_isolated_input(id)
-
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/resolvers/design_management/versions_resolver.rb b/app/graphql/resolvers/design_management/versions_resolver.rb
index 23ba3c86d98..de636655087 100644
--- a/app/graphql/resolvers/design_management/versions_resolver.rb
+++ b/app/graphql/resolvers/design_management/versions_resolver.rb
@@ -9,8 +9,6 @@ module Resolvers
VersionID = ::Types::GlobalIDType[::DesignManagement::Version]
- extras [:parent]
-
argument :earlier_or_equal_to_sha, GraphQL::Types::String,
as: :sha,
required: false,
@@ -26,11 +24,8 @@ module Resolvers
::Resolvers::DesignManagement::VersionInCollectionResolver
end
- def resolve(parent: nil, id: nil, sha: nil)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id &&= VersionID.coerce_isolated_input(id)
- version = cutoff(parent, id, sha)
+ def resolve(id: nil, sha: nil)
+ version = cutoff(id, sha)
raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, 'cutoff not found' unless version.present?
@@ -44,11 +39,11 @@ module Resolvers
private
# Find the most recent version that the client will accept
- def cutoff(parent, id, sha)
+ def cutoff(id, sha)
if sha.present? || id.present?
specific_version(id, sha)
- elsif at_version = at_version_arg(parent)
- by_id(at_version)
+ elsif at_version = context[:at_version_argument]
+ by_id(at_version) # See: DesignsResolver
else
:unconstrained
end
@@ -68,20 +63,6 @@ module Resolvers
def by_id(gid)
::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(gid))
end
-
- # Find an `at_version` argument passed to a parent node.
- #
- # If one is found, then a design collection further up the AST
- # has been filtered to reflect designs at that version, and so
- # for consistency we should only present versions up to the given
- # version here.
- def at_version_arg(parent)
- # TODO: remove coercion when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- version_id = ::Gitlab::Graphql::FindArgumentInParent.find(parent, :at_version, limit_depth: 4)
- version_id &&= VersionID.coerce_isolated_input(version_id)
- version_id
- end
end
end
end
diff --git a/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb b/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb
index 7032af46221..27bba6c8144 100644
--- a/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb
+++ b/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb
@@ -10,10 +10,6 @@ module Resolvers
description: 'ID of the Sentry issue.'
def resolve(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::Gitlab::ErrorTracking::DetailedError].coerce_isolated_input(id)
-
# Get data from Sentry
response = ::ErrorTracking::IssueDetailsService.new(
project,
diff --git a/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb b/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb
index 8876f8badcd..3867634dd8b 100644
--- a/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb
+++ b/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb
@@ -10,10 +10,6 @@ module Resolvers
description: 'ID of the Sentry issue.'
def resolve(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::Gitlab::ErrorTracking::DetailedError].coerce_isolated_input(id)
-
# Get data from Sentry
response = ::ErrorTracking::IssueLatestEventService.new(
project,
diff --git a/app/graphql/resolvers/group_members_resolver.rb b/app/graphql/resolvers/group_members_resolver.rb
index d3662b08cdf..f787f12e82a 100644
--- a/app/graphql/resolvers/group_members_resolver.rb
+++ b/app/graphql/resolvers/group_members_resolver.rb
@@ -11,6 +11,10 @@ module Resolvers
required: false,
default_value: GroupMembersFinder::DEFAULT_RELATIONS
+ argument :access_levels, [Types::AccessLevelEnum],
+ description: 'Filter members by the given access levels.',
+ required: false
+
private
def finder_class
diff --git a/app/graphql/resolvers/group_packages_resolver.rb b/app/graphql/resolvers/group_packages_resolver.rb
index d91fe84317d..b48e0b75190 100644
--- a/app/graphql/resolvers/group_packages_resolver.rb
+++ b/app/graphql/resolvers/group_packages_resolver.rb
@@ -26,7 +26,10 @@ module Resolvers
def resolve(sort:, **filters)
return unless packages_available?
- ::Packages::GroupPackagesFinder.new(current_user, object, filters.merge(GROUP_SORT_TO_PARAMS_MAP.fetch(sort))).execute
+ params = filters.merge(GROUP_SORT_TO_PARAMS_MAP.fetch(sort))
+ params[:preload_pipelines] = false
+
+ ::Packages::GroupPackagesFinder.new(current_user, object, params).execute
end
end
end
diff --git a/app/graphql/resolvers/incident_management/timeline_events_resolver.rb b/app/graphql/resolvers/incident_management/timeline_events_resolver.rb
new file mode 100644
index 00000000000..b9978259e6b
--- /dev/null
+++ b/app/graphql/resolvers/incident_management/timeline_events_resolver.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module IncidentManagement
+ class TimelineEventsResolver < BaseResolver
+ include LooksAhead
+
+ alias_method :project, :object
+
+ type ::Types::IncidentManagement::TimelineEventType.connection_type, null: true
+
+ argument :incident_id,
+ ::Types::GlobalIDType[::Issue],
+ required: true,
+ description: 'ID of the incident.'
+
+ when_single do
+ argument :id,
+ ::Types::GlobalIDType[::IncidentManagement::TimelineEvent],
+ required: true,
+ description: 'ID of the timeline event.',
+ prepare: ->(id, ctx) { id.model_id }
+ end
+
+ def resolve(**args)
+ incident = args[:incident_id].find
+
+ apply_lookahead(::IncidentManagement::TimelineEventsFinder.new(current_user, incident, args).execute)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/package_details_resolver.rb b/app/graphql/resolvers/package_details_resolver.rb
index 42cb23e701d..705d3900cd2 100644
--- a/app/graphql/resolvers/package_details_resolver.rb
+++ b/app/graphql/resolvers/package_details_resolver.rb
@@ -17,9 +17,6 @@ module Resolvers
end
def resolve(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::Packages::Package].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/resolvers/package_pipelines_resolver.rb b/app/graphql/resolvers/package_pipelines_resolver.rb
index 59a1cd173a4..9ff77f02547 100644
--- a/app/graphql/resolvers/package_pipelines_resolver.rb
+++ b/app/graphql/resolvers/package_pipelines_resolver.rb
@@ -12,21 +12,33 @@ module Resolvers
alias_method :package, :object
- def resolve(first: nil, last: nil, after: nil, before: nil, lookahead:)
- finder = ::Packages::BuildInfosFinder.new(
- package,
- first: first,
- last: last,
- after: decode_cursor(after),
- before: decode_cursor(before),
- max_page_size: context.schema.default_max_page_size,
- support_next_page: lookahead.selects?(:page_info)
- )
+ # this resolver can be called for 100 packages max and we want to limit the
+ # number of build infos returned for _each_ package when using the new finder.
+ MAX_PAGE_SIZE = 20
- build_infos = finder.execute
+ # This returns a promise for a connection of promises for pipelines:
+ # Lazy[Connection[Lazy[Pipeline]]] structure
+ def resolve(first: nil, last: nil, after: nil, before: nil, lookahead:)
+ default_value = default_value_for(first: first, last: last, after: after, before: before)
+ BatchLoader::GraphQL.for(package.id)
+ .batch(default_value: default_value) do |package_ids, loader|
+ build_infos = ::Packages::BuildInfosFinder.new(
+ package_ids,
+ first: first,
+ last: last,
+ after: decode_cursor(after),
+ before: decode_cursor(before),
+ max_page_size: MAX_PAGE_SIZE,
+ support_next_page: lookahead.selects?(:page_info)
+ ).execute
- # this .pluck_pipeline_ids can load max 101 pipelines ids
- ::Ci::Pipeline.id_in(build_infos.pluck_pipeline_ids)
+ build_infos.each do |build_info|
+ loader.call(build_info.package_id) do |connection|
+ connection.items << lazy_load_pipeline(build_info.pipeline_id)
+ connection
+ end
+ end
+ end
end
# we manage the pagination manually, so opt out of the connection field extension
@@ -39,6 +51,22 @@ module Resolvers
private
+ def lazy_load_pipeline(id)
+ ::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Pipeline, id)
+ .find
+ end
+
+ def default_value_for(first:, last:, after:, before:)
+ Gitlab::Graphql::Pagination::ActiveRecordArrayConnection.new(
+ [],
+ first: first,
+ last: last,
+ after: after,
+ before: before,
+ max_page_size: MAX_PAGE_SIZE
+ )
+ end
+
def decode_cursor(encoded)
return unless encoded
diff --git a/app/graphql/resolvers/project_packages_resolver.rb b/app/graphql/resolvers/project_packages_resolver.rb
index 6d66c2fe460..284d3c594da 100644
--- a/app/graphql/resolvers/project_packages_resolver.rb
+++ b/app/graphql/resolvers/project_packages_resolver.rb
@@ -8,7 +8,10 @@ module Resolvers
def resolve(sort:, **filters)
return unless packages_available?
- ::Packages::PackagesFinder.new(object, filters.merge(SORT_TO_PARAMS_MAP.fetch(sort))).execute
+ params = filters.merge(SORT_TO_PARAMS_MAP.fetch(sort))
+ params[:preload_pipelines] = false
+
+ ::Packages::PackagesFinder.new(object, params).execute
end
end
end
diff --git a/app/graphql/resolvers/project_pipeline_resolver.rb b/app/graphql/resolvers/project_pipeline_resolver.rb
index ea733ab08ad..69327cd17b1 100644
--- a/app/graphql/resolvers/project_pipeline_resolver.rb
+++ b/app/graphql/resolvers/project_pipeline_resolver.rb
@@ -47,5 +47,10 @@ module Resolvers
{ statuses: [:needs] }
]
end
+
+ def self.resolver_complexity(args, child_complexity:)
+ complexity = super
+ complexity - 10
+ end
end
end
diff --git a/app/graphql/resolvers/snippets_resolver.rb b/app/graphql/resolvers/snippets_resolver.rb
index 149bd8fa1ce..90f5f2cb534 100644
--- a/app/graphql/resolvers/snippets_resolver.rb
+++ b/app/graphql/resolvers/snippets_resolver.rb
@@ -38,11 +38,9 @@ module Resolvers
private
def snippet_finder_params(args)
- # TODO: remove the type arguments when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
super
- .merge(author: resolve_ids(args[:author_id], ::Types::GlobalIDType[::User]),
- project: resolve_ids(args[:project_id], ::Types::GlobalIDType[::Project]),
+ .merge(author: resolve_ids(args[:author_id]),
+ project: resolve_ids(args[:project_id]),
explore: args[:explore])
end
end
diff --git a/app/graphql/resolvers/timelog_resolver.rb b/app/graphql/resolvers/timelog_resolver.rb
index 14831a29d90..52c4508003a 100644
--- a/app/graphql/resolvers/timelog_resolver.rb
+++ b/app/graphql/resolvers/timelog_resolver.rb
@@ -3,7 +3,6 @@
module Resolvers
class TimelogResolver < BaseResolver
include LooksAhead
- include ResolvesIds
type ::Types::TimelogType.connection_type, null: false
@@ -100,14 +99,13 @@ module Resolvers
def apply_project_filter(timelogs, args)
return timelogs unless args[:project_id]
- project = resolve_ids(args[:project_id], ::Types::GlobalIDType[::Project])
- timelogs.in_project(project)
+ timelogs.in_project(args[:project_id].model_id)
end
def apply_group_filter(timelogs, args)
return timelogs unless args[:group_id]
- group = Group.find_by_id(resolve_ids(args[:group_id], ::Types::GlobalIDType[::Group]))
+ group = Group.find_by_id(args[:group_id].model_id)
timelogs.in_group(group)
end
diff --git a/app/graphql/resolvers/user_merge_requests_resolver_base.rb b/app/graphql/resolvers/user_merge_requests_resolver_base.rb
index e0201e45664..b2d85307c49 100644
--- a/app/graphql/resolvers/user_merge_requests_resolver_base.rb
+++ b/app/graphql/resolvers/user_merge_requests_resolver_base.rb
@@ -57,9 +57,6 @@ module Resolvers
end
def load_project(project_path, project_id)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- project_id &&= ::Types::GlobalIDType[::Project].coerce_isolated_input(project_id)
@project = ::Gitlab::Graphql::Lazy.force(resolve_project(full_path: project_path, project_id: project_id))
end
diff --git a/app/graphql/resolvers/work_item_resolver.rb b/app/graphql/resolvers/work_item_resolver.rb
index ad510849f31..9eb7d6bc693 100644
--- a/app/graphql/resolvers/work_item_resolver.rb
+++ b/app/graphql/resolvers/work_item_resolver.rb
@@ -20,9 +20,6 @@ module Resolvers
private
def find_object(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::WorkItem].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/subscriptions/issuable_updated.rb b/app/graphql/subscriptions/issuable_updated.rb
index c1d82bfcf9c..ad78fd4b4a1 100644
--- a/app/graphql/subscriptions/issuable_updated.rb
+++ b/app/graphql/subscriptions/issuable_updated.rb
@@ -15,10 +15,6 @@ module Subscriptions
end
def authorized?(issuable_id:)
- # TODO: remove this check when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- raise Gitlab::Graphql::Errors::ArgumentError, 'Invalid IssuableID' unless issuable_id.is_a?(GlobalID)
-
issuable = force(GitlabSchema.find_by_gid(issuable_id))
unauthorized! unless issuable && Ability.allowed?(current_user, :"read_#{issuable.to_ability_name}", issuable)
diff --git a/app/graphql/types/alert_management/domain_filter_enum.rb b/app/graphql/types/alert_management/domain_filter_enum.rb
index 3ee01e4c391..cd70cdd8ecf 100644
--- a/app/graphql/types/alert_management/domain_filter_enum.rb
+++ b/app/graphql/types/alert_management/domain_filter_enum.rb
@@ -7,7 +7,11 @@ module Types
description 'Filters the alerts based on given domain'
value 'operations', description: 'Alerts for operations domain.'
- value 'threat_monitoring', description: 'Alerts for threat monitoring domain.'
+ value 'threat_monitoring', description: 'Alerts for threat monitoring domain.',
+ deprecated: {
+ reason: 'Network policies are deprecated and will be removed in GitLab 16.0',
+ milestone: '15.0'
+ }
end
end
end
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index 75909592c6c..b4cd54b1332 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -68,7 +68,7 @@ module Types
end
def visible?(context)
- return false if feature_flag.present? && !Feature.enabled?(feature_flag, default_enabled: :yaml)
+ return false if feature_flag.present? && !Feature.enabled?(feature_flag)
super
end
diff --git a/app/graphql/types/boards/board_issue_input_type.rb b/app/graphql/types/boards/board_issue_input_type.rb
index afa66c1c510..0dd7fbc87da 100644
--- a/app/graphql/types/boards/board_issue_input_type.rb
+++ b/app/graphql/types/boards/board_issue_input_type.rb
@@ -7,7 +7,6 @@ module Types
argument :not, NegatedBoardIssueInputType,
required: false,
- prepare: ->(negated_args, ctx) { negated_args.to_h },
description: 'List of negated arguments.'
argument :search, GraphQL::Types::String,
diff --git a/app/graphql/types/ci/config/config_type.rb b/app/graphql/types/ci/config/config_type.rb
index de355c8eacf..a7a6927136d 100644
--- a/app/graphql/types/ci/config/config_type.rb
+++ b/app/graphql/types/ci/config/config_type.rb
@@ -9,6 +9,8 @@ module Types
field :errors, [GraphQL::Types::String], null: true,
description: 'Linting errors.'
+ field :includes, [Types::Ci::Config::IncludeType], null: true,
+ description: 'List of included files.'
field :merged_yaml, GraphQL::Types::String, null: true,
description: 'Merged CI configuration YAML.'
field :stages, Types::Ci::Config::StageType.connection_type, null: true,
diff --git a/app/graphql/types/ci/config/include_type.rb b/app/graphql/types/ci/config/include_type.rb
new file mode 100644
index 00000000000..71eb8f755ab
--- /dev/null
+++ b/app/graphql/types/ci/config/include_type.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ module Config
+ class IncludeType < BaseObject
+ graphql_name 'CiConfigInclude'
+
+ field :type,
+ Types::Ci::Config::IncludeTypeEnum,
+ null: true,
+ description: 'Include type.'
+
+ field :location,
+ GraphQL::Types::String,
+ null: true,
+ description: 'File location. It can be masked if it contains masked variables, e.g., ' \
+ '".gitlab/ci/build-images.gitlab-ci.yml".'
+
+ field :blob,
+ GraphQL::Types::String,
+ null: true,
+ description: 'File blob location. It can be masked if it contains masked variables, e.g., ' \
+ '"https://gitlab.com/gitlab-org/gitlab/-/blob/e52d6d0246d7375291850e61f0abc101fbda9dc2' \
+ '/.gitlab/ci/build-images.gitlab-ci.yml".'
+
+ field :raw,
+ GraphQL::Types::String,
+ null: true,
+ description: 'File raw location. It can be masked if it contains masked variables, e.g., ' \
+ '"https://gitlab.com/gitlab-org/gitlab/-/raw/e52d6d0246d7375291850e61f0abc101fbda9dc2' \
+ '/.gitlab/ci/build-images.gitlab-ci.yml".'
+
+ field :extra, # rubocop:disable Graphql/JSONType
+ GraphQL::Types::JSON,
+ null: true,
+ description: 'Extra information for the `include`, which can contain `job_name`, `project`, and `ref`. ' \
+ 'Values can be masked if they contain masked variables.'
+
+ field :context_project,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Current project scope, e.g., "gitlab-org/gitlab".'
+
+ field :context_sha,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Current sha scope.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/config/include_type_enum.rb b/app/graphql/types/ci/config/include_type_enum.rb
new file mode 100644
index 00000000000..328824ae996
--- /dev/null
+++ b/app/graphql/types/ci/config/include_type_enum.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ module Config
+ class IncludeTypeEnum < BaseEnum
+ graphql_name 'CiConfigIncludeType'
+ description 'Include type.'
+
+ value 'remote', description: 'Remote include.', value: :remote
+ value 'local', description: 'Local include.', value: :local
+ value 'file', description: 'Project file include.', value: :file
+ value 'template', description: 'Template include.', value: :template
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index 537b8e42ad1..81afc7f0f42 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -195,7 +195,6 @@ module Types
raise ::Gitlab::Graphql::Errors::ArgumentError, 'One of id or name is required' unless id || name
if id
- id = ::Types::GlobalIDType[::CommitStatus].coerce_isolated_input(id) if id
pipeline.statuses.id_in(id.model_id)
else
pipeline.statuses.by_name(name)
diff --git a/app/graphql/types/ci/runner_status_enum.rb b/app/graphql/types/ci/runner_status_enum.rb
index 2e65e2d4e1e..e8bd3d15a54 100644
--- a/app/graphql/types/ci/runner_status_enum.rb
+++ b/app/graphql/types/ci/runner_status_enum.rb
@@ -28,21 +28,17 @@ module Types
value: :online
value 'OFFLINE',
- description: "Runner that has not contacted this instance within the last #{::Ci::Runner::ONLINE_CONTACT_TIMEOUT.inspect}.",
- deprecated: { reason: 'This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period offline', milestone: '14.6' },
+ description: "Runner that has not contacted this instance within the " \
+ "last #{::Ci::Runner::ONLINE_CONTACT_TIMEOUT.inspect}. Will be considered `STALE` if offline for " \
+ "more than #{::Ci::Runner::STALE_TIMEOUT.inspect}.",
value: :offline
value 'STALE',
- description: "Runner that has not contacted this instance within the last #{::Ci::Runner::STALE_TIMEOUT.inspect}. Only available if legacyMode is null. Will be a possible return value starting in 15.0.",
+ description: "Runner that has not contacted this instance within the last #{::Ci::Runner::STALE_TIMEOUT.inspect}.",
value: :stale
- value 'NOT_CONNECTED',
- description: 'Runner that has never contacted this instance.',
- deprecated: { reason: "Use NEVER_CONTACTED instead. NEVER_CONTACTED will have a slightly different scope starting in 15.0, with STALE being returned instead after #{::Ci::Runner::STALE_TIMEOUT.inspect} of no contact", milestone: '14.6' },
- value: :not_connected
-
value 'NEVER_CONTACTED',
- description: 'Runner that has never contacted this instance. Set legacyMode to null to utilize this value. Will replace NOT_CONNECTED starting in 15.0.',
+ description: 'Runner that has never contacted this instance.',
value: :never_contacted
end
end
diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb
index a7f0730f07e..6f957d2511f 100644
--- a/app/graphql/types/ci/runner_type.rb
+++ b/app/graphql/types/ci/runner_type.rb
@@ -34,8 +34,15 @@ module Types
description: 'Admin form URL of the runner. Only available for administrators.'
field :executor_name, GraphQL::Types::String, null: true,
description: 'Executor last advertised by the runner.',
- method: :executor_name,
- feature_flag: :graphql_ci_runner_executor
+ method: :executor_name
+ field :platform_name, GraphQL::Types::String, null: true,
+ description: 'Platform provided by the runner.',
+ method: :platform
+ field :architecture_name, GraphQL::Types::String, null: true,
+ description: 'Architecture provided by the the runner.',
+ method: :architecture
+ field :maintenance_note, GraphQL::Types::String, null: true,
+ description: 'Runner\'s maintenance notes.'
field :groups, ::Types::GroupType.connection_type, null: true,
description: 'Groups the runner is associated with. For group runners only.'
field :id, ::Types::GlobalIDType[::Ci::Runner], null: false,
@@ -70,7 +77,7 @@ module Types
Types::Ci::RunnerStatusEnum,
null: false,
description: 'Status of the runner.',
- resolver: ::Resolvers::Ci::RunnerStatusResolver
+ resolver: ::Resolvers::Ci::RunnerStatusResolver # TODO: Remove :resolver in %17.0
field :tag_list, [GraphQL::Types::String], null: true,
description: 'Tags associated with the runner.'
field :token_expires_at, Types::TimeType, null: true,
diff --git a/app/graphql/types/ci/runner_upgrade_status_type_enum.rb b/app/graphql/types/ci/runner_upgrade_status_type_enum.rb
index e3d77e485bc..02feafe3df9 100644
--- a/app/graphql/types/ci/runner_upgrade_status_type_enum.rb
+++ b/app/graphql/types/ci/runner_upgrade_status_type_enum.rb
@@ -5,17 +5,11 @@ module Types
class RunnerUpgradeStatusTypeEnum < BaseEnum
graphql_name 'CiRunnerUpgradeStatusType'
- value 'NOT_AVAILABLE',
- description: "An update is not available for the runner.",
- value: :not_available
+ value 'UNKNOWN', description: 'Upgrade status is unknown.', value: :unknown
- value 'AVAILABLE',
- description: "An update is available for the runner.",
- value: :available
-
- value 'RECOMMENDED',
- description: "An update is available and recommended for the runner.",
- value: :recommended
+ Gitlab::Ci::RunnerUpgradeCheck::STATUSES.each do |status, description|
+ value status.to_s.upcase, description: description, value: status
+ end
end
end
end
diff --git a/app/graphql/types/color_type.rb b/app/graphql/types/color_type.rb
new file mode 100644
index 00000000000..ee5c0c8737b
--- /dev/null
+++ b/app/graphql/types/color_type.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Types
+ class ColorType < BaseScalar
+ graphql_name 'Color'
+ description <<~DESC
+ Color represented as a hex code or named color.
+
+ For example: "#fefefe".
+ DESC
+
+ def self.coerce_input(value, ctx)
+ color = Gitlab::Color.of(value)
+ raise GraphQL::CoercionError, 'Not a color' unless color.valid?
+
+ color
+ rescue ArgumentError => e
+ raise GraphQL::CoercionError, e.message
+ end
+
+ def self.coerce_result(value, ctx)
+ value.to_s
+ end
+ end
+end
diff --git a/app/graphql/types/concerns/gitlab_style_deprecations.rb b/app/graphql/types/concerns/gitlab_style_deprecations.rb
index 802562ed958..cd8e393b235 100644
--- a/app/graphql/types/concerns/gitlab_style_deprecations.rb
+++ b/app/graphql/types/concerns/gitlab_style_deprecations.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
# Concern for handling deprecation arguments.
-# https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-fields-and-enum-values
+# https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-schema-items
module GitlabStyleDeprecations
extend ActiveSupport::Concern
@@ -11,7 +11,7 @@ module GitlabStyleDeprecations
def gitlab_deprecation(kwargs)
if kwargs[:deprecation_reason].present?
raise ArgumentError, 'Use `deprecated` property instead of `deprecation_reason`. ' \
- 'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-fields-arguments-and-enum-values'
+ 'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-schema-items'
end
deprecation = ::Gitlab::Graphql::Deprecation.parse(kwargs.delete(:deprecated))
diff --git a/app/graphql/types/container_expiration_policy_type.rb b/app/graphql/types/container_expiration_policy_type.rb
index 0e9534be684..22020aff36b 100644
--- a/app/graphql/types/container_expiration_policy_type.rb
+++ b/app/graphql/types/container_expiration_policy_type.rb
@@ -6,7 +6,7 @@ module Types
description 'A tag expiration policy designed to keep only the images that matter most'
- authorize :destroy_container_image
+ authorize :admin_container_image
field :cadence, Types::ContainerExpirationPolicyCadenceEnum, null: false, description: 'This container expiration policy schedule.'
field :created_at, Types::TimeType, null: false, description: 'Timestamp of when the container expiration policy was created.'
diff --git a/app/graphql/types/container_repository_type.rb b/app/graphql/types/container_repository_type.rb
index dddf9a3ee97..cb818ac5e92 100644
--- a/app/graphql/types/container_repository_type.rb
+++ b/app/graphql/types/container_repository_type.rb
@@ -21,6 +21,7 @@ module Types
field :status, Types::ContainerRepositoryStatusEnum, null: true, description: 'Status of the container repository.'
field :tags_count, GraphQL::Types::Int, null: false, description: 'Number of tags associated with this image.'
field :updated_at, Types::TimeType, null: false, description: 'Timestamp when the container repository was updated.'
+ field :last_cleanup_deleted_tags_count, GraphQL::Types::Int, null: true, description: 'Number of deleted tags from the last cleanup.'
def can_delete
Ability.allowed?(current_user, :update_container_image, object)
diff --git a/app/graphql/types/current_user_todos.rb b/app/graphql/types/current_user_todos.rb
index 2551db875b0..4c4cb516979 100644
--- a/app/graphql/types/current_user_todos.rb
+++ b/app/graphql/types/current_user_todos.rb
@@ -17,9 +17,24 @@ module Types
def current_user_todos(state: nil)
state ||= %i[done pending] # TodosFinder treats a `nil` state param as `pending`
- klass = unpresented.class
+ key = [state, unpresented.class.name]
- TodosFinder.new(current_user, state: state, type: klass.name, target_id: object.id).execute
+ BatchLoader::GraphQL.for(unpresented).batch(default_value: [], key: key) do |targets, loader, args|
+ state, klass_name = args[:key]
+
+ targets_by_id = targets.index_by(&:id)
+ ids = targets_by_id.keys
+
+ results = TodosFinder.new(current_user, state: state, type: klass_name, target_id: ids).execute
+
+ by_target_id = results.group_by(&:target_id)
+
+ by_target_id.each do |target_id, todos|
+ target = targets_by_id[target_id]
+ todos.each { _1.target = target } # prevent extra loads
+ loader.call(target, todos)
+ end
+ end
end
end
end
diff --git a/app/graphql/types/customer_relations/contact_type.rb b/app/graphql/types/customer_relations/contact_type.rb
index 6a3882c66af..c07d2164d14 100644
--- a/app/graphql/types/customer_relations/contact_type.rb
+++ b/app/graphql/types/customer_relations/contact_type.rb
@@ -50,6 +50,11 @@ module Types
Types::TimeType,
null: false,
description: 'Timestamp the contact was last updated.'
+
+ field :active,
+ GraphQL::Types::Boolean,
+ null: false,
+ description: 'State of the contact.', method: :active?
end
end
end
diff --git a/app/graphql/types/customer_relations/organization_type.rb b/app/graphql/types/customer_relations/organization_type.rb
index 0f97f0a2433..d7835470795 100644
--- a/app/graphql/types/customer_relations/organization_type.rb
+++ b/app/graphql/types/customer_relations/organization_type.rb
@@ -36,6 +36,11 @@ module Types
Types::TimeType,
null: false,
description: 'Timestamp the organization was last updated.'
+
+ field :active,
+ GraphQL::Types::Boolean,
+ null: false,
+ description: 'State of the organization.', method: :active?
end
end
end
diff --git a/app/graphql/types/dependency_proxy/group_setting_type.rb b/app/graphql/types/dependency_proxy/group_setting_type.rb
index 8b8b8572aa9..6c6f848d019 100644
--- a/app/graphql/types/dependency_proxy/group_setting_type.rb
+++ b/app/graphql/types/dependency_proxy/group_setting_type.rb
@@ -6,7 +6,7 @@ module Types
description 'Group-level Dependency Proxy settings'
- authorize :read_dependency_proxy
+ authorize :admin_dependency_proxy
field :enabled, GraphQL::Types::Boolean, null: false, description: 'Indicates whether the dependency proxy is enabled for the group.'
end
diff --git a/app/graphql/types/dependency_proxy/image_ttl_group_policy_type.rb b/app/graphql/types/dependency_proxy/image_ttl_group_policy_type.rb
index 9ab7c50998d..b8c178539a0 100644
--- a/app/graphql/types/dependency_proxy/image_ttl_group_policy_type.rb
+++ b/app/graphql/types/dependency_proxy/image_ttl_group_policy_type.rb
@@ -6,7 +6,7 @@ module Types
description 'Group-level Dependency Proxy TTL policy settings'
- authorize :read_dependency_proxy
+ authorize :admin_dependency_proxy
field :created_at, Types::TimeType, null: true, description: 'Timestamp of creation.'
field :enabled, GraphQL::Types::Boolean, null: false, description: 'Indicates whether the policy is enabled or disabled.'
diff --git a/app/graphql/types/deprecated_mutations.rb b/app/graphql/types/deprecated_mutations.rb
index 70d5fc31cd1..49bad56b6f9 100644
--- a/app/graphql/types/deprecated_mutations.rb
+++ b/app/graphql/types/deprecated_mutations.rb
@@ -5,8 +5,7 @@ module Types
extend ActiveSupport::Concern
prepended do
- mount_mutation Mutations::Clusters::AgentTokens::Delete,
- deprecated: { reason: 'Tokens must be revoked with ClusterAgentTokenRevoke', milestone: '14.7' }
+ # placeholder for any FOSS mutations to be deprecated
end
end
end
diff --git a/app/graphql/types/design_management/design_fields.rb b/app/graphql/types/design_management/design_fields.rb
index 364f72a519f..c3a35cfe1ad 100644
--- a/app/graphql/types/design_management/design_fields.rb
+++ b/app/graphql/types/design_management/design_fields.rb
@@ -43,13 +43,12 @@ module Types
end
def image_v432x230(parent:)
- version = cached_stateful_version(parent)
- action = design.actions.up_to_version(version).most_recent.first
-
- # A `nil` return value indicates that the image has not been processed
- return unless action.image_v432x230.file
+ Gitlab::Graphql::Lazy.with_value(lazy_action(parent)) do |action|
+ # A `nil` return value indicates that the image has not been processed
+ next unless action&.image_v432x230&.file
- Gitlab::UrlBuilder.build(design, ref: version.sha, size: :v432x230)
+ Gitlab::UrlBuilder.build(action.design, ref: action.version.sha, size: :v432x230)
+ end
end
def event(parent:)
@@ -73,6 +72,25 @@ module Types
def issue
::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Issue, design.issue_id).find
end
+
+ private
+
+ def lazy_action(parent)
+ version = cached_stateful_version(parent)
+
+ BatchLoader::GraphQL.for([version, design]).batch do |ids, loader|
+ by_version = ids.group_by(&:first).transform_values { _1.map(&:second) }
+ designs_by_id = ids.map(&:second).index_by(&:id)
+
+ by_version.each do |v, designs|
+ actions = ::DesignManagement::Action.most_recent.up_to_version(v).by_design(designs).with_version
+ actions.each do |action|
+ action.design = designs_by_id[action.design_id] # eliminate duplicate load
+ loader.call([v, action.design], action)
+ end
+ end
+ end
+ end
end
end
end
diff --git a/app/graphql/types/design_management/design_type.rb b/app/graphql/types/design_management/design_type.rb
index 4c0b1162306..cc4c0e19ec7 100644
--- a/app/graphql/types/design_management/design_type.rb
+++ b/app/graphql/types/design_management/design_type.rb
@@ -23,14 +23,13 @@ module Types
field :versions,
Types::DesignManagement::VersionType.connection_type,
resolver: Resolvers::DesignManagement::VersionsResolver,
- description: "All versions related to this design ordered newest first.",
- extras: [:parent]
+ description: "All versions related to this design ordered newest first."
# Returns a `DesignManagement::Version` for this query based on the
# `atVersion` argument passed to a parent node if present, or otherwise
# the most recent `Version` for the issue.
def cached_stateful_version(parent_node)
- version_gid = Gitlab::Graphql::FindArgumentInParent.find(parent_node, :at_version)
+ version_gid = context[:at_version_argument] # See: DesignsResolver
# Caching is scoped to an `issue_id` to allow us to cache the
# most recent `Version` for an issue
diff --git a/app/graphql/types/global_id_type.rb b/app/graphql/types/global_id_type.rb
index 4f92b5e8cc2..6a924c13a3c 100644
--- a/app/graphql/types/global_id_type.rb
+++ b/app/graphql/types/global_id_type.rb
@@ -1,21 +1,5 @@
# frozen_string_literal: true
-module GraphQLExtensions
- module ScalarExtensions
- # Allow ID to unify with GlobalID Types
- def ==(other)
- if name == 'ID' && other.is_a?(self.class) &&
- other.type_class.ancestors.include?(::Types::GlobalIDType)
- return true
- end
-
- super
- end
- end
-end
-
-::GraphQL::ScalarType.prepend(GraphQLExtensions::ScalarExtensions)
-
module Types
class GlobalIDType < BaseScalar
graphql_name 'GlobalID'
diff --git a/app/graphql/types/incident_management/timeline_event_type.rb b/app/graphql/types/incident_management/timeline_event_type.rb
new file mode 100644
index 00000000000..a6d3f57404b
--- /dev/null
+++ b/app/graphql/types/incident_management/timeline_event_type.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module Types
+ module IncidentManagement
+ class TimelineEventType < BaseObject
+ graphql_name 'TimelineEventType'
+ description 'Describes an incident management timeline event'
+
+ authorize :read_incident_management_timeline_event
+
+ field :id,
+ Types::GlobalIDType[::IncidentManagement::TimelineEvent],
+ null: false,
+ description: 'ID of the timeline event.'
+
+ field :author,
+ Types::UserType,
+ null: true,
+ description: 'User that created the timeline event.'
+
+ field :updated_by_user,
+ Types::UserType,
+ null: true,
+ description: 'User that updated the timeline event.'
+
+ field :incident,
+ Types::IssueType,
+ null: false,
+ description: 'Incident of the timeline event.'
+
+ field :note,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Text note of the timeline event.'
+
+ field :note_html,
+ GraphQL::Types::String,
+ null: true,
+ description: 'HTML note of the timeline event.'
+
+ field :promoted_from_note,
+ Types::Notes::NoteType,
+ null: true,
+ description: 'Note from which the timeline event was created.'
+
+ field :editable,
+ GraphQL::Types::Boolean,
+ null: false,
+ description: 'Indicates the timeline event is editable.'
+
+ field :action,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Indicates the timeline event icon.'
+
+ field :occurred_at,
+ Types::TimeType,
+ null: false,
+ description: 'Timestamp when the event occurred.'
+
+ field :created_at,
+ Types::TimeType,
+ null: false,
+ description: 'Timestamp when the event created.'
+
+ field :updated_at,
+ Types::TimeType,
+ null: false,
+ description: 'Timestamp when the event updated.'
+ end
+ end
+end
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index 07450c38616..c83200bd614 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -170,7 +170,7 @@ module Types
end
def hidden?
- object.hidden? if Feature.enabled?(:ban_user_feature_flag, default_enabled: :yaml)
+ object.hidden? if Feature.enabled?(:ban_user_feature_flag)
end
def escalation_status
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index af198d03c3f..cc3df474bef 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -67,9 +67,6 @@ module Types
description: 'Indicates if members of the target project can push to the fork.'
field :default_merge_commit_message, GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'Default merge commit message of the merge request.'
- field :default_merge_commit_message_with_description, GraphQL::Types::String, null: true,
- description: 'Default merge commit message of the merge request with description. Will have the same value as `defaultMergeCommitMessage` when project has `mergeCommitTemplate` set.',
- deprecated: { reason: 'Define merge commit template in project and use `defaultMergeCommitMessage`', milestone: '14.5' }
field :default_squash_commit_message, GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'Default squash commit message of the merge request.'
field :diff_stats_summary, Types::DiffStatsSummaryType, null: true, calls_gitaly: true,
@@ -97,6 +94,7 @@ module Types
method: :public_merge_status, null: true,
description: 'Merge status of the merge request.'
field :mergeable_discussions_state, GraphQL::Types::Boolean, null: true,
+ calls_gitaly: true,
description: 'Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged.'
field :rebase_commit_sha, GraphQL::Types::String, null: true,
description: 'Rebase commit SHA of the merge request.'
@@ -254,10 +252,6 @@ module Types
object.default_merge_commit_message(include_description: false, user: current_user)
end
- def default_merge_commit_message_with_description
- object.default_merge_commit_message(include_description: true)
- end
-
def default_squash_commit_message
object.default_squash_commit_message(user: current_user)
end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 2297912ac35..7d8ada82d40 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -47,6 +47,10 @@ module Types
mount_mutation Mutations::DependencyProxy::ImageTtlGroupPolicy::Update
mount_mutation Mutations::DependencyProxy::GroupSettings::Update
mount_mutation Mutations::Environments::CanaryIngress::Update
+ mount_mutation Mutations::IncidentManagement::TimelineEvent::Create
+ mount_mutation Mutations::IncidentManagement::TimelineEvent::PromoteFromNote
+ mount_mutation Mutations::IncidentManagement::TimelineEvent::Update
+ mount_mutation Mutations::IncidentManagement::TimelineEvent::Destroy
mount_mutation Mutations::Issues::Create
mount_mutation Mutations::Issues::SetAssignees
mount_mutation Mutations::Issues::SetCrmContacts
@@ -69,7 +73,9 @@ module Types
mount_mutation Mutations::MergeRequests::SetDraft, calls_gitaly: true
mount_mutation Mutations::MergeRequests::SetAssignees
mount_mutation Mutations::MergeRequests::ReviewerRereview
- mount_mutation Mutations::MergeRequests::ToggleAttentionRequested, feature_flag: :mr_attention_requests
+ mount_mutation Mutations::MergeRequests::RequestAttention
+ mount_mutation Mutations::MergeRequests::RemoveAttentionRequest
+ mount_mutation Mutations::MergeRequests::ToggleAttentionRequested
mount_mutation Mutations::Metrics::Dashboard::Annotations::Create
mount_mutation Mutations::Metrics::Dashboard::Annotations::Delete
mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true
@@ -88,6 +94,7 @@ module Types
mount_mutation Mutations::Terraform::State::Delete
mount_mutation Mutations::Terraform::State::Lock
mount_mutation Mutations::Terraform::State::Unlock
+ mount_mutation Mutations::Timelogs::Delete
mount_mutation Mutations::Todos::Create
mount_mutation Mutations::Todos::MarkDone
mount_mutation Mutations::Todos::Restore
@@ -108,7 +115,12 @@ module Types
mount_mutation Mutations::Ci::Pipeline::Cancel
mount_mutation Mutations::Ci::Pipeline::Destroy
mount_mutation Mutations::Ci::Pipeline::Retry
- mount_mutation Mutations::Ci::CiCdSettingsUpdate
+ mount_mutation Mutations::Ci::CiCdSettingsUpdate, deprecated: {
+ reason: :renamed,
+ replacement: 'ProjectCiCdSettingsUpdate',
+ milestone: '15.0'
+ }
+ mount_mutation Mutations::Ci::ProjectCiCdSettingsUpdate
mount_mutation Mutations::Ci::Job::Play
mount_mutation Mutations::Ci::Job::Retry
mount_mutation Mutations::Ci::Job::Cancel
@@ -128,6 +140,7 @@ module Types
mount_mutation Mutations::WorkItems::Create
mount_mutation Mutations::WorkItems::CreateFromTask
mount_mutation Mutations::WorkItems::Delete
+ mount_mutation Mutations::WorkItems::DeleteTask
mount_mutation Mutations::WorkItems::Update
mount_mutation Mutations::SavedReplies::Create
mount_mutation Mutations::SavedReplies::Update
diff --git a/app/graphql/types/namespace/package_settings_type.rb b/app/graphql/types/namespace/package_settings_type.rb
index cb546bbf3ec..7a0abe619a5 100644
--- a/app/graphql/types/namespace/package_settings_type.rb
+++ b/app/graphql/types/namespace/package_settings_type.rb
@@ -6,7 +6,7 @@ module Types
description 'Namespace-level Package Registry settings'
- authorize :read_package_settings
+ authorize :admin_package
field :generic_duplicate_exception_regex, Types::UntrustedRegexp, null: true, description: 'When generic_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.'
field :generic_duplicates_allowed, GraphQL::Types::Boolean, null: false, description: 'Indicates whether duplicate generic packages are allowed for this namespace.'
diff --git a/app/graphql/types/notes/update_diff_image_position_input_type.rb b/app/graphql/types/notes/update_diff_image_position_input_type.rb
index 0c6e4a16434..913d24cb513 100644
--- a/app/graphql/types/notes/update_diff_image_position_input_type.rb
+++ b/app/graphql/types/notes/update_diff_image_position_input_type.rb
@@ -28,6 +28,8 @@ module Types
raise GraphQL::ExecutionError, "At least one property of `#{self.class.graphql_name}` must be set"
end
end
+
+ super
end
end
end
diff --git a/app/graphql/types/packages/package_base_type.rb b/app/graphql/types/packages/package_base_type.rb
new file mode 100644
index 00000000000..06ccde94cd4
--- /dev/null
+++ b/app/graphql/types/packages/package_base_type.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ class PackageBaseType < ::Types::BaseObject
+ graphql_name 'PackageBase'
+ description 'Represents a package in the Package Registry'
+
+ connection_type_class(Types::CountableConnectionType)
+
+ authorize :read_package
+
+ field :id, ::Types::GlobalIDType[::Packages::Package], null: false,
+ description: 'ID of the package.'
+
+ field :can_destroy, GraphQL::Types::Boolean, null: false, 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,
+ description: 'Package metadata.'
+ field :name, GraphQL::Types::String, null: false, description: 'Name of the package.'
+ field :package_type, Types::Packages::PackageTypeEnum, null: false, description: 'Package type.'
+ field :project, Types::ProjectType, null: false, description: 'Project where the package is stored.'
+ field :status, Types::Packages::PackageStatusEnum, null: false, description: 'Package status.'
+ field :tags, Types::Packages::PackageTagType.connection_type, null: true, description: 'Package tags.'
+ field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.'
+ field :version, GraphQL::Types::String, null: true, description: 'Version string.'
+
+ def project
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
+ end
+
+ def can_destroy
+ Ability.allowed?(current_user, :destroy_package, object)
+ end
+
+ # NOTE: This method must be kept in sync with the union
+ # type: `Types::Packages::MetadataType`.
+ #
+ # `Types::Packages::MetadataType.resolve_type(metadata, ctx)` must never raise.
+ # rubocop: disable GraphQL/ResolverMethodLength
+ def metadata
+ case object.package_type
+ when 'composer'
+ object.composer_metadatum
+ when 'conan'
+ object.conan_metadatum
+ when 'maven'
+ object.maven_metadatum
+ when 'nuget'
+ object.nuget_metadatum
+ when 'pypi'
+ object.pypi_metadatum
+ else
+ nil
+ end
+ end
+ # rubocop: enable GraphQL/ResolverMethodLength
+ end
+ end
+end
diff --git a/app/graphql/types/packages/package_details_type.rb b/app/graphql/types/packages/package_details_type.rb
index 444ecb5e792..ae57e103f40 100644
--- a/app/graphql/types/packages/package_details_type.rb
+++ b/app/graphql/types/packages/package_details_type.rb
@@ -4,26 +4,19 @@ module Types
module Packages
class PackageDetailsType < PackageType
graphql_name 'PackageDetailsType'
- description 'Represents a package details in the Package Registry. Note that this type is in beta and susceptible to changes'
+ description 'Represents a package details in the Package Registry'
include ::PackagesHelper
authorize :read_package
- field :versions, ::Types::Packages::PackageType.connection_type, null: true,
+ field :versions, ::Types::Packages::PackageBaseType.connection_type, null: true,
description: 'Other versions of the package.'
field :package_files, Types::Packages::PackageFileType.connection_type, null: true, method: :installable_package_files, description: 'Package files.'
field :dependency_links, Types::Packages::PackageDependencyLinkType.connection_type, null: true, description: 'Dependency link.'
- # this is an override of Types::Packages::PackageType.pipelines
- # in order to use a custom resolver: Resolvers::PackagePipelinesResolver
- field :pipelines,
- resolver: Resolvers::PackagePipelinesResolver,
- description: 'Pipelines that built the package.',
- deprecated: { reason: 'Due to scalability concerns, this field is going to be removed', milestone: '14.6' }
-
field :composer_config_repository_url, GraphQL::Types::String, null: true, description: 'Url of the Composer setup endpoint.'
field :composer_url, GraphQL::Types::String, null: true, description: 'Url of the Composer endpoint.'
field :conan_url, GraphQL::Types::String, null: true, description: 'Url of the Conan project endpoint.'
diff --git a/app/graphql/types/packages/package_type.rb b/app/graphql/types/packages/package_type.rb
index 1155be28e08..f6586670c72 100644
--- a/app/graphql/types/packages/package_type.rb
+++ b/app/graphql/types/packages/package_type.rb
@@ -2,67 +2,17 @@
module Types
module Packages
- class PackageType < ::Types::BaseObject
+ class PackageType < Types::Packages::PackageBaseType
graphql_name 'Package'
- description 'Represents a package in the Package Registry. Note that this type is in beta and susceptible to changes'
-
- connection_type_class(Types::CountableConnectionType)
+ description 'Represents a package with pipelines in the Package Registry'
authorize :read_package
- field :id, ::Types::GlobalIDType[::Packages::Package], null: false,
- description: 'ID of the package.'
-
- field :can_destroy, GraphQL::Types::Boolean, null: false, 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,
- description: 'Package metadata.'
- field :name, GraphQL::Types::String, null: false, description: 'Name of the package.'
- field :package_type, Types::Packages::PackageTypeEnum, null: false, description: 'Package type.'
- field :pipelines, Types::Ci::PipelineType.connection_type, null: true,
- description: 'Pipelines that built the package.',
- deprecated: { reason: 'Due to scalability concerns, this field is going to be removed', milestone: '14.6' }
- field :project, Types::ProjectType, null: false, description: 'Project where the package is stored.'
- field :status, Types::Packages::PackageStatusEnum, null: false, description: 'Package status.'
- field :tags, Types::Packages::PackageTagType.connection_type, null: true, description: 'Package tags.'
- field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.'
- field :version, GraphQL::Types::String, null: true, description: 'Version string.'
- field :versions, ::Types::Packages::PackageType.connection_type, null: true,
- description: 'Other versions of the package.',
- deprecated: { reason: 'This field is now only returned in the PackageDetailsType', milestone: '13.11' }
-
- def project
- Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
- end
-
- def versions
- []
- end
-
- def can_destroy
- Ability.allowed?(current_user, :destroy_package, object)
- end
-
- # NOTE: This method must be kept in sync with the union
- # type: `Types::Packages::MetadataType`.
- #
- # `Types::Packages::MetadataType.resolve_type(metadata, ctx)` must never raise.
- def metadata
- case object.package_type
- when 'composer'
- object.composer_metadatum
- when 'conan'
- object.conan_metadatum
- when 'maven'
- object.maven_metadatum
- when 'nuget'
- object.nuget_metadatum
- when 'pypi'
- object.pypi_metadatum
- else
- nil
- end
- end
+ field :pipelines,
+ resolver: Resolvers::PackagePipelinesResolver,
+ description: <<-DESC
+ Pipelines that built the package. Max page size #{Resolvers::PackagePipelinesResolver::MAX_PAGE_SIZE}.
+ DESC
end
end
end
diff --git a/app/graphql/types/permission_types/timelog.rb b/app/graphql/types/permission_types/timelog.rb
new file mode 100644
index 00000000000..c35f3101e39
--- /dev/null
+++ b/app/graphql/types/permission_types/timelog.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ class Timelog < BasePermissionType
+ graphql_name 'TimelogPermissions'
+
+ abilities :admin_timelog
+ end
+ end
+end
diff --git a/app/graphql/types/permission_types/work_item.rb b/app/graphql/types/permission_types/work_item.rb
new file mode 100644
index 00000000000..bae1dae4834
--- /dev/null
+++ b/app/graphql/types/permission_types/work_item.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ class WorkItem < BasePermissionType
+ graphql_name 'WorkItemPermissions'
+ description 'Check permissions for the current user on a work item'
+
+ abilities :read_work_item, :update_work_item, :delete_work_item
+ end
+ end
+end
diff --git a/app/graphql/types/project_statistics_type.rb b/app/graphql/types/project_statistics_type.rb
index 1146774b43c..5ab3cc33e85 100644
--- a/app/graphql/types/project_statistics_type.rb
+++ b/app/graphql/types/project_statistics_type.rb
@@ -27,5 +27,7 @@ module Types
description: 'Uploads size of the project in bytes.'
field :wiki_size, GraphQL::Types::Float, null: true,
description: 'Wiki size of the project in bytes.'
+ field :container_registry_size, GraphQL::Types::Float, null: true,
+ description: 'Container Registry size of the project in bytes.'
end
end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 47e9a6c11fc..f1de8e985b3 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -295,6 +295,19 @@ module Types
description: 'HTTP Integrations which can receive alerts for the project.',
resolver: Resolvers::AlertManagement::HttpIntegrationsResolver
+ field :incident_management_timeline_events,
+ Types::IncidentManagement::TimelineEventType.connection_type,
+ null: true,
+ description: 'Incident Management Timeline events associated with the incident.',
+ extras: [:lookahead],
+ resolver: Resolvers::IncidentManagement::TimelineEventsResolver
+
+ field :incident_management_timeline_event,
+ Types::IncidentManagement::TimelineEventType,
+ null: true,
+ description: 'Incident Management Timeline event associated with the incident.',
+ resolver: Resolvers::IncidentManagement::TimelineEventsResolver.single
+
field :releases,
Types::ReleaseType.connection_type,
null: true,
diff --git a/app/graphql/types/projects/topic_type.rb b/app/graphql/types/projects/topic_type.rb
index c579f2f2b9d..bde6d79ddbf 100644
--- a/app/graphql/types/projects/topic_type.rb
+++ b/app/graphql/types/projects/topic_type.rb
@@ -12,6 +12,10 @@ module Types
field :name, GraphQL::Types::String, null: false,
description: 'Name of the topic.'
+ field :title, GraphQL::Types::String, null: false,
+ method: :title_or_name,
+ description: 'Title of the topic.'
+
field :description, GraphQL::Types::String, null: true,
description: 'Description of the topic.'
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index cc46c7e86e4..01b1a71896a 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -99,17 +99,6 @@ module Types
argument :id, ::Types::GlobalIDType[::MergeRequest], required: true, description: 'Global ID of the merge request.'
end
- field :instance_statistics_measurements,
- type: Types::Admin::Analytics::UsageTrends::MeasurementType.connection_type,
- null: true,
- description: 'Get statistics on the instance.',
- resolver: Resolvers::Admin::Analytics::UsageTrends::MeasurementsResolver,
- deprecated: {
- reason: :renamed,
- replacement: 'Query.usageTrendsMeasurements',
- milestone: '13.10'
- }
-
field :usage_trends_measurements, Types::Admin::Analytics::UsageTrends::MeasurementType.connection_type,
null: true,
description: 'Get statistics on the instance.',
@@ -160,30 +149,18 @@ module Types
end
def issue(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::Issue].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
def merge_request(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::MergeRequest].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
def milestone(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[Milestone].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
def container_repository(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::ContainerRepository].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
diff --git a/app/graphql/types/range_input_type.rb b/app/graphql/types/range_input_type.rb
index 9580b37d6c0..15b8121da93 100644
--- a/app/graphql/types/range_input_type.rb
+++ b/app/graphql/types/range_input_type.rb
@@ -21,7 +21,7 @@ module Types
raise ::Gitlab::Graphql::Errors::ArgumentError, 'start must be before end'
end
- to_h
+ super
end
end
end
diff --git a/app/graphql/types/root_storage_statistics_type.rb b/app/graphql/types/root_storage_statistics_type.rb
index 467331c5643..b1b712aab38 100644
--- a/app/graphql/types/root_storage_statistics_type.rb
+++ b/app/graphql/types/root_storage_statistics_type.rb
@@ -16,5 +16,6 @@ module Types
field :storage_size, GraphQL::Types::Float, null: false, description: 'Total storage in bytes.'
field :uploads_size, GraphQL::Types::Float, null: false, description: 'Uploads size in bytes.'
field :wiki_size, GraphQL::Types::Float, null: false, description: 'Wiki size in bytes.'
+ field :container_registry_size, GraphQL::Types::Float, null: false, description: 'Container Registry size in bytes.'
end
end
diff --git a/app/graphql/types/timelog_type.rb b/app/graphql/types/timelog_type.rb
index d348fa698fa..c3fb9b77927 100644
--- a/app/graphql/types/timelog_type.rb
+++ b/app/graphql/types/timelog_type.rb
@@ -6,6 +6,13 @@ module Types
authorize :read_issue
+ expose_permissions Types::PermissionTypes::Timelog
+
+ field :id,
+ GraphQL::Types::ID,
+ null: false,
+ description: 'Internal ID of the timelog.'
+
field :spent_at,
Types::TimeType,
null: true,
diff --git a/app/graphql/types/work_item_type.rb b/app/graphql/types/work_item_type.rb
index 512b9ef64d2..cd784d54959 100644
--- a/app/graphql/types/work_item_type.rb
+++ b/app/graphql/types/work_item_type.rb
@@ -23,5 +23,7 @@ module Types
markdown_field :title_html, null: true
markdown_field :description_html, null: true
+
+ expose_permissions Types::PermissionTypes::WorkItem
end
end
diff --git a/app/graphql/types/work_items/convert_task_input_type.rb b/app/graphql/types/work_items/convert_task_input_type.rb
index 1f142c6815c..2e66c1c1b3f 100644
--- a/app/graphql/types/work_items/convert_task_input_type.rb
+++ b/app/graphql/types/work_items/convert_task_input_type.rb
@@ -24,10 +24,6 @@ module Types
class << self
def work_item_type_global_id(global_id)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- global_id = ::Types::GlobalIDType[::WorkItems::Type].coerce_isolated_input(global_id)
-
global_id&.model_id
end
end
diff --git a/app/graphql/types/work_items/deleted_task_input_type.rb b/app/graphql/types/work_items/deleted_task_input_type.rb
new file mode 100644
index 00000000000..92297876c89
--- /dev/null
+++ b/app/graphql/types/work_items/deleted_task_input_type.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Types
+ module WorkItems
+ class DeletedTaskInputType < BaseInputObject
+ graphql_name 'WorkItemDeletedTaskInput'
+
+ argument :id, ::Types::GlobalIDType[::WorkItem],
+ required: true,
+ description: 'Global ID of the task referenced in the work item\'s description.'
+ argument :line_number_end, GraphQL::Types::Int,
+ required: true,
+ description: 'Last line in the Markdown source that defines the list item task.'
+ argument :line_number_start, GraphQL::Types::Int,
+ required: true,
+ description: 'First line in the Markdown source that defines the list item task.'
+ end
+ end
+end
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index cb43d911a2f..6dbd0f7bd7b 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -35,23 +35,21 @@ module AppearancesHelper
end
end
- def brand_header_logo
+ def brand_header_logo(options = {})
+ add_gitlab_white_text = options[:add_gitlab_white_text] || false
+ add_gitlab_black_text = options[:add_gitlab_black_text] || false
+
if current_appearance&.header_logo?
image_tag current_appearance.header_logo_path, class: 'brand-header-logo'
- elsif Feature.enabled?(:ukraine_support_tanuki)
- render partial: 'shared/logo_ukraine', formats: :svg
+ elsif add_gitlab_white_text
+ render partial: 'shared/logo_with_white_text', formats: :svg
+ elsif add_gitlab_black_text
+ render partial: 'shared/logo_with_black_text', formats: :svg
else
render partial: 'shared/logo', formats: :svg
end
end
- # Skip the 'GitLab' type logo when custom brand logo is set
- def brand_header_logo_type
- unless current_appearance&.header_logo?
- render partial: 'shared/logo_type', formats: :svg
- end
- end
-
def header_message
return unless current_appearance&.show_header?
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index feeedb0a501..8cdfc267693 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -21,7 +21,7 @@ module ApplicationHelper
def dispensable_render(...)
render(...)
rescue StandardError => error
- if Feature.enabled?(:dispensable_render, default_enabled: :yaml)
+ if Feature.enabled?(:dispensable_render)
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(error)
nil
else
@@ -32,7 +32,7 @@ module ApplicationHelper
def dispensable_render_if_exists(...)
render_if_exists(...)
rescue StandardError => error
- if Feature.enabled?(:dispensable_render, default_enabled: :yaml)
+ if Feature.enabled?(:dispensable_render)
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(error)
nil
else
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 57e08eeb4f4..9023cca18dc 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -97,24 +97,18 @@ module ApplicationSettingsHelper
end
end
- def oauth_providers_checkboxes
+ def oauth_providers_checkboxes(form)
button_based_providers.map do |source|
- disabled = @application_setting.disabled_oauth_sign_in_sources.include?(source.to_s)
+ checked = !@application_setting.disabled_oauth_sign_in_sources.include?(source.to_s)
name = Gitlab::Auth::OAuth::Provider.label_for(source)
- checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]'
- checkbox_id = "application_setting_enabled_oauth_sign_in_sources_#{name.parameterize(separator: '_')}"
-
- content_tag :div, class: 'form-check' do
- check_box_tag(
- checkbox_name,
- source,
- !disabled,
- autocomplete: 'off',
- id: checkbox_id,
- class: 'form-check-input'
- ) +
- label_tag(checkbox_id, name, class: 'form-check-label')
- end
+
+ form.gitlab_ui_checkbox_component(
+ :enabled_oauth_sign_in_sources,
+ name,
+ checkbox_options: { checked: checked, multiple: true, autocomplete: 'off' },
+ checked_value: source,
+ unchecked_value: nil
+ )
end
end
@@ -278,6 +272,7 @@ module ApplicationSettingsHelper
:invisible_captcha_enabled,
:max_artifacts_size,
:max_attachment_size,
+ :max_export_size,
:max_import_size,
:max_pages_size,
:max_yaml_size_bytes,
@@ -412,6 +407,8 @@ module ApplicationSettingsHelper
:container_registry_import_max_retries,
:container_registry_import_start_max_retries,
:container_registry_import_max_step_duration,
+ :container_registry_pre_import_timeout,
+ :container_registry_import_timeout,
:container_registry_import_target_plan,
:container_registry_import_created_before,
:keep_latest_artifact,
@@ -431,7 +428,8 @@ module ApplicationSettingsHelper
:users_get_by_id_limit_allowlist_raw,
:runner_token_expiration_interval,
:group_runner_token_expiration_interval,
- :project_runner_token_expiration_interval
+ :project_runner_token_expiration_interval,
+ :pipeline_limit_per_project_user_sha
].tap do |settings|
settings << :deactivate_dormant_users unless Gitlab.com?
end
@@ -468,7 +466,7 @@ module ApplicationSettingsHelper
def instance_clusters_enabled?
clusterable = Clusters::Instance.new
- Feature.enabled?(:certificate_based_clusters, clusterable, default_enabled: :yaml, type: :ops) &&
+ clusterable.certificate_based_clusters_enabled? &&
can?(current_user, :read_cluster, clusterable)
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 6ac4a12bcd5..07152133402 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -91,7 +91,9 @@ module AuthHelper
end
def saml_providers
- auth_providers.select { |provider| auth_strategy_class(provider) == 'OmniAuth::Strategies::SAML' }
+ auth_providers.select do |provider|
+ provider == :saml || auth_strategy_class(provider) == 'OmniAuth::Strategies::SAML'
+ end
end
def auth_strategy_class(provider)
diff --git a/app/helpers/badges_helper.rb b/app/helpers/badges_helper.rb
index a03f7f4097a..26ebe8a6470 100644
--- a/app/helpers/badges_helper.rb
+++ b/app/helpers/badges_helper.rb
@@ -69,6 +69,7 @@ module BadgesHelper
icon_only = options[:icon_only]
variant_class = VARIANT_CLASSES[options.fetch(:variant, :muted)]
size_class = SIZE_CLASSES[options.fetch(:size, :md)]
+ icon_classes = GL_ICON_CLASSES.dup << options.fetch(:icon_classes, nil)
html_options = html_options.merge(
class: [
@@ -85,7 +86,6 @@ module BadgesHelper
end
if options[:icon]
- icon_classes = GL_ICON_CLASSES.dup
icon_classes << "gl-mr-2" unless icon_only
icon = sprite_icon(options[:icon], css_class: icon_classes.join(' '))
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index f849f36bf84..f98e70e41d8 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -25,7 +25,6 @@ module BoardsHelper
labels_manage_path: labels_manage_path,
releases_fetch_path: releases_fetch_path,
board_type: board.to_type,
- has_scope: board.scoped?.to_s,
has_missing_boards: has_missing_boards?.to_s,
multiple_boards_available: multiple_boards_available?.to_s,
board_base_url: board_base_url
diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb
index b138e9aeb0c..10cfa97030d 100644
--- a/app/helpers/broadcast_messages_helper.rb
+++ b/app/helpers/broadcast_messages_helper.rb
@@ -73,7 +73,7 @@ module BroadcastMessagesHelper
private
def current_user_access_level_for_project_or_group
- return if Feature.disabled?(:role_targeted_broadcast_messages, default_enabled: :yaml)
+ return if Feature.disabled?(:role_targeted_broadcast_messages)
return unless current_user.present?
strong_memoize(:current_user_access_level_for_project_or_group) do
diff --git a/app/helpers/ci/builds_helper.rb b/app/helpers/ci/builds_helper.rb
index bfdb830f2c3..b4a2cf7bb1e 100644
--- a/app/helpers/ci/builds_helper.rb
+++ b/app/helpers/ci/builds_helper.rb
@@ -36,5 +36,15 @@ module Ci
description: project_job_url(@project, @build)
}
end
+
+ def prepare_failed_jobs_summary_data(failed_builds)
+ failed_builds.map do |build|
+ {
+ id: build.id,
+ failure: build.present.callout_failure_message,
+ failure_summary: build_summary(build)
+ }
+ end.to_json
+ end
end
end
diff --git a/app/helpers/ci/pipeline_editor_helper.rb b/app/helpers/ci/pipeline_editor_helper.rb
index 18557afcb99..da773e3e8a8 100644
--- a/app/helpers/ci/pipeline_editor_helper.rb
+++ b/app/helpers/ci/pipeline_editor_helper.rb
@@ -21,6 +21,7 @@ module Ci
"default-branch" => project.default_branch_or_main,
"empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'),
"initial-branch-name" => initial_branch,
+ "includes-help-page-path" => help_page_path('ci/yaml/includes'),
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'check-cicd-syntax'),
"lint-unavailable-help-page-path" => help_page_path('ci/pipeline_editor/index', anchor: 'configuration-validation-currently-not-available-message'),
"needs-help-page-path" => help_page_path('ci/yaml/index', anchor: 'needs'),
diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb
index 70d2a4fafd1..7722677e503 100644
--- a/app/helpers/ci/pipelines_helper.rb
+++ b/app/helpers/ci/pipelines_helper.rb
@@ -42,7 +42,7 @@ module Ci
{ name: 'Django', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/django.svg') },
{ name: 'Docker', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/docker.svg') },
{ name: 'Elixir', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/elixir.svg') },
- { name: 'iOS-Fastlane', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/fastlane.svg') },
+ { name: 'iOS-Fastlane', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/fastlane.svg'), title: 'iOS with Fastlane' },
{ name: 'Flutter', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/flutter.svg') },
{ name: 'Go', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/go_logo.svg') },
{ name: 'Gradle', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/gradle.svg') },
@@ -51,6 +51,7 @@ module Ci
{ name: 'Julia', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/julia.svg') },
{ name: 'Laravel', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/laravel.svg') },
{ name: 'LaTeX', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/latex.svg') },
+ { name: 'MATLAB', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/matlab.svg') },
{ name: 'Maven', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/maven.svg') },
{ name: 'Mono', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/mono.svg') },
{ name: 'Nodejs', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/node_js.svg') },
@@ -109,6 +110,7 @@ module Ci
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
diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb
index 0e8b6fa6d25..6366ca0dfb1 100644
--- a/app/helpers/ci/runners_helper.rb
+++ b/app/helpers/ci/runners_helper.rb
@@ -17,7 +17,7 @@ module Ci
title = s_("Runners|Runner is online; last contact was %{runner_contact} ago") % { runner_contact: time_ago_in_words(contacted_at) }
icon = 'status-active'
span_class = 'gl-text-green-500'
- when :not_connected, :never_contacted
+ when :never_contacted
title = s_("Runners|Runner has never contacted this instance")
icon = 'warning-solid'
when :offline
@@ -72,11 +72,11 @@ module Ci
def group_shared_runners_settings_data(group)
{
update_path: api_v4_groups_path(id: group.id),
- shared_runners_availability: group.shared_runners_setting,
- parent_shared_runners_availability: group.parent&.shared_runners_setting,
- runner_enabled: Namespace::SR_ENABLED,
- runner_disabled: Namespace::SR_DISABLED_AND_UNOVERRIDABLE,
- runner_allow_override: Namespace::SR_DISABLED_WITH_OVERRIDE
+ shared_runners_setting: group.shared_runners_setting,
+ parent_shared_runners_setting: group.parent&.shared_runners_setting,
+ runner_enabled_value: Namespace::SR_ENABLED,
+ runner_disabled_value: Namespace::SR_DISABLED_AND_UNOVERRIDABLE,
+ runner_allow_override_value: Namespace::SR_DISABLED_WITH_OVERRIDE
}
end
diff --git a/app/helpers/ci/secure_files_helper.rb b/app/helpers/ci/secure_files_helper.rb
new file mode 100644
index 00000000000..30b2e12ac3b
--- /dev/null
+++ b/app/helpers/ci/secure_files_helper.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+module Ci
+ module SecureFilesHelper
+ def show_secure_files_setting(project, user)
+ return false if user.nil?
+
+ Feature.enabled?(:ci_secure_files, project) && user.can?(:read_secure_files, project)
+ end
+ end
+end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index fe057fb3412..8449bccd285 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -17,17 +17,17 @@ module ClustersHelper
clusters_empty_state_image: image_path('illustrations/empty-state/empty-state-clusters.svg'),
empty_state_image: image_path('illustrations/empty-state/empty-state-agents.svg'),
empty_state_help_text: clusterable.empty_state_help_text,
- new_cluster_path: clusterable.new_path,
add_cluster_path: clusterable.connect_path,
new_cluster_docs_path: clusterable.new_cluster_docs_path,
can_add_cluster: clusterable.can_add_cluster?.to_s,
can_admin_cluster: clusterable.can_admin_cluster?.to_s,
display_cluster_agents: display_cluster_agents?(clusterable).to_s,
- certificate_based_clusters_enabled: Feature.enabled?(:certificate_based_clusters, clusterable, default_enabled: :yaml, type: :ops).to_s,
+ certificate_based_clusters_enabled: clusterable.certificate_based_clusters_enabled?.to_s,
default_branch_name: default_branch_name(clusterable),
project_path: clusterable_project_path(clusterable),
kas_address: Gitlab::Kas.external_url,
- gitlab_version: Gitlab.version_info
+ gitlab_version: Gitlab.version_info,
+ kas_version: Gitlab::Kas.version_info
}
end
@@ -39,13 +39,7 @@ module ClustersHelper
base_domain: cluster.base_domain,
application_ingress_external_ip: cluster.application_ingress_external_ip,
auto_devops_help_path: help_page_path('topics/autodevops/index'),
- external_endpoint_help_path: help_page_path('user/project/clusters/index.md', anchor: 'base-domain')
- }
- end
-
- def js_cluster_new
- {
- cluster_connect_help_path: help_page_path('user/project/clusters/add_remove_clusters', anchor: 'add-existing-cluster')
+ external_endpoint_help_path: help_page_path('user/project/clusters/gitlab_managed_clusters.md', anchor: 'base-domain')
}
end
diff --git a/app/helpers/container_registry_helper.rb b/app/helpers/container_registry_helper.rb
index 255b8183164..52ef276c097 100644
--- a/app/helpers/container_registry_helper.rb
+++ b/app/helpers/container_registry_helper.rb
@@ -1,10 +1,6 @@
# frozen_string_literal: true
module ContainerRegistryHelper
- def container_registry_expiration_policies_throttling?
- Feature.enabled?(:container_registry_expiration_policies_throttling, default_enabled: :yaml)
- end
-
def container_repository_gid_prefix
"gid://#{GlobalID.app}/#{ContainerRepository.name}/"
end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 79b04ae0e2b..59731dc2f6f 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -69,8 +69,8 @@ module EmailsHelper
)
else
image_tag(
- image_url('mailers/gitlab_header_logo.gif'),
- size: '55x50',
+ image_url('mailers/gitlab_logo.png'),
+ size: '55x55',
alt: 'GitLab'
)
end
diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb
index 80ab303357b..ca61c4da41c 100644
--- a/app/helpers/groups/group_members_helper.rb
+++ b/app/helpers/groups/group_members_helper.rb
@@ -53,7 +53,7 @@ module Groups::GroupMembersHelper
end
def group_group_links_list_data(group, include_relations, search)
- if ::Feature.enabled?(:group_member_inherited_group, group, default_enabled: :yaml)
+ if ::Feature.enabled?(:group_member_inherited_group, group)
group_links = group_group_links(group, include_relations)
group_links = group_links.search(search) if search
else
diff --git a/app/helpers/instance_configuration_helper.rb b/app/helpers/instance_configuration_helper.rb
index f695be32743..b06e3ff2904 100644
--- a/app/helpers/instance_configuration_helper.rb
+++ b/app/helpers/instance_configuration_helper.rb
@@ -17,4 +17,8 @@ module InstanceConfigurationHelper
number_to_human_size(v, strip_insignificant_zeros: true, significant: false)
end
end
+
+ def instance_configuration_disabled_cell_html(value)
+ instance_configuration_cell_html(value == 0 ? nil : value)
+ end
end
diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb
index b960ed46ba9..862938ac961 100644
--- a/app/helpers/integrations_helper.rb
+++ b/app/helpers/integrations_helper.rb
@@ -112,6 +112,7 @@ module IntegrationsHelper
enable_comments: integration.comment_on_event_enabled.to_s,
comment_detail: integration.comment_detail,
learn_more_path: integrations_help_page_path,
+ about_pricing_url: Gitlab::Saas.about_pricing_url,
trigger_events: trigger_events_for_integration(integration),
sections: integration.sections.to_json,
fields: fields_for_integration(integration),
diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb
index a682d2712be..e46270ab819 100644
--- a/app/helpers/invite_members_helper.rb
+++ b/app/helpers/invite_members_helper.rb
@@ -20,30 +20,17 @@ module InviteMembersHelper
end
end
- def group_select_data(group)
- # This should only be used for groups to load the invite group modal.
- # For instance the invite groups modal should not call this from a project scope
- # this is only to be called in scope of a group context as noted in this thread
- # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79036#note_821465513
- # the group sharing in projects disabling is explained there as well
- if group.root_ancestor.namespace_settings.prevent_sharing_groups_outside_hierarchy
- { groups_filter: 'descendant_groups', parent_id: group.root_ancestor.id }
- else
- {}
- end
- end
-
def common_invite_group_modal_data(source, member_class, is_project)
{
id: source.id,
- root_id: source.root_ancestor&.id,
+ root_id: source.root_ancestor.id,
name: source.name,
default_access_level: Gitlab::Access::GUEST,
invalid_groups: source.related_group_ids,
help_link: help_page_url('user/permissions'),
is_project: is_project,
access_levels: member_class.access_level_roles.to_json
- }
+ }.merge(group_select_data(source))
end
# Overridden in EE
@@ -68,6 +55,14 @@ module InviteMembersHelper
private
+ def group_select_data(source)
+ if source.root_ancestor.prevent_sharing_groups_outside_hierarchy
+ { groups_filter: 'descendant_groups', parent_id: source.root_ancestor.id }
+ else
+ {}
+ end
+ end
+
# Overridden in EE
def users_filter_data(group)
{}
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 98eca3785e7..486d5bb3866 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -13,6 +13,8 @@ module IssuablesHelper
end
def sidebar_gutter_collapsed_class
+ return "right-sidebar-expanded" if moved_mr_sidebar_enabled?
+
"right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}"
end
@@ -236,6 +238,7 @@ module IssuablesHelper
markdownPreviewPath: preview_markdown_path(parent, target_type: issuable.model_name, target_id: issuable.iid),
markdownDocsPath: help_page_path('user/markdown'),
lockVersion: issuable.lock_version,
+ state: issuable.state,
issuableTemplateNamesPath: template_names_path(parent, issuable),
initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title,
@@ -341,14 +344,20 @@ module IssuablesHelper
end
def state_name_with_icon(issuable)
- if issuable.is_a?(MergeRequest) && issuable.merged?
- [_("Merged"), "git-merge"]
- elsif issuable.is_a?(MergeRequest) && issuable.closed?
- [_("Closed"), "close"]
- elsif issuable.closed?
- [_("Closed"), "mobile-issue-close"]
+ if issuable.is_a?(MergeRequest)
+ if issuable.open?
+ [_("Open"), "merge-request-open"]
+ elsif issuable.merged?
+ [_("Merged"), "merge"]
+ else
+ [_("Closed"), "merge-request-close"]
+ end
else
- [_("Open"), "issue-open-m"]
+ if issuable.open?
+ [_("Open"), "issues"]
+ else
+ [_("Closed"), "issue-closed"]
+ end
end
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index c8c9ea32184..04de77dd484 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -28,16 +28,18 @@ module IssuesHelper
end
def status_box_class(item)
+ updated_mr_header_enabled = Feature.enabled?(:updated_mr_header, @project)
+
if item.try(:expired?)
'status-box-expired'
elsif item.try(:merged?)
- 'status-box-mr-merged'
+ updated_mr_header_enabled ? 'badge-info' : 'status-box-mr-merged'
elsif item.closed?
- 'status-box-mr-closed'
+ item.is_a?(MergeRequest) && updated_mr_header_enabled ? 'badge-danger' : 'status-box-mr-closed'
elsif item.try(:upcoming?)
'status-box-upcoming'
else
- 'status-box-open'
+ item.is_a?(MergeRequest) && updated_mr_header_enabled ? 'badge-success' : 'status-box-open'
end
end
@@ -63,7 +65,7 @@ module IssuesHelper
end
def issue_hidden?(issue)
- Feature.enabled?(:ban_user_feature_flag, default_enabled: :yaml) && issue.hidden?
+ Feature.enabled?(:ban_user_feature_flag) && issue.hidden?
end
def hidden_issue_icon(issue)
@@ -152,7 +154,7 @@ module IssuesHelper
end
def issue_closed_text(issue, current_user)
- link = issue_closed_link(issue, current_user, css_class: 'text-white text-underline')
+ link = issue_closed_link(issue, current_user, css_class: 'text-underline gl-reset-color!')
if link
s_('IssuableStatus|Closed (%{link})').html_safe % { link: link }
@@ -202,6 +204,8 @@ module IssuesHelper
initial_sort: current_user&.user_preference&.issues_sort,
is_anonymous_search_disabled: Feature.enabled?(:disable_anonymous_search, type: :ops).to_s,
is_issue_repositioning_disabled: issue_repositioning_disabled?.to_s,
+ is_public_visibility_restricted:
+ Gitlab::CurrentSettings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC).to_s,
is_signed_in: current_user.present?.to_s,
jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
rss_path: url_for(safe_params.merge(rss_url_options)),
diff --git a/app/helpers/jira_connect_helper.rb b/app/helpers/jira_connect_helper.rb
index 67b85b26f9e..30f29e002b8 100644
--- a/app/helpers/jira_connect_helper.rb
+++ b/app/helpers/jira_connect_helper.rb
@@ -7,7 +7,8 @@ module JiraConnectHelper
{
groups_path: api_v4_groups_path(params: { min_access_level: Gitlab::Access::MAINTAINER, skip_groups: skip_groups }),
subscriptions: subscriptions.map { |s| serialize_subscription(s) }.to_json,
- subscriptions_path: jira_connect_subscriptions_path,
+ add_subscriptions_path: jira_connect_subscriptions_path,
+ subscriptions_path: jira_connect_subscriptions_path(format: :json),
users_path: current_user ? nil : jira_connect_users_path, # users_path is used to determine if user is signed in
gitlab_user_path: current_user ? user_path(current_user) : nil,
oauth_metadata: Feature.enabled?(:jira_connect_oauth, current_user) ? jira_connect_oauth_data.to_json : nil
diff --git a/app/helpers/lazy_image_tag_helper.rb b/app/helpers/lazy_image_tag_helper.rb
index d0bdaaae5f8..10d603ef5d3 100644
--- a/app/helpers/lazy_image_tag_helper.rb
+++ b/app/helpers/lazy_image_tag_helper.rb
@@ -8,8 +8,11 @@ module LazyImageTagHelper
end
# Override the default ActionView `image_tag` helper to support lazy-loading
+ # accept :auto_dark boolean to enable automatic dark variant of the image
+ # (see: https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2698)
+ # accept :dark_variant path to be used as a source when dark mode is enabled
def image_tag(source, options = {})
- source = options[:dark_variant] if options[:dark_variant] && user_application_dark_mode?
+ source, options = prepare_dark_variant(source, options)
options = options.symbolize_keys
unless options.delete(:lazy) == false
@@ -29,4 +32,25 @@ module LazyImageTagHelper
# Required for Banzai::Filter::ImageLazyLoadFilter
module_function :placeholder_image # rubocop: disable Style/AccessModifierDeclarations
+
+ private
+
+ def prepare_dark_variant(source, options)
+ dark_variant = options.delete(:dark_variant)
+ auto_dark = options.delete(:auto_dark)
+
+ if dark_variant && auto_dark
+ raise ArgumentError, "dark_variant and auto_dark are mutually exclusive"
+ end
+
+ if (auto_dark || dark_variant) && user_application_dark_mode?
+ if auto_dark
+ options[:class] = 'gl-dark-invert-keep-hue'
+ elsif dark_variant
+ source = dark_variant
+ end
+ end
+
+ [source, options]
+ end
end
diff --git a/app/helpers/learn_gitlab_helper.rb b/app/helpers/learn_gitlab_helper.rb
index 60f3b12d736..890f7f099df 100644
--- a/app/helpers/learn_gitlab_helper.rb
+++ b/app/helpers/learn_gitlab_helper.rb
@@ -34,7 +34,8 @@ module LearnGitlabHelper
action,
url: url,
completed: attributes[OnboardingProgress.column_name(action)].present?,
- svg: image_path("learn_gitlab/#{action}.svg")
+ svg: image_path("learn_gitlab/#{action}.svg"),
+ enabled: true
]
end
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 2d93813d5ee..e1c9e7d3896 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -156,7 +156,7 @@ module MergeRequestsHelper
total: total_count
}
- if Feature.enabled?(:mr_attention_requests, default_enabled: :yaml)
+ if current_user&.mr_attention_requests_enabled?
attention_requested_count = attention_requested_merge_requests_count
counts[:attention_requested_count] = attention_requested_count
@@ -206,6 +206,19 @@ module MergeRequestsHelper
api_v4_projects_merge_requests_award_emoji_path(id: merge_request.project.id, merge_request_iid: merge_request.iid)
end
+ def how_merge_modal_data(merge_request)
+ {
+ is_fork: merge_request.for_fork?.to_s,
+ can_merge: merge_request.can_be_merged_by?(current_user).to_s,
+ source_branch: merge_request.source_branch,
+ source_project_path: merge_request.source_project&.path,
+ 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")
+ }
+ end
+
private
def review_requested_merge_requests_count
@@ -219,6 +232,34 @@ module MergeRequestsHelper
def default_suggestion_commit_message
@project.suggestion_commit_message.presence || Gitlab::Suggestions::CommitMessage::DEFAULT_SUGGESTION_COMMIT_MESSAGE
end
+
+ def merge_request_source_branch(merge_request)
+ branch = if merge_request.for_fork?
+ "#{merge_request.source_project_path}:#{merge_request.source_branch}"
+ else
+ merge_request.source_branch
+ end
+
+ branch_path = if merge_request.source_project
+ project_tree_path(merge_request.source_project, merge_request.source_branch)
+ else
+ ''
+ end
+
+ link_to branch, branch_path, title: branch, class: 'gl-link gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mb-n2'
+ end
+
+ def merge_request_header(project, merge_request)
+ link_to_author = link_to_member(project, merge_request.author, size: 24, extra_class: 'gl-font-weight-bold', avatar: false)
+ copy_button = clipboard_button(text: merge_request.source_branch, title: _('Copy branch name'), class: 'btn btn-default btn-sm gl-button btn-default-tertiary btn-icon gl-display-none! gl-md-display-inline-block! js-source-branch-copy')
+ target_branch = link_to merge_request.target_branch, project_tree_path(merge_request.target_project, merge_request.target_branch), title: merge_request.target_branch, class: 'gl-link gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mb-n2'
+
+ _('%{author} requested to merge %{source_branch} %{copy_button} into %{target_branch} %{created_at}').html_safe % { author: link_to_author.html_safe, source_branch: merge_request_source_branch(merge_request).html_safe, copy_button: copy_button.html_safe, target_branch: target_branch.html_safe, created_at: time_ago_with_tooltip(merge_request.created_at, html_class: 'gl-display-inline-block').html_safe }
+ end
+
+ def moved_mr_sidebar_enabled?
+ Feature.enabled?(:moved_mr_sidebar, @project) && defined?(@merge_request)
+ end
end
MergeRequestsHelper.prepend_mod_with('MergeRequestsHelper')
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index cf386ee398a..a50629b7996 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -88,11 +88,12 @@ module NamespacesHelper
}.to_json
end
- def pipeline_usage_quota_app_data(namespace)
+ def pipeline_usage_app_data(namespace)
{
namespace_actual_plan_name: namespace.actual_plan_name,
namespace_path: namespace.full_path,
namespace_id: namespace.id,
+ user_namespace: namespace.user_namespace?.to_s,
page_size: page_size
}
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 1c4d294baa7..37cd491e19f 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -19,11 +19,13 @@ module NavHelper
end
def page_gutter_class
+ moved_sidebar_enabled = current_controller?('merge_requests') && moved_mr_sidebar_enabled?
+
if page_has_markdown?
if cookies[:collapsed_gutter] == 'true'
- %w[page-gutter right-sidebar-collapsed]
+ ["page-gutter", "#{'right-sidebar-collapsed' unless moved_sidebar_enabled}"]
else
- %w[page-gutter right-sidebar-expanded]
+ ["page-gutter", "#{'right-sidebar-expanded' unless moved_sidebar_enabled}"]
end
elsif current_path?('jobs#show')
%w[page-gutter build-sidebar right-sidebar-expanded]
@@ -56,7 +58,7 @@ module NavHelper
end
def admin_monitoring_nav_links
- %w(system_info background_migrations background_jobs health_check requests_profiles)
+ %w(system_info background_migrations background_jobs health_check)
end
def admin_analytics_nav_links
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index fb74a52fcda..0c057a29bec 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -55,7 +55,7 @@ module PageLayoutHelper
end
def page_image
- default = image_url('gitlab_logo.png')
+ default = image_url('twitter_card.jpg')
subject = @project || @user || @group
diff --git a/app/helpers/personal_access_tokens_helper.rb b/app/helpers/personal_access_tokens_helper.rb
deleted file mode 100644
index 5cc8d21096f..00000000000
--- a/app/helpers/personal_access_tokens_helper.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-module PersonalAccessTokensHelper
- def personal_access_token_expiration_enforced?
- false
- end
-end
diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb
index 0d514773891..20d0dd9b30c 100644
--- a/app/helpers/profiles_helper.rb
+++ b/app/helpers/profiles_helper.rb
@@ -53,13 +53,11 @@ module ProfilesHelper
# Overridden in EE::ProfilesHelper#ssh_key_expiration_tooltip
def ssh_key_expiration_tooltip(key)
return key.errors.full_messages.join(', ') if key.errors.full_messages.any?
-
- s_('Profiles|Key usable beyond expiration date.') if key.expired?
end
# Overridden in EE::ProfilesHelper#ssh_key_expires_field_description
def ssh_key_expires_field_description
- s_('Profiles|Key can still be used after expiration.')
+ s_('Profiles|Key becomes invalid on this date.')
end
# Overridden in EE::ProfilesHelper#ssh_key_expiration_policy_enabled?
diff --git a/app/helpers/projects/pipeline_helper.rb b/app/helpers/projects/pipeline_helper.rb
index 185632a49b5..286026bc290 100644
--- a/app/helpers/projects/pipeline_helper.rb
+++ b/app/helpers/projects/pipeline_helper.rb
@@ -7,6 +7,7 @@ module Projects
can_generate_codequality_reports: pipeline.can_generate_codequality_reports?.to_json,
graphql_resource_etag: graphql_etag_pipeline_path(pipeline),
metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: project.namespace, project_id: project, format: :json),
+ pipeline_iid: pipeline.iid,
pipeline_project_path: project.full_path
}
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 21c7a54670c..c3f22dc7693 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -212,7 +212,7 @@ module ProjectsHelper
end
def no_password_message
- push_pull_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('gitlab-basics/start-using-git', anchor: 'pull-and-push') }
+ push_pull_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('topics/git/terminology', anchor: 'pull-and-push') }
clone_with_https_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('gitlab-basics/start-using-git', anchor: 'clone-with-https') }
set_password_link_start = '<a href="%{url}">'.html_safe % { url: edit_profile_password_path }
set_up_pat_link_start = '<a href="%{url}">'.html_safe % { url: profile_personal_access_tokens_path }
@@ -379,7 +379,7 @@ module ProjectsHelper
end
def show_terraform_banner?(project)
- Feature.enabled?(:show_terraform_banner, type: :ops, default_enabled: true) && project.repository_languages.with_programming_language('HCL').exists? && project.terraform_states.empty?
+ Feature.enabled?(:show_terraform_banner, type: :ops) && project.repository_languages.with_programming_language('HCL').exists? && project.terraform_states.empty?
end
def project_permissions_panel_data(project)
@@ -614,6 +614,7 @@ module ProjectsHelper
operationsAccessLevel: feature.operations_access_level,
showDefaultAwardEmojis: project.show_default_award_emojis?,
warnAboutPotentiallyUnwantedCharacters: project.warn_about_potentially_unwanted_characters?,
+ enforceAuthChecksOnUploads: project.enforce_auth_checks_on_uploads?,
securityAndComplianceAccessLevel: project.security_and_compliance_access_level,
containerRegistryAccessLevel: feature.container_registry_access_level
}
diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb
index 4fa61191ba5..a516ac85131 100644
--- a/app/helpers/releases_helper.rb
+++ b/app/helpers/releases_helper.rb
@@ -12,7 +12,7 @@ module ReleasesHelper
image_path(IMAGE_PATH)
end
- def help_page(anchor: nil)
+ def releases_help_page_path(anchor: nil)
help_page_path(DOCUMENTATION_PATH, anchor: anchor)
end
@@ -21,7 +21,7 @@ module ReleasesHelper
project_id: @project.id,
project_path: @project.full_path,
illustration_path: illustration,
- documentation_path: help_page
+ documentation_path: releases_help_page_path
}.tap do |data|
if can?(current_user, :create_release, @project)
data[:new_release_path] = new_project_release_path(@project)
@@ -78,9 +78,10 @@ module ReleasesHelper
project_path: @project.full_path,
markdown_preview_path: preview_markdown_path(@project),
markdown_docs_path: help_page_path('user/markdown'),
- release_assets_docs_path: help_page(anchor: 'release-assets'),
+ release_assets_docs_path: releases_help_page_path(anchor: 'release-assets'),
manage_milestones_path: project_milestones_path(@project),
- new_milestone_path: new_project_milestone_path(@project)
+ new_milestone_path: new_project_milestone_path(@project),
+ edit_release_docs_path: releases_help_page_path(anchor: 'edit-a-release')
}
end
end
diff --git a/app/helpers/routing/projects_helper.rb b/app/helpers/routing/projects_helper.rb
index 859070d59ec..8c0bd9b1ecc 100644
--- a/app/helpers/routing/projects_helper.rb
+++ b/app/helpers/routing/projects_helper.rb
@@ -35,7 +35,15 @@ module Routing
end
def issue_url(entity, *args)
- project_issue_url(entity.project, entity, *args)
+ if use_work_items_path?(entity)
+ work_item_url(entity, *args)
+ else
+ project_issue_url(entity.project, entity, *args)
+ end
+ end
+
+ def work_item_url(entity, *args)
+ project_work_items_url(entity.project, entity.id, *args)
end
def merge_request_url(entity, *args)
@@ -77,5 +85,11 @@ module Routing
toggle_subscription_project_merge_request_path(entity.project, entity)
end
end
+
+ private
+
+ def use_work_items_path?(issue)
+ issue.issue_type == 'task' && issue.project.work_items_feature_flag_enabled?
+ end
end
end
diff --git a/app/helpers/routing/pseudonymization_helper.rb b/app/helpers/routing/pseudonymization_helper.rb
index 3e5d4ee21c0..eb4e5d1c01c 100644
--- a/app/helpers/routing/pseudonymization_helper.rb
+++ b/app/helpers/routing/pseudonymization_helper.rb
@@ -10,6 +10,8 @@ module Routing
sortDesc
state
tab
+ glm_source
+ glm_content
].freeze
def initialize(request_object, group, project)
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index f8bfc74b344..69bea0abd88 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -374,8 +374,6 @@ module SearchHelper
autocomplete: 'off'
}
- opts[:data]['runner-tags-endpoint'] = tag_list_admin_runners_path
-
if @project.present?
opts[:data]['project-id'] = @project.id
opts[:data]['labels-endpoint'] = project_labels_path(@project)
@@ -445,7 +443,7 @@ module SearchHelper
return false unless can?(current_user, :read_users_list)
return true if @group
- Feature.enabled?(:global_search_users_tab, current_user, type: :ops, default_enabled: :yaml)
+ Feature.enabled?(:global_search_users_tab, current_user, type: :ops)
end
def issuable_state_to_badge_class(issuable)
@@ -479,7 +477,7 @@ module SearchHelper
end
def feature_flag_tab_enabled?(flag)
- @group || Feature.enabled?(flag, current_user, type: :ops, default_enabled: true)
+ @group || Feature.enabled?(flag, current_user, type: :ops)
end
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index b4ad9db815d..43ec02b6537 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -250,6 +250,51 @@ module SortingHelper
sort_options_hash[sort_value]
end
+ def issuable_sort_options(viewing_issues, viewing_merge_requests)
+ options = [
+ { value: sort_value_priority, text: sort_title_priority, href: page_filter_path(sort: sort_value_priority) },
+ { value: sort_value_created_date, text: sort_title_created_date, href: page_filter_path(sort: sort_value_created_date) },
+ { value: sort_value_recently_updated, text: sort_title_recently_updated, href: page_filter_path(sort: sort_value_recently_updated) },
+ { value: sort_value_milestone, text: sort_title_milestone, href: page_filter_path(sort: sort_value_milestone) }
+ ]
+
+ options.concat([due_date_option]) if viewing_issues
+
+ options.concat([popularity_option, label_priority_option])
+ options.concat([merged_option, closed_option]) if viewing_merge_requests
+ options.concat([relative_position_option]) if viewing_issues
+
+ options.concat([title_option])
+ end
+
+ def due_date_option
+ { value: sort_value_due_date, text: sort_title_due_date, href: page_filter_path(sort: sort_value_due_date) }
+ end
+
+ def popularity_option
+ { value: sort_value_popularity, text: sort_title_popularity, href: page_filter_path(sort: sort_value_popularity) }
+ end
+
+ def label_priority_option
+ { value: sort_value_label_priority, text: sort_title_label_priority, href: page_filter_path(sort: sort_value_label_priority) }
+ end
+
+ def merged_option
+ { value: sort_value_merged_date, text: sort_title_merged_date, href: page_filter_path(sort: sort_value_merged_date) }
+ end
+
+ def closed_option
+ { value: sort_value_closed_date, text: sort_title_closed_date, href: page_filter_path(sort: sort_value_closed_date) }
+ end
+
+ def relative_position_option
+ { value: sort_value_relative_position, text: sort_title_relative_position, href: page_filter_path(sort: sort_value_relative_position) }
+ end
+
+ def title_option
+ { value: sort_value_title, text: sort_title_title, href: page_filter_path(sort: sort_value_title) }
+ end
+
def sort_direction_icon(sort_value)
case sort_value
when sort_value_milestone, sort_value_due_date, sort_value_merged_date, sort_value_closed_date, /_asc\z/
diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb
index a075ccc38f5..cb1a5f5ce0c 100644
--- a/app/helpers/storage_helper.rb
+++ b/app/helpers/storage_helper.rb
@@ -32,8 +32,9 @@ module StorageHelper
{
text: html_escape_once(s_("UsageQuota|From %{storage_enforcement_date} storage limits will apply to this namespace. " \
- "View and manage your usage in %{strong_start}%{namespace_type} settings &gt; Usage quotas%{strong_end}.")).html_safe %
- { storage_enforcement_date: namespace.storage_enforcement_date, strong_start: "<strong>".html_safe, strong_end: "</strong>".html_safe, namespace_type: namespace.type },
+ "You are currently using %{used_storage} of namespace storage. " \
+ "View and manage your usage from %{strong_start}%{namespace_type} settings &gt; Usage quotas%{strong_end}.")).html_safe %
+ { storage_enforcement_date: namespace.storage_enforcement_date, used_storage: storage_counter(namespace.root_storage_statistics&.storage_size || 0), strong_start: "<strong>".html_safe, strong_end: "</strong>".html_safe, namespace_type: namespace.type },
variant: 'warning',
callouts_path: namespace.user_namespace? ? callouts_path : group_callouts_path,
callouts_feature_name: storage_enforcement_banner_user_callouts_feature_name(namespace),
diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb
index b45ce10a2f6..82847534d8e 100644
--- a/app/helpers/system_note_helper.rb
+++ b/app/helpers/system_note_helper.rb
@@ -43,7 +43,8 @@ module SystemNoteHelper
'issue_type' => 'pencil-square',
'attention_requested' => 'user',
'attention_request_removed' => 'user',
- 'contact' => 'users'
+ 'contact' => 'users',
+ 'timeline_event' => 'clock'
}.freeze
def system_note_icon_name(note)
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 60bf79f3114..d3cc922423d 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -17,12 +17,11 @@ module TodosHelper
case todo.action
when Todo::ASSIGNED then todo.self_added? ? 'assigned' : 'assigned you'
when Todo::REVIEW_REQUESTED then 'requested a review of'
- when Todo::MENTIONED then "mentioned #{todo_action_subject(todo)} on"
+ when Todo::MENTIONED, Todo::DIRECTLY_ADDRESSED then "mentioned #{todo_action_subject(todo)} on"
when Todo::BUILD_FAILED then 'The pipeline failed in'
when Todo::MARKED then 'added a todo for'
when Todo::APPROVAL_REQUIRED then "set #{todo_action_subject(todo)} as an approver for"
when Todo::UNMERGEABLE then 'Could not merge'
- when Todo::DIRECTLY_ADDRESSED then "directly addressed #{todo_action_subject(todo)} on"
when Todo::MERGE_TRAIN_REMOVED then "Removed from Merge Train:"
when Todo::ATTENTION_REQUESTED then 'requested your attention on'
end
@@ -151,8 +150,7 @@ module TodosHelper
{ id: Todo::REVIEW_REQUESTED, text: 'Review requested' },
{ id: Todo::MENTIONED, text: 'Mentioned' },
{ id: Todo::MARKED, text: 'Added' },
- { id: Todo::BUILD_FAILED, text: 'Pipelines' },
- { id: Todo::DIRECTLY_ADDRESSED, text: 'Directly addressed' }
+ { id: Todo::BUILD_FAILED, text: 'Pipelines' }
]
end
diff --git a/app/helpers/tracking_helper.rb b/app/helpers/tracking_helper.rb
index 1beb88548c5..9ef57a57d62 100644
--- a/app/helpers/tracking_helper.rb
+++ b/app/helpers/tracking_helper.rb
@@ -2,7 +2,7 @@
module TrackingHelper
def tracking_attrs(label, action, property)
- return {} unless tracking_enabled?
+ return {} unless ::Gitlab::Tracking.enabled?
{
data: {
@@ -16,11 +16,4 @@ module TrackingHelper
def tracking_attrs_data(label, action, property)
tracking_attrs(label, action, property).fetch(:data, {})
end
-
- private
-
- def tracking_enabled?
- Rails.env.production? &&
- ::Gitlab::Tracking.enabled?
- end
end
diff --git a/app/helpers/users/group_callouts_helper.rb b/app/helpers/users/group_callouts_helper.rb
index 9a9fce4d7e3..0aa4eb89499 100644
--- a/app/helpers/users/group_callouts_helper.rb
+++ b/app/helpers/users/group_callouts_helper.rb
@@ -31,5 +31,3 @@ module Users
end
end
end
-
-Users::GroupCalloutsHelper.prepend_mod
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 1247f9ae260..fd460d71867 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -125,7 +125,7 @@ module UsersHelper
end
def ban_feature_available?
- Feature.enabled?(:ban_user_feature_flag, default_enabled: :yaml)
+ Feature.enabled?(:ban_user_feature_flag)
end
def confirm_user_data(user)
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index 70df696510a..f1ddc2e902e 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -38,6 +38,8 @@ module WorkhorseHelper
# Send an entry from artifacts through Workhorse and set safe content type
def send_artifacts_entry(file, entry)
headers.store(*Gitlab::Workhorse.send_artifacts_entry(file, entry))
+ headers.store(*Gitlab::Workhorse.detect_content_type)
+
head :ok
end
diff --git a/app/mailers/emails/in_product_marketing.rb b/app/mailers/emails/in_product_marketing.rb
index 317e1545350..1b46d4841b0 100644
--- a/app/mailers/emails/in_product_marketing.rb
+++ b/app/mailers/emails/in_product_marketing.rb
@@ -21,6 +21,12 @@ module Emails
mail_to(to: email, subject: @message.subject_line)
end
+ def build_ios_app_guide_email(recipient_email)
+ @message = ::Gitlab::Email::Message::BuildIosAppGuide.new
+
+ mail_to(to: recipient_email, subject: @message.subject_line)
+ end
+
private
def mail_to(to:, subject:)
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 341accaea32..5cbc3c9ef9c 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -155,6 +155,20 @@ module Emails
end
end
+ def approved_merge_request_email(recipient_id, merge_request_id, approved_by_user_id, reason = nil)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
+ @approved_by = User.find(approved_by_user_id)
+ mail_answer_thread(@merge_request, merge_request_thread_options(approved_by_user_id, reason))
+ end
+
+ def unapproved_merge_request_email(recipient_id, merge_request_id, unapproved_by_user_id, reason = nil)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
+ @unapproved_by = User.find(unapproved_by_user_id)
+ mail_answer_thread(@merge_request, merge_request_thread_options(unapproved_by_user_id, reason))
+ end
+
private
def setup_merge_request_mail(merge_request_id, recipient_id, present: false)
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index 14c724b5b91..efc6ce163c0 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -68,6 +68,20 @@ module Emails
mail(to: user.notification_email_for(@project.group), subject: subject(subject_text))
end
+ def inactive_project_deletion_warning_email(project, user, deletion_date)
+ @project = project
+ @user = user
+ @deletion_date = deletion_date
+ subject_text = "Action required: Project #{project.name} is scheduled to be deleted on " \
+ "#{deletion_date} due to inactivity"
+
+ mail(to: user.notification_email_for(project.group),
+ subject: subject(subject_text)) do |format|
+ format.html { render layout: 'mailer' }
+ format.text { render layout: 'mailer' }
+ end
+ end
+
private
def add_alert_headers
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index e7c8964a733..60d59465165 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -201,6 +201,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.merge_when_pipeline_succeeds_email(user.id, merge_request.id, user.id).message
end
+ def inactive_project_deletion_warning
+ Notify.inactive_project_deletion_warning_email(project, user, '2022-04-22').message
+ end
+
private
def project
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index 1ec3cb62c76..9f05c87018d 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -81,7 +81,6 @@ module AlertManagement
scope :search, -> (query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) }
scope :not_resolved, -> { without_status(:resolved) }
scope :with_prometheus_alert, -> { includes(:prometheus_alert) }
- scope :with_threat_monitoring_alerts, -> { where(domain: :threat_monitoring ) }
scope :with_operations_alerts, -> { where(domain: :operations) }
scope :order_start_time, -> (sort_order) { order(started_at: sort_order) }
@@ -119,6 +118,10 @@ module AlertManagement
end
end
+ def self.find_unresolved_alert(project, fingerprint)
+ for_fingerprint(project, fingerprint).not_resolved.take
+ end
+
def self.last_prometheus_alert_by_project_id
ids = select(arel_table[:id].maximum).group(:project_id)
with_prometheus_alert.where(id: ids)
@@ -143,10 +146,6 @@ module AlertManagement
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
- def metric_images_available?
- ::AlertManagement::MetricImage.available_for?(project)
- end
-
def prometheus?
monitoring_tool == Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
end
diff --git a/app/models/alert_management/metric_image.rb b/app/models/alert_management/metric_image.rb
index 8175a31be7a..4ed28c3b1eb 100644
--- a/app/models/alert_management/metric_image.rb
+++ b/app/models/alert_management/metric_image.rb
@@ -7,10 +7,6 @@ module AlertManagement
belongs_to :alert, class_name: 'AlertManagement::Alert', foreign_key: 'alert_id', inverse_of: :metric_images
- def self.available_for?(project)
- true
- end
-
private
def local_path
diff --git a/app/models/analytics/cycle_analytics/aggregation.rb b/app/models/analytics/cycle_analytics/aggregation.rb
index 2c04e67a04b..2e58d64ae95 100644
--- a/app/models/analytics/cycle_analytics/aggregation.rb
+++ b/app/models/analytics/cycle_analytics/aggregation.rb
@@ -26,6 +26,14 @@ class Analytics::CycleAnalytics::Aggregation < ApplicationRecord
}.compact
end
+ def consistency_check_cursor_for(model)
+ {
+ :start_event_timestamp => self["last_consistency_check_#{model.issuable_model.table_name}_start_event_timestamp"],
+ :end_event_timestamp => self["last_consistency_check_#{model.issuable_model.table_name}_end_event_timestamp"],
+ model.issuable_id_column => self["last_consistency_check_#{model.issuable_model.table_name}_issuable_id"]
+ }.compact
+ end
+
def refresh_last_run(mode)
self["last_#{mode}_run_at"] = Time.current
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 7cd2fe705e3..6afd8875ad3 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -12,6 +12,9 @@ class ApplicationSetting < ApplicationRecord
ignore_columns %i[static_objects_external_storage_auth_token], remove_with: '14.9', remove_after: '2022-03-22'
ignore_column %i[max_package_files_for_package_destruction], remove_with: '14.9', remove_after: '2022-03-22'
ignore_column :user_email_lookup_limit, remove_with: '15.0', remove_after: '2022-04-18'
+ ignore_column :pseudonymizer_enabled, remove_with: '15.1', remove_after: '2022-06-22'
+ ignore_column :enforce_ssh_key_expiration, remove_with: '15.2', remove_after: '2022-07-22'
+ ignore_column :enforce_pat_expiration, remove_with: '15.2', remove_after: '2022-07-22'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
@@ -199,6 +202,10 @@ class ApplicationSetting < ApplicationRecord
presence: true,
numericality: { only_integer: true, greater_than: 0 }
+ validates :max_export_size,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
validates :max_import_size,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
@@ -370,6 +377,8 @@ class ApplicationSetting < ApplicationRecord
:container_registry_import_max_retries,
:container_registry_import_start_max_retries,
:container_registry_import_max_step_duration,
+ :container_registry_pre_import_timeout,
+ :container_registry_import_timeout,
allow_nil: false,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
@@ -480,6 +489,9 @@ class ApplicationSetting < ApplicationRecord
validates :raw_blob_request_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :pipeline_limit_per_project_user_sha,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
validates :ci_jwt_signing_key,
rsa_key: true, allow_nil: true
@@ -613,6 +625,7 @@ class ApplicationSetting < ApplicationRecord
attr_encrypted :recaptcha_private_key, encryption_options_base_32_aes_256_gcm
attr_encrypted :recaptcha_site_key, encryption_options_base_32_aes_256_gcm
attr_encrypted :slack_app_secret, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :slack_app_signing_secret, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
attr_encrypted :slack_app_verification_token, encryption_options_base_32_aes_256_gcm
attr_encrypted :ci_jwt_signing_key, encryption_options_base_32_aes_256_gcm
attr_encrypted :customers_dot_jwt_signing_key, encryption_options_base_32_aes_256_gcm
@@ -638,6 +651,7 @@ class ApplicationSetting < ApplicationRecord
reset_memoized_terms
end
after_commit :expire_performance_bar_allowed_user_ids_cache, if: -> { previous_changes.key?('performance_bar_allowed_group_id') }
+ after_commit :reset_deletion_warning_redis_key, if: :saved_change_to_inactive_projects_delete_after_months?
def validate_grafana_url
validate_url(parsed_grafana_url, :grafana_url, GRAFANA_URL_ERROR_MESSAGE)
@@ -768,6 +782,10 @@ class ApplicationSetting < ApplicationRecord
)
end
end
+
+ def reset_deletion_warning_redis_key
+ Gitlab::InactiveProjectsDeletionWarningTracker.reset_all
+ end
end
ApplicationSetting.prepend_mod_with('ApplicationSetting')
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 194356acc51..a54dc4f691d 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -108,6 +108,7 @@ module ApplicationSettingImplementation
mailgun_events_enabled: false,
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
+ max_export_size: 0,
max_import_size: 0,
max_yaml_size_bytes: 1.megabyte,
max_yaml_depth: 100,
@@ -223,6 +224,8 @@ module ApplicationSettingImplementation
container_registry_import_max_retries: 3,
container_registry_import_start_max_retries: 50,
container_registry_import_max_step_duration: 5.minutes,
+ container_registry_pre_import_timeout: 30.minutes,
+ container_registry_import_timeout: 10.minutes,
container_registry_import_target_plan: 'free',
container_registry_import_created_before: '2022-01-23 00:00:00',
kroki_enabled: false,
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index b255c774347..1f921c71984 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -131,7 +131,7 @@ class BroadcastMessage < ApplicationRecord
end
def matches_current_user_access_level?(user_access_level)
- return false if target_access_levels.present? && Feature.disabled?(:role_targeted_broadcast_messages, default_enabled: :yaml)
+ return false if target_access_levels.present? && Feature.disabled?(:role_targeted_broadcast_messages)
return true unless target_access_levels.present?
target_access_levels.include? user_access_level
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index ff444ddefa3..a06b920342c 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -57,6 +57,14 @@ module Ci
end
end
+ def retryable?
+ return false unless Feature.enabled?(:ci_recreate_downstream_pipeline, project)
+
+ return false if failed? && (pipeline_loop_detected? || reached_max_descendant_pipelines_depth?)
+
+ super
+ end
+
def self.with_preloads
preload(
:metadata,
@@ -65,8 +73,11 @@ module Ci
)
end
- def retryable?
- false
+ def self.clone_accessors
+ %i[pipeline project ref tag options name
+ allow_failure stage stage_id stage_idx
+ yaml_variables when description needs_attributes
+ scheduling_type].freeze
end
def inherit_status_from_downstream!(pipeline)
@@ -204,7 +215,7 @@ module Ci
end
def downstream_variables
- if ::Feature.enabled?(:ci_trigger_forward_variables, project, default_enabled: :yaml)
+ if ::Feature.enabled?(:ci_trigger_forward_variables, project)
calculate_downstream_variables
.reverse # variables priority
.uniq { |var| var[:key] } # only one variable key to pass
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index a8ad55fd5a4..eea8086d71d 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -45,6 +45,7 @@ module Ci
has_one :runtime_metadata, class_name: 'Ci::RunningBuild', foreign_key: :build_id
has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id, inverse_of: :build
has_many :report_results, class_name: 'Ci::BuildReportResult', inverse_of: :build
+ has_one :namespace, through: :project
# Projects::DestroyService destroys Ci::Pipelines, which use_fast_destroy on :job_artifacts
# before we delete builds. By doing this, the relation should be empty and not fire any
@@ -74,6 +75,7 @@ module Ci
delegate :gitlab_deploy_token, to: :project
delegate :harbor_integration, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
+ delegate :ensure_persistent_ref, to: :pipeline
##
# Since Gitlab 11.5, deployments records started being created right after
@@ -325,7 +327,7 @@ module Ci
after_transition pending: :running do |build|
build.run_after_commit do
- build.pipeline.persistent_ref.create
+ build.ensure_persistent_ref
BuildHooksWorker.perform_async(id)
end
@@ -335,7 +337,7 @@ module Ci
build.run_after_commit do
build.run_status_commit_hooks!
- if Feature.enabled?(:ci_build_finished_worker_namespace_changed, build.project, default_enabled: :yaml)
+ if Feature.enabled?(:ci_build_finished_worker_namespace_changed, build.project)
Ci::BuildFinishedWorker.perform_async(id)
else
::BuildFinishedWorker.perform_async(id)
@@ -504,7 +506,7 @@ module Ci
if metadata&.expanded_environment_name.present?
metadata.expanded_environment_name
else
- if ::Feature.enabled?(:ci_expand_environment_name_and_url, project, default_enabled: :yaml)
+ if ::Feature.enabled?(:ci_expand_environment_name_and_url, project)
ExpandVariables.expand(environment, -> { simple_variables.sort_and_expand_all })
else
ExpandVariables.expand(environment, -> { simple_variables })
@@ -675,7 +677,7 @@ module Ci
end
def has_archived_trace?
- trace.archived_trace_exist?
+ trace.archived?
end
def artifacts_file
@@ -752,7 +754,7 @@ module Ci
end
def valid_token?(token)
- self.token && ActiveSupport::SecurityUtils.secure_compare(token, self.token)
+ self.token && token.present? && ActiveSupport::SecurityUtils.secure_compare(token, self.token)
end
# acts_as_taggable uses this method create/remove tags with contexts
@@ -823,7 +825,6 @@ module Ci
end
end
- # and use that for `ExpireBuildInstanceArtifactsWorker`?
def erase_erasable_artifacts!
job_artifacts.erasable.destroy_all # rubocop: disable Cop/DestroyAll
end
@@ -884,10 +885,6 @@ module Ci
job_artifacts.find_by(file_type: file_types_ids)&.file
end
- def coverage_regex
- super || project.try(:build_coverage_regex)
- end
-
def steps
[Gitlab::Ci::Build::Step.from_commands(self),
Gitlab::Ci::Build::Step.from_release(self),
@@ -911,6 +908,8 @@ module Ci
end
end
+ return cache unless project.ci_separated_caches
+
type_suffix = pipeline.protected_ref? ? 'protected' : 'non_protected'
cache.map do |entry|
entry.merge(key: "#{entry[:key]}-#{type_suffix}")
@@ -1224,7 +1223,7 @@ module Ci
def job_jwt_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- break variables unless Feature.enabled?(:ci_job_jwt, project, default_enabled: true)
+ break variables unless Feature.enabled?(:ci_job_jwt, project)
jwt = Gitlab::Ci::Jwt.for_build(self)
jwt_v2 = Gitlab::Ci::JwtV2.for_build(self)
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index ca68989002c..4ee661d89f4 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -38,7 +38,7 @@ module Ci
job_timeout_source: 4
}
- ignore_columns :runner_features, remove_with: '14.7', remove_after: '2021-11-22'
+ ignore_columns :runner_features, remove_with: '15.1', remove_after: '2022-05-22'
def update_timeout_state
timeout = timeout_with_highest_precedence
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index dff8bb89021..c831ef12501 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -45,7 +45,7 @@ module Ci
dotenv: '.env',
cobertura: 'cobertura-coverage.xml',
terraform: 'tfplan.json',
- cluster_applications: 'gl-cluster-applications.json', # DEPRECATED: https://gitlab.com/gitlab-org/gitlab/-/issues/333441
+ cluster_applications: 'gl-cluster-applications.json', # DEPRECATED: https://gitlab.com/gitlab-org/gitlab/-/issues/361094
requirements: 'requirements.json',
coverage_fuzzing: 'gl-coverage-fuzzing.json',
api_fuzzing: 'gl-api-fuzzing-report.json'
@@ -64,7 +64,7 @@ module Ci
network_referee: :gzip,
dotenv: :gzip,
cobertura: :gzip,
- cluster_applications: :gzip,
+ cluster_applications: :gzip, # DEPRECATED: https://gitlab.com/gitlab-org/gitlab/-/issues/361094
lsif: :zip,
# Security reports and license scanning reports are raw artifacts
@@ -187,7 +187,6 @@ module Ci
scope :downloadable, -> { where(file_type: DOWNLOADABLE_TYPES) }
scope :unlocked, -> { joins(job: :pipeline).merge(::Ci::Pipeline.unlocked) }
scope :order_expired_asc, -> { order(expire_at: :asc) }
- scope :order_expired_desc, -> { order(expire_at: :desc) }
scope :with_destroy_preloads, -> { includes(project: [:route, :statistics]) }
scope :for_project, ->(project) { where(project_id: project) }
@@ -323,12 +322,12 @@ module Ci
end
end
- def archived_trace_exists?
+ def stored?
file&.file&.exists?
end
def self.archived_trace_exists_for?(job_id)
- where(job_id: job_id).trace.take&.archived_trace_exists?
+ where(job_id: job_id).trace.take&.stored?
end
def self.max_artifact_size(type:, project:)
diff --git a/app/models/ci/namespace_settings.rb b/app/models/ci/namespace_settings.rb
new file mode 100644
index 00000000000..d519a48311f
--- /dev/null
+++ b/app/models/ci/namespace_settings.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+# CI::NamespaceSettings mixin
+#
+# This module is intended to encapsulate CI/CD settings-specific logic
+# and be prepended in the `Namespace` model
+module Ci
+ module NamespaceSettings
+ # Overridden in EE::Namespace
+ def allow_stale_runner_pruning?
+ false
+ end
+
+ # Overridden in EE::Namespace
+ def allow_stale_runner_pruning=(_value)
+ raise NotImplementedError
+ end
+ end
+end
diff --git a/app/models/ci/pending_build.rb b/app/models/ci/pending_build.rb
index 41dc74ef050..d900a056242 100644
--- a/app/models/ci/pending_build.rb
+++ b/app/models/ci/pending_build.rb
@@ -31,7 +31,7 @@ module Ci
end
def maintain_denormalized_data?
- ::Feature.enabled?(:ci_pending_builds_maintain_denormalized_data, default_enabled: :yaml)
+ ::Feature.enabled?(:ci_pending_builds_maintain_denormalized_data)
end
private
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 2d0479e02a3..c10069382f2 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -36,10 +36,10 @@ module Ci
# Ci::CreatePipelineService returns Ci::Pipeline so this is the only place
# where we can pass additional information from the service. This accessor
- # is used for storing the processed CI YAML contents for linting purposes.
+ # is used for storing the processed metadata for linting purposes.
# There is an open issue to address this:
# https://gitlab.com/gitlab-org/gitlab/-/issues/259010
- attr_accessor :merged_yaml
+ attr_accessor :config_metadata
# This is used to retain access to the method defined by `Ci::HasRef`
# before being overridden in this class.
@@ -198,7 +198,7 @@ module Ci
# Create a separate worker for each new operation
before_transition [:created, :waiting_for_resource, :preparing, :pending] => :running do |pipeline|
- pipeline.started_at = Time.current
+ pipeline.started_at ||= Time.current
end
before_transition any => [:success, :failed, :canceled] do |pipeline|
@@ -253,8 +253,6 @@ module Ci
after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline|
pipeline.run_after_commit do
- pipeline.persistent_ref.delete
-
pipeline.all_merge_requests.each do |merge_request|
next unless merge_request.auto_merge_enabled?
@@ -288,6 +286,12 @@ module Ci
end
end
+ after_transition any => ::Ci::Pipeline.stopped_statuses do |pipeline|
+ pipeline.run_after_commit do
+ pipeline.persistent_ref.delete
+ end
+ end
+
after_transition any => [:success, :failed] do |pipeline|
ref_status = pipeline.ci_ref&.update_status_by!(pipeline)
@@ -336,7 +340,7 @@ module Ci
scope :with_only_interruptible_builds, -> do
where('NOT EXISTS (?)',
Ci::Build.where('ci_builds.commit_id = ci_pipelines.id')
- .with_status(:running, :success, :failed)
+ .with_status(STARTED_STATUSES)
.not_interruptible
)
end
@@ -978,7 +982,7 @@ module Ci
end
# With multi-project and parent-child pipelines
- def self_with_upstreams_and_downstreams
+ def all_pipelines_in_hierarchy
object_hierarchy.all_objects
end
@@ -992,6 +996,7 @@ module Ci
object_hierarchy(project_condition: :same).base_and_descendants
end
+ # Follow the parent-child relationships and return the top-level parent
def root_ancestor
return self unless child?
@@ -1000,6 +1005,12 @@ module Ci
.first
end
+ # Follow the upstream pipeline relationships, regardless of multi-project or
+ # parent-child, and return the top-level ancestor.
+ def upstream_root
+ object_hierarchy.base_and_ancestors(hierarchy_order: :desc).first
+ end
+
def bridge_triggered?
source_bridge.present?
end
@@ -1257,6 +1268,12 @@ module Ci
self.ci_ref = Ci::Ref.ensure_for(self)
end
+ def ensure_persistent_ref
+ return if persistent_ref.exist?
+
+ persistent_ref.create
+ end
+
def reset_source_bridge!(current_user)
return unless bridge_waiting?
@@ -1271,10 +1288,11 @@ module Ci
def security_reports(report_types: [])
reports_scope = report_types.empty? ? ::Ci::JobArtifact.security_reports : ::Ci::JobArtifact.security_reports(file_types: report_types)
+ types_to_collect = report_types.empty? ? ::Ci::JobArtifact::SECURITY_REPORT_FILE_TYPES : report_types
::Gitlab::Ci::Reports::Security::Reports.new(self).tap do |security_reports|
latest_report_builds(reports_scope).each do |build|
- build.collect_security_reports!(security_reports)
+ build.collect_security_reports!(security_reports, report_types: types_to_collect)
end
end
end
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index d79ff74753a..f666629c8fd 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -101,6 +101,21 @@ module Ci
:merge_train_pipeline?,
to: :pipeline
+ def clone(current_user:)
+ new_attributes = self.class.clone_accessors.to_h do |attribute|
+ [attribute, public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ if persisted_environment.present?
+ new_attributes[:metadata_attributes] ||= {}
+ new_attributes[:metadata_attributes][:expanded_environment_name] = expanded_environment_name
+ end
+
+ new_attributes[:user] = current_user
+
+ self.class.new(new_attributes)
+ end
+
def retryable?
return false if retried? || archived? || deployment_rejected?
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index b9ba9d8e1b0..7a1d52f5aea 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -12,6 +12,7 @@ module Ci
include Gitlab::Utils::StrongMemoize
include TaggableQueries
include Presentable
+ include EachBatch
add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration, expiration_enforced?: :token_expiration_enforced?
@@ -59,7 +60,7 @@ module Ci
AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze
AVAILABLE_TYPES = runner_types.keys.freeze
- AVAILABLE_STATUSES = %w[active paused online offline not_connected never_contacted stale].freeze # TODO: Remove in %15.0: not_connected. In %16.0: active, paused. Relevant issues: https://gitlab.com/gitlab-org/gitlab/-/issues/347303, https://gitlab.com/gitlab-org/gitlab/-/issues/347305, https://gitlab.com/gitlab-org/gitlab/-/issues/344648
+ AVAILABLE_STATUSES = %w[active paused online offline never_contacted stale].freeze # TODO: Remove in %16.0: active, paused. Relevant issue: https://gitlab.com/gitlab-org/gitlab/-/issues/344648
AVAILABLE_SCOPES = (AVAILABLE_TYPES_LEGACY + AVAILABLE_TYPES + AVAILABLE_STATUSES).freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
@@ -83,7 +84,6 @@ module Ci
scope :recent, -> { where('ci_runners.created_at >= :date OR ci_runners.contacted_at >= :date', date: stale_deadline) }
scope :stale, -> { where('ci_runners.created_at < :date AND (ci_runners.contacted_at IS NULL OR ci_runners.contacted_at < :date)', date: stale_deadline) }
scope :offline, -> { where(arel_table[:contacted_at].lteq(online_contact_time_deadline)) }
- scope :not_connected, -> { where(contacted_at: nil) } # TODO: Remove in 15.0
scope :never_contacted, -> { where(contacted_at: nil) }
scope :ordered, -> { order(id: :desc) }
@@ -289,7 +289,7 @@ module Ci
def assign_to(project, current_user = nil)
if instance_type?
- self.runner_type = :project_type
+ raise ArgumentError, 'Transitioning an instance runner to a project runner is not supported'
elsif group_type?
raise ArgumentError, 'Transitioning a group runner to a project runner is not supported'
end
@@ -322,6 +322,9 @@ module Ci
end
def status(legacy_mode = nil)
+ # TODO Deprecate legacy_mode in %16.0 and make it a no-op
+ # (see https://gitlab.com/gitlab-org/gitlab/-/issues/360545)
+ # TODO Remove legacy_mode in %17.0
return deprecated_rest_status if legacy_mode == '14.5'
return :stale if stale?
@@ -331,10 +334,12 @@ module Ci
end
# DEPRECATED
- # TODO Remove in %16.0 in favor of `status` for REST calls
+ # TODO Remove in %16.0 in favor of `status` for REST calls, see https://gitlab.com/gitlab-org/gitlab/-/issues/344648
def deprecated_rest_status
- if contacted_at.nil? # TODO Remove in %15.0, see https://gitlab.com/gitlab-org/gitlab/-/issues/344648
- :not_connected
+ return :stale if stale?
+
+ if contacted_at.nil?
+ :never_contacted
elsif active?
online? ? :online : :offline
else
@@ -462,7 +467,7 @@ module Ci
end
def self.token_expiration_enforced?
- Feature.enabled?(:enforce_runner_token_expires_at, default_enabled: :yaml)
+ Feature.enabled?(:enforce_runner_token_expires_at)
end
private
diff --git a/app/models/ci/secure_file.rb b/app/models/ci/secure_file.rb
index 6a26a5341aa..9c82e106d6e 100644
--- a/app/models/ci/secure_file.rb
+++ b/app/models/ci/secure_file.rb
@@ -3,8 +3,11 @@
module Ci
class SecureFile < Ci::ApplicationRecord
include FileStoreMounter
+ include IgnorableColumns
include Limitable
+ ignore_column :permissions, remove_with: '15.2', remove_after: '2022-06-22'
+
FILE_SIZE_LIMIT = 5.megabytes.freeze
CHECKSUM_ALGORITHM = 'sha256'
@@ -14,14 +17,12 @@ module Ci
belongs_to :project, optional: false
validates :file, presence: true, file_size: { maximum: FILE_SIZE_LIMIT }
- validates :checksum, :file_store, :name, :permissions, :project_id, presence: true
+ validates :checksum, :file_store, :name, :project_id, presence: true
validates :name, uniqueness: { scope: :project }
after_initialize :generate_key_data
before_validation :assign_checksum
- enum permissions: { read_only: 0, read_write: 1, execute: 2 }
-
default_value_for(:file_store) { Ci::SecureFileUploader.default_store }
mount_file_store_uploader Ci::SecureFileUploader
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 21f7e410843..d1e169a1f78 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -18,7 +18,6 @@ module Clusters
default_value_for :version, VERSION
scope :preload_cluster_platform, -> { preload(cluster: [:platform_kubernetes]) }
- scope :with_clusters_with_cilium, -> { joins(:cluster).merge(Clusters::Cluster.with_available_cilium) }
attr_encrypted :alert_manager_token,
mode: :per_attribute_iv,
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 87afa9f9491..014f7530357 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -137,7 +137,6 @@ module Clusters
scope :aws_installed, -> { aws_provided.joins(:provider_aws).merge(Clusters::Providers::Aws.with_status(:created)) }
scope :with_available_elasticstack, -> { joins(:application_elastic_stack).merge(::Clusters::Applications::ElasticStack.available) }
- scope :with_available_cilium, -> { joins(:application_cilium).merge(::Clusters::Applications::Cilium.available) }
scope :distinct_with_deployed_environments, -> { joins(:environments).merge(::Deployment.success).distinct }
scope :managed, -> { where(managed: true) }
diff --git a/app/models/clusters/instance.rb b/app/models/clusters/instance.rb
index 2a09ba11564..b5acc6a68f9 100644
--- a/app/models/clusters/instance.rb
+++ b/app/models/clusters/instance.rb
@@ -9,5 +9,11 @@ module Clusters
def flipper_id
self.class.to_s
end
+
+ def certificate_based_clusters_enabled?
+ ::Gitlab::SafeRequestStore.fetch("certificate_based_clusters:") do
+ Feature.enabled?(:certificate_based_clusters, type: :ops)
+ end
+ end
end
end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 1bd8e8b44cb..9d4f0a89403 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -11,6 +11,16 @@ module Clusters
RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze
+ IGNORED_CONNECTION_EXCEPTIONS = [
+ Gitlab::UrlBlocker::BlockedUrlError,
+ Kubeclient::HttpError,
+ Errno::ECONNREFUSED,
+ URI::InvalidURIError,
+ Errno::EHOSTUNREACH,
+ OpenSSL::X509::StoreError,
+ OpenSSL::SSL::SSLError
+ ].freeze
+
self.table_name = 'cluster_platforms_kubernetes'
self.reactive_cache_work_type = :external_dependency
@@ -102,10 +112,23 @@ module Clusters
def calculate_reactive_cache_for(environment)
return unless enabled?
- pods = read_pods(environment.deployment_namespace)
- deployments = read_deployments(environment.deployment_namespace)
+ pods = []
+ deployments = []
+ ingresses = []
+
+ begin
+ pods = read_pods(environment.deployment_namespace)
+ deployments = read_deployments(environment.deployment_namespace)
+
+ ingresses = read_ingresses(environment.deployment_namespace)
+ rescue *IGNORED_CONNECTION_EXCEPTIONS => e
+ log_kube_connection_error(e)
- ingresses = read_ingresses(environment.deployment_namespace)
+ # Return hash with default values so that it is cached.
+ return {
+ pods: pods, deployments: deployments, ingresses: ingresses
+ }
+ end
# extract only the data required for display to avoid unnecessary caching
{
@@ -292,6 +315,23 @@ module Clusters
}
end
end
+
+ def log_kube_connection_error(error)
+ logger.error({
+ exception: {
+ class: error.class.name,
+ message: error.message
+ },
+ status_code: error.try(:error_code),
+ namespace: self.namespace,
+ class_name: self.class.name,
+ event: :kube_connection_error
+ })
+ end
+
+ def logger
+ @logger ||= Gitlab::Kubernetes::Logger.build
+ end
end
end
end
diff --git a/app/models/concerns/bulk_member_access_load.rb b/app/models/concerns/bulk_member_access_load.rb
index efc65e55e40..c3aa3019abb 100644
--- a/app/models/concerns/bulk_member_access_load.rb
+++ b/app/models/concerns/bulk_member_access_load.rb
@@ -12,6 +12,11 @@ module BulkMemberAccessLoad
end
end
+ def purge_resource_id_from_request_store(resource_klass, resource_id)
+ Gitlab::SafeRequestPurger.execute(resource_key: max_member_access_for_resource_key(resource_klass),
+ resource_ids: [resource_id])
+ end
+
def max_member_access_for_resource_key(klass)
"max_member_access_for_#{klass.name.underscore.pluralize}:#{self.class}:#{self.id}"
end
diff --git a/app/models/concerns/ci/has_deployment_name.rb b/app/models/concerns/ci/has_deployment_name.rb
index fe288134872..887653e846e 100644
--- a/app/models/concerns/ci/has_deployment_name.rb
+++ b/app/models/concerns/ci/has_deployment_name.rb
@@ -5,7 +5,7 @@ module Ci
extend ActiveSupport::Concern
def count_user_deployment?
- Feature.enabled?(:job_deployment_count) && deployment_name?
+ deployment_name?
end
def deployment_name?
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index 313c767e59f..cca66c3ec94 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -7,16 +7,15 @@ 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
- # TODO: replace STARTED_STATUSES with data from BUILD_STARTED_RUNNING_STATUSES in https://gitlab.com/gitlab-org/gitlab/-/issues/273378
- # see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82149#note_865508501
- BUILD_STARTED_RUNNING_STATUSES = %w[running success failed].freeze
- STARTED_STATUSES = %w[running success failed skipped manual scheduled].freeze
+ STARTED_STATUSES = %w[running success failed].freeze
ACTIVE_STATUSES = %w[waiting_for_resource preparing 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
PASSED_WITH_WARNINGS_STATUSES = %w[failed canceled].to_set.freeze
EXCLUDE_IGNORED_STATUSES = %w[manual failed canceled].to_set.freeze
- CANCELABLE_STATUSES = %w[running waiting_for_resource preparing pending created scheduled].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
@@ -47,6 +46,10 @@ module Ci
def completed_statuses
COMPLETED_STATUSES.map(&:to_sym)
end
+
+ def stopped_statuses
+ STOPPED_STATUSES.map(&:to_sym)
+ end
end
included do
@@ -78,8 +81,8 @@ module Ci
scope :skipped, -> { with_status(:skipped) }
scope :manual, -> { with_status(:manual) }
scope :scheduled, -> { with_status(:scheduled) }
- scope :alive, -> { with_status(:created, :waiting_for_resource, :preparing, :pending, :running) }
- scope :alive_or_scheduled, -> { with_status(:created, :waiting_for_resource, :preparing, :pending, :running, :scheduled) }
+ scope :alive, -> { with_status(*ALIVE_STATUSES) }
+ scope :alive_or_scheduled, -> { with_status(*klass::CANCELABLE_STATUSES) }
scope :created_or_pending, -> { with_status(:created, :pending) }
scope :running_or_pending, -> { with_status(:running, :pending) }
scope :finished, -> { with_status(:success, :failed, :canceled) }
@@ -98,7 +101,7 @@ module Ci
end
def started?
- STARTED_STATUSES.include?(status) && started_at
+ STARTED_STATUSES.include?(status) && !!started_at
end
def active?
diff --git a/app/models/concerns/cross_database_modification.rb b/app/models/concerns/cross_database_modification.rb
index 85645e482f6..dea62f03f91 100644
--- a/app/models/concerns/cross_database_modification.rb
+++ b/app/models/concerns/cross_database_modification.rb
@@ -103,7 +103,7 @@ module CrossDatabaseModification
def track_gitlab_schema_in_current_transaction?
return false unless Feature::FlipperFeature.table_exists?
- Feature.enabled?(:track_gitlab_schema_in_current_transaction, default_enabled: :yaml)
+ Feature.enabled?(:track_gitlab_schema_in_current_transaction)
rescue ActiveRecord::NoDatabaseError, PG::ConnectionBad
false
end
diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb
index d9c622f247a..2b5e1a204cb 100644
--- a/app/models/concerns/deployment_platform.rb
+++ b/app/models/concerns/deployment_platform.rb
@@ -3,7 +3,7 @@
module DeploymentPlatform
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def deployment_platform(environment: nil)
- return if Feature.disabled?(:certificate_based_clusters, default_enabled: :yaml, type: :ops)
+ return unless self.namespace.certificate_based_clusters_enabled?
@deployment_platform ||= {}
diff --git a/app/models/concerns/integrations/loggable.rb b/app/models/concerns/integrations/loggable.rb
new file mode 100644
index 00000000000..57847ea335c
--- /dev/null
+++ b/app/models/concerns/integrations/loggable.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Integrations
+ module Loggable
+ def log_info(message, params = {})
+ message = build_message(message, params)
+
+ logger.info(message)
+ end
+
+ def log_error(message, params = {})
+ message = build_message(message, params)
+
+ logger.error(message)
+ end
+
+ def log_exception(error, params = {})
+ Gitlab::ExceptionLogFormatter.format!(error, params)
+
+ log_error(params[:message] || error.message, params)
+ end
+
+ def build_message(message, params = {})
+ {
+ integration_class: self.class.name,
+ integration_id: id,
+ project_id: project&.id,
+ project_path: project&.full_path,
+ message: message
+ }.merge(params)
+ end
+
+ def logger
+ Gitlab::IntegrationsLogger
+ end
+ end
+end
diff --git a/app/models/concerns/integrations/reset_secret_fields.rb b/app/models/concerns/integrations/reset_secret_fields.rb
new file mode 100644
index 00000000000..f79c4392f19
--- /dev/null
+++ b/app/models/concerns/integrations/reset_secret_fields.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+# Integrations should reset their "secret" fields (type: 'password') when certain "exposing"
+# fields are changed (e.g. URLs), to avoid leaking secrets to unauthorized parties.
+# The result of this is that users have to reenter the secrets to confirm the change.
+module Integrations
+ module ResetSecretFields
+ extend ActiveSupport::Concern
+
+ included do
+ before_validation :reset_secret_fields!, if: :reset_secret_fields?
+ end
+
+ def exposing_secrets_fields
+ # TODO: Once all integrations use `Integrations::Field` we can remove the `.try` here.
+ # See: https://gitlab.com/groups/gitlab-org/-/epics/7652
+ fields.select { _1.try(:exposes_secrets) }.pluck(:name)
+ end
+
+ private
+
+ def reset_secret_fields?
+ exposing_secrets_fields.any? do |field|
+ public_send("#{field}_changed?") # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ def reset_secret_fields!
+ secret_fields.each do |field|
+ next if public_send("#{field}_touched?") # rubocop:disable GitlabSecurity/PublicSend
+
+ public_send("#{field}=", nil) # rubocop:disable GitlabSecurity/PublicSend
+
+ # NOTE: Some of our specs also write to properties in addition to data fields,
+ # in order to test backwards compatibility. So in those cases we also need to
+ # clear the field in properties, since the setter above will only affect the data field.
+ self.properties = properties.except(field) if properties.present?
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/integrations/slack_mattermost_notifier.rb b/app/models/concerns/integrations/slack_mattermost_notifier.rb
index be13701289a..3bdaa852ddf 100644
--- a/app/models/concerns/integrations/slack_mattermost_notifier.rb
+++ b/app/models/concerns/integrations/slack_mattermost_notifier.rb
@@ -14,11 +14,21 @@ module Integrations
# - https://gitlab.com/gitlab-org/slack-notifier#middleware
# - https://gitlab.com/gitlab-org/gitlab/-/issues/347048
notifier = ::Slack::Messenger.new(webhook, opts.merge(http_client: HTTPClient))
- notifier.ping(
+ responses = notifier.ping(
message.pretext,
attachments: message.attachments,
fallback: message.fallback
)
+
+ responses.each do |response|
+ unless response.success?
+ log_error('SlackMattermostNotifier HTTP error response',
+ request_host: response.request.uri.host,
+ response_code: response.code,
+ response_body: response.body
+ )
+ end
+ end
end
class HTTPClient
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index dbd760a9c45..713a4386fee 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -195,7 +195,7 @@ module Issuable
end
def supports_escalation?
- return false unless ::Feature.enabled?(:incident_escalations, project, default_enabled: :yaml)
+ return false unless ::Feature.enabled?(:incident_escalations, project)
incident?
end
@@ -520,7 +520,7 @@ module Issuable
changes.merge!(hook_association_changes(old_associations))
end
- Gitlab::HookData::IssuableBuilder.new(self).build(user: user, changes: changes)
+ Gitlab::DataBuilder::Issuable.new(self).build(user: user, changes: changes)
end
def labels_array
diff --git a/app/models/concerns/limitable.rb b/app/models/concerns/limitable.rb
index fab1aa21634..6ff540b7866 100644
--- a/app/models/concerns/limitable.rb
+++ b/app/models/concerns/limitable.rb
@@ -28,8 +28,8 @@ module Limitable
def validate_scoped_plan_limit_not_exceeded
scope_relation = self.public_send(limit_scope) # rubocop:disable GitlabSecurity/PublicSend
return unless scope_relation
- return if limit_feature_flag && ::Feature.disabled?(limit_feature_flag, scope_relation, default_enabled: :yaml)
- return if limit_feature_flag_for_override && ::Feature.enabled?(limit_feature_flag_for_override, scope_relation, default_enabled: :yaml)
+ return if limit_feature_flag && ::Feature.disabled?(limit_feature_flag, scope_relation)
+ return if limit_feature_flag_for_override && ::Feature.enabled?(limit_feature_flag_for_override, scope_relation)
relation = limit_relation ? self.public_send(limit_relation) : self.class.where(limit_scope => scope_relation) # rubocop:disable GitlabSecurity/PublicSend
limits = scope_relation.actual_limits
diff --git a/app/models/concerns/merge_request_reviewer_state.rb b/app/models/concerns/merge_request_reviewer_state.rb
index 893d06b4da8..18ec1c253e1 100644
--- a/app/models/concerns/merge_request_reviewer_state.rb
+++ b/app/models/concerns/merge_request_reviewer_state.rb
@@ -16,8 +16,6 @@ module MergeRequestReviewerState
belongs_to :updated_state_by, class_name: 'User', foreign_key: :updated_state_by_user_id
- after_initialize :set_state, unless: :persisted?
-
def attention_requested_by
return unless attention_requested?
diff --git a/app/models/concerns/packages/destructible.rb b/app/models/concerns/packages/destructible.rb
index a3b7d8580c1..647c63b7f60 100644
--- a/app/models/concerns/packages/destructible.rb
+++ b/app/models/concerns/packages/destructible.rb
@@ -5,7 +5,7 @@ module Packages
extend ActiveSupport::Concern
class_methods do
- def next_pending_destruction(order_by: nil)
+ def next_pending_destruction(order_by:)
set = pending_destruction.limit(1).lock('FOR UPDATE SKIP LOCKED')
set = set.order(order_by) if order_by
set.take
diff --git a/app/models/concerns/pg_full_text_searchable.rb b/app/models/concerns/pg_full_text_searchable.rb
index 68357c44300..bfc539ee392 100644
--- a/app/models/concerns/pg_full_text_searchable.rb
+++ b/app/models/concerns/pg_full_text_searchable.rb
@@ -80,6 +80,15 @@ module PgFullTextSearchable
pg_full_text_searchable_columns[column[:name]] = column[:weight]
end
+ # When multiple updates are done in a transaction, `saved_changes` will only report the latest save
+ # and we may miss an update to the searchable columns.
+ # As a workaround, we set a dirty flag here and update the search data in `after_save_commit`.
+ after_save do
+ next unless pg_full_text_searchable_columns.keys.any? { |f| saved_changes.has_key?(f) }
+
+ @update_pg_full_text_search_data = true
+ end
+
# We update this outside the transaction because this could raise an error if the resulting tsvector
# is too long. When that happens, we still persist the create / update but the model will not have a
# search data record. This is fine in most cases because this is a very rare occurrence and only happens
@@ -87,9 +96,8 @@ module PgFullTextSearchable
#
# We also do not want to use a subtransaction here due to: https://gitlab.com/groups/gitlab-org/-/epics/6540
after_save_commit do
- next unless pg_full_text_searchable_columns.keys.any? { |f| saved_changes.has_key?(f) }
-
- update_search_data!
+ update_search_data! if @update_pg_full_text_search_data
+ @update_pg_full_text_search_data = nil
end
end
diff --git a/app/models/concerns/project_services_loggable.rb b/app/models/concerns/project_services_loggable.rb
deleted file mode 100644
index e5385435138..00000000000
--- a/app/models/concerns/project_services_loggable.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module ProjectServicesLoggable
- def log_info(message, params = {})
- message = build_message(message, params)
-
- logger.info(message)
- end
-
- def log_error(message, params = {})
- message = build_message(message, params)
-
- logger.error(message)
- end
-
- def build_message(message, params = {})
- {
- service_class: self.class.name,
- project_id: project&.id,
- project_path: project&.full_path,
- message: message
- }.merge(params)
- end
-
- def logger
- Gitlab::ProjectServiceLogger
- end
-end
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 2cf95ac0dae..5b759dedb26 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -97,7 +97,7 @@ module Routable
def full_name
# We have to test for persistence as the cache key uses #updated_at
- return (route&.name || build_full_name) unless persisted? && Feature.enabled?(:cached_route_lookups, self, type: :ops, default_enabled: :yaml)
+ return (route&.name || build_full_name) unless persisted? && Feature.enabled?(:cached_route_lookups, self, type: :ops)
# Return the name as-is if the parent is missing
return name if route.nil? && parent.nil? && name.present?
@@ -115,7 +115,7 @@ module Routable
def full_path
# We have to test for persistence as the cache key uses #updated_at
- return (route&.path || build_full_path) unless persisted? && Feature.enabled?(:cached_route_lookups, self, type: :ops, default_enabled: :yaml)
+ return (route&.path || build_full_path) unless persisted? && Feature.enabled?(:cached_route_lookups, self, type: :ops)
# Return the path as-is if the parent is missing
return path if route.nil? && parent.nil? && path.present?
diff --git a/app/models/concerns/sha256_attribute.rb b/app/models/concerns/sha256_attribute.rb
deleted file mode 100644
index 3c906642b1a..00000000000
--- a/app/models/concerns/sha256_attribute.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-module Sha256Attribute
- extend ActiveSupport::Concern
-
- class_methods do
- def sha256_attribute(name)
- return if ENV['STATIC_VERIFICATION']
-
- validate_binary_column_exists!(name) unless Rails.env.production?
-
- attribute(name, Gitlab::Database::Sha256Attribute.new)
- end
-
- # This only gets executed in non-production environments as an additional check to ensure
- # the column is the correct type. In production it should behave like any other attribute.
- # See https://gitlab.com/gitlab-org/gitlab/merge_requests/5502 for more discussion
- def validate_binary_column_exists!(name)
- return unless database_exists?
-
- unless table_exists?
- warn "WARNING: sha256_attribute #{name.inspect} is invalid since the table doesn't exist - you may need to run database migrations"
- return
- end
-
- column = columns.find { |c| c.name == name.to_s }
-
- unless column
- warn "WARNING: sha256_attribute #{name.inspect} is invalid since the column doesn't exist - you may need to run database migrations"
- return
- end
-
- unless column.type == :binary
- raise ArgumentError, "sha256_attribute #{name.inspect} is invalid since the column type is not :binary"
- end
- rescue StandardError => error
- Gitlab::AppLogger.error "Sha256Attribute initialization: #{error.message}"
- raise
- end
-
- def database_exists?
- database.exists?
- end
- end
-end
diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb
index e49f4d03bda..701d2fda5c5 100644
--- a/app/models/concerns/sha_attribute.rb
+++ b/app/models/concerns/sha_attribute.rb
@@ -3,39 +3,71 @@
module ShaAttribute
extend ActiveSupport::Concern
- # Needed for the database method
- include DatabaseReflection
+ class ShaAttributeTypeMismatchError < StandardError
+ def initialize(column_name, column_type)
+ @column_name = column_name
+ @column_type = column_type
+ end
+
+ def message
+ "sha_attribute :#{@column_name} should be a :binary column but it is :#{@column_type}"
+ end
+ end
+
+ class Sha256AttributeTypeMismatchError < ShaAttributeTypeMismatchError
+ def message
+ "sha256_attribute :#{@column_name} should be a :binary column but it is :#{@column_type}"
+ end
+ end
class_methods do
def sha_attribute(name)
return if ENV['STATIC_VERIFICATION']
- validate_binary_column_exists!(name) if Rails.env.development? || Rails.env.test?
+ sha_attribute_fields << name
attribute(name, Gitlab::Database::ShaAttribute.new)
end
+ def sha_attribute_fields
+ @sha_attribute_fields ||= []
+ end
+
+ def sha256_attribute(name)
+ return if ENV['STATIC_VERIFICATION']
+
+ sha256_attribute_fields << name
+
+ attribute(name, Gitlab::Database::Sha256Attribute.new)
+ end
+
+ def sha256_attribute_fields
+ @sha256_attribute_fields ||= []
+ end
+
# This only gets executed in non-production environments as an additional check to ensure
# the column is the correct type. In production it should behave like any other attribute.
# See https://gitlab.com/gitlab-org/gitlab/merge_requests/5502 for more discussion
- def validate_binary_column_exists!(name)
- return unless database_exists?
- return unless table_exists?
+ def load_schema!
+ super
- column = columns.find { |c| c.name == name.to_s }
+ return if Rails.env.production?
- return unless column
+ sha_attribute_fields.each do |field|
+ column = columns_hash[field.to_s]
- unless column.type == :binary
- raise ArgumentError, "sha_attribute #{name.inspect} is invalid since the column type is not :binary"
+ if column && column.type != :binary
+ raise ShaAttributeTypeMismatchError.new(column.name, column.type)
+ end
end
- rescue StandardError => error
- Gitlab::AppLogger.error "ShaAttribute initialization: #{error.message}"
- raise
- end
- def database_exists?
- database.exists?
+ sha256_attribute_fields.each do |field|
+ column = columns_hash[field.to_s]
+
+ if column && column.type != :binary
+ raise Sha256AttributeTypeMismatchError.new(column.name, column.type)
+ end
+ end
end
end
end
diff --git a/app/models/container_registry/event.rb b/app/models/container_registry/event.rb
index c1b865ae578..5409bdf5af4 100644
--- a/app/models/container_registry/event.rb
+++ b/app/models/container_registry/event.rb
@@ -2,6 +2,8 @@
module ContainerRegistry
class Event
+ include Gitlab::Utils::StrongMemoize
+
ALLOWED_ACTIONS = %w(push delete).freeze
PUSH_ACTION = 'push'
EVENT_TRACKING_CATEGORY = 'container_registry:notification'
@@ -17,7 +19,7 @@ module ContainerRegistry
end
def handle!
- # no op
+ update_project_statistics
end
def track!
@@ -58,10 +60,25 @@ module ContainerRegistry
end
def container_registry_path
- path = event.dig('target', 'repository')
- return unless path
+ strong_memoize(:container_registry_path) do
+ path = event.dig('target', 'repository')
+ next unless path
+
+ ContainerRegistry::Path.new(path)
+ end
+ end
+
+ def project
+ container_registry_path&.repository_project
+ end
+
+ def update_project_statistics
+ return unless supported?
+ return unless target_tag?
+ return unless project
+ return unless Feature.enabled?(:container_registry_project_statistics, project)
- ContainerRegistry::Path.new(path)
+ ProjectCacheWorker.perform_async(project.id, [], [:container_registry_size])
end
end
end
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 78bd520d5d5..c965d7cffe1 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -43,7 +43,8 @@ class ContainerRepository < ApplicationRecord
migration_canceled: 4,
not_found: 5,
native_import: 6,
- migration_forced_canceled: 7
+ migration_forced_canceled: 7,
+ migration_canceled_by_registry: 8
}
delegate :client, :gitlab_api_client, to: :registry
@@ -214,9 +215,9 @@ class ContainerRepository < ApplicationRecord
container_repository.migration_skipped_at = Time.zone.now
end
- before_transition any => %i[import_done import_aborted] do |container_repository|
+ before_transition any => %i[import_done import_aborted import_skipped] do |container_repository|
container_repository.run_after_commit do
- ::ContainerRegistry::Migration::EnqueuerWorker.perform_async
+ ::ContainerRegistry::Migration::EnqueuerWorker.enqueue_a_job
end
end
end
@@ -325,17 +326,13 @@ class ContainerRepository < ApplicationRecord
return if importing?
start_import(forced: true)
- when 'import_canceled', 'pre_import_canceled'
- return if import_skipped?
-
- skip_import(reason: :migration_canceled)
when 'import_complete'
finish_import
- when 'import_failed'
+ when 'import_failed', 'import_canceled'
retry_import
when 'pre_import_complete'
finish_pre_import_and_start_import
- when 'pre_import_failed'
+ when 'pre_import_failed', 'pre_import_canceled'
retry_pre_import
else
yield
@@ -376,6 +373,10 @@ class ContainerRepository < ApplicationRecord
migration_retries_count >= ContainerRegistry::Migration.max_retries
end
+ def nearing_or_exceeded_retry_limit?
+ migration_retries_count >= ContainerRegistry::Migration.max_retries - 1
+ end
+
def last_import_step_done_at
[migration_pre_import_done_at, migration_import_done_at, migration_aborted_at, migration_skipped_at].compact.max
end
@@ -460,12 +461,8 @@ class ContainerRepository < ApplicationRecord
client.delete_repository_tag_by_name(self.path, name)
end
- def reset_expiration_policy_started_at!
- update!(expiration_policy_started_at: nil)
- end
-
def start_expiration_policy!
- update!(expiration_policy_started_at: Time.zone.now)
+ update!(expiration_policy_started_at: Time.zone.now, last_cleanup_deleted_tags_count: nil)
end
def size
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index 360a9ffbc53..3c0f7d91a03 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -5,7 +5,11 @@ class DeployToken < ApplicationRecord
include TokenAuthenticatable
include PolicyActor
include Gitlab::Utils::StrongMemoize
- add_authentication_token_field :token, encrypted: :optional
+ include IgnorableColumns
+
+ ignore_column :token, remove_with: '15.2', remove_after: '2022-07-22'
+
+ add_authentication_token_field :token, encrypted: :required
AVAILABLE_SCOPES = %i(read_repository read_registry write_registry
read_package_registry write_package_registry).freeze
@@ -126,6 +130,10 @@ class DeployToken < ApplicationRecord
end
end
+ def impersonated?
+ false
+ end
+
def expires_at
expires_at = read_attribute(:expires_at)
expires_at != Forever.date ? expires_at : nil
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 63d531d82c3..4204ad707b2 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -70,6 +70,11 @@ class Deployment < ApplicationRecord
transition created: :blocked
end
+ # This transition is possible when we have manual jobs.
+ event :create do
+ transition skipped: :created
+ end
+
event :unblock do
transition blocked: :created
end
@@ -348,7 +353,7 @@ class Deployment < ApplicationRecord
def sync_status_with(build)
return false unless ::Deployment.statuses.include?(build.status)
- return false if build.created? || build.status == self.status
+ return false if build.status == self.status
update_status!(build.status)
rescue StandardError => e
@@ -403,6 +408,8 @@ class Deployment < ApplicationRecord
skip!
when 'blocked'
block!
+ when 'created'
+ create!
else
raise ArgumentError, "The status #{status.inspect} is invalid"
end
diff --git a/app/models/design_management/action.rb b/app/models/design_management/action.rb
index b9df2873a73..5f407a5867d 100644
--- a/app/models/design_management/action.rb
+++ b/app/models/design_management/action.rb
@@ -19,6 +19,7 @@ module DesignManagement
scope :ordered, -> { order(version_id: :asc) }
scope :by_design, -> (design) { where(design: design) }
scope :by_event, -> (event) { where(event: event) }
+ scope :with_version, -> { preload(:version) }
# For each design, only select the most recent action
scope :most_recent, -> do
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 9e663b2ee74..865f5c68af1 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -26,7 +26,7 @@ class Environment < ApplicationRecord
has_many :self_managed_prometheus_alert_events, inverse_of: :environment
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :environment
- has_one :last_deployment, -> { Feature.enabled?(:env_last_deployment_by_finished_at, default_enabled: :yaml) ? success.ordered : success.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
+ has_one :last_deployment, -> { success.ordered }, class_name: 'Deployment', inverse_of: :environment
has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus', disable_joins: true
has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline', disable_joins: true
@@ -314,13 +314,9 @@ class Environment < ApplicationRecord
def stop_actions
strong_memoize(:stop_actions) do
- if ::Feature.enabled?(:environment_multiple_stop_actions, project, default_enabled: :yaml)
- # Fix N+1 queries it brings to the serializer.
- # Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780
- last_deployment_group.map(&:stop_action).compact
- else
- [last_deployment&.stop_action].compact
- end
+ # Fix N+1 queries it brings to the serializer.
+ # Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780
+ last_deployment_group.map(&:stop_action).compact
end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index e9a98c06b59..7760be3e817 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -357,6 +357,8 @@ class Event < ApplicationRecord
Project.unscoped.where(id: project_id)
.where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago)
.touch_all(:last_activity_at, time: created_at) # rubocop: disable Rails/SkipsModelValidations
+
+ Gitlab::InactiveProjectsDeletionWarningTracker.new(project.id).reset
end
def authored_by?(user)
@@ -369,6 +371,10 @@ class Event < ApplicationRecord
Event._to_partial_path
end
+ def has_no_project_and_group?
+ project_id.nil? && group_id.nil?
+ end
+
protected
def capability
diff --git a/app/models/event_collection.rb b/app/models/event_collection.rb
index fc093894847..4258027aa56 100644
--- a/app/models/event_collection.rb
+++ b/app/models/event_collection.rb
@@ -8,6 +8,8 @@
class EventCollection
include Gitlab::Utils::StrongMemoize
+ attr_reader :filter
+
# To prevent users from putting too much pressure on the database by cycling
# through thousands of events we put a limit on the number of pages.
MAX_PAGE = 10
@@ -19,7 +21,7 @@ class EventCollection
@projects = projects
@limit = limit
@offset = offset
- @filter = filter
+ @filter = filter || EventFilter.new(EventFilter::ALL)
@groups = groups
end
@@ -44,35 +46,46 @@ class EventCollection
private
def project_events
- in_operator_optimized_relation('project_id', projects)
+ in_operator_optimized_relation('project_id', projects, Project)
end
def group_events
- in_operator_optimized_relation('group_id', groups)
+ in_operator_optimized_relation('group_id', groups, Namespace)
end
def project_and_group_events
- Event.from_union([project_events, group_events]).recent
+ if EventFilter::PROJECT_ONLY_EVENT_TYPES.include?(filter.filter)
+ project_events
+ else
+ Event.from_union([project_events, group_events]).recent
+ end
end
- def in_operator_optimized_relation(parent_column, parents)
- scope = filtered_events
- array_scope = parents.select(:id)
- array_mapping_scope = -> (parent_id_expression) { Event.where(Event.arel_table[parent_column].eq(parent_id_expression)).reorder(id: :desc) }
- finder_query = -> (id_expression) { Event.where(Event.arel_table[:id].eq(id_expression)) }
+ def in_operator_optimized_relation(parent_column, parents, parent_model)
+ query_builder_params = if Feature.enabled?(:optimized_project_and_group_activity_queries)
+ array_data = {
+ scope_ids: parents.pluck(:id),
+ scope_model: parent_model,
+ mapping_column: parent_column
+ }
+ filter.in_operator_query_builder_params(array_data)
+ else
+ {
+ scope: filtered_events,
+ array_scope: parents.select(:id),
+ array_mapping_scope: -> (parent_id_expression) { Event.where(Event.arel_table[parent_column].eq(parent_id_expression)).reorder(id: :desc) },
+ finder_query: -> (id_expression) { Event.where(Event.arel_table[:id].eq(id_expression)) }
+ }
+ end
Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
- .new(
- scope: scope,
- array_scope: array_scope,
- array_mapping_scope: array_mapping_scope,
- finder_query: finder_query
- )
+ .new(**query_builder_params)
.execute
+ .limit(@limit + @offset)
end
def filtered_events
- @filter ? @filter.apply_filter(base_relation) : base_relation
+ filter.apply_filter(base_relation)
end
def paginate_events(events)
@@ -99,3 +112,5 @@ class EventCollection
end
end
end
+
+EventCollection.prepend_mod
diff --git a/app/models/group.rb b/app/models/group.rb
index 990c06fdc41..86f4b14cb6c 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -146,7 +146,7 @@ class Group < Namespace
validates :group_feature, presence: true
add_authentication_token_field :runners_token,
- encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
+ encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption) ? :optional : :required },
prefix: RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
after_create :post_create_hook
@@ -870,7 +870,7 @@ class Group < Namespace
actors << self if root_ancestor != self
actors.any? do |actor|
- ::Feature.enabled?(feature_flag, actor, default_enabled: :yaml)
+ ::Feature.enabled?(feature_flag, actor)
end
end
diff --git a/app/models/group_group_link.rb b/app/models/group_group_link.rb
index b0020f097b5..a70110c4076 100644
--- a/app/models/group_group_link.rb
+++ b/app/models/group_group_link.rb
@@ -41,3 +41,5 @@ class GroupGroupLink < ApplicationRecord
Gitlab::Access.human_access(self.group_access)
end
end
+
+GroupGroupLink.prepend_mod_with('GroupGroupLink')
diff --git a/app/models/incident_management/timeline_event.rb b/app/models/incident_management/timeline_event.rb
new file mode 100644
index 00000000000..d30d6906e14
--- /dev/null
+++ b/app/models/incident_management/timeline_event.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ class TimelineEvent < ApplicationRecord
+ include CacheMarkdownField
+
+ self.table_name = 'incident_management_timeline_events'
+
+ cache_markdown_field :note,
+ pipeline: :'incident_management/timeline_event',
+ issuable_reference_expansion_enabled: true
+
+ belongs_to :project
+ belongs_to :author, class_name: 'User', foreign_key: :author_id
+ belongs_to :incident, class_name: 'Issue', foreign_key: :issue_id, inverse_of: :incident_management_timeline_events
+ belongs_to :updated_by_user, class_name: 'User', foreign_key: :updated_by_user_id
+ belongs_to :promoted_from_note, class_name: 'Note', foreign_key: :promoted_from_note_id
+
+ validates :project, :incident, :occurred_at, presence: true
+ validates :action, presence: true, length: { maximum: 128 }
+ validates :note, :note_html, presence: true, length: { maximum: 10_000 }
+
+ scope :order_occurred_at_asc, -> { reorder(occurred_at: :asc) }
+ end
+end
diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb
index 00e55d0fd89..8a8c1a29375 100644
--- a/app/models/instance_configuration.rb
+++ b/app/models/instance_configuration.rb
@@ -13,6 +13,7 @@ class InstanceConfiguration
{ ssh_algorithms_hashes: ssh_algorithms_hashes,
host: host,
gitlab_pages: gitlab_pages,
+ ci_cd_limits: ci_cd_limits,
size_limits: size_limits,
package_file_size_limits: package_file_size_limits,
rate_limits: rate_limits }.deep_symbolize_keys
@@ -47,6 +48,7 @@ class InstanceConfiguration
{
max_attachment_size: application_settings[:max_attachment_size].megabytes,
receive_max_input_size: application_settings[:receive_max_input_size]&.megabytes,
+ max_export_size: application_settings[:max_export_size] > 0 ? application_settings[:max_export_size].megabytes : nil,
max_import_size: application_settings[:max_import_size] > 0 ? application_settings[:max_import_size].megabytes : nil,
diff_max_patch_bytes: application_settings[:diff_max_patch_bytes].bytes,
max_artifacts_size: application_settings[:max_artifacts_size].megabytes,
@@ -128,6 +130,23 @@ class InstanceConfiguration
}
end
+ def ci_cd_limits
+ Plan.all.to_h { |plan| [plan.name.capitalize, plan_ci_cd_limits(plan)] }
+ end
+
+ def plan_ci_cd_limits(plan)
+ plan.actual_limits.slice(
+ :ci_pipeline_size,
+ :ci_active_jobs,
+ :ci_active_pipelines,
+ :ci_project_subscriptions,
+ :ci_pipeline_schedules,
+ :ci_needs_size_limit,
+ :ci_registered_group_runners,
+ :ci_registered_project_runners
+ )
+ end
+
def ssh_algorithm_file(algorithm)
File.join(SSH_ALGORITHMS_PATH, "ssh_host_#{algorithm.downcase}_key.pub")
end
diff --git a/app/models/integration.rb b/app/models/integration.rb
index c0e244e38b6..b5064cfae2d 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -5,14 +5,14 @@
class Integration < ApplicationRecord
include Sortable
include Importable
- include ProjectServicesLoggable
+ include Integrations::Loggable
include Integrations::HasDataFields
+ include Integrations::ResetSecretFields
include FromUnion
include EachBatch
include IgnorableColumns
extend ::Gitlab::Utils::Override
- ignore_column :template, remove_with: '15.0', remove_after: '2022-04-22'
ignore_column :type, remove_with: '15.0', remove_after: '2022-04-22'
ignore_column :properties, remove_with: '15.1', remove_after: '2022-05-22'
@@ -161,7 +161,7 @@ class Integration < ApplicationRecord
end
def fields
- self.class.fields
+ self.class.fields.dup
end
# Provide convenient accessor methods for each serialized property.
@@ -279,7 +279,7 @@ class Integration < ApplicationRecord
end
def self.dev_integration_names
- return [] unless Rails.env.development?
+ return [] unless Gitlab.dev_or_test_env?
DEV_INTEGRATION_NAMES
end
@@ -447,6 +447,7 @@ class Integration < ApplicationRecord
# TODO: Once all integrations use `Integrations::Field` we can
# use `#secret?` here.
+ # See: https://gitlab.com/groups/gitlab-org/-/epics/7652
def secret_fields
fields.select { |f| f[:type] == 'password' }.pluck(:name)
end
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index b384a94d713..4e144a688f6 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -5,7 +5,26 @@ module Integrations
include ReactivelyCached
prepend EnableSslVerification
- prop_accessor :bamboo_url, :build_key, :username, :password
+ field :bamboo_url,
+ title: s_('BambooService|Bamboo URL'),
+ placeholder: s_('https://bamboo.example.com'),
+ help: s_('BambooService|Bamboo service root URL.'),
+ required: true
+
+ field :build_key,
+ help: s_('BambooService|Bamboo build plan key.'),
+ non_empty_password_title: s_('BambooService|Enter new build key'),
+ non_empty_password_help: s_('BambooService|Leave blank to use your current build key.'),
+ placeholder: s_('KEY'),
+ required: true
+
+ field :username,
+ help: s_('BambooService|The user with API access to the Bamboo server.')
+
+ field :password,
+ type: 'password',
+ non_empty_password_title: s_('ProjectService|Enter new password'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
validates :bamboo_url, presence: true, public_url: true, if: :activated?
validates :build_key, presence: true, if: :activated?
@@ -43,39 +62,6 @@ module Integrations
'bamboo'
end
- def fields
- [
- {
- type: 'text',
- name: 'bamboo_url',
- title: s_('BambooService|Bamboo URL'),
- placeholder: s_('https://bamboo.example.com'),
- help: s_('BambooService|Bamboo service root URL.'),
- required: true
- },
- {
- type: 'password',
- name: 'build_key',
- help: s_('BambooService|Bamboo build plan key.'),
- non_empty_password_title: s_('BambooService|Enter new build key'),
- non_empty_password_help: s_('BambooService|Leave blank to use your current build key.'),
- placeholder: s_('KEY'),
- required: true
- },
- {
- type: 'text',
- name: 'username',
- help: s_('BambooService|The user with API access to the Bamboo server.')
- },
- {
- type: 'password',
- name: 'password',
- non_empty_password_title: s_('ProjectService|Enter new password'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
- }
- ]
- end
-
def build_page(sha, ref)
with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
end
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index 54bd595892f..9bf208abcf7 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -149,6 +149,10 @@ module Integrations
raise NotImplementedError
end
+ def webhook_placeholder
+ raise NotImplementedError
+ end
+
private
def log_usage(_, _)
diff --git a/app/models/integrations/base_ci.rb b/app/models/integrations/base_ci.rb
index b2e269b1b50..4f8732da703 100644
--- a/app/models/integrations/base_ci.rb
+++ b/app/models/integrations/base_ci.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-# Base class for CI services
-# List methods you need to implement to get your CI service
+# Base class for CI integrations
+# List methods you need to implement to get your CI integration
# working with GitLab merge requests
module Integrations
class BaseCi < Integration
@@ -12,7 +12,7 @@ module Integrations
end
def self.supported_events
- %w(push)
+ %w[push]
end
# Return complete url to build page
@@ -30,10 +30,10 @@ module Integrations
#
#
# Ex.
- # @service.commit_status('13be4ac', 'master')
+ # @integration.commit_status('13be4ac', 'master')
# # => 'success'
#
- # @service.commit_status('2abe4ac', 'dev')
+ # @integration.commit_status('2abe4ac', 'dev')
# # => 'running'
#
#
diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb
index 3b802271a36..d1e54ce86ee 100644
--- a/app/models/integrations/buildkite.rb
+++ b/app/models/integrations/buildkite.rb
@@ -10,7 +10,18 @@ module Integrations
ENDPOINT = "https://buildkite.com"
- prop_accessor :project_url, :token
+ field :project_url,
+ title: _('Pipeline URL'),
+ placeholder: "#{ENDPOINT}/example-org/test-pipeline",
+ required: true
+
+ field :token,
+ type: 'password',
+ title: _('Token'),
+ help: s_('ProjectService|The token you get after you create a Buildkite pipeline with a GitLab repository.'),
+ non_empty_password_title: s_('ProjectService|Enter new token'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
+ required: true
validates :project_url, presence: true, public_url: true, if: :activated?
validates :token, presence: true, if: :activated?
@@ -74,24 +85,6 @@ module Integrations
s_('ProjectService|Run CI/CD pipelines with Buildkite.')
end
- def fields
- [
- { type: 'password',
- name: 'token',
- title: _('Token'),
- help: s_('ProjectService|The token you get after you create a Buildkite pipeline with a GitLab repository.'),
- non_empty_password_title: s_('ProjectService|Enter new token'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
- required: true },
-
- { type: 'text',
- name: 'project_url',
- title: _('Pipeline URL'),
- placeholder: "#{ENDPOINT}/example-org/test-pipeline",
- required: true }
- ]
- end
-
def calculate_reactive_cache(sha, ref)
response = Gitlab::HTTP.try_get(commit_status_path(sha), request_options)
diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb
index 73f78bec381..0c65ed8cd5f 100644
--- a/app/models/integrations/drone_ci.rb
+++ b/app/models/integrations/drone_ci.rb
@@ -10,7 +10,17 @@ module Integrations
DRONE_SAAS_HOSTNAME = 'cloud.drone.io'
- prop_accessor :drone_url, :token
+ field :drone_url,
+ title: s_('ProjectService|Drone server URL'),
+ placeholder: 'http://drone.example.com',
+ required: true
+
+ field :token,
+ type: 'password',
+ help: s_('ProjectService|Token for the Drone project.'),
+ non_empty_password_title: s_('ProjectService|Enter new token'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
+ required: true
validates :drone_url, presence: true, public_url: true, if: :activated?
validates :token, presence: true, if: :activated?
@@ -94,26 +104,6 @@ module Integrations
s_('ProjectService|Run CI/CD pipelines with Drone.')
end
- def fields
- [
- {
- type: 'password',
- name: 'token',
- help: s_('ProjectService|Token for the Drone project.'),
- non_empty_password_title: s_('ProjectService|Enter new token'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
- required: true
- },
- {
- type: 'text',
- name: 'drone_url',
- title: s_('ProjectService|Drone server URL'),
- placeholder: 'http://drone.example.com',
- required: true
- }
- ]
- end
-
override :hook_url
def hook_url
[drone_url, "/hook", "?owner=#{project.namespace.full_path}", "&name=#{project.path}", "&access_token=#{token}"].join
diff --git a/app/models/integrations/field.rb b/app/models/integrations/field.rb
index f00c4236a92..ca7833c1a56 100644
--- a/app/models/integrations/field.rb
+++ b/app/models/integrations/field.rb
@@ -10,6 +10,7 @@ module Integrations
non_empty_password_help
non_empty_password_title
api_only
+ exposes_secrets
].freeze
attr_reader :name
diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb
index 32f11ee23eb..a1abbce72bc 100644
--- a/app/models/integrations/jenkins.rb
+++ b/app/models/integrations/jenkins.rb
@@ -7,7 +7,25 @@ module Integrations
prepend EnableSslVerification
extend Gitlab::Utils::Override
- prop_accessor :jenkins_url, :project_name, :username, :password
+ field :jenkins_url,
+ title: s_('ProjectService|Jenkins server URL'),
+ required: true,
+ placeholder: 'http://jenkins.example.com',
+ help: s_('The URL of the Jenkins server.')
+
+ field :project_name,
+ required: true,
+ placeholder: 'my_project_name',
+ help: s_('The name of the Jenkins project. Copy the name from the end of the URL to the project.')
+
+ field :username,
+ help: s_('The username for the Jenkins server.')
+
+ field :password,
+ type: 'password',
+ help: s_('The password for the Jenkins server.'),
+ non_empty_password_title: s_('ProjectService|Enter new password.'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current password.')
before_validation :reset_password
@@ -15,7 +33,6 @@ module Integrations
validates :project_name, presence: true, if: :activated?
validates :username, presence: true, if: ->(service) { service.activated? && service.password_touched? && service.password.present? }
- default_value_for :push_events, true
default_value_for :merge_requests_events, false
default_value_for :tag_push_events, false
@@ -72,37 +89,5 @@ module Integrations
def self.to_param
'jenkins'
end
-
- def fields
- [
- {
- type: 'text',
- name: 'jenkins_url',
- title: s_('ProjectService|Jenkins server URL'),
- required: true,
- placeholder: 'http://jenkins.example.com',
- help: s_('The URL of the Jenkins server.')
- },
- {
- type: 'text',
- name: 'project_name',
- required: true,
- placeholder: 'my_project_name',
- help: s_('The name of the Jenkins project. Copy the name from the end of the URL to the project.')
- },
- {
- type: 'text',
- name: 'username',
- help: s_('The username for the Jenkins server.')
- },
- {
- type: 'password',
- name: 'password',
- help: s_('The password for the Jenkins server.'),
- non_empty_password_title: s_('ProjectService|Enter new password.'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current password.')
- }
- ]
- end
end
end
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index a800b9e5baa..992bd01bf5f 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -31,7 +31,6 @@ module Integrations
# We should use username/password for Jira Server and email/api_token for Jira Cloud,
# for more information check: https://gitlab.com/gitlab-org/gitlab-foss/issues/49936.
- before_validation :reset_password
after_commit :update_deployment_type, on: [:create, :update], if: :update_deployment_type?
enum comment_detail: {
@@ -46,12 +45,14 @@ module Integrations
required: true,
title: -> { s_('JiraService|Web URL') },
help: -> { s_('JiraService|Base URL of the Jira instance.') },
- placeholder: 'https://jira.example.com'
+ placeholder: 'https://jira.example.com',
+ exposes_secrets: true
field :api_url,
section: SECTION_TYPE_CONNECTION,
title: -> { s_('JiraService|Jira API URL') },
- help: -> { s_('JiraService|If different from Web URL.') }
+ help: -> { s_('JiraService|If different from Web URL.') },
+ exposes_secrets: true
field :username,
section: SECTION_TYPE_CONNECTION,
@@ -98,13 +99,6 @@ module Integrations
jira_tracker_data || self.build_jira_tracker_data
end
- def reset_password
- return unless reset_password?
-
- data_fields.password = nil
- self.properties = properties.except('password')
- end
-
def set_default_data
return unless issues_tracker.present?
@@ -174,7 +168,8 @@ module Integrations
sections.push({
type: SECTION_TYPE_JIRA_ISSUES,
title: _('Issues'),
- description: jira_issues_section_description
+ description: jira_issues_section_description,
+ plan: 'premium'
})
end
@@ -358,16 +353,7 @@ module Integrations
true
rescue StandardError => error
- log_error(
- "Issue transition failed",
- error: {
- exception_class: error.class.name,
- exception_message: error.message,
- exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace)
- },
- client_url: client_url
- )
-
+ log_exception(error, message: 'Issue transition failed', client_url: client_url)
false
end
@@ -544,9 +530,7 @@ module Integrations
yield
rescue StandardError => error
@error = error
- payload = { client_url: client_url }
- Gitlab::ExceptionLogFormatter.format!(error, payload)
- log_error("Error sending message", payload)
+ log_exception(error, message: 'Error sending message', client_url: client_url)
nil
end
@@ -554,15 +538,6 @@ module Integrations
api_url.presence || url
end
- def reset_password?
- # don't reset the password if a new one is provided
- return false if password_touched?
- return true if api_url_changed?
- return false if api_url.present?
-
- url_changed?
- end
-
def update_deployment_type?
api_url_changed? || url_changed? || username_changed? || password_changed?
end
diff --git a/app/models/integrations/mock_ci.rb b/app/models/integrations/mock_ci.rb
index 568fb609a44..cd2928136ef 100644
--- a/app/models/integrations/mock_ci.rb
+++ b/app/models/integrations/mock_ci.rb
@@ -7,7 +7,11 @@ module Integrations
ALLOWED_STATES = %w[failed canceled running pending success success-with-warnings skipped not_found].freeze
- prop_accessor :mock_service_url
+ field :mock_service_url,
+ title: s_('ProjectService|Mock service URL'),
+ placeholder: 'http://localhost:4004',
+ required: true
+
validates :mock_service_url, presence: true, public_url: true, if: :activated?
def title
@@ -22,18 +26,6 @@ module Integrations
'mock_ci'
end
- def fields
- [
- {
- type: 'text',
- name: 'mock_service_url',
- title: s_('ProjectService|Mock service URL'),
- placeholder: 'http://localhost:4004',
- required: true
- }
- ]
- end
-
# Return complete url to build page
#
# Ex.
diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb
index 738319ce835..758c9e4761b 100644
--- a/app/models/integrations/packagist.rb
+++ b/app/models/integrations/packagist.rb
@@ -10,9 +10,6 @@ module Integrations
validates :username, presence: true, if: :activated?
validates :token, presence: true, if: :activated?
- default_value_for :push_events, true
- default_value_for :tag_push_events, true
-
def title
'Packagist'
end
diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb
index d6aafe45ae9..427034edb79 100644
--- a/app/models/integrations/prometheus.rb
+++ b/app/models/integrations/prometheus.rb
@@ -27,10 +27,7 @@ module Integrations
after_commit :track_events
- after_create_commit :create_default_alerts
-
scope :preload_project, -> { preload(:project) }
- scope :with_clusters_with_cilium, -> { joins(project: [:clusters]).merge(Clusters::Cluster.with_available_cilium) }
def show_active_box?
false
@@ -169,12 +166,6 @@ module Integrations
manual_configuration_changed? && !manual_configuration?
end
- def create_default_alerts
- return unless project_id
-
- ::Prometheus::CreateDefaultAlertsWorker.perform_async(project_id)
- end
-
def behind_iap?
manual_configuration? && google_iap_audience_client_id.present? && google_iap_service_account_json.present?
end
diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb
index f0f83f118d7..1205173e40b 100644
--- a/app/models/integrations/teamcity.rb
+++ b/app/models/integrations/teamcity.rb
@@ -8,7 +8,22 @@ module Integrations
TEAMCITY_SAAS_HOSTNAME = /\A[^\.]+\.teamcity\.com\z/i.freeze
- prop_accessor :teamcity_url, :build_type, :username, :password
+ field :teamcity_url,
+ title: s_('ProjectService|TeamCity server URL'),
+ placeholder: 'https://teamcity.example.com',
+ required: true
+
+ field :build_type,
+ help: s_('ProjectService|The build configuration ID of the TeamCity project.'),
+ required: true
+
+ field :username,
+ help: s_('ProjectService|Must have permission to trigger a manual build in TeamCity.')
+
+ field :password,
+ type: 'password',
+ non_empty_password_title: s_('ProjectService|Enter new password'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
validates :teamcity_url, presence: true, public_url: true, if: :activated?
validates :build_type, presence: true, if: :activated?
@@ -51,35 +66,6 @@ module Integrations
s_('To run CI/CD pipelines with JetBrains TeamCity, input the GitLab project details in the TeamCity project Version Control Settings.')
end
- def fields
- [
- {
- type: 'text',
- name: 'teamcity_url',
- title: s_('ProjectService|TeamCity server URL'),
- placeholder: 'https://teamcity.example.com',
- required: true
- },
- {
- type: 'text',
- name: 'build_type',
- help: s_('ProjectService|The build configuration ID of the TeamCity project.'),
- required: true
- },
- {
- type: 'text',
- name: 'username',
- help: s_('ProjectService|Must have permission to trigger a manual build in TeamCity.')
- },
- {
- type: 'password',
- name: 'password',
- non_empty_password_title: s_('ProjectService|Enter new password'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
- }
- ]
- end
-
def build_page(sha, ref)
with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 484cceb9129..d4eb77ef6de 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -88,6 +88,7 @@ class Issue < ApplicationRecord
has_many :prometheus_alerts, through: :prometheus_alert_events
has_many :issue_customer_relations_contacts, class_name: 'CustomerRelations::IssueContact', inverse_of: :issue
has_many :customer_relations_contacts, through: :issue_customer_relations_contacts, source: :contact, class_name: 'CustomerRelations::Contact', inverse_of: :issues
+ has_many :incident_management_timeline_events, class_name: 'IncidentManagement::TimelineEvent', foreign_key: :issue_id, inverse_of: :incident
alias_attribute :escalation_status, :incident_management_issuable_escalation_status
@@ -142,14 +143,12 @@ class Issue < ApplicationRecord
scope :with_issue_type, ->(types) { where(issue_type: types) }
scope :without_issue_type, ->(types) { where.not(issue_type: types) }
- scope :public_only, -> {
- without_hidden.where(confidential: false)
- }
+ scope :public_only, -> { where(confidential: false) }
scope :confidential_only, -> { where(confidential: true) }
scope :without_hidden, -> {
- if Feature.enabled?(:ban_user_feature_flag, default_enabled: :yaml)
+ if Feature.enabled?(:ban_user_feature_flag)
where('NOT EXISTS (?)', Users::BannedUser.select(1).where('issues.author_id = banned_users.user_id'))
else
all
diff --git a/app/models/key.rb b/app/models/key.rb
index 42ea0f29171..e093f9faad3 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -5,7 +5,7 @@ require 'digest/md5'
class Key < ApplicationRecord
include AfterCommitQueue
include Sortable
- include Sha256Attribute
+ include ShaAttribute
include Expirable
include FromUnion
@@ -24,17 +24,12 @@ class Key < ApplicationRecord
length: { maximum: 5000 },
format: { with: /\A(#{Gitlab::SSHPublicKey.supported_algorithms.join('|')})/ }
- validates :fingerprint,
- uniqueness: true,
- presence: { message: 'cannot be generated' },
- unless: -> { Gitlab::FIPS.enabled? }
-
validates :fingerprint_sha256,
uniqueness: true,
- presence: { message: 'cannot be generated' },
- if: -> { Gitlab::FIPS.enabled? }
+ presence: { message: 'cannot be generated' }
validate :key_meets_restrictions
+ validate :expiration, on: :create
delegate :name, :email, to: :user, prefix: true
@@ -154,6 +149,10 @@ class Key < ApplicationRecord
"type is forbidden. Must be #{Gitlab::Utils.to_exclusive_sentence(allowed_types)}"
end
+
+ def expiration
+ errors.add(:key, message: 'has expired') if expired?
+ end
end
Key.prepend_mod_with('Key')
diff --git a/app/models/label.rb b/app/models/label.rb
index 4c9f071f43a..7f4556c11c9 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -160,11 +160,6 @@ class Label < ApplicationRecord
on_project_boards(project_id).where(id: label_id).exists?
end
- # Generate a hex color based on hex-encoded value
- def self.color_for(value)
- "##{Digest::MD5.hexdigest(value)[0..5]}"
- end
-
def open_issues_count(user = nil)
issues_count(user, state: 'opened')
end
diff --git a/app/models/loose_foreign_keys/deleted_record.rb b/app/models/loose_foreign_keys/deleted_record.rb
index ebda5872f1c..6dfd6ea2aae 100644
--- a/app/models/loose_foreign_keys/deleted_record.rb
+++ b/app/models/loose_foreign_keys/deleted_record.rb
@@ -10,7 +10,7 @@ class LooseForeignKeys::DeletedRecord < Gitlab::Database::SharedModel
partitioned_by :partition, strategy: :sliding_list,
next_partition_if: -> (active_partition) do
- return false if Feature.disabled?(:lfk_automatic_partition_creation, default_enabled: :yaml)
+ return false if Feature.disabled?(:lfk_automatic_partition_creation)
oldest_record_in_partition = LooseForeignKeys::DeletedRecord
.select(:id, :created_at)
@@ -22,7 +22,7 @@ class LooseForeignKeys::DeletedRecord < Gitlab::Database::SharedModel
oldest_record_in_partition.present? && oldest_record_in_partition.created_at < PARTITION_DURATION.ago
end,
detach_partition_if: -> (partition) do
- return false if Feature.disabled?(:lfk_automatic_partition_dropping, default_enabled: :yaml)
+ return false if Feature.disabled?(:lfk_automatic_partition_dropping)
!LooseForeignKeys::DeletedRecord
.for_partition(partition)
diff --git a/app/models/member.rb b/app/models/member.rb
index 18ad2785d6e..a5084c8a60c 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -77,6 +77,10 @@ class Member < ApplicationRecord
]).merge(self)
end
+ scope :excluding_users, ->(user_ids) do
+ where.not(user_id: user_ids)
+ end
+
# This scope encapsulates (most of) the conditions a row in the member table
# must satisfy if it is a valid permission. Of particular note:
#
@@ -165,6 +169,7 @@ class Member < ApplicationRecord
scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
scope :with_user, -> (user) { where(user: user) }
+ scope :by_access_level, -> (access_level) { active.where(access_level: access_level) }
scope :preload_user_and_notification_settings, -> { preload(user: :notification_settings) }
@@ -516,7 +521,7 @@ class Member < ApplicationRecord
end
def blocking_refresh
- return true unless Feature.enabled?(:allow_non_blocking_member_refresh, default_enabled: :yaml)
+ return true unless Feature.enabled?(:allow_non_blocking_member_refresh)
return true if @blocking_refresh.nil?
@blocking_refresh
diff --git a/app/models/members_preloader.rb b/app/models/members_preloader.rb
index 8b8eca54550..ba7e4b39989 100644
--- a/app/models/members_preloader.rb
+++ b/app/models/members_preloader.rb
@@ -13,7 +13,7 @@ class MembersPreloader
ActiveRecord::Associations::Preloader.new.preload(members, :created_by)
ActiveRecord::Associations::Preloader.new.preload(members, user: :status)
ActiveRecord::Associations::Preloader.new.preload(members, user: :u2f_registrations)
- ActiveRecord::Associations::Preloader.new.preload(members, user: :webauthn_registrations) if Feature.enabled?(:webauthn, default_enabled: :yaml)
+ ActiveRecord::Associations::Preloader.new.preload(members, user: :webauthn_registrations) if Feature.enabled?(:webauthn)
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 4c6ed399bf9..39b5949ea7a 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1152,7 +1152,7 @@ class MergeRequest < ApplicationRecord
# rubocop: disable CodeReuse/ServiceClass
def mergeable_state?(skip_ci_check: false, skip_discussions_check: false)
- if Feature.enabled?(:improved_mergeability_checks, self.project, default_enabled: :yaml)
+ if Feature.enabled?(:improved_mergeability_checks, self.project)
additional_checks = MergeRequests::Mergeability::RunChecksService.new(
merge_request: self,
params: {
@@ -1438,30 +1438,8 @@ class MergeRequest < ApplicationRecord
actual_head_pipeline.success?
end
- ##
- # This method is for looking for active environments which created via pipelines for merge requests.
- # Since deployments run on a merge request ref (e.g. `refs/merge-requests/:iid/head`),
- # we cannot look up environments with source branch name.
- def legacy_environments
- return Environment.none unless actual_head_pipeline&.merge_request?
-
- build_for_actual_head_pipeline = Ci::Build.latest.where(pipeline: actual_head_pipeline)
-
- environments = build_for_actual_head_pipeline.joins(:metadata)
- .where.not('ci_builds_metadata.expanded_environment_name' => nil)
- .distinct('ci_builds_metadata.expanded_environment_name')
- .limit(100)
- .pluck(:expanded_environment_name)
-
- Environment.where(project: project, name: environments)
- end
-
def environments_in_head_pipeline(deployment_status: nil)
- if ::Feature.enabled?(:fix_related_environments_for_merge_requests, target_project, default_enabled: :yaml)
- actual_head_pipeline&.environments_in_self_and_descendants(deployment_status: deployment_status) || Environment.none
- else
- legacy_environments
- end
+ actual_head_pipeline&.environments_in_self_and_descendants(deployment_status: deployment_status) || Environment.none
end
def fetch_ref!
@@ -1977,10 +1955,6 @@ class MergeRequest < ApplicationRecord
end
end
- def attention_requested_enabled?
- Feature.enabled?(:mr_attention_requests, project, default_enabled: :yaml)
- end
-
private
attr_accessor :skip_fetch_ref
diff --git a/app/models/merge_request_assignee.rb b/app/models/merge_request_assignee.rb
index 77b46fa50f4..fd8e5860040 100644
--- a/app/models/merge_request_assignee.rb
+++ b/app/models/merge_request_assignee.rb
@@ -10,12 +10,6 @@ class MergeRequestAssignee < ApplicationRecord
scope :in_projects, ->(project_ids) { joins(:merge_request).where(merge_requests: { target_project_id: project_ids }) }
- def set_state
- if Feature.enabled?(:mr_attention_requests, self.merge_request&.project, default_enabled: :yaml)
- self.state = MergeRequestReviewer.find_by(user_id: self.user_id, merge_request_id: self.merge_request_id)&.state || :attention_requested
- end
- end
-
def cache_key
[model_name.cache_key, id, state, assignee.cache_key]
end
diff --git a/app/models/merge_request_reviewer.rb b/app/models/merge_request_reviewer.rb
index 8c75fb2e4e6..4abf0fa09f0 100644
--- a/app/models/merge_request_reviewer.rb
+++ b/app/models/merge_request_reviewer.rb
@@ -6,12 +6,6 @@ class MergeRequestReviewer < ApplicationRecord
belongs_to :merge_request
belongs_to :reviewer, class_name: 'User', foreign_key: :user_id, inverse_of: :merge_request_reviewers
- def set_state
- if Feature.enabled?(:mr_attention_requests, self.merge_request&.project, default_enabled: :yaml)
- self.state = MergeRequestAssignee.find_by(user_id: self.user_id, merge_request_id: self.merge_request_id)&.state || :attention_requested
- end
- end
-
def cache_key
[model_name.cache_key, id, state, reviewer.cache_key]
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 3b75b6d163a..fcd641671f5 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -16,6 +16,7 @@ class Namespace < ApplicationRecord
include Namespaces::Traversal::Linear
include EachBatch
include BlocksUnsafeSerialization
+ include Ci::NamespaceSettings
# Temporary column used for back-filling project namespaces.
# Remove it once the back-filling of all project namespaces is done.
@@ -44,6 +45,7 @@ class Namespace < ApplicationRecord
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :project_statistics
has_one :namespace_settings, inverse_of: :namespace, class_name: 'NamespaceSetting', autosave: true
+ has_one :ci_cd_settings, inverse_of: :namespace, class_name: 'NamespaceCiCdSetting', autosave: true
has_one :namespace_statistics
has_one :namespace_route, foreign_key: :namespace_id, autosave: false, inverse_of: :namespace, class_name: 'Route'
has_many :namespace_members, foreign_key: :member_namespace_id, inverse_of: :member_namespace, class_name: 'Member'
@@ -110,6 +112,8 @@ class Namespace < ApplicationRecord
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :avatar_url, to: :owner, allow_nil: true
+ delegate :prevent_sharing_groups_outside_hierarchy, :prevent_sharing_groups_outside_hierarchy=,
+ to: :namespace_settings, allow_nil: true
after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_parent_id? }
@@ -126,7 +130,7 @@ class Namespace < ApplicationRecord
before_destroy(prepend: true) { prepare_for_destroy }
after_destroy :rm_dir
after_commit :expire_child_caches, on: :update, if: -> {
- Feature.enabled?(:cached_route_lookups, self, type: :ops, default_enabled: :yaml) &&
+ Feature.enabled?(:cached_route_lookups, self, type: :ops) &&
saved_change_to_name? || saved_change_to_path? || saved_change_to_parent_id?
}
@@ -238,11 +242,11 @@ class Namespace < ApplicationRecord
return unless host.ends_with?(gitlab_host)
name = host.delete_suffix(gitlab_host)
- Namespace.where(parent_id: nil).by_path(name)
+ Namespace.top_most.by_path(name)
end
def top_most
- where(parent_id: nil)
+ by_parent(nil)
end
end
@@ -351,7 +355,7 @@ class Namespace < ApplicationRecord
# Includes projects from this namespace and projects from all subgroups
# that belongs to this namespace
def all_projects
- if Feature.enabled?(:recursive_approach_for_all_projects, default_enabled: :yaml)
+ if Feature.enabled?(:recursive_approach_for_all_projects)
namespace = user_namespace? ? self : self_and_descendant_ids
Project.where(namespace: namespace)
else
@@ -372,6 +376,10 @@ class Namespace < ApplicationRecord
false
end
+ def all_project_ids_except(ids)
+ all_projects.where.not(id: ids).pluck(:id)
+ end
+
# Deprecated, use #licensed_feature_available? instead. Remove once Namespace#feature_available? isn't used anymore.
def feature_available?(feature, _user = nil)
licensed_feature_available?(feature)
@@ -512,7 +520,7 @@ class Namespace < ApplicationRecord
end
def issue_repositioning_disabled?
- Feature.enabled?(:block_issue_repositioning, self, type: :ops, default_enabled: :yaml)
+ Feature.enabled?(:block_issue_repositioning, self, type: :ops)
end
def storage_enforcement_date
@@ -521,6 +529,12 @@ class Namespace < ApplicationRecord
nil
end
+ def certificate_based_clusters_enabled?
+ ::Gitlab::SafeRequestStore.fetch("certificate_based_clusters:ns:#{self.id}") do
+ Feature.enabled?(:certificate_based_clusters, self, type: :ops)
+ end
+ end
+
private
def expire_child_caches
@@ -634,7 +648,7 @@ class Namespace < ApplicationRecord
end
def cache_first_auto_devops_config?
- ::Feature.enabled?(:namespaces_cache_first_auto_devops_config, default_enabled: :yaml)
+ ::Feature.enabled?(:namespaces_cache_first_auto_devops_config)
end
def write_projects_repository_config
diff --git a/app/models/namespace_ci_cd_setting.rb b/app/models/namespace_ci_cd_setting.rb
new file mode 100644
index 00000000000..c9c3a3206b6
--- /dev/null
+++ b/app/models/namespace_ci_cd_setting.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class NamespaceCiCdSetting < ApplicationRecord # rubocop:disable Gitlab/NamespacedClass
+ belongs_to :namespace, inverse_of: :ci_cd_settings
+
+ self.primary_key = :namespace_id
+end
+
+NamespaceCiCdSetting.prepend_mod
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index 6320e0bc39d..b0350b0288f 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -77,38 +77,38 @@ module Namespaces
end
def sync_traversal_ids?
- Feature.enabled?(:sync_traversal_ids, root_ancestor, default_enabled: :yaml)
+ Feature.enabled?(:sync_traversal_ids, root_ancestor)
end
def use_traversal_ids?
- return false unless Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
+ return false unless Feature.enabled?(:use_traversal_ids)
traversal_ids.present?
end
def use_traversal_ids_for_self_and_hierarchy?
return false unless use_traversal_ids?
- return false unless Feature.enabled?(:use_traversal_ids_for_self_and_hierarchy, root_ancestor, default_enabled: :yaml)
+ return false unless Feature.enabled?(:use_traversal_ids_for_self_and_hierarchy, root_ancestor)
traversal_ids.present?
end
def use_traversal_ids_for_ancestors?
return false unless use_traversal_ids?
- return false unless Feature.enabled?(:use_traversal_ids_for_ancestors, root_ancestor, default_enabled: :yaml)
+ return false unless Feature.enabled?(:use_traversal_ids_for_ancestors, root_ancestor)
traversal_ids.present?
end
def use_traversal_ids_for_ancestors_upto?
return false unless use_traversal_ids?
- return false unless Feature.enabled?(:use_traversal_ids_for_ancestors_upto, root_ancestor, default_enabled: :yaml)
+ return false unless Feature.enabled?(:use_traversal_ids_for_ancestors_upto, root_ancestor)
traversal_ids.present?
end
def use_traversal_ids_for_root_ancestor?
- return false unless Feature.enabled?(:use_traversal_ids_for_root_ancestor, default_enabled: :yaml)
+ return false unless Feature.enabled?(:use_traversal_ids_for_root_ancestor)
traversal_ids.present?
end
diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb
index 0cac4c9143a..f0e9a8feeb2 100644
--- a/app/models/namespaces/traversal/linear_scopes.rb
+++ b/app/models/namespaces/traversal/linear_scopes.rb
@@ -55,7 +55,7 @@ module Namespaces
def self_and_descendants(include_self: true)
return super unless use_traversal_ids_for_descendants_scopes?
- if Feature.enabled?(:traversal_ids_btree, default_enabled: :yaml)
+ if Feature.enabled?(:traversal_ids_btree)
self_and_descendants_with_comparison_operators(include_self: include_self)
else
records = self_and_descendants_with_duplicates_with_array_operator(include_self: include_self)
@@ -67,7 +67,7 @@ module Namespaces
def self_and_descendant_ids(include_self: true)
return super unless use_traversal_ids_for_descendants_scopes?
- if Feature.enabled?(:traversal_ids_btree, default_enabled: :yaml)
+ if Feature.enabled?(:traversal_ids_btree)
self_and_descendants_with_comparison_operators(include_self: include_self).as_ids
else
self_and_descendants_with_duplicates_with_array_operator(include_self: include_self)
@@ -102,26 +102,26 @@ module Namespaces
private
def use_traversal_ids?
- Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
+ Feature.enabled?(:use_traversal_ids)
end
def use_traversal_ids_roots?
- Feature.enabled?(:use_traversal_ids_roots, default_enabled: :yaml) &&
+ Feature.enabled?(:use_traversal_ids_roots) &&
use_traversal_ids?
end
def use_traversal_ids_for_ancestor_scopes?
- Feature.enabled?(:use_traversal_ids_for_ancestor_scopes, default_enabled: :yaml) &&
+ Feature.enabled?(:use_traversal_ids_for_ancestor_scopes) &&
use_traversal_ids?
end
def use_traversal_ids_for_descendants_scopes?
- Feature.enabled?(:use_traversal_ids_for_descendants_scopes, default_enabled: :yaml) &&
+ Feature.enabled?(:use_traversal_ids_for_descendants_scopes) &&
use_traversal_ids?
end
def use_traversal_ids_for_self_and_hierarchy_scopes?
- Feature.enabled?(:use_traversal_ids_for_self_and_hierarchy_scopes, default_enabled: :yaml) &&
+ Feature.enabled?(:use_traversal_ids_for_self_and_hierarchy_scopes) &&
use_traversal_ids?
end
diff --git a/app/models/packages/build_info.rb b/app/models/packages/build_info.rb
index 38245bef7a5..61e2194006b 100644
--- a/app/models/packages/build_info.rb
+++ b/app/models/packages/build_info.rb
@@ -7,6 +7,6 @@ class Packages::BuildInfo < ApplicationRecord
scope :pluck_pipeline_ids, -> { pluck(:pipeline_id) }
scope :without_empty_pipelines, -> { where.not(pipeline_id: nil) }
scope :order_by_pipeline_id, -> (direction) { order(pipeline_id: direction) }
- scope :with_pipeline_id_less_than, -> (pipeline_id) { where("pipeline_id < ?", pipeline_id) }
- scope :with_pipeline_id_greater_than, -> (pipeline_id) { where("pipeline_id > ?", pipeline_id) }
+ scope :with_pipeline_id_less_than, -> (pipeline_id) { where("#{table_name}.pipeline_id < ?", pipeline_id) }
+ scope :with_pipeline_id_greater_than, -> (pipeline_id) { where("#{table_name}.pipeline_id > ?", pipeline_id) }
end
diff --git a/app/models/packages/cleanup.rb b/app/models/packages/cleanup.rb
new file mode 100644
index 00000000000..16bba4f445d
--- /dev/null
+++ b/app/models/packages/cleanup.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+module Packages
+ module Cleanup
+ def self.table_name_prefix
+ 'packages_cleanup_'
+ end
+ end
+end
diff --git a/app/models/packages/cleanup/policy.rb b/app/models/packages/cleanup/policy.rb
new file mode 100644
index 00000000000..87c101cfb8c
--- /dev/null
+++ b/app/models/packages/cleanup/policy.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Packages
+ module Cleanup
+ class Policy < ApplicationRecord
+ include Schedulable
+
+ KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES = %w[all 1 10 20 30 40 50].freeze
+
+ self.primary_key = :project_id
+
+ belongs_to :project
+
+ validates :project, presence: true
+ validates :keep_n_duplicated_package_files,
+ inclusion: {
+ in: KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES,
+ message: 'keep_n_duplicated_package_files is invalid'
+ }
+
+ # used by Schedulable
+ def self.active
+ where.not(keep_n_duplicated_package_files: 'all')
+ end
+
+ def set_next_run_at
+ # fixed cadence of 12 hours
+ self.next_run_at = Time.zone.now + 12.hours
+ end
+ end
+ end
+end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 2804588be85..93119bbff1f 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -245,8 +245,9 @@ class PagesDomain < ApplicationRecord
def validate_pages_domain
return unless domain
- if domain.downcase.ends_with?(".#{Settings.pages.host.downcase}") || domain.casecmp(Settings.pages.host) == 0
- self.errors.add(:domain, "#{Settings.pages.host} and its subdomains cannot be used as custom pages domains. Please compare our documentation at https://docs.gitlab.com/ee/administration/pages/#advanced-configuration against your configuration.")
+ if domain.downcase.ends_with?(".#{Settings.pages.host.downcase}")
+ error_template = _("Subdomains of the Pages root domain %{root_domain} are reserved and cannot be used as custom Pages domains.")
+ self.errors.add(:domain, error_template % { root_domain: Settings.pages.host })
end
end
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 021ff789b13..68ba3d6eab4 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -49,10 +49,6 @@ class PersonalAccessToken < ApplicationRecord
!revoked? && !expired?
end
- def expired_but_not_enforced?
- false
- end
-
def self.redis_getdel(user_id)
Gitlab::Redis::SharedState.with do |redis|
redis_key = redis_shared_state_key(user_id)
diff --git a/app/models/preloaders/group_root_ancestor_preloader.rb b/app/models/preloaders/group_root_ancestor_preloader.rb
index 3ca713d9635..29c60e90964 100644
--- a/app/models/preloaders/group_root_ancestor_preloader.rb
+++ b/app/models/preloaders/group_root_ancestor_preloader.rb
@@ -8,7 +8,7 @@ module Preloaders
end
def execute
- return unless ::Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
+ return unless ::Feature.enabled?(:use_traversal_ids)
# type == 'Group' condition located on subquery to prevent a filter in the query
root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
diff --git a/app/models/preloaders/user_max_access_level_in_groups_preloader.rb b/app/models/preloaders/user_max_access_level_in_groups_preloader.rb
index 2cd54b975f3..8df986b47a2 100644
--- a/app/models/preloaders/user_max_access_level_in_groups_preloader.rb
+++ b/app/models/preloaders/user_max_access_level_in_groups_preloader.rb
@@ -10,7 +10,7 @@ module Preloaders
end
def execute
- if ::Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
+ if ::Feature.enabled?(:use_traversal_ids)
preload_with_traversal_ids
else
preload_direct_memberships
diff --git a/app/models/preloaders/user_max_access_level_in_projects_preloader.rb b/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
index 3764e9dcb16..2e2272a2ef5 100644
--- a/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
+++ b/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
@@ -5,25 +5,50 @@ module Preloaders
# stores the values in requests store via the ProjectTeam class.
class UserMaxAccessLevelInProjectsPreloader
def initialize(projects, user)
- @projects = projects
+ @projects = if projects.is_a?(Array)
+ Project.where(id: projects)
+ else
+ # Push projects base query in to a sub-select to avoid
+ # table name clashes. Performs better than aliasing.
+ Project.where(id: projects.reselect(:id))
+ end
+
@user = user
end
def execute
- # Use reselect to override the existing select to prevent
- # the error `subquery has too many columns`
- # NotificationsController passes in an Array so we need to check the type
- project_ids = @projects.is_a?(ActiveRecord::Relation) ? @projects.reselect(:id) : @projects
- access_levels = @user
- .project_authorizations
- .where(project_id: project_ids)
- .group(:project_id)
- .maximum(:access_level)
-
- @projects.each do |project|
- access_level = access_levels[project.id] || Gitlab::Access::NO_ACCESS
+ project_authorizations = ProjectAuthorization.arel_table
+
+ auths = @projects
+ .select(
+ Project.default_select_columns,
+ project_authorizations[:user_id],
+ project_authorizations[:access_level]
+ )
+ .joins(project_auth_join)
+
+ auths.each do |project|
+ access_level = project.access_level || Gitlab::Access::NO_ACCESS
ProjectTeam.new(project).write_member_access_for_user_id(@user.id, access_level)
end
end
+
+ private
+
+ def project_auth_join
+ project_authorizations = ProjectAuthorization.arel_table
+ projects = Project.arel_table
+
+ projects
+ .join(
+ project_authorizations.as(project_authorizations.name),
+ Arel::Nodes::OuterJoin
+ )
+ .on(
+ project_authorizations[:project_id].eq(projects[:id])
+ .and(project_authorizations[:user_id].eq(@user.id))
+ )
+ .join_sources
+ end
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index f7182d1645c..f4e39524e47 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -49,6 +49,7 @@ class Project < ApplicationRecord
ignore_columns :container_registry_enabled, remove_after: '2021-09-22', remove_with: '14.4'
BoardLimitExceeded = Class.new(StandardError)
+ ExportLimitExceeded = Class.new(StandardError)
ignore_columns :mirror_last_update_at, :mirror_last_successful_update_at, remove_after: '2021-09-22', remove_with: '14.4'
ignore_columns :pull_mirror_branch_prefix, remove_after: '2021-09-22', remove_with: '14.4'
@@ -112,7 +113,7 @@ class Project < ApplicationRecord
default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path }
add_authentication_token_field :runners_token,
- encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
+ encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption) ? :optional : :required },
prefix: RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
@@ -237,6 +238,7 @@ class Project < ApplicationRecord
has_many :package_files, through: :packages, class_name: 'Packages::PackageFile'
# debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
has_many :debian_distributions, class_name: 'Packages::Debian::ProjectDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_one :packages_cleanup_policy, class_name: 'Packages::Cleanup::Policy', inverse_of: :project
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -409,7 +411,6 @@ class Project < ApplicationRecord
has_one :operations_feature_flags_client, class_name: 'Operations::FeatureFlagsClient'
has_many :operations_feature_flags_user_lists, class_name: 'Operations::FeatureFlags::UserList'
- has_many :error_tracking_errors, inverse_of: :project, class_name: 'ErrorTracking::Error'
has_many :error_tracking_client_keys, inverse_of: :project, class_name: 'ErrorTracking::ClientKey'
has_many :timelogs
@@ -448,6 +449,7 @@ class Project < ApplicationRecord
to: :project_feature, allow_nil: true
alias_method :container_registry_enabled, :container_registry_enabled?
delegate :show_default_award_emojis, :show_default_award_emojis=, :show_default_award_emojis?,
+ :enforce_auth_checks_on_uploads, :enforce_auth_checks_on_uploads=, :enforce_auth_checks_on_uploads?,
:warn_about_potentially_unwanted_characters, :warn_about_potentially_unwanted_characters=, :warn_about_potentially_unwanted_characters?,
to: :project_setting, allow_nil: true
delegate :scheduled?, :started?, :in_progress?, :failed?, :finished?,
@@ -462,7 +464,7 @@ class Project < ApplicationRecord
delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_owner, :add_role, to: :team
delegate :group_runners_enabled, :group_runners_enabled=, to: :ci_cd_settings, allow_nil: true
- delegate :root_ancestor, to: :namespace, allow_nil: true
+ delegate :root_ancestor, :certificate_based_clusters_enabled?, to: :namespace, allow_nil: true
delegate :last_pipeline, to: :commit, allow_nil: true
delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true
delegate :dashboard_timezone, to: :metrics_setting, allow_nil: true, prefix: true
@@ -471,6 +473,7 @@ class Project < ApplicationRecord
delegate :job_token_scope_enabled, :job_token_scope_enabled=, to: :ci_cd_settings, prefix: :ci, allow_nil: true
delegate :keep_latest_artifact, :keep_latest_artifact=, to: :ci_cd_settings, allow_nil: true
delegate :restrict_user_defined_variables, :restrict_user_defined_variables=, to: :ci_cd_settings, allow_nil: true
+ delegate :separated_caches, :separated_caches=, to: :ci_cd_settings, prefix: :ci, allow_nil: true
delegate :runner_token_expiration_interval, :runner_token_expiration_interval=, :runner_token_expiration_interval_human_readable, :runner_token_expiration_interval_human_readable=, to: :ci_cd_settings, allow_nil: true
delegate :actual_limits, :actual_plan_name, :actual_plan, to: :namespace, allow_nil: true
delegate :allow_merge_on_skipped_pipeline, :allow_merge_on_skipped_pipeline?,
@@ -742,6 +745,16 @@ class Project < ApplicationRecord
Project.with(cte.to_arel).from(cte.alias_to(Project.arel_table))
end
+ def self.inactive
+ project_statistics = ::ProjectStatistics.arel_table
+ minimum_size_mb = ::Gitlab::CurrentSettings.inactive_projects_min_size_mb.megabytes
+ last_activity_cutoff = ::Gitlab::CurrentSettings.inactive_projects_send_warning_email_after_months.months.ago
+
+ joins(:statistics)
+ .where((project_statistics[:storage_size]).gt(minimum_size_mb))
+ .where('last_activity_at < ?', last_activity_cutoff)
+ end
+
scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
@@ -962,7 +975,7 @@ class Project < ApplicationRecord
end
def ancestors(hierarchy_order: nil)
- if Feature.enabled?(:linear_project_ancestors, self, default_enabled: :yaml)
+ if Feature.enabled?(:linear_project_ancestors, self)
group&.self_and_ancestors(hierarchy_order: hierarchy_order) || Group.none
else
ancestors_upto(hierarchy_order: hierarchy_order)
@@ -1013,6 +1026,10 @@ class Project < ApplicationRecord
packages.where(package_type: package_type).exists?
end
+ def packages_cleanup_policy
+ super || build_packages_cleanup_policy
+ end
+
def first_auto_devops_config
return namespace.first_auto_devops_config if auto_devops&.enabled.nil?
@@ -1020,7 +1037,7 @@ class Project < ApplicationRecord
end
def unlink_forks_upon_visibility_decrease_enabled?
- Feature.enabled?(:unlink_fork_network_upon_visibility_decrease, self, default_enabled: true)
+ Feature.enabled?(:unlink_fork_network_upon_visibility_decrease, self)
end
# LFS and hashed repository storage are required for using Design Management.
@@ -2047,6 +2064,8 @@ class Project < ApplicationRecord
end
def add_export_job(current_user:, after_export_strategy: nil, params: {})
+ check_project_export_limit!
+
job_id = ProjectExportWorker.perform_async(current_user.id, self.id, after_export_strategy, params)
if job_id
@@ -2866,12 +2885,12 @@ class Project < ApplicationRecord
end
def work_items_feature_flag_enabled?
- group&.work_items_feature_flag_enabled? || Feature.enabled?(:work_items, self, default_enabled: :yaml)
+ group&.work_items_feature_flag_enabled? || Feature.enabled?(:work_items, self)
end
def enqueue_record_project_target_platforms
return unless Gitlab.com?
- return unless Feature.enabled?(:record_projects_target_platforms, self, default_enabled: :yaml)
+ return unless Feature.enabled?(:record_projects_target_platforms, self)
Projects::RecordTargetPlatformsWorker.perform_async(id)
end
@@ -2903,7 +2922,7 @@ class Project < ApplicationRecord
if @topic_list != self.topic_list
self.topics.delete_all
self.topics = @topic_list.map do |topic|
- Projects::Topic.where('lower(name) = ?', topic.downcase).order(total_projects_count: :desc).first_or_create(name: topic)
+ Projects::Topic.where('lower(name) = ?', topic.downcase).order(total_projects_count: :desc).first_or_create(name: topic, title: topic)
end
end
@@ -3105,6 +3124,14 @@ class Project < ApplicationRecord
Projects::SyncEvent.enqueue_worker
end
end
+
+ def check_project_export_limit!
+ return if Gitlab::CurrentSettings.current_application_settings.max_export_size == 0
+
+ if self.statistics.storage_size > Gitlab::CurrentSettings.current_application_settings.max_export_size.megabytes
+ raise ExportLimitExceeded, _('The project size exceeds the export limit.')
+ end
+ end
end
Project.prepend_mod_with('Project')
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index 28a493cae33..38740aa20dd 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -18,11 +18,12 @@ class ProjectCiCdSetting < ApplicationRecord
allow_nil: true
default_value_for :forward_deployment_enabled, true
+ default_value_for :separated_caches, true
chronic_duration_attr :runner_token_expiration_interval_human_readable, :runner_token_expiration_interval
def forward_deployment_enabled?
- super && ::Feature.enabled?(:forward_deployment_enabled, project, default_enabled: true)
+ super && ::Feature.enabled?(:forward_deployment_enabled, project)
end
def keep_latest_artifacts_available?
diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb
index fabbd5b49cb..b1c1a5b6697 100644
--- a/app/models/project_import_state.rb
+++ b/app/models/project_import_state.rb
@@ -69,11 +69,9 @@ class ProjectImportState < ApplicationRecord
project.reset_cache_and_import_attrs
if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
- # rubocop: disable CodeReuse/ServiceClass
state.run_after_commit do
- Projects::AfterImportService.new(project).execute
+ Projects::AfterImportWorker.perform_async(project.id)
end
- # rubocop: enable CodeReuse/ServiceClass
end
end
end
diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb
index dc1e9319340..7a3ece4bc92 100644
--- a/app/models/project_pages_metadatum.rb
+++ b/app/models/project_pages_metadatum.rb
@@ -8,8 +8,6 @@ class ProjectPagesMetadatum < ApplicationRecord
self.primary_key = :project_id
- ignore_columns :artifacts_archive_id, remove_with: '15.0', remove_after: '2022-04-22'
-
belongs_to :project, inverse_of: :pages_metadatum
belongs_to :pages_deployment
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index 6cd6eee2616..e9fd7e4446c 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class ProjectSetting < ApplicationRecord
- ALLOWED_TARGET_PLATFORMS = %w(ios osx tvos watchos).freeze
+ ALLOWED_TARGET_PLATFORMS = %w(ios osx tvos watchos android).freeze
belongs_to :project, inverse_of: :project_setting
@@ -21,7 +21,7 @@ class ProjectSetting < ApplicationRecord
validate :validates_mr_default_target_self
default_value_for(:legacy_open_source_license_available) do
- Feature.enabled?(:legacy_open_source_license_available, default_enabled: :yaml, type: :ops)
+ Feature.enabled?(:legacy_open_source_license_available, type: :ops)
end
def squash_enabled_by_default?
@@ -36,6 +36,15 @@ class ProjectSetting < ApplicationRecord
super(val&.map(&:to_s)&.sort)
end
+ def human_squash_option
+ case squash_option
+ when 'never' then 'Do not allow'
+ when 'always' then 'Require'
+ when 'default_on' then 'Encourage'
+ when 'default_off' then 'Allow'
+ end
+ end
+
private
def validates_mr_default_target_self
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 99cec647a98..95fc135f38f 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -19,7 +19,7 @@ class ProjectStatistics < ApplicationRecord
before_save :update_storage_size
- COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count, :snippets_size, :uploads_size].freeze
+ COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count, :snippets_size, :uploads_size, :container_registry_size].freeze
INCREMENTABLE_COLUMNS = {
build_artifacts_size: %i[storage_size],
packages_size: %i[storage_size],
@@ -76,6 +76,12 @@ class ProjectStatistics < ApplicationRecord
self.uploads_size = project.uploads.sum(:size)
end
+ def update_container_registry_size
+ return unless Feature.enabled?(:container_registry_project_statistics, project)
+
+ self.container_registry_size = project.container_repositories_size || 0
+ end
+
# `wiki_size` and `snippets_size` have no default value in the database
# and the column can be nil.
# This means that, when the columns were added, all rows had nil
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 4b89d95c1a3..bb5363598df 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -193,6 +193,10 @@ class ProjectTeam
project.merge_value_to_request_store(User, user_id, project_access_level)
end
+ def purge_member_access_cache_for_user_id(user_id)
+ project.purge_resource_id_from_request_store(User, user_id)
+ end
+
def max_member_access(user_id)
max_member_access_for_user_ids([user_id])[user_id]
end
diff --git a/app/models/projects/topic.rb b/app/models/projects/topic.rb
index 9214a23e259..bc7f94e4374 100644
--- a/app/models/projects/topic.rb
+++ b/app/models/projects/topic.rb
@@ -9,6 +9,7 @@ module Projects
validates :name, presence: true, length: { maximum: 255 }
validates :name, uniqueness: { case_sensitive: false }, if: :name_changed?
+ validates :title, presence: true, length: { maximum: 255 }, on: :create
validates :description, length: { maximum: 1024 }
has_many :project_topics, class_name: 'Projects::ProjectTopic'
@@ -22,13 +23,17 @@ module Projects
reorder(order_expression.desc, arel_table['non_private_projects_count'].desc, arel_table['id'])
end
+ def title_or_name
+ title || name
+ end
+
class << self
def find_by_name_case_insensitive(name)
find_by('LOWER(name) = ?', name.downcase)
end
def search(query)
- fuzzy_search(query, [:name])
+ fuzzy_search(query, [:name, :title])
end
def update_non_private_projects_counter(ids_before, ids_after, project_visibility_level_before, project_visibility_level_after)
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 96002c8668a..77038d52efe 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -68,6 +68,10 @@ class ProtectedBranch < ApplicationRecord
def allow_multiple?(type)
type == :push
end
+
+ def self.downcase_humanized_name
+ name.underscore.humanize.downcase
+ end
end
ProtectedBranch.prepend_mod_with('ProtectedBranch')
diff --git a/app/models/raw_usage_data.rb b/app/models/raw_usage_data.rb
index 6fe3b26b58b..a6844eb8616 100644
--- a/app/models/raw_usage_data.rb
+++ b/app/models/raw_usage_data.rb
@@ -1,9 +1,16 @@
# frozen_string_literal: true
class RawUsageData < ApplicationRecord
+ REPORTING_CADENCE = 7.days.freeze
+
validates :payload, presence: true
validates :recorded_at, presence: true, uniqueness: true
+ scope :for_current_reporting_cycle, -> do
+ where('created_at >= ?', REPORTING_CADENCE.ago.beginning_of_day)
+ .order(created_at: :desc)
+ end
+
def update_version_metadata!(usage_data_id:)
self.update_columns(sent_at: Time.current, version_usage_data_id_value: usage_data_id)
end
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 0be56d8b4a4..2643ef272d8 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -24,7 +24,7 @@ class SystemNoteMetadata < ApplicationRecord
opened closed merged duplicate locked unlocked outdated reviewer
tag due_date pinned_embed cherry_pick health_status approved unapproved
status alert_issue_added relate unrelate new_alert_added severity
- attention_requested attention_request_removed contact
+ attention_requested attention_request_removed contact timeline_event
].freeze
validates :note, presence: true, unless: :importing?
diff --git a/app/models/user.rb b/app/models/user.rb
index 26d47de4f00..8aae4441852 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -239,6 +239,8 @@ class User < ApplicationRecord
has_many :timelogs
+ has_many :resource_label_events, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
+
#
# Validations
#
@@ -941,7 +943,7 @@ class User < ApplicationRecord
end
def two_factor_u2f_enabled?
- return false if Feature.enabled?(:webauthn, default_enabled: :yaml)
+ return false if Feature.enabled?(:webauthn)
if u2f_registrations.loaded?
u2f_registrations.any?
@@ -955,7 +957,7 @@ class User < ApplicationRecord
end
def two_factor_webauthn_enabled?
- return false unless Feature.enabled?(:webauthn, default_enabled: :yaml)
+ return false unless Feature.enabled?(:webauthn)
(webauthn_registrations.loaded? && webauthn_registrations.any?) || (!webauthn_registrations.loaded? && webauthn_registrations.exists?)
end
@@ -1583,7 +1585,7 @@ class User < ApplicationRecord
end
def manageable_groups(include_groups_with_developer_maintainer_access: false)
- owned_and_maintainer_group_hierarchy = if Feature.enabled?(:linear_user_manageable_groups, self, default_enabled: :yaml)
+ owned_and_maintainer_group_hierarchy = if Feature.enabled?(:linear_user_manageable_groups, self)
owned_or_maintainers_groups.self_and_descendants
else
Gitlab::ObjectHierarchy.new(owned_or_maintainers_groups).base_and_descendants
@@ -1673,7 +1675,7 @@ class User < ApplicationRecord
def ci_owned_runners_cross_joins_fix_enabled?
strong_memoize(:ci_owned_runners_cross_joins_fix_enabled) do
- Feature.enabled?(:ci_owned_runners_cross_joins_fix, self, default_enabled: :yaml)
+ Feature.enabled?(:ci_owned_runners_cross_joins_fix, self)
end
end
@@ -1735,7 +1737,7 @@ class User < ApplicationRecord
end
def attention_requested_open_merge_requests_count(force: false)
- if Feature.enabled?(:uncached_mr_attention_requests_count, self, default_enabled: :yaml)
+ if Feature.enabled?(:uncached_mr_attention_requests_count, self)
MergeRequestsFinder.new(self, attention: self.username, state: 'opened', non_archived: true).execute.count
else
Rails.cache.fetch(attention_request_cache_key, force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD) do
@@ -2070,6 +2072,10 @@ class User < ApplicationRecord
end
end
+ def mr_attention_requests_enabled?
+ Feature.enabled?(:mr_attention_requests, self)
+ end
+
protected
# override, from Devise::Validatable
@@ -2313,7 +2319,7 @@ class User < ApplicationRecord
# to avoid querying descendants since they are already covered
# by ancestor namespaces. If the FF is not available fallback to
# inefficient search: https://gitlab.com/gitlab-org/gitlab/-/issues/336436
- unless Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
+ unless Feature.enabled?(:use_traversal_ids)
return Ci::NamespaceMirror.contains_any_of_namespaces(search_members.pluck(:source_id))
end
@@ -2322,7 +2328,7 @@ class User < ApplicationRecord
.shortest_traversal_ids_prefixes
# Use efficient btree index to perform search
- if Feature.enabled?(:ci_owned_runners_unnest_index, self, default_enabled: :yaml)
+ if Feature.enabled?(:ci_owned_runners_unnest_index, self)
Ci::NamespaceMirror.contains_traversal_ids(traversal_ids)
else
Ci::NamespaceMirror.contains_any_of_namespaces(traversal_ids.map(&:last))
diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb
index 62614a851c1..559e93be360 100644
--- a/app/models/user_custom_attribute.rb
+++ b/app/models/user_custom_attribute.rb
@@ -6,13 +6,34 @@ class UserCustomAttribute < ApplicationRecord
validates :user_id, :key, :value, presence: true
validates :key, uniqueness: { scope: [:user_id] }
- def self.upsert_custom_attributes(custom_attributes)
- created_at = DateTime.now
- updated_at = DateTime.now
+ scope :by_key, ->(key) { where(key: key) }
+ scope :by_user_id, ->(user_id) { where(user_id: user_id) }
+ scope :by_updated_at, ->(updated_at) { where(updated_at: updated_at) }
+ scope :arkose_sessions, -> { by_key('arkose_session') }
- custom_attributes.map! do |custom_attribute|
- custom_attribute.merge({ created_at: created_at, updated_at: updated_at })
+ class << self
+ def upsert_custom_attributes(custom_attributes)
+ created_at = DateTime.now
+ updated_at = DateTime.now
+
+ custom_attributes.map! do |custom_attribute|
+ custom_attribute.merge({ created_at: created_at, updated_at: updated_at })
+ end
+ upsert_all(custom_attributes, unique_by: [:user_id, :key])
+ end
+
+ def sessions
+ return none if blocked_users.empty?
+
+ arkose_sessions
+ .by_user_id(blocked_users.map(&:user_id))
+ .select(:value)
+ end
+
+ private
+
+ def blocked_users
+ by_key('blocked_at').by_updated_at(Date.yesterday.all_day)
end
- upsert_all(custom_attributes, unique_by: [:user_id, :key])
end
end
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index a91a3406b22..b3729c84dd6 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -49,7 +49,9 @@ module Users
storage_enforcement_banner_fourth_enforcement_threshold: 46,
attention_requests_top_nav: 47,
attention_requests_side_nav: 48,
- minute_limit_banner: 49
+ minute_limit_banner: 49,
+ preview_user_over_limit_free_plan_alert: 50, # EE-only
+ user_reached_limit_free_plan_alert: 51 # EE-only
}
validates :feature_name,
diff --git a/app/models/users/in_product_marketing_email.rb b/app/models/users/in_product_marketing_email.rb
index f2f1d18339e..82c2e336a09 100644
--- a/app/models/users/in_product_marketing_email.rb
+++ b/app/models/users/in_product_marketing_email.rb
@@ -4,15 +4,28 @@ module Users
class InProductMarketingEmail < ApplicationRecord
include BulkInsertSafe
+ BUILD_IOS_APP_GUIDE = 'build_ios_app_guide'
+ CAMPAIGNS = [BUILD_IOS_APP_GUIDE].freeze
+
belongs_to :user
validates :user, presence: true
- validates :track, presence: true
- validates :series, presence: true
+
+ validates :track, :series, presence: true, if: -> { campaign.blank? }
+ validates :campaign, presence: true, if: -> { track.blank? && series.blank? }
+ validates :campaign, inclusion: { in: CAMPAIGNS }, allow_nil: true
+
validates :user_id, uniqueness: {
scope: [:track, :series],
- message: 'has already been sent'
- }
+ message: 'track series email has already been sent'
+ }, if: -> { track.present? }
+
+ validates :user_id, uniqueness: {
+ scope: :campaign,
+ message: 'campaign email has already been sent'
+ }, if: -> { campaign.present? }
+
+ validate :campaign_or_track_series
enum track: {
create: 0,
@@ -31,23 +44,47 @@ module Users
INACTIVE_TRACK_NAMES = %w(invite_team).freeze
ACTIVE_TRACKS = tracks.except(*INACTIVE_TRACK_NAMES)
+ scope :for_user_with_track_and_series, -> (user, track, series) do
+ where(user: user, track: track, series: series)
+ end
+
scope :without_track_and_series, -> (track, series) do
- users = User.arel_table
- product_emails = arel_table
+ join_condition = for_user.and(for_track_and_series(track, series))
+ users_without_records(join_condition)
+ end
+
+ scope :without_campaign, -> (campaign) do
+ join_condition = for_user.and(for_campaign(campaign))
+ users_without_records(join_condition)
+ end
- join_condition = users[:id].eq(product_emails[:user_id])
- .and(product_emails[:track]).eq(ACTIVE_TRACKS[track])
- .and(product_emails[:series]).eq(series)
+ def self.users_table
+ User.arel_table
+ end
- arel_join = users.join(product_emails, Arel::Nodes::OuterJoin).on(join_condition)
+ def self.distinct_users_sql
+ name = users_table.table_name
+ Arel.sql("DISTINCT ON(#{name}.id) #{name}.*")
+ end
+ def self.users_without_records(condition)
+ arel_join = users_table.join(arel_table, Arel::Nodes::OuterJoin).on(condition)
joins(arel_join.join_sources)
.where(in_product_marketing_emails: { id: nil })
- .select(Arel.sql("DISTINCT ON(#{users.table_name}.id) #{users.table_name}.*"))
+ .select(distinct_users_sql)
end
- scope :for_user_with_track_and_series, -> (user, track, series) do
- where(user: user, track: track, series: series)
+ def self.for_user
+ arel_table[:user_id].eq(users_table[:id])
+ end
+
+ def self.for_campaign(campaign)
+ arel_table[:campaign].eq(campaign)
+ end
+
+ def self.for_track_and_series(track, series)
+ arel_table[:track].eq(ACTIVE_TRACKS[track])
+ .and(arel_table[:series]).eq(series)
end
def self.save_cta_click(user, track, series)
@@ -55,5 +92,13 @@ module Users
email.update(cta_clicked_at: Time.zone.now) if email && email.cta_clicked_at.blank?
end
+
+ private
+
+ def campaign_or_track_series
+ if campaign.present? && (track.present? || series.present?)
+ errors.add(:campaign, 'should be a campaign or a track and series but not both')
+ end
+ end
end
end
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index b3f09b20463..32d70fcd3b7 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -13,43 +13,65 @@ class Wiki
markdown: {
name: 'Markdown',
default_extension: :md,
+ extension_regex: Regexp.new('md|mkdn?|mdown|markdown', 'i'),
created_by_user: true
},
rdoc: {
name: 'RDoc',
default_extension: :rdoc,
+ extension_regex: Regexp.new('rdoc', 'i'),
created_by_user: true
},
asciidoc: {
name: 'AsciiDoc',
default_extension: :asciidoc,
+ extension_regex: Regexp.new('adoc|asciidoc', 'i'),
created_by_user: true
},
org: {
name: 'Org',
default_extension: :org,
+ extension_regex: Regexp.new('org', 'i'),
created_by_user: true
},
textile: {
name: 'Textile',
- default_extension: :textile
+ default_extension: :textile,
+ extension_regex: Regexp.new('textile', 'i')
},
creole: {
name: 'Creole',
- default_extension: :creole
+ default_extension: :creole,
+ extension_regex: Regexp.new('creole', 'i')
},
rest: {
name: 'reStructuredText',
- default_extension: :rst
+ default_extension: :rst,
+ extension_regex: Regexp.new('re?st(\.txt)?', 'i')
},
mediawiki: {
name: 'MediaWiki',
- default_extension: :mediawiki
+ default_extension: :mediawiki,
+ extension_regex: Regexp.new('(media)?wiki', 'i')
+ },
+ pod: {
+ name: 'Pod',
+ default_extension: :pod,
+ extension_regex: Regexp.new('pod', 'i')
+ },
+ plaintext: {
+ name: 'Plain Text',
+ default_extension: :txt,
+ extension_regex: Regexp.new('txt', 'i')
}
}.freeze unless defined?(MARKUPS)
VALID_USER_MARKUPS = MARKUPS.select { |_, v| v[:created_by_user] }.freeze unless defined?(VALID_USER_MARKUPS)
+ unless defined?(ALLOWED_EXTENSIONS_REGEX)
+ ALLOWED_EXTENSIONS_REGEX = Regexp.union(MARKUPS.map { |key, value| value[:extension_regex] }).freeze
+ end
+
CouldNotCreateWikiError = Class.new(StandardError)
HOMEPAGE = 'home'
@@ -205,50 +227,61 @@ class Wiki
end
def create_page(title, content, format = :markdown, message = nil)
- commit = commit_details(:created, message, title)
-
- wiki.write_page(title, format.to_sym, content, commit)
- repository.expire_status_cache if repository.empty?
- after_wiki_activity
-
- true
- rescue Gitlab::Git::Wiki::DuplicatePageError => e
- @error_message = "Duplicate page: #{e.message}"
- false
- end
-
- def update_page(page, content:, title: nil, format: :markdown, message: nil)
- if Feature.enabled?(:gitaly_replace_wiki_update_page, container, default_enabled: :yaml)
+ if Feature.enabled?(:gitaly_replace_wiki_create_page, container, type: :undefined)
with_valid_format(format) do |default_extension|
- title = title.presence || Pathname(page.path).sub_ext('').to_s
-
- # If the format is the same we keep the former extension. This check is for formats
- # that can have more than one extension like Markdown (.md, .markdown)
- # If we don't do this we will override the existing extension.
- extension = page.format != format.to_sym ? default_extension : File.extname(page.path).downcase[1..]
-
- capture_git_error(:updated) do
- repository.update_file(
- user,
- sluggified_full_path(title, extension),
- content,
- previous_path: page.path,
- **multi_commit_options(:updated, message, title))
+ if file_exists_by_regex?(title)
+ raise_duplicate_page_error!
+ end
+ capture_git_error(:created) do
+ create_wiki_repository unless repository_exists?
+ sanitized_path = sluggified_full_path(title, default_extension)
+ repository.create_file(user, sanitized_path, content, **multi_commit_options(:created, message, title))
+ repository.expire_status_cache if repository.empty?
after_wiki_activity
true
+ rescue Gitlab::Git::Index::IndexError
+ raise_duplicate_page_error!
end
end
else
- commit = commit_details(:updated, message, page.title)
-
- wiki.update_page(page.path, title || page.name, format.to_sym, content, commit)
+ commit = commit_details(:created, message, title)
+ wiki.write_page(title, format.to_sym, content, commit)
+ repository.expire_status_cache if repository.empty?
after_wiki_activity
true
end
+ rescue Gitlab::Git::Wiki::DuplicatePageError => e
+ @error_message = _("Duplicate page: %{error_message}" % { error_message: e.message })
+
+ false
+ end
+
+ def update_page(page, content:, title: nil, format: :markdown, message: nil)
+ with_valid_format(format) do |default_extension|
+ title = title.presence || Pathname(page.path).sub_ext('').to_s
+
+ # If the format is the same we keep the former extension. This check is for formats
+ # that can have more than one extension like Markdown (.md, .markdown)
+ # If we don't do this we will override the existing extension.
+ extension = page.format != format.to_sym ? default_extension : File.extname(page.path).downcase[1..]
+
+ capture_git_error(:updated) do
+ repository.update_file(
+ user,
+ sluggified_full_path(title, extension),
+ content,
+ previous_path: page.path,
+ **multi_commit_options(:updated, message, title))
+
+ after_wiki_activity
+
+ true
+ end
+ end
end
def delete_page(page, message = nil)
@@ -393,12 +426,33 @@ class Wiki
yield default_extension
end
+ def file_exists_by_regex?(title)
+ return false unless repository_exists?
+
+ escaped_title = Regexp.escape(sluggified_title(title))
+ regex = Regexp.new("^#{escaped_title}\.#{ALLOWED_EXTENSIONS_REGEX}$", 'i')
+
+ repository.ls_files('HEAD').any? { |s| s =~ regex }
+ end
+
+ def raise_duplicate_page_error!
+ raise Gitlab::Git::Wiki::DuplicatePageError, _('A page with that title already exists')
+ end
+
def sluggified_full_path(title, extension)
sluggified_title(title) + '.' + extension
end
def sluggified_title(title)
- Gitlab::EncodingHelper.encode_utf8_no_detect(title).tr(' ', '-')
+ utf8_encoded_title = Gitlab::EncodingHelper.encode_utf8_no_detect(title)
+
+ sanitized_title(utf8_encoded_title).tr(' ', '-')
+ end
+
+ def sanitized_title(title)
+ clean_absolute_path = File.expand_path(title, '/')
+
+ Pathname.new(clean_absolute_path).relative_path_from('/').to_s
end
end
diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb
index e2d38dc9903..0d390fa131d 100644
--- a/app/models/work_items/type.rb
+++ b/app/models/work_items/type.rb
@@ -41,6 +41,10 @@ module WorkItems
scope :by_type, ->(base_type) { where(base_type: base_type) }
def self.default_by_type(type)
+ found_type = find_by(namespace_id: nil, base_type: type)
+ return found_type if found_type
+
+ Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.upsert_types
find_by(namespace_id: nil, base_type: type)
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 7a49ad3d4aa..a4600c720a3 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -22,6 +22,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
condition(:share_with_group_locked, scope: :subject) { @subject.share_with_group_lock? }
condition(:parent_share_with_group_locked, scope: :subject) { @subject.parent&.share_with_group_lock? }
condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) }
+ condition(:migration_bot, scope: :user) { @user.migration_bot? }
desc "User is a project bot"
condition(:project_bot) { user.project_bot? && access_level >= GroupMember::GUEST }
@@ -54,11 +55,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
end
condition(:dependency_proxy_access_allowed) do
- if Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true)
- access_level(for_any_session: true) >= GroupMember::GUEST || valid_dependency_proxy_deploy_token
- else
- can?(:read_group)
- end
+ access_level(for_any_session: true) >= GroupMember::GUEST || valid_dependency_proxy_deploy_token
end
desc "Deploy token with read_package_registry scope"
@@ -81,7 +78,11 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
condition(:crm_enabled, score: 0, scope: :subject) { Feature.enabled?(:customer_relations, @subject) && @subject.crm_enabled? }
condition(:group_runner_registration_allowed) do
- Feature.disabled?(:runner_registration_control, default_enabled: :yaml) || Gitlab::CurrentSettings.valid_runner_registrars.include?('group')
+ Feature.disabled?(:runner_registration_control) || Gitlab::CurrentSettings.valid_runner_registrars.include?('group')
+ end
+
+ condition(:change_prevent_sharing_groups_outside_hierarchy_available) do
+ change_prevent_sharing_groups_outside_hierarchy_available?
end
rule { can?(:read_group) & design_management_enabled }.policy do
@@ -134,13 +135,11 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
rule { has_access }.enable :read_namespace
rule { developer }.policy do
- enable :admin_milestone
enable :create_metrics_dashboard_annotation
enable :delete_metrics_dashboard_annotation
enable :update_metrics_dashboard_annotation
enable :create_custom_emoji
enable :create_package
- enable :create_package_settings
enable :developer_access
enable :admin_crm_organization
enable :admin_crm_contact
@@ -152,18 +151,19 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :read_container_image
enable :admin_issue_board
enable :admin_label
+ enable :admin_milestone
enable :admin_issue_board_list
enable :admin_issue
enable :read_metrics_dashboard_annotation
enable :read_prometheus
enable :read_package
- enable :read_package_settings
enable :read_crm_organization
enable :read_crm_contact
end
rule { maintainer }.policy do
enable :destroy_package
+ enable :admin_package
enable :create_projects
enable :admin_pipeline
enable :admin_build
@@ -188,7 +188,6 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :set_note_created_at
enable :set_emails_disabled
- enable :change_prevent_sharing_groups_outside_hierarchy
enable :change_new_user_signups_cap
enable :update_default_branch_protection
enable :create_deploy_token
@@ -197,6 +196,10 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :owner_access
end
+ rule { owner & change_prevent_sharing_groups_outside_hierarchy_available }.policy do
+ enable :change_prevent_sharing_groups_outside_hierarchy
+ end
+
rule { can?(:read_nested_project_resources) }.policy do
enable :read_group_activity
enable :read_group_issues
@@ -248,7 +251,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
rule { dependency_proxy_access_allowed & dependency_proxy_available }
.enable :read_dependency_proxy
- rule { developer & dependency_proxy_available }.policy do
+ rule { maintainer & dependency_proxy_available }.policy do
enable :admin_dependency_proxy
end
@@ -283,6 +286,11 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
prevent :register_group_runners
end
+ rule { migration_bot }.policy do
+ enable :read_resource_access_tokens
+ enable :destroy_resource_access_tokens
+ end
+
def access_level(for_any_session: false)
return GroupMember::NO_ACCESS if @user.nil?
return GroupMember::NO_ACCESS unless user_is_user?
@@ -315,6 +323,10 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
def valid_dependency_proxy_deploy_token
@user.is_a?(DeployToken) && @user&.valid_for_dependency_proxy? && @user&.has_access_to_group?(@subject)
end
+
+ def change_prevent_sharing_groups_outside_hierarchy_available?
+ true
+ end
end
GroupPolicy.prepend_mod_with('GroupPolicy')
diff --git a/app/policies/incident_management/timeline_event_policy.rb b/app/policies/incident_management/timeline_event_policy.rb
new file mode 100644
index 00000000000..514a2bf0a56
--- /dev/null
+++ b/app/policies/incident_management/timeline_event_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ class TimelineEventPolicy < ::BasePolicy
+ delegate { @subject.incident }
+ end
+end
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index ed5a0f24ed0..4e6df79773e 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -34,6 +34,14 @@ class IssuablePolicy < BasePolicy
prevent :resolve_note
prevent :award_emoji
end
+
+ rule { can?(:read_issue) }.policy do
+ enable :read_incident_management_timeline_event
+ end
+
+ rule { can?(:read_issue) & can?(:developer_access) }.policy do
+ enable :admin_incident_management_timeline_event
+ end
end
IssuablePolicy.prepend_mod_with('IssuablePolicy')
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index a667c843bc6..a341d1ef661 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -12,8 +12,11 @@ class IssuePolicy < IssuablePolicy
@user && IssueCollection.new([@subject]).visible_to(@user).any?
end
- desc "User can read contacts belonging to the issue group"
- condition(:can_read_crm_contacts, scope: :subject) { @user.can?(:read_crm_contact, @subject.project.root_ancestor) }
+ desc "Project belongs to a group, crm is enabled and user can read contacts in the root group"
+ condition(:can_read_crm_contacts, scope: :subject) do
+ subject.project.group&.crm_enabled? &&
+ @user.can?(:read_crm_contact, @subject.project.root_ancestor)
+ end
desc "Issue is confidential"
condition(:confidential, scope: :subject) { @subject.confidential? }
@@ -81,6 +84,10 @@ class IssuePolicy < IssuablePolicy
enable :set_confidentiality
end
+ rule { can_read_crm_contacts }.policy do
+ enable :read_crm_contacts
+ end
+
rule { can?(:set_issue_metadata) & can_read_crm_contacts }.policy do
enable :set_issue_crm_contacts
end
diff --git a/app/policies/namespace_ci_cd_setting_policy.rb b/app/policies/namespace_ci_cd_setting_policy.rb
new file mode 100644
index 00000000000..d883526b86d
--- /dev/null
+++ b/app/policies/namespace_ci_cd_setting_policy.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class NamespaceCiCdSettingPolicy < BasePolicy # rubocop:disable Gitlab/NamespacedClass
+ delegate { @subject.namespace }
+end
diff --git a/app/policies/namespaces/user_namespace_policy.rb b/app/policies/namespaces/user_namespace_policy.rb
index 09b0f5d608d..028247497e5 100644
--- a/app/policies/namespaces/user_namespace_policy.rb
+++ b/app/policies/namespaces/user_namespace_policy.rb
@@ -14,8 +14,7 @@ module Namespaces
enable :read_namespace
enable :read_statistics
enable :create_jira_connect_subscription
- enable :create_package_settings
- enable :read_package_settings
+ enable :admin_package
end
rule { ~can_create_personal_project }.prevent :create_projects
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 68b288bdc87..60519dc346b 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -195,7 +195,7 @@ class ProjectPolicy < BasePolicy
end
condition(:project_runner_registration_allowed) do
- Feature.disabled?(:runner_registration_control, default_enabled: :yaml) || Gitlab::CurrentSettings.valid_runner_registrars.include?('project')
+ Feature.disabled?(:runner_registration_control) || Gitlab::CurrentSettings.valid_runner_registrars.include?('project')
end
# `:read_project` may be prevented in EE, but `:read_project_for_iids` should
@@ -285,6 +285,7 @@ class ProjectPolicy < BasePolicy
enable :reopen_issue
enable :admin_issue
enable :admin_label
+ enable :admin_milestone
enable :admin_issue_board_list
enable :admin_issue_link
enable :read_commit_status
@@ -370,7 +371,6 @@ class ProjectPolicy < BasePolicy
enable :create_package
enable :admin_issue_board
enable :admin_merge_request
- enable :admin_milestone
enable :update_merge_request
enable :reopen_merge_request
enable :create_commit_status
diff --git a/app/policies/timelog_policy.rb b/app/policies/timelog_policy.rb
index f71c4204639..02380604c60 100644
--- a/app/policies/timelog_policy.rb
+++ b/app/policies/timelog_policy.rb
@@ -2,4 +2,11 @@
class TimelogPolicy < BasePolicy
delegate { @subject.issuable }
+
+ desc "User who created the timelog"
+ condition(:is_author) { @user && @subject.user == @user }
+
+ rule { is_author | can?(:maintainer_access) }.policy do
+ enable :admin_timelog
+ end
end
diff --git a/app/policies/work_item_policy.rb b/app/policies/work_item_policy.rb
index b4723bc7ed8..e191e8d26ca 100644
--- a/app/policies/work_item_policy.rb
+++ b/app/policies/work_item_policy.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
class WorkItemPolicy < IssuePolicy
- rule { can?(:owner_access) | is_author }.enable :delete_work_item
+ condition(:is_member_and_author) { is_project_member? & is_author? }
+
+ rule { can?(:destroy_issue) | is_member_and_author }.enable :delete_work_item
rule { can?(:update_issue) }.enable :update_work_item
diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb
index c2ed40d8b0c..9558eee55a6 100644
--- a/app/presenters/clusterable_presenter.rb
+++ b/app/presenters/clusterable_presenter.rb
@@ -28,10 +28,6 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
polymorphic_path([clusterable, :clusters], options)
end
- def new_path(options = {})
- new_polymorphic_path([clusterable, :cluster], options)
- end
-
def connect_path
polymorphic_path([clusterable, :clusters], action: :connect)
end
@@ -40,22 +36,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
polymorphic_path([clusterable, :clusters], action: :new_cluster_docs)
end
- def authorize_aws_role_path
- polymorphic_path([clusterable, :clusters], action: :authorize_aws_role)
- end
-
def create_user_clusters_path
polymorphic_path([clusterable, :clusters], action: :create_user)
end
- def create_gcp_clusters_path
- polymorphic_path([clusterable, :clusters], action: :create_gcp)
- end
-
- def create_aws_clusters_path
- polymorphic_path([clusterable, :clusters], action: :create_aws)
- end
-
def cluster_status_cluster_path(cluster, params = {})
raise NotImplementedError
end
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index e2fc2b4b485..454e5c0e44a 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -70,7 +70,7 @@ module Clusters
{
'clusters-path': clusterable.index_path,
'dashboard-endpoint': clusterable.metrics_dashboard_path(cluster),
- 'documentation-path': help_page_path('user/project/clusters/index', anchor: 'monitoring-your-kubernetes-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'),
'empty-getting-started-svg-path': image_path('illustrations/monitoring/getting_started.svg'),
'empty-loading-svg-path': image_path('illustrations/monitoring/loading.svg'),
diff --git a/app/presenters/dev_ops_report/metric_presenter.rb b/app/presenters/dev_ops_report/metric_presenter.rb
index ec85c5d3809..1a5b12fa408 100644
--- a/app/presenters/dev_ops_report/metric_presenter.rb
+++ b/app/presenters/dev_ops_report/metric_presenter.rb
@@ -60,7 +60,7 @@ module DevOpsReport
description: 'created per active user',
feature: 'environments',
blog: 'https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/',
- docs: help_page_path('ci/environments')
+ docs: help_page_path('ci/environments/index')
),
Card.new(
metric: metric,
diff --git a/app/presenters/instance_clusterable_presenter.rb b/app/presenters/instance_clusterable_presenter.rb
index 001c9cbb4e9..12b7ce2f5e7 100644
--- a/app/presenters/instance_clusterable_presenter.rb
+++ b/app/presenters/instance_clusterable_presenter.rb
@@ -18,11 +18,6 @@ class InstanceClusterablePresenter < ClusterablePresenter
admin_clusters_path(options)
end
- override :new_path
- def new_path(options = {})
- new_admin_cluster_path(options)
- end
-
override :cluster_status_cluster_path
def cluster_status_cluster_path(cluster, params = {})
cluster_status_admin_cluster_path(cluster, params)
@@ -53,21 +48,6 @@ class InstanceClusterablePresenter < ClusterablePresenter
create_user_admin_clusters_path
end
- override :create_gcp_clusters_path
- def create_gcp_clusters_path
- create_gcp_admin_clusters_path
- end
-
- override :create_aws_clusters_path
- def create_aws_clusters_path
- create_aws_admin_clusters_path
- end
-
- override :authorize_aws_role_path
- def authorize_aws_role_path
- authorize_aws_role_admin_clusters_path
- end
-
override :empty_state_help_text
def empty_state_help_text
s_('ClusterIntegration|Adding an integration will share the cluster across all projects.')
diff --git a/app/presenters/issue_presenter.rb b/app/presenters/issue_presenter.rb
index 75f6d749acb..9b4e7e22165 100644
--- a/app/presenters/issue_presenter.rb
+++ b/app/presenters/issue_presenter.rb
@@ -4,9 +4,7 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
presents ::Issue, as: :issue
def issue_path
- return url_builder.build(issue, only_path: true) unless use_work_items_path?
-
- project_work_items_path(issue.project, work_items_path: issue.id)
+ web_path
end
delegator_override :subscribed?
@@ -17,18 +15,6 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
def project_emails_disabled?
issue.project.emails_disabled?
end
-
- def web_url
- return super unless use_work_items_path?
-
- project_work_items_url(issue.project, work_items_path: issue.id)
- end
-
- private
-
- def use_work_items_path?
- issue.issue_type == 'task' && issue.project.work_items_feature_flag_enabled?
- end
end
IssuePresenter.prepend_mod_with('IssuePresenter')
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 098519cdffe..af1b254c46f 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -36,7 +36,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def statistics_buttons(show_auto_devops_callout:)
[
- upload_anchor_data,
readme_anchor_data,
license_anchor_data,
changelog_anchor_data,
@@ -458,7 +457,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def project_topic_list
strong_memoize(:project_topic_list) do
- project.topics.map(&:name)
+ project.topics.map { |topic| { name: topic.name, title: topic.title_or_name } }
end
end
diff --git a/app/presenters/projects/security/configuration_presenter.rb b/app/presenters/projects/security/configuration_presenter.rb
index 77f4d57ae09..772be0125a0 100644
--- a/app/presenters/projects/security/configuration_presenter.rb
+++ b/app/presenters/projects/security/configuration_presenter.rb
@@ -68,7 +68,7 @@ module Projects
end
def latest_pipeline_path
- return help_page_path('ci/pipelines') unless latest_default_branch_pipeline
+ return help_page_path('ci/pipelines/index') unless latest_default_branch_pipeline
project_pipeline_path(self, latest_default_branch_pipeline)
end
diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb
index dc775fb4160..26b8169d2b0 100644
--- a/app/presenters/user_presenter.rb
+++ b/app/presenters/user_presenter.rb
@@ -21,7 +21,7 @@ class UserPresenter < Gitlab::View::Presenter::Delegated
delegator_override :saved_replies
def saved_replies
- return ::Users::SavedReply.none unless Feature.enabled?(:saved_replies, current_user, default_enabled: :yaml)
+ 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/award_emoji_entity.rb b/app/serializers/award_emoji_entity.rb
index 212931a2fa9..6ca782d8203 100644
--- a/app/serializers/award_emoji_entity.rb
+++ b/app/serializers/award_emoji_entity.rb
@@ -3,4 +3,5 @@
class AwardEmojiEntity < Grape::Entity
expose :name
expose :user, using: API::Entities::UserSafe
+ expose :url
end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 9fd35faf0b7..5f72259f34a 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -148,12 +148,14 @@ class BuildDetailsEntity < Ci::JobEntity
end
def failure_message
- _("This job depends on other jobs with expired/erased artifacts: %{invalid_dependencies}") %
- { invalid_dependencies: invalid_dependencies }
+ # We do not return the invalid_dependencies for all scenarios see https://gitlab.com/gitlab-org/gitlab/-/issues/287772#note_914406387
+ punctuation = invalid_dependencies.empty? ? '.' : ': '
+ _("This job could not start because it could not retrieve the needed artifacts%{punctuation}%{invalid_dependencies}") %
+ { invalid_dependencies: invalid_dependencies, punctuation: punctuation }
end
def help_message(docs_url)
- html_escape(_("Please refer to %{docs_url}")) % { docs_url: "<a href=\"#{docs_url}\">#{html_escape(docs_url)}</a>".html_safe }
+ html_escape(_("<a href=\"#{docs_url}\">Learn more.</a>".html_safe))
end
end
diff --git a/app/serializers/ci/job_entity.rb b/app/serializers/ci/job_entity.rb
index fca3dec74d4..b6b11e54a16 100644
--- a/app/serializers/ci/job_entity.rb
+++ b/app/serializers/ci/job_entity.rb
@@ -8,6 +8,7 @@ module Ci
expose :name
expose :started?, as: :started
+ expose :started_at, if: -> (job) { job.started? }
expose :complete?, as: :complete
expose :archived?, as: :archived
diff --git a/app/serializers/cluster_entity.rb b/app/serializers/cluster_entity.rb
index e2d24e74b29..df72a994143 100644
--- a/app/serializers/cluster_entity.rb
+++ b/app/serializers/cluster_entity.rb
@@ -19,7 +19,7 @@ class ClusterEntity < Grape::Entity
Clusters::ClusterPresenter.new(cluster).show_path # rubocop: disable CodeReuse/Presenter
end
- expose :gitlab_managed_apps_logs_path do |cluster|
+ expose :gitlab_managed_apps_logs_path, if: -> (*) { logging_enabled? } do |cluster|
Clusters::ClusterPresenter.new(cluster, current_user: request.current_user).gitlab_managed_apps_logs_path # rubocop: disable CodeReuse/Presenter
end
@@ -27,7 +27,13 @@ class ClusterEntity < Grape::Entity
Clusters::KubernetesErrorEntity.new(cluster)
end
- expose :enable_advanced_logs_querying do |cluster|
+ expose :enable_advanced_logs_querying, if: -> (*) { logging_enabled? } do |cluster|
cluster.elastic_stack_available?
end
+
+ private
+
+ def logging_enabled?
+ Feature.enabled?(:monitor_logging, object.project)
+ end
end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index 634be365a9d..ac99463bd64 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -103,7 +103,8 @@ class EnvironmentEntity < Grape::Entity
end
def can_read_pod_logs?
- can?(current_user, :read_pod_logs, environment.project)
+ Feature.enabled?(:monitor_logging, environment.project) &&
+ can?(current_user, :read_pod_logs, environment.project)
end
def can_read_deploy_board?
diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb
index a8645c8670d..b13140efea7 100644
--- a/app/serializers/environment_serializer.rb
+++ b/app/serializers/environment_serializer.rb
@@ -81,7 +81,10 @@ class EnvironmentSerializer < BaseSerializer
{
user: [],
cluster: [],
- project: [],
+ project: {
+ route: [],
+ namespace: :route
+ },
deployable: {
user: [],
metadata: [],
diff --git a/app/serializers/issue_board_entity.rb b/app/serializers/issue_board_entity.rb
index 4b9c48f3f7c..bcad28d6aad 100644
--- a/app/serializers/issue_board_entity.rb
+++ b/app/serializers/issue_board_entity.rb
@@ -37,7 +37,7 @@ class IssueBoardEntity < Grape::Entity
end
expose :real_path, if: -> (issue) { issue.project } do |issue|
- project_issue_path(issue.project, issue)
+ Gitlab::UrlBuilder.build(issue, only_path: true)
end
expose :issue_sidebar_endpoint, if: -> (issue) { issue.project } do |issue|
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index fbcfcf84d9b..852a2e62b7d 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -31,7 +31,7 @@ class IssueEntity < IssuableEntity
end
expose :web_url do |issue|
- project_issue_path(issue.project, issue)
+ Gitlab::UrlBuilder.build(issue, only_path: true)
end
expose :current_user do
diff --git a/app/serializers/issue_sidebar_basic_entity.rb b/app/serializers/issue_sidebar_basic_entity.rb
index 7222b5df425..2450c6a4d85 100644
--- a/app/serializers/issue_sidebar_basic_entity.rb
+++ b/app/serializers/issue_sidebar_basic_entity.rb
@@ -12,7 +12,7 @@ class IssueSidebarBasicEntity < IssuableSidebarBasicEntity
end
expose :show_crm_contacts do |issuable|
- current_user&.can?(:read_crm_contact, issuable.project.root_ancestor) &&
+ current_user&.can?(:read_crm_contacts, issuable) &&
CustomerRelations::Contact.exists_for_group?(issuable.project.root_ancestor)
end
end
diff --git a/app/serializers/linked_issue_entity.rb b/app/serializers/linked_issue_entity.rb
index 6ae3f4044db..769e3ed7310 100644
--- a/app/serializers/linked_issue_entity.rb
+++ b/app/serializers/linked_issue_entity.rb
@@ -18,7 +18,7 @@ class LinkedIssueEntity < Grape::Entity
end
expose :path do |link|
- project_issue_path(link.project, link.iid)
+ Gitlab::UrlBuilder.build(link, only_path: true)
end
expose :relation_path
diff --git a/app/serializers/merge_request_user_entity.rb b/app/serializers/merge_request_user_entity.rb
index 97912656bbb..12c573d1a13 100644
--- a/app/serializers/merge_request_user_entity.rb
+++ b/app/serializers/merge_request_user_entity.rb
@@ -20,7 +20,7 @@ class MergeRequestUserEntity < ::API::Entities::UserBasic
find_reviewer_or_assignee(user, options)&.reviewed?
end
- expose :attention_requested, if: satisfies(:present?, :allows_reviewers?, :attention_requested_enabled?) do |user, options|
+ expose :attention_requested, if: ->(_, options) { options[:merge_request].present? && options[:merge_request].allows_reviewers? && request.current_user&.mr_attention_requests_enabled? } do |user, options|
find_reviewer_or_assignee(user, options)&.attention_requested?
end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 21ab20747d0..5bf91ed0a51 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -143,7 +143,7 @@ class MergeRequestWidgetEntity < Grape::Entity
end
expose :security_reports_docs_path do |merge_request|
- help_page_path('user/application_security/index.md', anchor: 'viewing-security-scan-information-in-merge-requests')
+ help_page_path('user/application_security/index.md', anchor: 'view-security-scan-information-in-merge-requests')
end
expose :enabled_reports do |merge_request|
diff --git a/app/serializers/release_entity.rb b/app/serializers/release_entity.rb
index 6777b0f9780..5c3d06b82d3 100644
--- a/app/serializers/release_entity.rb
+++ b/app/serializers/release_entity.rb
@@ -1,6 +1,15 @@
# frozen_string_literal: true
+# TODO: consider removing this entity after https://gitlab.com/gitlab-org/gitlab/-/issues/360631
class ReleaseEntity < Grape::Entity
expose :id
expose :tag # see https://gitlab.com/gitlab-org/gitlab/-/issues/36338
+ expose :name
+ expose :description
+ expose :project_id
+ expose :author_id
+
+ expose :created_at
+ expose :updated_at
+ expose :released_at
end
diff --git a/app/services/alert_management/alerts/update_service.rb b/app/services/alert_management/alerts/update_service.rb
index 0769adc862e..6bdceb0f27b 100644
--- a/app/services/alert_management/alerts/update_service.rb
+++ b/app/services/alert_management/alerts/update_service.rb
@@ -162,8 +162,9 @@ module AlertManagement
end
def filter_duplicate
- # Only need to check if changing to an open status
- return unless params[:status_event] && AlertManagement::Alert.open_status?(status)
+ # Only need to check if changing to a not-resolved status
+ return if params[:status_event].blank? || params[:status_event] == :resolve
+ return unless alert.resolved?
param_errors << unresolved_alert_error if duplicate_alert?
end
@@ -171,24 +172,23 @@ module AlertManagement
def duplicate_alert?
return if alert.fingerprint.blank?
- open_alerts.any? && open_alerts.exclude?(alert)
+ unresolved_alert.present?
end
- def open_alerts
- strong_memoize(:open_alerts) do
- AlertManagement::Alert.for_fingerprint(project, alert.fingerprint).open
+ def unresolved_alert
+ strong_memoize(:unresolved_alert) do
+ AlertManagement::Alert.find_unresolved_alert(project, alert.fingerprint)
end
end
def unresolved_alert_error
_('An %{link_start}alert%{link_end} with the same fingerprint is already open. ' \
'To change the status of this alert, resolve the linked alert.'
- ) % open_alert_url_params
+ ) % unresolved_alert_url_params
end
- def open_alert_url_params
- open_alert = open_alerts.first
- alert_path = Gitlab::Routing.url_helpers.details_project_alert_management_path(project, open_alert)
+ def unresolved_alert_url_params
+ alert_path = Gitlab::Routing.url_helpers.details_project_alert_management_path(project, unresolved_alert)
{
link_start: '<a href="%{url}">'.html_safe % { url: alert_path },
diff --git a/app/services/alert_management/metric_images/upload_service.rb b/app/services/alert_management/metric_images/upload_service.rb
index e9db10594df..46e7e3dbedd 100644
--- a/app/services/alert_management/metric_images/upload_service.rb
+++ b/app/services/alert_management/metric_images/upload_service.rb
@@ -39,7 +39,7 @@ module AlertManagement
private
def can_upload_metrics?
- alert.metric_images_available? && current_user&.can?(:upload_alert_management_metric_image, alert)
+ current_user&.can?(:upload_alert_management_metric_image, alert)
end
end
end
diff --git a/app/services/authorized_project_update/project_access_changed_service.rb b/app/services/authorized_project_update/project_access_changed_service.rb
index 62bf4ced1ae..dafec1fef59 100644
--- a/app/services/authorized_project_update/project_access_changed_service.rb
+++ b/app/services/authorized_project_update/project_access_changed_service.rb
@@ -7,6 +7,8 @@ module AuthorizedProjectUpdate
end
def execute(blocking: true)
+ return if @project_ids.empty?
+
bulk_args = @project_ids.map { |id| [id] }
if blocking
diff --git a/app/services/authorized_project_update/project_create_service.rb b/app/services/authorized_project_update/project_create_service.rb
deleted file mode 100644
index 5809315a066..00000000000
--- a/app/services/authorized_project_update/project_create_service.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-module AuthorizedProjectUpdate
- class ProjectCreateService < BaseService
- BATCH_SIZE = 1000
-
- def initialize(project)
- @project = project
- end
-
- def execute
- group = project.group
-
- unless group
- return ServiceResponse.error(message: 'Project does not have a group')
- end
-
- group.members_from_self_and_ancestors_with_effective_access_level
- .each_batch(of: BATCH_SIZE, column: :user_id) do |members|
- attributes = members.map do |member|
- { user_id: member.user_id, project_id: project.id, access_level: member.access_level }
- end
-
- ProjectAuthorization.insert_all(attributes) unless attributes.empty?
- end
-
- ServiceResponse.success
- end
-
- private
-
- attr_reader :project
- end
-end
diff --git a/app/services/authorized_project_update/project_group_link_create_service.rb b/app/services/authorized_project_update/project_group_link_create_service.rb
deleted file mode 100644
index 10cf4c50569..00000000000
--- a/app/services/authorized_project_update/project_group_link_create_service.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-# frozen_string_literal: true
-
-module AuthorizedProjectUpdate
- class ProjectGroupLinkCreateService < BaseService
- include Gitlab::Utils::StrongMemoize
-
- BATCH_SIZE = 1000
-
- def initialize(project, group, group_access = nil)
- @project = project
- @group = group
- @group_access = group_access
- end
-
- def execute
- group.members_from_self_and_ancestors_with_effective_access_level
- .each_batch(of: BATCH_SIZE, column: :user_id) do |members|
- existing_authorizations = existing_project_authorizations(members)
- authorizations_to_create = []
- user_ids_to_delete = []
-
- members.each do |member|
- new_access_level = access_level(member.access_level)
- existing_access_level = existing_authorizations[member.user_id]
-
- if existing_access_level
- # User might already have access to the project unrelated to the
- # current project share
- next if existing_access_level >= new_access_level
-
- user_ids_to_delete << member.user_id
- end
-
- authorizations_to_create << { user_id: member.user_id,
- project_id: project.id,
- access_level: new_access_level }
- end
-
- update_authorizations(user_ids_to_delete, authorizations_to_create)
- end
-
- ServiceResponse.success
- end
-
- private
-
- attr_reader :project, :group, :group_access
-
- def access_level(membership_access_level)
- return membership_access_level unless group_access
-
- # access level (role) must not be higher than the max access level (role) set when
- # creating the project share
- [membership_access_level, group_access].min
- end
-
- def existing_project_authorizations(members)
- user_ids = members.map(&:user_id)
-
- ProjectAuthorization.where(project_id: project.id, user_id: user_ids) # rubocop: disable CodeReuse/ActiveRecord
- .select(:user_id, :access_level)
- .each_with_object({}) do |authorization, hash|
- hash[authorization.user_id] = authorization.access_level
- end
- end
-
- def update_authorizations(user_ids_to_delete, authorizations_to_create)
- project.remove_project_authorizations(user_ids_to_delete) if user_ids_to_delete.any?
- ProjectAuthorization.insert_all_in_batches(authorizations_to_create) if authorizations_to_create.any?
- end
- end
-end
diff --git a/app/services/bulk_imports/file_decompression_service.rb b/app/services/bulk_imports/file_decompression_service.rb
index b76746b199f..41616fc1c75 100644
--- a/app/services/bulk_imports/file_decompression_service.rb
+++ b/app/services/bulk_imports/file_decompression_service.rb
@@ -21,7 +21,7 @@ module BulkImports
def execute
validate_tmpdir
validate_filepath
- validate_decompressed_file_size if Feature.enabled?(:validate_import_decompressed_archive_size, default_enabled: :yaml)
+ validate_decompressed_file_size if Feature.enabled?(:validate_import_decompressed_archive_size)
validate_symlink(filepath)
decompress_file
diff --git a/app/services/ci/create_downstream_pipeline_service.rb b/app/services/ci/create_downstream_pipeline_service.rb
index 0a0c614bb87..b38b3b93353 100644
--- a/app/services/ci/create_downstream_pipeline_service.rb
+++ b/app/services/ci/create_downstream_pipeline_service.rb
@@ -6,6 +6,7 @@ module Ci
# specifications.
class CreateDownstreamPipelineService < ::BaseService
include Gitlab::Utils::StrongMemoize
+ include Ci::DownstreamPipelineHelpers
DuplicateDownstreamPipelineError = Class.new(StandardError)
@@ -37,6 +38,8 @@ module Ci
.execute(pipeline_params.fetch(:source), **pipeline_params[:execute_params])
.payload
+ log_downstream_pipeline_creation(downstream_pipeline)
+
downstream_pipeline.tap do |pipeline|
update_bridge_status!(@bridge, pipeline)
end
diff --git a/app/services/ci/expire_pipeline_cache_service.rb b/app/services/ci/expire_pipeline_cache_service.rb
index 8622b1a5863..bf2355c447a 100644
--- a/app/services/ci/expire_pipeline_cache_service.rb
+++ b/app/services/ci/expire_pipeline_cache_service.rb
@@ -86,7 +86,7 @@ module Ci
etag_paths << path
end
- pipeline.self_with_upstreams_and_downstreams.includes(project: [:route, { namespace: :route }]).each do |relative_pipeline| # rubocop: disable CodeReuse/ActiveRecord
+ pipeline.all_pipelines_in_hierarchy.includes(project: [:route, { namespace: :route }]).each do |relative_pipeline| # rubocop: disable CodeReuse/ActiveRecord
etag_paths << project_pipeline_path(relative_pipeline.project, relative_pipeline)
etag_paths << graphql_pipeline_path(relative_pipeline)
etag_paths << graphql_pipeline_sha_path(relative_pipeline.sha)
diff --git a/app/services/ci/generate_kubeconfig_service.rb b/app/services/ci/generate_kubeconfig_service.rb
index 18f68c0ff09..894ab8e8505 100644
--- a/app/services/ci/generate_kubeconfig_service.rb
+++ b/app/services/ci/generate_kubeconfig_service.rb
@@ -2,8 +2,9 @@
module Ci
class GenerateKubeconfigService
- def initialize(build)
- @build = build
+ def initialize(pipeline, token:)
+ @pipeline = pipeline
+ @token = token
@template = Gitlab::Kubernetes::Kubeconfig::Template.new
end
@@ -33,10 +34,10 @@ module Ci
private
- attr_reader :build, :template
+ attr_reader :pipeline, :token, :template
def agents
- build.pipeline.authorized_cluster_agents
+ pipeline.authorized_cluster_agents
end
def cluster_name
@@ -52,7 +53,7 @@ module Ci
end
def agent_token(agent)
- ['ci', agent.id, build.token].join(delimiter)
+ ['ci', agent.id, token].join(delimiter)
end
def delimiter
diff --git a/app/services/ci/job_artifacts/create_service.rb b/app/services/ci/job_artifacts/create_service.rb
index 7c67a2e175d..635111130d6 100644
--- a/app/services/ci/job_artifacts/create_service.rb
+++ b/app/services/ci/job_artifacts/create_service.rb
@@ -133,7 +133,7 @@ module Ci
job.update_column(:artifacts_expire_at, artifact.expire_at)
end
- success
+ success(artifact: artifact)
rescue ActiveRecord::RecordNotUnique => error
track_exception(error, params)
error('another artifact of the same type already exists', :bad_request)
diff --git a/app/services/ci/job_artifacts/destroy_batch_service.rb b/app/services/ci/job_artifacts/destroy_batch_service.rb
index 90d157373c3..5121a8b0a8b 100644
--- a/app/services/ci/job_artifacts/destroy_batch_service.rb
+++ b/app/services/ci/job_artifacts/destroy_batch_service.rb
@@ -33,9 +33,11 @@ module Ci
destroy_related_records(@job_artifacts)
- Ci::DeletedObject.transaction do
- Ci::DeletedObject.bulk_import(@job_artifacts, @pick_up_at)
- Ci::JobArtifact.id_in(@job_artifacts.map(&:id)).delete_all
+ destroy_around_hook(@job_artifacts) do
+ Ci::DeletedObject.transaction do
+ Ci::DeletedObject.bulk_import(@job_artifacts, @pick_up_at)
+ Ci::JobArtifact.id_in(@job_artifacts.map(&:id)).delete_all
+ end
end
after_batch_destroy_hook(@job_artifacts)
@@ -52,6 +54,13 @@ module Ci
private
# Overriden in EE
+ # :nocov:
+ def destroy_around_hook(artifacts)
+ yield
+ end
+ # :nocov:
+
+ # Overriden in EE
def destroy_related_records(artifacts); end
# Overriden in EE
@@ -121,7 +130,7 @@ module Ci
end
def fix_expire_at?
- Feature.enabled?(:ci_detect_wrongly_expired_artifacts, default_enabled: :yaml)
+ Feature.enabled?(:ci_detect_wrongly_expired_artifacts)
end
def wrongly_expired?(artifact)
diff --git a/app/services/ci/pipeline_creation/start_pipeline_service.rb b/app/services/ci/pipeline_creation/start_pipeline_service.rb
index 27c12caaa0a..65a045f32dd 100644
--- a/app/services/ci/pipeline_creation/start_pipeline_service.rb
+++ b/app/services/ci/pipeline_creation/start_pipeline_service.rb
@@ -10,6 +10,11 @@ module Ci
end
def execute
+ ##
+ # Create a persistent ref for the pipeline.
+ # The pipeline ref is fetched in the jobs and deleted when the pipeline transitions to a finished state.
+ pipeline.ensure_persistent_ref
+
Ci::ProcessPipelineService.new(pipeline).execute
end
end
diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb
index 7746382b845..06eb1aee8e6 100644
--- a/app/services/ci/pipeline_trigger_service.rb
+++ b/app/services/ci/pipeline_trigger_service.rb
@@ -4,6 +4,7 @@ module Ci
class PipelineTriggerService < BaseService
include Gitlab::Utils::StrongMemoize
include Services::ReturnServiceResponses
+ include Ci::DownstreamPipelineHelpers
def execute
if trigger_from_token
@@ -69,6 +70,7 @@ module Ci
pipeline.source_pipeline = source
end
+ log_downstream_pipeline_creation(response.payload)
pipeline_service_response(response.payload)
end
diff --git a/app/services/ci/queue/build_queue_service.rb b/app/services/ci/queue/build_queue_service.rb
index 9f476c8a785..fefbdb151ec 100644
--- a/app/services/ci/queue/build_queue_service.rb
+++ b/app/services/ci/queue/build_queue_service.rb
@@ -80,7 +80,7 @@ module Ci
def strategy
strong_memoize(:strategy) do
- if ::Feature.enabled?(:ci_pending_builds_queue_source, runner, default_enabled: :yaml)
+ if ::Feature.enabled?(:ci_pending_builds_queue_source, runner)
Queue::PendingBuildsStrategy.new(runner)
else
Queue::BuildsTableStrategy.new(runner)
diff --git a/app/services/ci/queue/builds_table_strategy.rb b/app/services/ci/queue/builds_table_strategy.rb
index 237dd510d50..c27c10bd18d 100644
--- a/app/services/ci/queue/builds_table_strategy.rb
+++ b/app/services/ci/queue/builds_table_strategy.rb
@@ -18,7 +18,7 @@ module Ci
.joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id')
.where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0')
- if Feature.enabled?(:ci_queueing_disaster_recovery_disable_fair_scheduling, runner, type: :ops, default_enabled: :yaml)
+ if Feature.enabled?(:ci_queueing_disaster_recovery_disable_fair_scheduling, runner, type: :ops)
# if disaster recovery is enabled, we fallback to FIFO scheduling
relation.order('ci_builds.id ASC')
else
diff --git a/app/services/ci/queue/pending_builds_strategy.rb b/app/services/ci/queue/pending_builds_strategy.rb
index 47158b8ea1d..f2eba0681db 100644
--- a/app/services/ci/queue/pending_builds_strategy.rb
+++ b/app/services/ci/queue/pending_builds_strategy.rb
@@ -51,7 +51,7 @@ module Ci
end
def use_denormalized_data_strategy?
- ::Feature.enabled?(:ci_queuing_use_denormalized_data_strategy, default_enabled: :yaml)
+ ::Feature.enabled?(:ci_queuing_use_denormalized_data_strategy)
end
private
@@ -70,7 +70,7 @@ module Ci
end
def builds_ordered_for_shared_runners(relation)
- if Feature.enabled?(:ci_queueing_disaster_recovery_disable_fair_scheduling, runner, type: :ops, default_enabled: :yaml)
+ if Feature.enabled?(:ci_queueing_disaster_recovery_disable_fair_scheduling, runner, type: :ops)
# if disaster recovery is enabled, we fallback to FIFO scheduling
relation.order('ci_pending_builds.build_id ASC')
else
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 6c9044b5089..8969b95b81f 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -157,15 +157,13 @@ module Ci
unless build.pending?
@metrics.increment_queue_operation(:build_not_pending)
- if Feature.enabled?(:ci_pending_builds_table_resiliency, default_enabled: :yaml)
- ##
- # If this build can not be picked because we had stale data in
- # `ci_pending_builds` table, we need to respond with 409 to retry
- # this operation.
- #
- if ::Ci::UpdateBuildQueueService.new.remove!(build)
- return Result.new(nil, nil, false)
- end
+ ##
+ # If this build can not be picked because we had stale data in
+ # `ci_pending_builds` table, we need to respond with 409 to retry
+ # this operation.
+ #
+ if ::Ci::UpdateBuildQueueService.new.remove!(build)
+ return Result.new(nil, nil, false)
end
return
diff --git a/app/services/ci/retry_job_service.rb b/app/services/ci/retry_job_service.rb
index af7e7fa16e9..e0ced3d0197 100644
--- a/app/services/ci/retry_job_service.rb
+++ b/app/services/ci/retry_job_service.rb
@@ -23,11 +23,11 @@ module Ci
# Cloning a job requires a strict type check to ensure
# the attributes being used for the clone are taken straight
# from the model and not overridden by other abstractions.
- raise TypeError unless job.instance_of?(Ci::Build)
+ raise TypeError unless job.instance_of?(Ci::Build) || job.instance_of?(Ci::Bridge)
check_access!(job)
- new_job = clone_job(job)
+ new_job = job.clone(current_user: current_user)
new_job.run_after_commit do
::Ci::CopyCrossDatabaseAssociationsService.new.execute(job, new_job)
@@ -53,9 +53,12 @@ module Ci
private
+ def check_assignable_runners!(job); end
+
def retry_job(job)
clone!(job).tap do |new_job|
- check_assignable_runners!(new_job)
+ check_assignable_runners!(new_job) if new_job.is_a?(Ci::Build)
+
next if new_job.failed?
Gitlab::OptimisticLocking.retry_lock(new_job, name: 'retry_build', &:enqueue)
@@ -68,26 +71,6 @@ module Ci
raise Gitlab::Access::AccessDeniedError, '403 Forbidden'
end
end
-
- def check_assignable_runners!(job); end
-
- def clone_job(job)
- project.builds.new(job_attributes(job))
- end
-
- def job_attributes(job)
- attributes = job.class.clone_accessors.to_h do |attribute|
- [attribute, job.public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend
- end
-
- if job.persisted_environment.present?
- attributes[:metadata_attributes] ||= {}
- attributes[:metadata_attributes][:expanded_environment_name] = job.expanded_environment_name
- end
-
- attributes[:user] = current_user
- attributes
- end
end
end
diff --git a/app/services/ci/runners/register_runner_service.rb b/app/services/ci/runners/register_runner_service.rb
index 7978d094d9b..196d2de1a65 100644
--- a/app/services/ci/runners/register_runner_service.rb
+++ b/app/services/ci/runners/register_runner_service.rb
@@ -47,7 +47,7 @@ module Ci
end
def runner_registrar_valid?(type)
- Feature.disabled?(:runner_registration_control, default_enabled: :yaml) || Gitlab::CurrentSettings.valid_runner_registrars.include?(type)
+ Feature.disabled?(:runner_registration_control) || Gitlab::CurrentSettings.valid_runner_registrars.include?(type)
end
def token_scope
diff --git a/app/services/ci/runners/reset_registration_token_service.rb b/app/services/ci/runners/reset_registration_token_service.rb
index bbe49c04644..2a3fb08c5e1 100644
--- a/app/services/ci/runners/reset_registration_token_service.rb
+++ b/app/services/ci/runners/reset_registration_token_service.rb
@@ -29,3 +29,5 @@ module Ci
end
end
end
+
+Ci::Runners::ResetRegistrationTokenService.prepend_mod
diff --git a/app/services/ci/stuck_builds/drop_running_service.rb b/app/services/ci/stuck_builds/drop_running_service.rb
index a79224cc231..dfcf3ca3836 100644
--- a/app/services/ci/stuck_builds/drop_running_service.rb
+++ b/app/services/ci/stuck_builds/drop_running_service.rb
@@ -16,7 +16,7 @@ module Ci
private
def running_timed_out_builds
- if Feature.enabled?(:ci_new_query_for_running_stuck_jobs, default_enabled: :yaml)
+ if Feature.enabled?(:ci_new_query_for_running_stuck_jobs)
Ci::Build
.running
.created_at_before(BUILD_RUNNING_OUTDATED_TIMEOUT.ago)
diff --git a/app/services/ci/update_build_state_service.rb b/app/services/ci/update_build_state_service.rb
index 9df36b86404..a74ddcfaf06 100644
--- a/app/services/ci/update_build_state_service.rb
+++ b/app/services/ci/update_build_state_service.rb
@@ -217,11 +217,11 @@ module Ci
def chunks_migration_enabled?
::Feature.enabled?(:ci_enable_live_trace, build.project) &&
- ::Feature.enabled?(:ci_accept_trace, build.project, type: :ops, default_enabled: true)
+ ::Feature.enabled?(:ci_accept_trace, build.project, type: :ops)
end
def log_invalid_chunks?
- ::Feature.enabled?(:ci_trace_log_invalid_chunks, build.project, type: :ops, default_enabled: false)
+ ::Feature.enabled?(:ci_trace_log_invalid_chunks, build.project, type: :ops)
end
end
end
diff --git a/app/services/clusters/kubernetes.rb b/app/services/clusters/kubernetes.rb
index ef549b56946..819ac4c8464 100644
--- a/app/services/clusters/kubernetes.rb
+++ b/app/services/clusters/kubernetes.rb
@@ -14,7 +14,5 @@ module Clusters
GITLAB_CROSSPLANE_DATABASE_ROLE_BINDING_NAME = 'gitlab-crossplane-database-rolebinding'
KNATIVE_SERVING_NAMESPACE = 'knative-serving'
ISTIO_SYSTEM_NAMESPACE = 'istio-system'
- GITLAB_CILIUM_ROLE_NAME = 'gitlab-cilium-role'
- GITLAB_CILIUM_ROLE_BINDING_NAME = 'gitlab-cilium-rolebinding'
end
end
diff --git a/app/services/clusters/kubernetes/create_or_update_service_account_service.rb b/app/services/clusters/kubernetes/create_or_update_service_account_service.rb
index ecad33fc7c0..eabc428d0d2 100644
--- a/app/services/clusters/kubernetes/create_or_update_service_account_service.rb
+++ b/app/services/clusters/kubernetes/create_or_update_service_account_service.rb
@@ -53,8 +53,6 @@ module Clusters
create_or_update_knative_serving_role_binding
create_or_update_crossplane_database_role
create_or_update_crossplane_database_role_binding
- create_or_update_cilium_role
- create_or_update_cilium_role_binding
end
private
@@ -99,14 +97,6 @@ module Clusters
kubeclient.update_role_binding(crossplane_database_role_binding_resource)
end
- def create_or_update_cilium_role
- kubeclient.update_role(cilium_role_resource)
- end
-
- def create_or_update_cilium_role_binding
- kubeclient.update_role_binding(cilium_role_binding_resource)
- end
-
def service_account_resource
Gitlab::Kubernetes::ServiceAccount.new(
service_account_name,
@@ -185,28 +175,6 @@ module Clusters
service_account_name: service_account_name
).generate
end
-
- def cilium_role_resource
- Gitlab::Kubernetes::Role.new(
- name: Clusters::Kubernetes::GITLAB_CILIUM_ROLE_NAME,
- namespace: service_account_namespace,
- rules: [{
- apiGroups: %w(cilium.io),
- resources: %w(ciliumnetworkpolicies),
- verbs: %w(get list create update patch)
- }]
- ).generate
- end
-
- def cilium_role_binding_resource
- Gitlab::Kubernetes::RoleBinding.new(
- name: Clusters::Kubernetes::GITLAB_CILIUM_ROLE_BINDING_NAME,
- role_name: Clusters::Kubernetes::GITLAB_CILIUM_ROLE_NAME,
- role_kind: :Role,
- namespace: service_account_namespace,
- service_account_name: service_account_name
- ).generate
- end
end
end
end
diff --git a/app/services/concerns/alert_management/alert_processing.rb b/app/services/concerns/alert_management/alert_processing.rb
index 2b556a4339d..abbfd4d66d4 100644
--- a/app/services/concerns/alert_management/alert_processing.rb
+++ b/app/services/concerns/alert_management/alert_processing.rb
@@ -104,7 +104,7 @@ module AlertManagement
def find_existing_alert
return unless incoming_payload.gitlab_fingerprint
- AlertManagement::Alert.not_resolved.for_fingerprint(project, incoming_payload.gitlab_fingerprint).first
+ AlertManagement::Alert.find_unresolved_alert(project, incoming_payload.gitlab_fingerprint)
end
def build_new_alert
diff --git a/app/services/concerns/ci/downstream_pipeline_helpers.rb b/app/services/concerns/ci/downstream_pipeline_helpers.rb
new file mode 100644
index 00000000000..39c0adb6e4e
--- /dev/null
+++ b/app/services/concerns/ci/downstream_pipeline_helpers.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Ci
+ module DownstreamPipelineHelpers
+ def log_downstream_pipeline_creation(downstream_pipeline)
+ return unless downstream_pipeline&.persisted?
+
+ hierarchy_size = downstream_pipeline.all_pipelines_in_hierarchy.count
+ root_pipeline = downstream_pipeline.upstream_root
+
+ ::Gitlab::AppLogger.info(
+ message: "downstream pipeline created",
+ class: self.class.name,
+ root_pipeline_id: root_pipeline.id,
+ downstream_pipeline_id: downstream_pipeline.id,
+ downstream_pipeline_relationship: downstream_pipeline.parent_pipeline? ? :parent_child : :multi_project,
+ hierarchy_size: hierarchy_size,
+ root_pipeline_plan: root_pipeline.project.actual_plan_name,
+ root_pipeline_namespace_path: root_pipeline.project.namespace.full_path,
+ root_pipeline_project_path: root_pipeline.project.full_path
+ )
+ end
+ end
+end
diff --git a/app/services/concerns/group_linkable.rb b/app/services/concerns/group_linkable.rb
new file mode 100644
index 00000000000..43d10e01a4a
--- /dev/null
+++ b/app/services/concerns/group_linkable.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module GroupLinkable
+ extend ActiveSupport::Concern
+
+ def execute
+ return error('Not Found', 404) unless valid_to_create?
+
+ build_link
+
+ if link.save
+ after_successful_save
+ success(link: link)
+ else
+ error(link.errors.full_messages.to_sentence, 409)
+ end
+ end
+
+ private
+
+ attr_reader :shared_with_group, :link
+
+ def sharing_allowed?
+ sharing_outside_hierarchy_allowed? || within_hierarchy?
+ end
+
+ def sharing_outside_hierarchy_allowed?
+ !root_ancestor.prevent_sharing_groups_outside_hierarchy
+ end
+
+ def within_hierarchy?
+ root_ancestor.self_and_descendants_ids.include?(shared_with_group.id)
+ end
+
+ def after_successful_save
+ setup_authorizations
+ end
+end
diff --git a/app/services/container_expiration_policies/cleanup_service.rb b/app/services/container_expiration_policies/cleanup_service.rb
index 0da5e552c48..34889e58127 100644
--- a/app/services/container_expiration_policies/cleanup_service.rb
+++ b/app/services/container_expiration_policies/cleanup_service.rb
@@ -35,7 +35,8 @@ module ContainerExpirationPolicies
if service_result[:status] == :success
repository.update!(
expiration_policy_cleanup_status: :cleanup_unscheduled,
- expiration_policy_completed_at: Time.zone.now
+ expiration_policy_completed_at: Time.zone.now,
+ last_cleanup_deleted_tags_count: service_result[:deleted_size]
)
success(:finished, service_result)
diff --git a/app/services/container_expiration_policies/update_service.rb b/app/services/container_expiration_policies/update_service.rb
index 2f34941d692..32ee47457b7 100644
--- a/app/services/container_expiration_policies/update_service.rb
+++ b/app/services/container_expiration_policies/update_service.rb
@@ -28,7 +28,7 @@ module ContainerExpirationPolicies
end
def allowed?
- Ability.allowed?(current_user, :destroy_container_image, @container)
+ Ability.allowed?(current_user, :admin_container_image, @container)
end
def container_expiration_policy_params
diff --git a/app/services/container_expiration_policy_service.rb b/app/services/container_expiration_policy_service.rb
deleted file mode 100644
index cf5d702a9ef..00000000000
--- a/app/services/container_expiration_policy_service.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-class ContainerExpirationPolicyService < BaseService
- InvalidPolicyError = Class.new(StandardError)
-
- def execute(container_expiration_policy)
- container_expiration_policy.schedule_next_run!
-
- container_expiration_policy.container_repositories.find_each do |container_repository|
- CleanupContainerRepositoryWorker.perform_async(
- nil,
- container_repository.id,
- container_expiration_policy.policy_params
- .merge(container_expiration_policy: true)
- )
- end
- end
-end
diff --git a/app/services/customer_relations/contacts/base_service.rb b/app/services/customer_relations/contacts/base_service.rb
index 1797e5021a1..4c6bcabe9e0 100644
--- a/app/services/customer_relations/contacts/base_service.rb
+++ b/app/services/customer_relations/contacts/base_service.rb
@@ -12,6 +12,23 @@ module CustomerRelations
def error(message)
ServiceResponse.error(message: Array(message))
end
+
+ def organization_valid?
+ return true unless params[:organization_id]
+
+ organization = Organization.find(params[:organization_id])
+ organization.group_id == group.id
+ rescue ActiveRecord::RecordNotFound
+ false
+ end
+
+ def error_organization_invalid
+ error('The specified organization was not found or does not belong to this group')
+ end
+
+ def error_no_permissions
+ error('You have insufficient permissions to manage contacts for this group')
+ end
end
end
end
diff --git a/app/services/customer_relations/contacts/create_service.rb b/app/services/customer_relations/contacts/create_service.rb
index 7ff8b731e0d..2fabc51eebf 100644
--- a/app/services/customer_relations/contacts/create_service.rb
+++ b/app/services/customer_relations/contacts/create_service.rb
@@ -8,7 +8,6 @@ module CustomerRelations
return error_organization_invalid unless organization_valid?
contact = Contact.create(params.merge(group_id: group.id))
-
return error_creating(contact) unless contact.persisted?
ServiceResponse.success(payload: contact)
@@ -16,23 +15,6 @@ module CustomerRelations
private
- def organization_valid?
- return true unless params[:organization_id]
-
- organization = Organization.find(params[:organization_id])
- organization.group_id == group.id
- rescue ActiveRecord::RecordNotFound
- false
- end
-
- def error_organization_invalid
- error('The specified organization was not found or does not belong to this group')
- end
-
- def error_no_permissions
- error('You have insufficient permissions to create a contact for this group')
- end
-
def error_creating(contact)
error(contact&.errors&.full_messages || 'Failed to create contact')
end
diff --git a/app/services/customer_relations/contacts/update_service.rb b/app/services/customer_relations/contacts/update_service.rb
index 473a80be262..66eb5731bc9 100644
--- a/app/services/customer_relations/contacts/update_service.rb
+++ b/app/services/customer_relations/contacts/update_service.rb
@@ -5,6 +5,9 @@ module CustomerRelations
class UpdateService < BaseService
def execute(contact)
return error_no_permissions unless allowed?
+
+ handle_active_param
+ return error_organization_invalid unless organization_valid?
return error_updating(contact) unless contact.update(params)
ServiceResponse.success(payload: contact)
@@ -12,8 +15,11 @@ module CustomerRelations
private
- def error_no_permissions
- error('You have insufficient permissions to update a contact for this group')
+ def handle_active_param
+ return if params[:active].nil?
+
+ active = params.delete(:active)
+ params[:state] = active ? 'active' : 'inactive'
end
def error_updating(contact)
diff --git a/app/services/customer_relations/organizations/update_service.rb b/app/services/customer_relations/organizations/update_service.rb
index 9d8f908db14..78fff9cf8f4 100644
--- a/app/services/customer_relations/organizations/update_service.rb
+++ b/app/services/customer_relations/organizations/update_service.rb
@@ -5,6 +5,8 @@ module CustomerRelations
class UpdateService < BaseService
def execute(organization)
return error_no_permissions unless allowed?
+
+ handle_active_param
return error_updating(organization) unless organization.update(params)
ServiceResponse.success(payload: organization)
@@ -12,6 +14,13 @@ module CustomerRelations
private
+ def handle_active_param
+ return if params[:active].nil?
+
+ active = params.delete(:active)
+ params[:state] = active ? 'active' : 'inactive'
+ end
+
def error_no_permissions
error('You have insufficient permissions to update an organization for this group')
end
diff --git a/app/services/database/consistency_fix_service.rb b/app/services/database/consistency_fix_service.rb
new file mode 100644
index 00000000000..402fa7541b3
--- /dev/null
+++ b/app/services/database/consistency_fix_service.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Database
+ class ConsistencyFixService
+ def initialize(source_model:, target_model:, sync_event_class:, source_sort_key:, target_sort_key:)
+ @source_model = source_model
+ @target_model = target_model
+ @sync_event_class = sync_event_class
+ @source_sort_key = source_sort_key
+ @target_sort_key = target_sort_key
+ end
+
+ attr_accessor :source_model, :target_model, :sync_event_class, :source_sort_key, :target_sort_key
+
+ def execute(ids:)
+ ids.each do |id|
+ if source_object(id) && target_object(id)
+ create_sync_event_for(id)
+ elsif target_object(id)
+ target_object(id).destroy!
+ end
+ end
+ sync_event_class.enqueue_worker
+ end
+
+ private
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def source_object(id)
+ source_model.find_by(source_sort_key => id)
+ end
+
+ def target_object(id)
+ target_model.find_by(target_sort_key => id)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def create_sync_event_for(id)
+ if source_model == Namespace
+ sync_event_class.create!(namespace_id: id)
+ elsif source_model == Project
+ sync_event_class.create!(project_id: id)
+ else
+ raise("Unknown Source Model #{source_model.name}")
+ end
+ end
+ end
+end
diff --git a/app/services/deployments/update_environment_service.rb b/app/services/deployments/update_environment_service.rb
index b0ba8ecaa47..b0eb153a7af 100644
--- a/app/services/deployments/update_environment_service.rb
+++ b/app/services/deployments/update_environment_service.rb
@@ -58,7 +58,7 @@ module Deployments
def expanded_environment_url
return unless environment_url
- if ::Feature.enabled?(:ci_expand_environment_name_and_url, deployment.project, default_enabled: :yaml)
+ if ::Feature.enabled?(:ci_expand_environment_name_and_url, deployment.project)
ExpandVariables.expand(environment_url, -> { variables.sort_and_expand_all })
else
ExpandVariables.expand(environment_url, -> { variables })
diff --git a/app/services/environments/stop_service.rb b/app/services/environments/stop_service.rb
index 24ae658d3d6..54ad94947ff 100644
--- a/app/services/environments/stop_service.rb
+++ b/app/services/environments/stop_service.rb
@@ -18,7 +18,9 @@ module Environments
environments.each { |environment| execute(environment) }
end
- def execute_for_merge_request(merge_request)
+ def execute_for_merge_request_pipeline(merge_request)
+ return unless merge_request.actual_head_pipeline&.merge_request?
+
merge_request.environments_in_head_pipeline(deployment_status: :success).each do |environment|
execute(environment)
end
diff --git a/app/services/error_tracking/base_service.rb b/app/services/error_tracking/base_service.rb
index 598621f70e1..d2ecd0a6d5a 100644
--- a/app/services/error_tracking/base_service.rb
+++ b/app/services/error_tracking/base_service.rb
@@ -71,5 +71,15 @@ module ErrorTracking
def can_update?
can?(current_user, :update_sentry_issue, project)
end
+
+ def error_repository
+ Gitlab::ErrorTracking::ErrorRepository.build(project)
+ end
+
+ def handle_error_repository_exceptions
+ yield
+ rescue Gitlab::ErrorTracking::ErrorRepository::DatabaseError => e
+ { error: e.message }
+ end
end
end
diff --git a/app/services/error_tracking/collect_error_service.rb b/app/services/error_tracking/collect_error_service.rb
index 6376b743255..8cb3793ba97 100644
--- a/app/services/error_tracking/collect_error_service.rb
+++ b/app/services/error_tracking/collect_error_service.rb
@@ -5,30 +5,24 @@ module ErrorTracking
include Gitlab::Utils::StrongMemoize
def execute
- # Error is a way to group events based on common data like name or cause
- # of exception. We need to keep a sane balance here between taking too little
- # and too much data into group logic.
- error = project.error_tracking_errors.report_error(
- name: exception['type'], # Example: ActionView::MissingTemplate
- description: exception['value'], # Example: Missing template posts/show in...
- actor: actor, # Example: PostsController#show
- platform: event['platform'], # Example: ruby
- timestamp: timestamp
- )
-
- # The payload field contains all the data on error including stacktrace in jsonb.
- # Together with occurred_at these are 2 main attributes that we need to save here.
- error.events.create!(
- environment: event['environment'],
+ error_repository.report_error(
+ name: exception['type'],
description: exception['value'],
- level: event['level'],
+ actor: actor,
+ platform: event['platform'],
occurred_at: timestamp,
+ environment: event['environment'],
+ level: event['level'],
payload: event
)
end
private
+ def error_repository
+ Gitlab::ErrorTracking::ErrorRepository.build(project)
+ end
+
def event
@event ||= format_event(params[:event])
end
diff --git a/app/services/error_tracking/issue_details_service.rb b/app/services/error_tracking/issue_details_service.rb
index 1614c597a8e..e82ad540e57 100644
--- a/app/services/error_tracking/issue_details_service.rb
+++ b/app/services/error_tracking/issue_details_service.rb
@@ -49,13 +49,10 @@ module ErrorTracking
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
#
if project_error_tracking_setting.integrated_client?
- error = project.error_tracking_errors.find(issue_id)
-
- # We use the same response format as project_error_tracking_setting
- # method below for compatibility with existing code.
- {
- issue: error.to_sentry_detailed_error
- }
+ handle_error_repository_exceptions do
+ error = error_repository.find_error(issue_id)
+ { issue: error }
+ end
else
project_error_tracking_setting.issue_details(issue_id: issue_id)
end
diff --git a/app/services/error_tracking/issue_latest_event_service.rb b/app/services/error_tracking/issue_latest_event_service.rb
index 1bf86c658fc..0290c8eac86 100644
--- a/app/services/error_tracking/issue_latest_event_service.rb
+++ b/app/services/error_tracking/issue_latest_event_service.rb
@@ -26,14 +26,13 @@ module ErrorTracking
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
#
if project_error_tracking_setting.integrated_client?
- error = project.error_tracking_errors.find(issue_id)
- event = error.events.last
+ handle_error_repository_exceptions do
+ event = error_repository.last_event_for(issue_id)
- # We use the same response format as project_error_tracking_setting
- # method below for compatibility with existing code.
- {
- latest_event: event.to_sentry_error_event
- }
+ # We use the same response format as project_error_tracking_setting
+ # method below for compatibility with existing code.
+ { latest_event: event }
+ end
else
project_error_tracking_setting.issue_latest_event(issue_id: issue_id)
end
diff --git a/app/services/error_tracking/issue_update_service.rb b/app/services/error_tracking/issue_update_service.rb
index 624e5f94dde..ca5e8d656a6 100644
--- a/app/services/error_tracking/issue_update_service.rb
+++ b/app/services/error_tracking/issue_update_service.rb
@@ -84,14 +84,12 @@ module ErrorTracking
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
#
if project_error_tracking_setting.integrated_client?
- error = project.error_tracking_errors.find(opts[:issue_id])
- error.status = opts[:params][:status]
- error.save!
+ updated = error_repository.update_error(opts[:issue_id], status: opts[:params][:status])
# We use the same response format as project_error_tracking_setting
# method below for compatibility with existing code.
{
- updated: true
+ updated: updated
}
else
project_error_tracking_setting.update_issue(**opts)
diff --git a/app/services/error_tracking/list_issues_service.rb b/app/services/error_tracking/list_issues_service.rb
index 1979816b88d..ca7208dba96 100644
--- a/app/services/error_tracking/list_issues_service.rb
+++ b/app/services/error_tracking/list_issues_service.rb
@@ -73,24 +73,24 @@ module ErrorTracking
if project_error_tracking_setting.integrated_client?
# We are going to support more options in the future.
# For now we implement the bare minimum for rendering the list in UI.
- filter_opts = {
- status: opts[:issue_status],
+ list_opts = {
+ filters: { status: opts[:issue_status] },
sort: opts[:sort],
limit: opts[:limit],
cursor: opts[:cursor]
}
- errors = ErrorTracking::ErrorsFinder.new(current_user, project, filter_opts).execute
+ errors, pagination = error_repository.list_errors(**list_opts)
- pagination = {}
- pagination[:next] = { cursor: errors.cursor_for_next_page } if errors.has_next_page?
- pagination[:previous] = { cursor: errors.cursor_for_previous_page } if errors.has_previous_page?
+ pagination_hash = {}
+ pagination_hash[:next] = { cursor: pagination.next } if pagination.next
+ pagination_hash[:previous] = { cursor: pagination.prev } if pagination.prev
# We use the same response format as project_error_tracking_setting
# method below for compatibility with existing code.
{
- issues: errors.map(&:to_sentry_error),
- pagination: pagination
+ issues: errors,
+ pagination: pagination_hash
}
else
project_error_tracking_setting.list_sentry_issues(**opts)
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 417680e37cf..5a2c29f8e7a 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -185,7 +185,7 @@ class EventCreateService
track_event(event_action: :pushed, event_target: Project, author_id: current_user.id)
namespace = project.namespace
- if Feature.enabled?(:route_hll_to_snowplow, namespace, default_enabled: :yaml)
+ if Feature.enabled?(:route_hll_to_snowplow, namespace)
Gitlab::Tracking.event(self.class.to_s, 'action_active_users_project_repo', namespace: namespace, user: current_user, project: project)
end
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
index 63f3f73905a..269637805ad 100644
--- a/app/services/git/base_hooks_service.rb
+++ b/app/services/git/base_hooks_service.rb
@@ -172,7 +172,7 @@ module Git
else
# This service runs in Sidekiq, so this shouldn't ever be
# called, but this is included just in case.
- Gitlab::ProjectServiceLogger
+ Gitlab::IntegrationsLogger
end
end
end
diff --git a/app/services/groups/group_links/create_service.rb b/app/services/groups/group_links/create_service.rb
index 8c3ba0a63f2..56ddf3ec0b4 100644
--- a/app/services/groups/group_links/create_service.rb
+++ b/app/services/groups/group_links/create_service.rb
@@ -3,50 +3,35 @@
module Groups
module GroupLinks
class CreateService < Groups::BaseService
- def initialize(shared_group, shared_with_group, user, params)
- @shared_group = shared_group
- super(shared_with_group, user, params)
- end
-
- def execute
- unless shared_with_group && shared_group &&
- can?(current_user, :admin_group_member, shared_group) &&
- can?(current_user, :read_group, shared_with_group) &&
- sharing_allowed?
- return error('Not Found', 404)
- end
+ include GroupLinkable
- link = GroupGroupLink.new(
- shared_group: shared_group,
- shared_with_group: shared_with_group,
- group_access: params[:shared_group_access],
- expires_at: params[:expires_at]
- )
+ def initialize(group, shared_with_group, user, params)
+ @shared_with_group = shared_with_group
- if link.save
- shared_with_group.refresh_members_authorized_projects(blocking: false, direct_members_only: true)
- success(link: link)
- else
- error(link.errors.full_messages.to_sentence, 409)
- end
+ super(group, user, params)
end
private
- attr_reader :shared_group
+ delegate :root_ancestor, to: :group
- alias_method :shared_with_group, :group
-
- def sharing_allowed?
- sharing_outside_hierarchy_allowed? || within_hierarchy?
+ def valid_to_create?
+ can?(current_user, :admin_group_member, group) &&
+ can?(current_user, :read_group, shared_with_group) &&
+ sharing_allowed?
end
- def sharing_outside_hierarchy_allowed?
- !shared_group.root_ancestor.namespace_settings.prevent_sharing_groups_outside_hierarchy
+ def build_link
+ @link = GroupGroupLink.new(
+ shared_group: group,
+ shared_with_group: shared_with_group,
+ group_access: params[:shared_group_access],
+ expires_at: params[:expires_at]
+ )
end
- def within_hierarchy?
- shared_group.root_ancestor.self_and_descendants_ids.include?(shared_with_group.id)
+ def setup_authorizations
+ shared_with_group.refresh_members_authorized_projects(blocking: false, direct_members_only: true)
end
end
end
diff --git a/app/services/groups/import_export/export_service.rb b/app/services/groups/import_export/export_service.rb
index ea26ebec20b..2bfd5a5ebab 100644
--- a/app/services/groups/import_export/export_service.rb
+++ b/app/services/groups/import_export/export_service.rb
@@ -78,7 +78,7 @@ module Groups
end
def ndjson?
- ::Feature.enabled?(:group_export_ndjson, group&.parent, default_enabled: :yaml)
+ ::Feature.enabled?(:group_export_ndjson, group&.parent)
end
def version_saver
diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb
index c8c2124078d..f026f1698a9 100644
--- a/app/services/groups/import_export/import_service.rb
+++ b/app/services/groups/import_export/import_service.rb
@@ -72,7 +72,7 @@ module Groups
end
def ndjson?
- ::Feature.enabled?(:group_import_ndjson, group&.parent, default_enabled: true) &&
+ ::Feature.enabled?(:group_import_ndjson, group&.parent) &&
File.exist?(File.join(shared.export_path, 'tree/groups/_all.ndjson'))
end
diff --git a/app/services/groups/open_issues_count_service.rb b/app/services/groups/open_issues_count_service.rb
index c18d239998b..17cf3d38987 100644
--- a/app/services/groups/open_issues_count_service.rb
+++ b/app/services/groups/open_issues_count_service.rb
@@ -3,15 +3,11 @@
module Groups
# Service class for counting and caching the number of open issues of a group.
class OpenIssuesCountService < Groups::CountService
- # TOTAL_COUNT_KEY includes confidential and hidden issues (admin)
- # TOTAL_COUNT_WITHOUT_HIDDEN_KEY includes confidential issues but not hidden issues (reporter and above)
- # PUBLIC_COUNT_WITHOUT_HIDDEN_KEY does not include confidential or hidden issues (guest)
- TOTAL_COUNT_KEY = 'group_open_issues_including_hidden_count'
- TOTAL_COUNT_WITHOUT_HIDDEN_KEY = 'group_open_issues_without_hidden_count'
- PUBLIC_COUNT_WITHOUT_HIDDEN_KEY = 'group_open_public_issues_without_hidden_count'
+ PUBLIC_COUNT_KEY = 'group_public_open_issues_count'
+ TOTAL_COUNT_KEY = 'group_total_open_issues_count'
def clear_all_cache_keys
- [cache_key(TOTAL_COUNT_KEY), cache_key(TOTAL_COUNT_WITHOUT_HIDDEN_KEY), cache_key(PUBLIC_COUNT_WITHOUT_HIDDEN_KEY)].each do |key|
+ [cache_key(PUBLIC_COUNT_KEY), cache_key(TOTAL_COUNT_KEY)].each do |key|
Rails.cache.delete(key)
end
end
@@ -19,19 +15,7 @@ module Groups
private
def cache_key_name
- if include_hidden?
- TOTAL_COUNT_KEY
- elsif public_only?
- PUBLIC_COUNT_WITHOUT_HIDDEN_KEY
- else
- TOTAL_COUNT_WITHOUT_HIDDEN_KEY
- end
- end
-
- def include_hidden?
- strong_memoize(:user_is_admin) do
- user&.can_admin_all_resources?
- end
+ public_only? ? PUBLIC_COUNT_KEY : TOTAL_COUNT_KEY
end
def public_only?
@@ -51,8 +35,7 @@ module Groups
state: 'opened',
non_archived: true,
include_subgroups: true,
- public_only: public_only?,
- include_hidden: include_hidden?
+ public_only: public_only?
).execute
end
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index f2e959396bc..a0021ae2ccb 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -191,18 +191,9 @@ module Groups
end
def refresh_project_authorizations
- projects_to_update = Set.new
+ project_ids = Groups::ProjectsRequiringAuthorizationsRefresh::OnTransferFinder.new(@group).execute
- # All projects in this hierarchy need to have their project authorizations recalculated
- @group.all_projects.each_batch { |prjs| projects_to_update.merge(prjs.ids) } # rubocop: disable CodeReuse/ActiveRecord
-
- # When a group is transferred, it also affects who gets access to the projects shared to
- # the subgroups within its hierarchy, so we also schedule jobs that refresh authorizations for all such shared projects.
- ProjectGroupLink.in_group(@group.self_and_descendants.select(:id)).each_batch do |project_group_links|
- projects_to_update.merge(project_group_links.pluck(:project_id)) # rubocop: disable CodeReuse/ActiveRecord
- end
-
- AuthorizedProjectUpdate::ProjectAccessChangedService.new(projects_to_update.to_a).execute unless projects_to_update.empty?
+ AuthorizedProjectUpdate::ProjectAccessChangedService.new(project_ids).execute
end
def raise_transfer_error(message)
diff --git a/app/services/import/bitbucket_server_service.rb b/app/services/import/bitbucket_server_service.rb
index cdb23370ddc..d1c22f06464 100644
--- a/app/services/import/bitbucket_server_service.rb
+++ b/app/services/import/bitbucket_server_service.rb
@@ -21,6 +21,8 @@ module Import
if project.persisted?
success(project)
+ elsif project.errors[:import_source_disabled].present?
+ error(project.errors[:import_source_disabled], :forbidden)
else
log_and_return_error(project_save_error(project), :unprocessable_entity)
end
diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb
index a891dcc11e3..033f6bcb043 100644
--- a/app/services/import/github_service.rb
+++ b/app/services/import/github_service.rb
@@ -25,6 +25,8 @@ module Import
if project.persisted?
success(project)
+ elsif project.errors[:import_source_disabled].present?
+ error(project.errors[:import_source_disabled], :forbidden)
else
error(project_save_error(project), :unprocessable_entity)
end
diff --git a/app/services/import/gitlab_projects/file_acquisition_strategies/remote_file.rb b/app/services/import/gitlab_projects/file_acquisition_strategies/remote_file.rb
index ae9a450660c..ac58711a0ac 100644
--- a/app/services/import/gitlab_projects/file_acquisition_strategies/remote_file.rb
+++ b/app/services/import/gitlab_projects/file_acquisition_strategies/remote_file.rb
@@ -58,7 +58,7 @@ module Import
end
def validate_aws_s3?
- ::Feature.enabled?(:import_project_from_remote_file_s3, default_enabled: :yaml)
+ ::Feature.enabled?(:import_project_from_remote_file_s3)
end
def headers
diff --git a/app/services/incident_management/timeline_events/base_service.rb b/app/services/incident_management/timeline_events/base_service.rb
new file mode 100644
index 00000000000..cae58465e4a
--- /dev/null
+++ b/app/services/incident_management/timeline_events/base_service.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ module TimelineEvents
+ class BaseService
+ def allowed?
+ user&.can?(:admin_incident_management_timeline_event, incident)
+ end
+
+ def success(timeline_event)
+ ServiceResponse.success(payload: { timeline_event: timeline_event })
+ end
+
+ def error(message)
+ ServiceResponse.error(message: message)
+ end
+
+ def error_no_permissions
+ error(_('You have insufficient permissions to manage timeline events for this incident'))
+ end
+
+ def error_in_save(timeline_event)
+ error(timeline_event.errors.full_messages.to_sentence)
+ end
+ end
+ end
+end
diff --git a/app/services/incident_management/timeline_events/create_service.rb b/app/services/incident_management/timeline_events/create_service.rb
new file mode 100644
index 00000000000..7d287e1bd82
--- /dev/null
+++ b/app/services/incident_management/timeline_events/create_service.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ module TimelineEvents
+ DEFAULT_ACTION = 'comment'
+
+ class CreateService < TimelineEvents::BaseService
+ def initialize(incident, user, params)
+ @project = incident.project
+ @incident = incident
+ @user = user
+ @params = params
+ end
+
+ def execute
+ return error_no_permissions unless allowed?
+
+ timeline_event_params = {
+ project: project,
+ incident: incident,
+ author: user,
+ note: params[:note],
+ action: params.fetch(:action, DEFAULT_ACTION),
+ note_html: params[:note_html].presence || params[:note],
+ occurred_at: params[:occurred_at],
+ promoted_from_note: params[:promoted_from_note]
+ }
+
+ timeline_event = IncidentManagement::TimelineEvent.new(timeline_event_params)
+
+ if timeline_event.save
+ add_system_note(timeline_event)
+
+ success(timeline_event)
+ else
+ error_in_save(timeline_event)
+ end
+ end
+
+ private
+
+ attr_reader :project, :user, :incident, :params
+
+ def add_system_note(timeline_event)
+ return unless Feature.enabled?(:incident_timeline, project)
+
+ SystemNoteService.add_timeline_event(timeline_event)
+ end
+ end
+ end
+end
diff --git a/app/services/incident_management/timeline_events/destroy_service.rb b/app/services/incident_management/timeline_events/destroy_service.rb
new file mode 100644
index 00000000000..8bb186c289a
--- /dev/null
+++ b/app/services/incident_management/timeline_events/destroy_service.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ module TimelineEvents
+ class DestroyService < TimelineEvents::BaseService
+ # @param timeline_event [IncidentManagement::TimelineEvent]
+ # @param user [User]
+ def initialize(timeline_event, user)
+ @timeline_event = timeline_event
+ @user = user
+ @incident = timeline_event.incident
+ @project = @incident.project
+ end
+
+ def execute
+ return error_no_permissions unless allowed?
+
+ if timeline_event.destroy
+ add_system_note(incident, user)
+
+ success(timeline_event)
+ else
+ error_in_save(timeline_event)
+ end
+ end
+
+ private
+
+ attr_reader :project, :timeline_event, :user, :incident
+
+ def add_system_note(incident, user)
+ return unless Feature.enabled?(:incident_timeline, project)
+
+ SystemNoteService.delete_timeline_event(incident, user)
+ end
+ end
+ end
+end
diff --git a/app/services/incident_management/timeline_events/update_service.rb b/app/services/incident_management/timeline_events/update_service.rb
new file mode 100644
index 00000000000..fe8b4879561
--- /dev/null
+++ b/app/services/incident_management/timeline_events/update_service.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ module TimelineEvents
+ # @param timeline_event [IncidentManagement::TimelineEvent]
+ # @param user [User]
+ # @param params [Hash]
+ # @option params [string] note
+ # @option params [datetime] occurred_at
+ class UpdateService < TimelineEvents::BaseService
+ def initialize(timeline_event, user, params)
+ @timeline_event = timeline_event
+ @incident = timeline_event.incident
+ @user = user
+ @note = params[:note]
+ @occurred_at = params[:occurred_at]
+ end
+
+ def execute
+ return error_no_permissions unless allowed?
+
+ if timeline_event.update(update_params)
+ add_system_note(timeline_event)
+
+ success(timeline_event)
+ else
+ error_in_save(timeline_event)
+ end
+ end
+
+ private
+
+ attr_reader :timeline_event, :incident, :user, :note, :occurred_at
+
+ def update_params
+ { updated_by_user: user, note: note.presence, occurred_at: occurred_at.presence }.compact
+ end
+
+ def add_system_note(timeline_event)
+ return unless Feature.enabled?(:incident_timeline, incident.project)
+
+ changes = was_changed(timeline_event)
+ return if changes == :none
+
+ SystemNoteService.edit_timeline_event(timeline_event, user, was_changed: changes)
+ end
+
+ def was_changed(timeline_event)
+ changes = timeline_event.previous_changes
+ occurred_at_changed = changes.key?('occurred_at')
+ note_changed = changes.key?('note')
+
+ return :occurred_at_and_note if occurred_at_changed && note_changed
+ return :occurred_at if occurred_at_changed
+ return :note if note_changed
+
+ :none
+ end
+ end
+ end
+end
diff --git a/app/services/issuable/destroy_service.rb b/app/services/issuable/destroy_service.rb
index b75905fb5b0..6aab56f0f68 100644
--- a/app/services/issuable/destroy_service.rb
+++ b/app/services/issuable/destroy_service.rb
@@ -26,13 +26,15 @@ module Issuable
end
def delete_todos(actor, issuable)
- TodosDestroyer::DestroyedIssuableWorker
- .perform_async(issuable.id, issuable.class.name)
+ issuable.run_after_commit_or_now do
+ TodosDestroyer::DestroyedIssuableWorker.perform_async(issuable.id, issuable.class.name)
+ end
end
def delete_label_links(actor, issuable)
- Issuable::LabelLinksDestroyWorker
- .perform_async(issuable.id, issuable.class.name)
+ issuable.run_after_commit_or_now do
+ Issuable::LabelLinksDestroyWorker.perform_async(issuable.id, issuable.class.name)
+ end
end
end
end
diff --git a/app/services/jira/requests/base.rb b/app/services/jira/requests/base.rb
index 3e15d47e8af..8e8511e5180 100644
--- a/app/services/jira/requests/base.rb
+++ b/app/services/jira/requests/base.rb
@@ -3,8 +3,6 @@
module Jira
module Requests
class Base
- include ProjectServicesLoggable
-
JIRA_API_VERSION = 2
# Limit the size of the JSON error message we will attempt to parse, as the JSON is external input.
JIRA_ERROR_JSON_SIZE_LIMIT = 5_000
@@ -54,17 +52,13 @@ module Jira
def request
response = client.get(url)
build_service_response(response)
- rescue *ALL_ERRORS => e
- log_error('Error sending message',
- client_url: client.options[:site],
- error: {
- exception_class: e.class.name,
- exception_message: e.message,
- exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(e.backtrace)
- }
+ rescue *ALL_ERRORS => error
+ jira_integration.log_exception(error,
+ message: 'Error sending message',
+ client_url: client.options[:site]
)
- ServiceResponse.error(message: error_message(e))
+ ServiceResponse.error(message: error_message(error))
end
def auth_docs_link_start
diff --git a/app/services/jira_connect/sync_service.rb b/app/services/jira_connect/sync_service.rb
index bddc7cbe5a0..92255711399 100644
--- a/app/services/jira_connect/sync_service.rb
+++ b/app/services/jira_connect/sync_service.rb
@@ -39,7 +39,7 @@ module JiraConnect
end
def logger
- Gitlab::ProjectServiceLogger
+ Gitlab::IntegrationsLogger
end
end
end
diff --git a/app/services/jira_import/start_import_service.rb b/app/services/jira_import/start_import_service.rb
index c9ffdeb2a16..4d1f2c94ac8 100644
--- a/app/services/jira_import/start_import_service.rb
+++ b/app/services/jira_import/start_import_service.rb
@@ -73,7 +73,7 @@ module JiraImport
jira_imports_for_project = project.jira_imports.by_jira_project_key(jira_project_key).size + 1
title = "jira-import::#{jira_project_key}-#{jira_imports_for_project}"
description = "Label for issues that were imported from Jira on #{import_start_time.strftime('%Y-%m-%d %H:%M:%S')}"
- color = "#{Label.color_for(title)}"
+ color = "#{::Gitlab::Color.color_for(title)}"
{ title: title, description: description, color: color }
end
diff --git a/app/services/loose_foreign_keys/batch_cleaner_service.rb b/app/services/loose_foreign_keys/batch_cleaner_service.rb
index b89de15a568..95eb8b47009 100644
--- a/app/services/loose_foreign_keys/batch_cleaner_service.rb
+++ b/app/services/loose_foreign_keys/batch_cleaner_service.rb
@@ -54,8 +54,6 @@ module LooseForeignKeys
attr_reader :parent_table, :loose_foreign_key_definitions, :deleted_parent_records, :modification_tracker, :deleted_records_counter, :deleted_records_rescheduled_count, :deleted_records_incremented_count
def handle_over_limit
- return if Feature.disabled?(:lfk_fair_queueing, default_enabled: :yaml)
-
records_to_reschedule = []
records_to_increment = []
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index 8f7b63c32c8..8485e7cbafa 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -46,9 +46,11 @@ module Members
:tasks_to_be_done_members, :member_created_member_task_id
def invites_from_params
- return params[:user_ids] if params[:user_ids].is_a?(Array)
+ # String, Nil, Array, Integer
+ return params[:user_id] if params[:user_id].is_a?(Array)
+ return [] unless params[:user_id]
- params[:user_ids]&.to_s&.split(',')&.uniq&.flatten || []
+ params[:user_id].to_s.split(',').uniq
end
def validate_invite_source!
diff --git a/app/services/members/creator_service.rb b/app/services/members/creator_service.rb
index 321658ac9c5..81986a2883f 100644
--- a/app/services/members/creator_service.rb
+++ b/app/services/members/creator_service.rb
@@ -82,7 +82,13 @@ module Members
if member.request?
approve_request
else
- member.save
+ # Calling #save triggers callbacks even if there is no change on object.
+ # This previously caused an incident due to the hard to predict
+ # behaviour caused by the large number of callbacks.
+ # See https://gitlab.com/gitlab-com/gl-infra/production/-/issues/6351
+ # and https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80920#note_911569038
+ # for details.
+ member.save if member.changed?
end
end
diff --git a/app/services/members/projects/creator_service.rb b/app/services/members/projects/creator_service.rb
index d92fe60c54a..9e9389d3c18 100644
--- a/app/services/members/projects/creator_service.rb
+++ b/app/services/members/projects/creator_service.rb
@@ -6,16 +6,11 @@ module Members
private
def can_create_new_member?
- # order is important here!
- # The `admin_project_member` check has side-effects that causes projects not be created if this area is hit
- # during project creation.
- # Call that triggers is current_user.can?(:admin_project_member, member.project)
- # I tracked back to base_policy.rb admin check and specifically in
- # Gitlab::Auth::CurrentUserMode.new(@user).admin_mode? call.
- # This calls user.admin? and that specific call causes issues with project creation in
- # spec/requests/api/projects_spec.rb specs and others, mostly around project creation.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/358931 for investigation
- adding_the_creator_as_owner_in_a_personal_project? || current_user.can?(:admin_project_member, member.project)
+ # This access check(`admin_project_member`) will write to safe request store cache for the user being added.
+ # This means any operations inside the same request will need to purge that safe request
+ # store cache if operations are needed to be done inside the same request that checks max member access again on
+ # that user.
+ current_user.can?(:admin_project_member, member.project) || adding_the_creator_as_owner_in_a_personal_project?
end
def can_update_existing_member?
diff --git a/app/services/merge_requests/approval_service.rb b/app/services/merge_requests/approval_service.rb
index 37c2676e51c..e3f0758699b 100644
--- a/app/services/merge_requests/approval_service.rb
+++ b/app/services/merge_requests/approval_service.rb
@@ -33,6 +33,8 @@ module MergeRequests
def execute_approval_hooks(merge_request, current_user)
# Only one approval is required for a merge request to be approved
+ notification_service.async.approve_mr(merge_request, current_user)
+
execute_hooks(merge_request, 'approved')
end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index d197c13378a..44be254441d 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -43,6 +43,8 @@ module MergeRequests
end
def handle_assignees_change(merge_request, old_assignees)
+ bulk_update_assignees_state(merge_request, merge_request.assignees - old_assignees)
+
MergeRequests::HandleAssigneesChangeService
.new(project: project, current_user: current_user)
.async_execute(merge_request, old_assignees)
@@ -58,17 +60,16 @@ module MergeRequests
new_reviewers = merge_request.reviewers - old_reviewers
merge_request_activity_counter.track_users_review_requested(users: new_reviewers)
merge_request_activity_counter.track_reviewers_changed_action(user: current_user)
+ bulk_update_reviewers_state(merge_request, new_reviewers)
unless new_reviewers.include?(current_user)
remove_attention_requested(merge_request)
-
- merge_request.merge_request_reviewers_with(new_reviewers).update_all(updated_state_by_user_id: current_user.id)
end
end
def cleanup_environments(merge_request)
Environments::StopService.new(merge_request.source_project, current_user)
- .execute_for_merge_request(merge_request)
+ .execute_for_merge_request_pipeline(merge_request)
end
def cancel_review_app_jobs!(merge_request)
@@ -246,7 +247,7 @@ module MergeRequests
end
def remove_all_attention_requests(merge_request)
- return unless merge_request.attention_requested_enabled?
+ return unless current_user.mr_attention_requests_enabled?
users = merge_request.reviewers + merge_request.assignees
@@ -254,9 +255,49 @@ module MergeRequests
end
def remove_attention_requested(merge_request)
- return unless merge_request.attention_requested_enabled?
+ return unless current_user.mr_attention_requests_enabled?
+
+ ::MergeRequests::RemoveAttentionRequestedService.new(project: merge_request.project, current_user: current_user, merge_request: merge_request, user: current_user).execute
+ end
+
+ def bulk_update_assignees_state(merge_request, new_assignees)
+ return unless current_user.mr_attention_requests_enabled?
+ return if new_assignees.empty?
+
+ assignees_map = merge_request.merge_request_assignees_with(new_assignees).to_h do |assignee|
+ state = if assignee.user_id == current_user&.id
+ :unreviewed
+ else
+ merge_request.find_reviewer(assignee.assignee)&.state || :attention_requested
+ end
+
+ [
+ assignee,
+ { state: MergeRequestAssignee.states[state], updated_state_by_user_id: current_user.id }
+ ]
+ end
+
+ ::Gitlab::Database::BulkUpdate.execute(%i[state updated_state_by_user_id], assignees_map)
+ end
+
+ def bulk_update_reviewers_state(merge_request, new_reviewers)
+ return unless current_user.mr_attention_requests_enabled?
+ return if new_reviewers.empty?
+
+ reviewers_map = merge_request.merge_request_reviewers_with(new_reviewers).to_h do |reviewer|
+ state = if reviewer.user_id == current_user&.id
+ :unreviewed
+ else
+ merge_request.find_assignee(reviewer.reviewer)&.state || :attention_requested
+ end
+
+ [
+ reviewer,
+ { state: MergeRequestReviewer.states[state], updated_state_by_user_id: current_user.id }
+ ]
+ end
- ::MergeRequests::RemoveAttentionRequestedService.new(project: merge_request.project, current_user: current_user, merge_request: merge_request).execute
+ ::Gitlab::Database::BulkUpdate.execute(%i[state updated_state_by_user_id], reviewers_map)
end
end
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 878e42172b7..ee6f204be45 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -203,6 +203,12 @@ module MergeRequests
target_branch.blank? || target_project.commit(target_branch)
end
+ def set_draft_title_if_needed
+ return unless compare_commits.empty? || Gitlab::Utils.to_boolean(params[:draft])
+
+ merge_request.title = wip_title
+ end
+
# When your branch name starts with an iid followed by a dash this pattern will be
# interpreted as the user wants to close that issue on this project.
#
@@ -220,7 +226,7 @@ module MergeRequests
assign_title_and_description_from_commits
merge_request.title ||= title_from_issue if target_project.issues_enabled? || target_project.external_issue_tracker
merge_request.title ||= source_branch.titleize.humanize
- merge_request.title = wip_title if compare_commits.empty?
+ set_draft_title_if_needed
append_closes_description
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 9c525ae8489..8e0f72eb380 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -25,9 +25,7 @@ module MergeRequests
# expose issuable create method so it can be called from email
# handler CreateMergeRequestHandler
- def create(merge_request)
- super
- end
+ public :create
private
diff --git a/app/services/merge_requests/handle_assignees_change_service.rb b/app/services/merge_requests/handle_assignees_change_service.rb
index a169a6dc0b6..78c93d10f2a 100644
--- a/app/services/merge_requests/handle_assignees_change_service.rb
+++ b/app/services/merge_requests/handle_assignees_change_service.rb
@@ -21,8 +21,6 @@ module MergeRequests
merge_request_activity_counter.track_users_assigned_to_mr(users: new_assignees)
merge_request_activity_counter.track_assignees_changed_action(user: current_user)
- merge_request.merge_request_assignees_with(new_assignees).update_all(updated_state_by_user_id: current_user.id)
-
execute_assignees_hooks(merge_request, old_assignees) if options[:execute_hooks]
unless new_assignees.include?(current_user)
diff --git a/app/services/merge_requests/mergeability/run_checks_service.rb b/app/services/merge_requests/mergeability/run_checks_service.rb
index 03c6d985c23..fd6907c976b 100644
--- a/app/services/merge_requests/mergeability/run_checks_service.rb
+++ b/app/services/merge_requests/mergeability/run_checks_service.rb
@@ -37,7 +37,7 @@ module MergeRequests
attr_reader :merge_request, :params
def run_check(check)
- return check.execute unless Feature.enabled?(:mergeability_caching, merge_request.project, default_enabled: :yaml)
+ return check.execute unless Feature.enabled?(:mergeability_caching, merge_request.project)
return check.execute unless check.cacheable?
cached_result = results.read(merge_check: check)
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index e475b57e4a2..980c757bcbc 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -45,7 +45,19 @@ module MergeRequests
closed_issues = merge_request.visible_closing_issues_for(current_user)
closed_issues.each do |issue|
- Issues::CloseService.new(project: project, current_user: current_user).execute(issue, commit: merge_request)
+ # We are intentionally only closing Issues asynchronously (excluding ExternalIssues)
+ # as the worker only supports finding an Issue. We are also only experiencing
+ # SQL timeouts when closing an Issue.
+ if Feature.enabled?(:async_mr_close_issue, project) && issue.is_a?(Issue)
+ MergeRequests::CloseIssueWorker.perform_async(
+ project.id,
+ current_user.id,
+ issue.id,
+ merge_request.id
+ )
+ else
+ Issues::CloseService.new(project: project, current_user: current_user).execute(issue, commit: merge_request)
+ end
end
end
diff --git a/app/services/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb
index adbe3ddfdad..076fe8c3b21 100644
--- a/app/services/merge_requests/push_options_handler_service.rb
+++ b/app/services/merge_requests/push_options_handler_service.rb
@@ -126,6 +126,7 @@ module MergeRequests
params = {
title: push_options[:title],
description: push_options[:description],
+ draft: push_options[:draft],
target_branch: push_options[:target],
force_remove_source_branch: push_options[:remove_source_branch],
label: push_options[:label],
@@ -147,6 +148,10 @@ module MergeRequests
params[:milestone] = milestone if milestone
end
+ if params.key?(:description)
+ params[:description] = params[:description].gsub('\n', "\n")
+ end
+
params
end
diff --git a/app/services/merge_requests/remove_approval_service.rb b/app/services/merge_requests/remove_approval_service.rb
index c7bc3532264..d9bb17a7b1b 100644
--- a/app/services/merge_requests/remove_approval_service.rb
+++ b/app/services/merge_requests/remove_approval_service.rb
@@ -33,6 +33,7 @@ module MergeRequests
def trigger_approval_hooks(merge_request)
yield
+ notification_service.async.unapprove_mr(merge_request, current_user)
execute_hooks(merge_request, 'unapproved')
end
diff --git a/app/services/merge_requests/remove_attention_requested_service.rb b/app/services/merge_requests/remove_attention_requested_service.rb
index a32a8071471..8a410fda691 100644
--- a/app/services/merge_requests/remove_attention_requested_service.rb
+++ b/app/services/merge_requests/remove_attention_requested_service.rb
@@ -2,22 +2,26 @@
module MergeRequests
class RemoveAttentionRequestedService < MergeRequests::BaseService
- attr_accessor :merge_request
+ attr_accessor :merge_request, :user
- def initialize(project:, current_user:, merge_request:)
+ def initialize(project:, current_user:, merge_request:, user:)
super(project: project, current_user: current_user)
@merge_request = merge_request
+ @user = user
end
def execute
return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request)
if reviewer || assignee
+ return success if reviewer&.reviewed? || assignee&.reviewed?
+
update_state(reviewer)
update_state(assignee)
- current_user.invalidate_attention_requested_count
+ user.invalidate_attention_requested_count
+ create_remove_attention_request_note
success
else
@@ -28,15 +32,19 @@ module MergeRequests
private
def assignee
- merge_request.find_assignee(current_user)
+ @assignee ||= merge_request.find_assignee(user)
end
def reviewer
- merge_request.find_reviewer(current_user)
+ @reviewer ||= merge_request.find_reviewer(user)
end
def update_state(reviewer_or_assignee)
reviewer_or_assignee&.update(state: :reviewed)
end
+
+ def create_remove_attention_request_note
+ SystemNoteService.remove_attention_request(merge_request, merge_request.project, current_user, user)
+ end
end
end
diff --git a/app/services/merge_requests/request_attention_service.rb b/app/services/merge_requests/request_attention_service.rb
new file mode 100644
index 00000000000..07e9996f87b
--- /dev/null
+++ b/app/services/merge_requests/request_attention_service.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class RequestAttentionService < MergeRequests::BaseService
+ attr_accessor :merge_request, :user
+
+ def initialize(project:, current_user:, merge_request:, user:)
+ super(project: project, current_user: current_user)
+
+ @merge_request = merge_request
+ @user = user
+ end
+
+ def execute
+ return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request)
+
+ if reviewer || assignee
+ return success if reviewer&.attention_requested? || assignee&.attention_requested?
+
+ update_state(reviewer)
+ update_state(assignee)
+
+ user.invalidate_attention_requested_count
+ create_attention_request_note
+ notity_user
+
+ if current_user.id != user.id
+ remove_attention_requested(merge_request)
+ end
+
+ success
+ else
+ error("User is not a reviewer or assignee of the merge request")
+ end
+ end
+
+ private
+
+ def notity_user
+ notification_service.async.attention_requested_of_merge_request(merge_request, current_user, user)
+ todo_service.create_attention_requested_todo(merge_request, current_user, user)
+ end
+
+ def create_attention_request_note
+ SystemNoteService.request_attention(merge_request, merge_request.project, current_user, user)
+ end
+
+ def assignee
+ @assignee ||= merge_request.find_assignee(user)
+ end
+
+ def reviewer
+ @reviewer ||= merge_request.find_reviewer(user)
+ end
+
+ def update_state(reviewer_or_assignee)
+ reviewer_or_assignee&.update(state: :attention_requested, updated_state_by: current_user)
+ end
+ end
+end
diff --git a/app/services/merge_requests/update_assignees_service.rb b/app/services/merge_requests/update_assignees_service.rb
index d52c1bbbcda..5b23f69ac4a 100644
--- a/app/services/merge_requests/update_assignees_service.rb
+++ b/app/services/merge_requests/update_assignees_service.rb
@@ -20,6 +20,8 @@ module MergeRequests
attrs = update_attrs.merge(assignee_ids: new_ids)
merge_request.update!(**attrs)
+ bulk_update_assignees_state(merge_request, merge_request.assignees - old_assignees)
+
# Defer the more expensive operations (handle_assignee_changes) to the background
MergeRequests::HandleAssigneesChangeService
.new(project: project, current_user: current_user)
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 391079223ca..6e8afaecbba 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -11,6 +11,10 @@ module MergeRequests
end
def execute(merge_request)
+ if Gitlab::Utils.to_boolean(params[:draft])
+ merge_request.title = merge_request.draft_title
+ end
+
update_merge_request_with_specialized_service(merge_request) || general_fallback(merge_request)
end
diff --git a/app/services/namespaces/in_product_marketing_emails_service.rb b/app/services/namespaces/in_product_marketing_emails_service.rb
index e42c3498c21..414f253deb8 100644
--- a/app/services/namespaces/in_product_marketing_emails_service.rb
+++ b/app/services/namespaces/in_product_marketing_emails_service.rb
@@ -61,7 +61,7 @@ module Namespaces
def initialize(track, interval)
@track = track
@interval = interval
- @sent_email_records = InProductMarketingEmailRecords.new
+ @sent_email_records = ::Users::InProductMarketingEmailRecords.new
end
def execute
@@ -86,7 +86,7 @@ module Namespaces
users_for_group(group).each do |user|
if can_perform_action?(user, group)
send_email(user, group)
- sent_email_records.add(user, track, series)
+ sent_email_records.add(user, track: track, series: series)
end
end
diff --git a/app/services/namespaces/package_settings/update_service.rb b/app/services/namespaces/package_settings/update_service.rb
index cbadbe5c907..c0af0900450 100644
--- a/app/services/namespaces/package_settings/update_service.rb
+++ b/app/services/namespaces/package_settings/update_service.rb
@@ -32,7 +32,7 @@ module Namespaces
end
def allowed?
- Ability.allowed?(current_user, :create_package_settings, @container)
+ Ability.allowed?(current_user, :admin_package, @container)
end
def package_settings_params
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index d32d1c8ca12..4074b1d1182 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -45,13 +45,13 @@ module Notes
def execute_quick_actions(note)
return yield(false) unless quick_actions_supported?(note)
- content, update_params, message = quick_actions_service.execute(note, quick_action_options)
+ content, update_params, message, command_names = quick_actions_service.execute(note, quick_action_options)
only_commands = content.empty?
note.note = content
yield(only_commands)
- do_commands(note, update_params, message, only_commands)
+ do_commands(note, update_params, message, command_names, only_commands)
end
def quick_actions_supported?(note)
@@ -84,7 +84,7 @@ module Notes
end
end
- def do_commands(note, update_params, message, only_commands)
+ def do_commands(note, update_params, message, command_names, only_commands)
return if quick_actions_service.commands_executed_count.to_i == 0
if update_params.present?
@@ -96,6 +96,7 @@ module Notes
# when #save is called
if only_commands
note.errors.add(:commands_only, message.presence || _('Failed to apply commands.'))
+ note.errors.add(:command_names, command_names.flatten)
# Allow consumers to detect problems applying commands
note.errors.add(:commands, _('Failed to apply commands.')) unless message.present?
end
@@ -112,6 +113,7 @@ module Notes
track_note_creation_usage_for_issues(note) if note.for_issue?
track_note_creation_usage_for_merge_requests(note) if note.for_merge_request?
track_incident_action(user, note.noteable, 'incident_comment') if note.for_issue?
+ track_note_creation_in_ipynb(note)
if Feature.enabled?(:notes_create_service_tracking, project)
Gitlab::Tracking.event('Notes::CreateService', 'execute', **tracking_data_for(note))
@@ -134,6 +136,16 @@ module Notes
def track_note_creation_usage_for_merge_requests(note)
Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter.track_create_comment_action(note: note)
end
+
+ def should_track_ipynb_notes?(note)
+ Feature.enabled?(:ipynbdiff_notes_tracker) && note.respond_to?(:diff_file) && note.diff_file&.ipynb?
+ end
+
+ def track_note_creation_in_ipynb(note)
+ return unless should_track_ipynb_notes?(note)
+
+ Gitlab::UsageDataCounters::IpynbDiffActivityCounter.note_created(note)
+ end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index a3f250bb235..32b23d4978f 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -761,6 +761,20 @@ class NotificationService
mailer.in_product_marketing_email(user_id, group_id, track, series).deliver_later
end
+ def approve_mr(merge_request, current_user)
+ approve_mr_email(merge_request, merge_request.target_project, current_user)
+ end
+
+ def unapprove_mr(merge_request, current_user)
+ unapprove_mr_email(merge_request, merge_request.target_project, current_user)
+ end
+
+ def inactive_project_deletion_warning(project, deletion_date)
+ owners_and_maintainers_without_invites(project).each do |recipient|
+ mailer.inactive_project_deletion_warning_email(project, recipient.user, deletion_date).deliver_later
+ end
+ end
+
protected
def new_resource_email(target, current_user, method)
@@ -866,6 +880,22 @@ class NotificationService
private
+ def approve_mr_email(merge_request, project, current_user)
+ recipients = ::NotificationRecipients::BuildService.build_recipients(merge_request, current_user, action: 'approve')
+
+ recipients.each do |recipient|
+ mailer.approved_merge_request_email(recipient.user.id, merge_request.id, current_user.id).deliver_later
+ end
+ end
+
+ def unapprove_mr_email(merge_request, project, current_user)
+ recipients = ::NotificationRecipients::BuildService.build_recipients(merge_request, current_user, action: 'unapprove')
+
+ recipients.each do |recipient|
+ mailer.unapproved_merge_request_email(recipient.user.id, merge_request.id, current_user.id).deliver_later
+ end
+ end
+
def pipeline_notification_status(ref_status, pipeline)
if Ci::Ref.failing_state?(ref_status)
'failed'
diff --git a/app/services/projects/android_target_platform_detector_service.rb b/app/services/projects/android_target_platform_detector_service.rb
new file mode 100644
index 00000000000..11635ad18d5
--- /dev/null
+++ b/app/services/projects/android_target_platform_detector_service.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Projects
+ # Service class to detect if a project is made to run on the Android platform.
+ #
+ # This service searches for an AndroidManifest.xml file which all Android app
+ # project must have. It returns the symbol :android if the given project is an
+ # Android app project.
+ #
+ # Ref: https://developer.android.com/guide/topics/manifest/manifest-intro
+ #
+ # Example usage:
+ # > AndroidTargetPlatformDetectorService.new(a_project).execute
+ # => nil
+ # > AndroidTargetPlatformDetectorService.new(an_android_project).execute
+ # => :android
+ class AndroidTargetPlatformDetectorService < BaseService
+ # <manifest> element is required and must occur once inside AndroidManifest.xml
+ MANIFEST_FILE_SEARCH_QUERY = '<manifest filename:AndroidManifest.xml'
+
+ def execute
+ detect
+ end
+
+ private
+
+ def file_finder
+ @file_finder ||= ::Gitlab::FileFinder.new(project, project.default_branch)
+ end
+
+ def detect
+ return :android if file_finder.find(MANIFEST_FILE_SEARCH_QUERY).present?
+ end
+ end
+end
diff --git a/app/services/projects/blame_service.rb b/app/services/projects/blame_service.rb
new file mode 100644
index 00000000000..f7c1240a3ba
--- /dev/null
+++ b/app/services/projects/blame_service.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+# Service class to correctly initialize Gitlab::Blame and Kaminari pagination
+# objects
+module Projects
+ class BlameService
+ PER_PAGE = 1000
+
+ def initialize(blob, commit, params)
+ @blob = blob
+ @commit = commit
+ @page = extract_page(params)
+ end
+
+ def blame
+ Gitlab::Blame.new(blob, commit, range: blame_range)
+ end
+
+ def pagination
+ return unless pagination_enabled?
+
+ Kaminari.paginate_array([], total_count: blob_lines_count)
+ .page(page)
+ .per(per_page)
+ .limit(per_page)
+ end
+
+ private
+
+ attr_reader :blob, :commit, :page
+
+ def blame_range
+ return unless pagination_enabled?
+
+ first_line = (page - 1) * per_page + 1
+ last_line = (first_line + per_page).to_i - 1
+
+ first_line..last_line
+ end
+
+ def extract_page(params)
+ page = params.fetch(:page, 1).to_i
+
+ return 1 if page < 1 || overlimit?(page)
+
+ page
+ end
+
+ def per_page
+ PER_PAGE
+ end
+
+ def overlimit?(page)
+ page * per_page >= blob_lines_count + per_page
+ end
+
+ def blob_lines_count
+ @blob_lines_count ||= blob.data.lines.count
+ end
+
+ def pagination_enabled?
+ Feature.enabled?(:blame_page_pagination, commit.project)
+ end
+ end
+end
diff --git a/app/services/projects/branches_by_mode_service.rb b/app/services/projects/branches_by_mode_service.rb
index 090671cc79a..0248f997a03 100644
--- a/app/services/projects/branches_by_mode_service.rb
+++ b/app/services/projects/branches_by_mode_service.rb
@@ -37,7 +37,7 @@ class Projects::BranchesByModeService
def use_gitaly_pagination?
return false if params[:page].present? || params[:search].present?
- Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: :yaml)
+ Feature.enabled?(:branch_list_keyset_pagination, project)
end
def fetch_branches_via_offset_pagination
diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb
index 72f3fddb4c3..0a8e8e72766 100644
--- a/app/services/projects/container_repository/cleanup_tags_service.rb
+++ b/app/services/projects/container_repository/cleanup_tags_service.rb
@@ -117,7 +117,6 @@ module Projects
@counts[:before_truncate_size] = @tags.size
@counts[:after_truncate_size] = @tags.size
- return unless throttling_enabled?
return if max_list_size == 0
# truncate the list to make sure that after the #filter_keep_n
@@ -151,10 +150,6 @@ module Projects
!!result
end
- def throttling_enabled?
- Feature.enabled?(:container_registry_expiration_policies_throttling, default_enabled: :yaml)
- end
-
def max_list_size
::Gitlab::CurrentSettings.current_application_settings.container_registry_cleanup_tags_service_max_list_size.to_i
end
diff --git a/app/services/projects/container_repository/delete_tags_service.rb b/app/services/projects/container_repository/delete_tags_service.rb
index b4a57c70111..a3e533c670e 100644
--- a/app/services/projects/container_repository/delete_tags_service.rb
+++ b/app/services/projects/container_repository/delete_tags_service.rb
@@ -8,13 +8,13 @@ module Projects
def execute(container_repository)
@container_repository = container_repository
- unless params[:container_expiration_policy]
+ unless container_expiration_policy?
return error('access denied') unless can?(current_user, :destroy_container_image, project)
end
@tag_names = params[:tags]
return error('not tags specified') if @tag_names.blank?
- return error('repository importing') if @container_repository.migration_importing?
+ return error('repository importing') if cancel_while_importing?
delete_tags
end
@@ -49,6 +49,20 @@ module Projects
log_error(log_data)
end
end
+
+ def cancel_while_importing?
+ return true if @container_repository.importing?
+
+ if container_expiration_policy?
+ return @container_repository.pre_importing? || @container_repository.pre_import_done?
+ end
+
+ false
+ end
+
+ def container_expiration_policy?
+ params[:container_expiration_policy].present?
+ end
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 f109cb0ca20..81cef554dec 100644
--- a/app/services/projects/container_repository/gitlab/delete_tags_service.rb
+++ b/app/services/projects/container_repository/gitlab/delete_tags_service.rb
@@ -46,18 +46,11 @@ module Projects
end
def timeout?(start_time)
- return false unless throttling_enabled?
return false if service_timeout.in?(DISABLED_TIMEOUTS)
(Time.zone.now - start_time) > service_timeout
end
- def throttling_enabled?
- strong_memoize(:feature_flag) do
- Feature.enabled?(:container_registry_expiration_policies_throttling, default_enabled: :yaml)
- end
- end
-
def service_timeout
::Gitlab::CurrentSettings.current_application_settings.container_registry_delete_tags_service_timeout
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 3e26c8c35b2..c7f284bec9b 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -4,6 +4,9 @@ module Projects
class CreateService < BaseService
include ValidatesClassificationLabel
+ ImportSourceDisabledError = Class.new(StandardError)
+ INTERNAL_IMPORT_SOURCES = %w[bare_repository gitlab_custom_project_template gitlab_project_migration].freeze
+
def initialize(user, params)
@current_user = user
@params = params.dup
@@ -25,6 +28,8 @@ module Projects
@project = Project.new(params)
+ validate_import_source_enabled!
+
@project.visibility_level = @project.group.visibility_level unless @project.visibility_level_allowed_by_group?
# If a project is newly created it should have shared runners settings
@@ -77,6 +82,9 @@ module Projects
rescue ActiveRecord::RecordInvalid => e
message = "Unable to save #{e.inspect}: #{e.record.errors.full_messages.join(", ")}"
fail(error: message)
+ rescue ImportSourceDisabledError => e
+ @project.errors.add(:import_source_disabled, e.message) if @project
+ fail(error: e.message)
rescue StandardError => e
@project.errors.add(:base, e.message) if @project
fail(error: e.message)
@@ -124,11 +132,7 @@ module Projects
end
def create_project_settings
- if Feature.enabled?(:create_project_settings, default_enabled: :yaml)
- @project.project_setting.save if @project.project_setting.changed?
- else
- @project.create_project_setting unless @project.project_setting
- end
+ @project.project_setting.save if @project.project_setting.changed?
end
# Add an authorization for the current user authorizations inline
@@ -157,6 +161,13 @@ module Projects
)
else
@project.add_owner(@project.namespace.owner, current_user: current_user)
+ # During the process of adding a project owner, a check on permissions is made on the user which caches
+ # the max member access for that user on this project.
+ # Since that is `0` before the member is created - and we are still inside the request
+ # cycle when we need to do other operations that might check those permissions (e.g. write a commit)
+ # we need to purge that cache so that the updated permissions is fetched instead of using the outdated cached value of 0
+ # from before member creation
+ @project.team.purge_member_access_cache_for_user_id(@project.namespace.owner.id)
end
end
@@ -242,6 +253,18 @@ module Projects
private
+ def validate_import_source_enabled!
+ return unless @params[:import_type]
+
+ import_type = @params[:import_type].to_s
+
+ return if INTERNAL_IMPORT_SOURCES.include?(import_type)
+
+ unless ::Gitlab::CurrentSettings.import_sources&.include?(import_type)
+ raise ImportSourceDisabledError, "#{import_type} import source is disabled"
+ end
+ end
+
def parent_namespace
@parent_namespace ||= Namespace.find_by_id(@params[:namespace_id]) || current_user.namespace
end
diff --git a/app/services/projects/group_links/create_service.rb b/app/services/projects/group_links/create_service.rb
index a0232779c97..72036aaff35 100644
--- a/app/services/projects/group_links/create_service.rb
+++ b/app/services/projects/group_links/create_service.rb
@@ -3,26 +3,31 @@
module Projects
module GroupLinks
class CreateService < BaseService
- def execute(group)
- return error('Not Found', 404) unless group && can?(current_user, :read_namespace, group)
+ include GroupLinkable
- link = project.project_group_links.new(
- group: group,
- group_access: params[:link_group_access],
- expires_at: params[:expires_at]
- )
+ def initialize(project, shared_with_group, user, params)
+ @shared_with_group = shared_with_group
- if link.save
- setup_authorizations(group)
- success(link: link)
- else
- error(link.errors.full_messages.to_sentence, 409)
- end
+ super(project, user, params)
end
private
- def setup_authorizations(group)
+ delegate :root_ancestor, to: :project
+
+ def valid_to_create?
+ can?(current_user, :read_namespace, shared_with_group) && sharing_allowed?
+ end
+
+ def build_link
+ @link = project.project_group_links.new(
+ group: shared_with_group,
+ group_access: params[:link_group_access],
+ expires_at: params[:expires_at]
+ )
+ end
+
+ def setup_authorizations
AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id)
# AuthorizedProjectsWorker uses an exclusive lease per user but
@@ -30,7 +35,7 @@ module Projects
# compare the inconsistency rates of both approaches, we still run
# AuthorizedProjectsWorker but with some delay and lower urgency as a
# safety net.
- group.refresh_members_authorized_projects(
+ shared_with_group.refresh_members_authorized_projects(
blocking: false,
priority: UserProjectAccessChangedService::LOW_PRIORITY
)
diff --git a/app/services/projects/in_product_marketing_campaign_emails_service.rb b/app/services/projects/in_product_marketing_campaign_emails_service.rb
new file mode 100644
index 00000000000..249a2d89fc1
--- /dev/null
+++ b/app/services/projects/in_product_marketing_campaign_emails_service.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Projects
+ class InProductMarketingCampaignEmailsService
+ include Gitlab::Experiment::Dsl
+
+ def initialize(project, campaign)
+ @project = project
+ @campaign = campaign
+ @sent_email_records = ::Users::InProductMarketingEmailRecords.new
+ end
+
+ def execute
+ send_emails
+ end
+
+ private
+
+ attr_reader :project, :campaign, :sent_email_records
+
+ def send_emails
+ project_users.each do |user|
+ send_email(user)
+ end
+
+ sent_email_records.save!
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def project_users
+ @project_users ||= project.users
+ .where(email_opted_in: true)
+ .merge(Users::InProductMarketingEmail.without_campaign(campaign))
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def project_users_max_access_levels
+ ids = project_users.map(&:id)
+ @project_users_max_access_levels ||= project.team.max_member_access_for_user_ids(ids)
+ end
+
+ def send_email(user)
+ return unless user.can?(:receive_notifications)
+ return unless target_user?(user)
+
+ Notify.build_ios_app_guide_email(user.notification_email_or_default).deliver_later
+
+ sent_email_records.add(user, campaign: campaign)
+ experiment(:build_ios_app_guide_email, project: project).track(:email_sent)
+ end
+
+ def target_user?(user)
+ max_access_level = project_users_max_access_levels[user.id]
+ max_access_level >= Gitlab::Access::DEVELOPER
+ end
+ end
+end
diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb
index 76005a1c96e..c032fbf1508 100644
--- a/app/services/projects/lfs_pointers/lfs_download_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_service.rb
@@ -23,7 +23,7 @@ module Projects
def execute
return unless project&.lfs_enabled? && lfs_download_object
return error("LFS file with oid #{lfs_oid} has invalid attributes") unless lfs_download_object.valid?
- return link_existing_lfs_object! if Feature.enabled?(:lfs_link_existing_object, project, default_enabled: :yaml) && lfs_size > LARGE_FILE_SIZE && lfs_object
+ return link_existing_lfs_object! if Feature.enabled?(:lfs_link_existing_object, project) && lfs_size > LARGE_FILE_SIZE && lfs_object
wrap_download_errors do
download_lfs_file!
diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb
index 8b7a418edf5..ee4d559e612 100644
--- a/app/services/projects/open_issues_count_service.rb
+++ b/app/services/projects/open_issues_count_service.rb
@@ -7,12 +7,8 @@ module Projects
include Gitlab::Utils::StrongMemoize
# Cache keys used to store issues count
- # TOTAL_COUNT_KEY includes confidential and hidden issues (admin)
- # TOTAL_COUNT_WITHOUT_HIDDEN_KEY includes confidential issues but not hidden issues (reporter and above)
- # PUBLIC_COUNT_WITHOUT_HIDDEN_KEY does not include confidential or hidden issues (guest)
- TOTAL_COUNT_KEY = 'project_open_issues_including_hidden_count'
- TOTAL_COUNT_WITHOUT_HIDDEN_KEY = 'project_open_issues_without_hidden_count'
- PUBLIC_COUNT_WITHOUT_HIDDEN_KEY = 'project_open_public_issues_without_hidden_count'
+ PUBLIC_COUNT_KEY = 'public_open_issues_count'
+ TOTAL_COUNT_KEY = 'total_open_issues_count'
def initialize(project, user = nil)
@user = user
@@ -20,98 +16,59 @@ module Projects
super(project)
end
- # rubocop: disable CodeReuse/ActiveRecord
- def refresh_cache(&block)
- if block_given?
- super(&block)
- else
- update_cache_for_key(total_count_cache_key) do
- issues_with_hidden
- end
-
- update_cache_for_key(public_count_without_hidden_cache_key) do
- issues_without_hidden_without_confidential
- end
-
- update_cache_for_key(total_count_without_hidden_cache_key) do
- issues_without_hidden_with_confidential
- end
- end
- end
-
- private
-
- def relation_for_count
- self.class.query(@project, public_only: public_only?, include_hidden: include_hidden?)
- end
-
def cache_key_name
- if include_hidden?
- TOTAL_COUNT_KEY
- elsif public_only?
- PUBLIC_COUNT_WITHOUT_HIDDEN_KEY
- else
- TOTAL_COUNT_WITHOUT_HIDDEN_KEY
- end
- end
-
- def include_hidden?
- user_is_admin?
+ public_only? ? PUBLIC_COUNT_KEY : TOTAL_COUNT_KEY
end
def public_only?
!user_is_at_least_reporter?
end
- def user_is_admin?
- strong_memoize(:user_is_admin) do
- @user&.can_admin_all_resources?
- end
- end
-
def user_is_at_least_reporter?
strong_memoize(:user_is_at_least_reporter) do
@user && @project.team.member?(@user, Gitlab::Access::REPORTER)
end
end
- def total_count_without_hidden_cache_key
- cache_key(TOTAL_COUNT_WITHOUT_HIDDEN_KEY)
+ def relation_for_count
+ self.class.query(@project, public_only: public_only?)
end
- def public_count_without_hidden_cache_key
- cache_key(PUBLIC_COUNT_WITHOUT_HIDDEN_KEY)
+ def public_count_cache_key
+ cache_key(PUBLIC_COUNT_KEY)
end
def total_count_cache_key
cache_key(TOTAL_COUNT_KEY)
end
- def issues_with_hidden
- self.class.query(@project, public_only: false, include_hidden: true).count
- end
+ # rubocop: disable CodeReuse/ActiveRecord
+ def refresh_cache(&block)
+ count_grouped_by_confidential = self.class.query(@project, public_only: false).group(:confidential).count
+ public_count = count_grouped_by_confidential[false] || 0
+ total_count = public_count + (count_grouped_by_confidential[true] || 0)
- def issues_without_hidden_without_confidential
- self.class.query(@project, public_only: true, include_hidden: false).count
- end
+ update_cache_for_key(public_count_cache_key) do
+ public_count
+ end
- def issues_without_hidden_with_confidential
- self.class.query(@project, public_only: false, include_hidden: false).count
+ update_cache_for_key(total_count_cache_key) do
+ total_count
+ end
end
- # We only show total issues count for admins, who are allowed to view hidden issues.
- # We also only show issues count including confidential for reporters, who are allowed to view confidential issues.
+ # We only show issues count including confidential for reporters, who are allowed to view confidential issues.
# This will still show a discrepancy on issues number but should be less than before.
# Check https://gitlab.com/gitlab-org/gitlab-foss/issues/38418 description.
+
# rubocop: disable CodeReuse/ActiveRecord
+ def self.query(projects, public_only: true)
+ issues_filtered_by_type = Issue.opened.with_issue_type(Issue::TYPES_FOR_LIST)
- def self.query(projects, public_only: true, include_hidden: false)
- if include_hidden
- Issue.opened.with_issue_type(Issue::TYPES_FOR_LIST).where(project: projects)
- elsif public_only
- Issue.public_only.opened.with_issue_type(Issue::TYPES_FOR_LIST).where(project: projects)
+ if public_only
+ issues_filtered_by_type.public_only.where(project: projects)
else
- Issue.without_hidden.opened.with_issue_type(Issue::TYPES_FOR_LIST).where(project: projects)
+ issues_filtered_by_type.where(project: projects)
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/projects/overwrite_project_service.rb b/app/services/projects/overwrite_project_service.rb
index eea8f867b45..d3fed43363c 100644
--- a/app/services/projects/overwrite_project_service.rb
+++ b/app/services/projects/overwrite_project_service.rb
@@ -41,7 +41,7 @@ module Projects
private
def track_service(start_time, source_project, exception)
- return if ::Feature.disabled?(:project_overwrite_service_tracking, source_project, default_enabled: :yaml)
+ return if ::Feature.disabled?(:project_overwrite_service_tracking, source_project)
duration = ::Gitlab::Metrics::System.monotonic_time - start_time
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index c7a34afffb3..c29770d0c5f 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -45,7 +45,7 @@ module Projects
def visible_groups
visible_groups = project.invited_groups
- unless project_owner?
+ unless project.team.owner?(current_user)
visible_groups = visible_groups.public_or_visible_to_user(current_user)
end
@@ -60,13 +60,5 @@ module Projects
def individual_project_members
project.project_members.select(*GroupMember.cached_column_list)
end
-
- def project_owner?
- if project.group.present?
- project.group.owners.include?(current_user)
- else
- project.namespace.owner == current_user
- end
- end
end
end
diff --git a/app/services/projects/prometheus/alerts/alert_params.rb b/app/services/projects/prometheus/alerts/alert_params.rb
deleted file mode 100644
index 1c39ed36b12..00000000000
--- a/app/services/projects/prometheus/alerts/alert_params.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module Prometheus
- module Alerts
- module AlertParams
- def alert_params
- return params if params[:operator].blank?
-
- params.merge(
- operator: PrometheusAlert.operator_to_enum(params[:operator])
- )
- end
- end
- end
- end
-end
diff --git a/app/services/projects/prometheus/alerts/create_service.rb b/app/services/projects/prometheus/alerts/create_service.rb
deleted file mode 100644
index 0d7d8ab1a62..00000000000
--- a/app/services/projects/prometheus/alerts/create_service.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module Prometheus
- module Alerts
- class CreateService < BaseProjectService
- include AlertParams
-
- def execute
- project.prometheus_alerts.create(alert_params)
- end
- end
- end
- end
-end
diff --git a/app/services/projects/prometheus/alerts/destroy_service.rb b/app/services/projects/prometheus/alerts/destroy_service.rb
deleted file mode 100644
index 243b12eb654..00000000000
--- a/app/services/projects/prometheus/alerts/destroy_service.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module Prometheus
- module Alerts
- class DestroyService < BaseProjectService
- def execute(alert)
- alert.destroy
- end
- end
- end
- end
-end
diff --git a/app/services/projects/prometheus/alerts/update_service.rb b/app/services/projects/prometheus/alerts/update_service.rb
deleted file mode 100644
index 1802f35dae9..00000000000
--- a/app/services/projects/prometheus/alerts/update_service.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module Prometheus
- module Alerts
- class UpdateService < BaseProjectService
- include AlertParams
-
- def execute(alert)
- alert.update(alert_params)
- end
- end
- end
- end
-end
diff --git a/app/services/projects/prometheus/metrics/base_service.rb b/app/services/projects/prometheus/metrics/base_service.rb
index be1783dde70..15247d45776 100644
--- a/app/services/projects/prometheus/metrics/base_service.rb
+++ b/app/services/projects/prometheus/metrics/base_service.rb
@@ -8,40 +8,12 @@ module Projects
def initialize(metric, params = {})
@metric = metric
- @project = metric.project
@params = params.dup
end
protected
- attr_reader :metric, :project, :params
-
- def application
- alert.environment.cluster_prometheus_adapter
- end
-
- def schedule_alert_update
- return unless alert
- return unless alert.environment
-
- ::Clusters::Applications::ScheduleUpdateService.new(
- alert.environment.cluster_prometheus_adapter, project).execute
- end
-
- def alert
- strong_memoize(:alert) { find_alert(metric) }
- end
-
- def find_alert(metric)
- Projects::Prometheus::AlertsFinder
- .new(project: project, metric: metric)
- .execute
- .first
- end
-
- def has_alert?
- alert.present?
- end
+ attr_reader :metric, :params
end
end
end
diff --git a/app/services/projects/prometheus/metrics/destroy_service.rb b/app/services/projects/prometheus/metrics/destroy_service.rb
index 6a46eb5516c..d85499dc4ae 100644
--- a/app/services/projects/prometheus/metrics/destroy_service.rb
+++ b/app/services/projects/prometheus/metrics/destroy_service.rb
@@ -5,7 +5,6 @@ module Projects
module Metrics
class DestroyService < Metrics::BaseService
def execute
- schedule_alert_update if has_alert?
metric.destroy
end
end
diff --git a/app/services/projects/prometheus/metrics/update_service.rb b/app/services/projects/prometheus/metrics/update_service.rb
deleted file mode 100644
index 9b51f4ab47d..00000000000
--- a/app/services/projects/prometheus/metrics/update_service.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module Prometheus
- module Metrics
- class UpdateService < Metrics::BaseService
- def execute
- metric.update!(params)
- schedule_alert_update if requires_alert_update?
- metric
- end
-
- private
-
- def requires_alert_update?
- has_alert? && (changing_title? || changing_query?)
- end
-
- def changing_title?
- metric.previous_changes.include?(:title)
- end
-
- def changing_query?
- metric.previous_changes.include?(:query)
- end
- end
- end
- end
-end
diff --git a/app/services/projects/record_target_platforms_service.rb b/app/services/projects/record_target_platforms_service.rb
index 224e16f53b3..664e72e9785 100644
--- a/app/services/projects/record_target_platforms_service.rb
+++ b/app/services/projects/record_target_platforms_service.rb
@@ -4,26 +4,50 @@ module Projects
class RecordTargetPlatformsService < BaseService
include Gitlab::Utils::StrongMemoize
+ def initialize(project, detector_service)
+ @project = project
+ @detector_service = detector_service
+ end
+
def execute
record_target_platforms
end
private
+ attr_reader :project, :detector_service
+
def target_platforms
strong_memoize(:target_platforms) do
- AppleTargetPlatformDetectorService.new(project).execute
+ Array(detector_service.new(project).execute)
end
end
def record_target_platforms
return unless target_platforms.present?
- setting = ::ProjectSetting.find_or_initialize_by(project: project) # rubocop:disable CodeReuse/ActiveRecord
- setting.target_platforms = target_platforms
- setting.save
+ project_setting.target_platforms = target_platforms
+ project_setting.save
+
+ send_build_ios_app_guide_email
+
+ project_setting.target_platforms
+ end
+
+ def project_setting
+ @project_setting ||= ::ProjectSetting.find_or_initialize_by(project: project) # rubocop:disable CodeReuse/ActiveRecord
+ end
+
+ def experiment_candidate?
+ experiment(:build_ios_app_guide_email, project: project).run
+ end
+
+ def send_build_ios_app_guide_email
+ return unless target_platforms.include? :ios
+ return unless experiment_candidate?
- setting.target_platforms
+ campaign = Users::InProductMarketingEmail::BUILD_IOS_APP_GUIDE
+ Projects::InProductMarketingCampaignEmailsService.new(project, campaign).execute
end
end
end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 2ec965fe2f4..c6ea364320f 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -30,6 +30,7 @@ module Projects
validate_state!
validate_max_size!
+ validate_public_folder!
validate_max_entries!
build.artifacts_file.use_file do |artifacts_path|
@@ -180,6 +181,10 @@ module Projects
end
end
+ def validate_public_folder!
+ raise InvalidStateError, 'Error: The `public/` folder is missing, or not declared in `.gitlab-ci.yml`.' unless total_size > 0
+ end
+
def entries_count
# we're using the full archive and pages daemon needs to read it
# so we want the total count from entries, not only "public/" directory
diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb
index f3ea0967a99..705d23ec704 100644
--- a/app/services/projects/update_remote_mirror_service.rb
+++ b/app/services/projects/update_remote_mirror_service.rb
@@ -65,7 +65,7 @@ module Projects
message += "Error synchronizing LFS files:"
message += "\n\n#{lfs_status[:message]}\n\n"
- failed = Feature.enabled?(:remote_mirror_fail_on_lfs, project, default_enabled: :yaml)
+ failed = Feature.enabled?(:remote_mirror_fail_on_lfs, project)
end
if response.divergent_refs.any?
diff --git a/app/services/prometheus/create_default_alerts_service.rb b/app/services/prometheus/create_default_alerts_service.rb
deleted file mode 100644
index eb8a9d45658..00000000000
--- a/app/services/prometheus/create_default_alerts_service.rb
+++ /dev/null
@@ -1,105 +0,0 @@
-# frozen_string_literal: true
-
-# DEPRECATED: To be removed as part of https://gitlab.com/groups/gitlab-org/-/epics/5877
-module Prometheus
- class CreateDefaultAlertsService < BaseService
- include Gitlab::Utils::StrongMemoize
-
- attr_reader :project
-
- DEFAULT_ALERTS = [
- {
- identifier: 'response_metrics_nginx_ingress_16_http_error_rate',
- operator: 'gt',
- threshold: 0.1
- },
- {
- identifier: 'response_metrics_nginx_ingress_http_error_rate',
- operator: 'gt',
- threshold: 0.1
- },
- {
- identifier: 'response_metrics_nginx_http_error_percentage',
- operator: 'gt',
- threshold: 0.1
- }
- ].freeze
-
- def initialize(project:)
- @project = project
- end
-
- def execute
- return ServiceResponse.error(message: 'Invalid project') unless project
- return ServiceResponse.error(message: 'Invalid environment') unless environment
-
- create_alerts
- schedule_prometheus_update
-
- ServiceResponse.success
- end
-
- private
-
- def create_alerts
- DEFAULT_ALERTS.each do |alert_hash|
- identifier = alert_hash[:identifier]
- next if alerts_by_identifier(environment).key?(identifier)
-
- metric = metrics_by_identifier[identifier]
- next unless metric
-
- create_alert(alert: alert_hash, metric: metric)
- end
- end
-
- def schedule_prometheus_update
- return unless prometheus_adapter
-
- ::Clusters::Applications::ScheduleUpdateService.new(prometheus_adapter, project).execute
- end
-
- def prometheus_adapter
- environment.cluster_prometheus_adapter
- end
-
- def metrics_by_identifier
- strong_memoize(:metrics_by_identifier) do
- metric_identifiers = DEFAULT_ALERTS.map { |alert| alert[:identifier] }
-
- PrometheusMetricsFinder
- .new(identifier: metric_identifiers, common: true)
- .execute
- .index_by(&:identifier)
- end
- end
-
- def alerts_by_identifier(environment)
- strong_memoize(:alerts_by_identifier) do
- Projects::Prometheus::AlertsFinder
- .new(project: project, metric: metrics_by_identifier.values, environment: environment)
- .execute
- .index_by { |alert| alert.prometheus_metric.identifier }
- end
- end
-
- def environment
- strong_memoize(:environment) do
- Environments::EnvironmentsFinder.new(project, nil, name: 'production').execute.first ||
- project.environments.first
- end
- end
-
- def create_alert(alert:, metric:)
- PrometheusAlert.create!(
- project: project,
- prometheus_metric: metric,
- environment: environment,
- threshold: alert[:threshold],
- operator: alert[:operator]
- )
- rescue ActiveRecord::RecordNotUnique
- # Ignore duplicate creations although it unlikely to happen
- end
- end
-end
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 47f4b9c6898..4bcb15b2d9c 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -44,7 +44,7 @@ module QuickActions
content, commands = extractor.extract_commands(content, only: only)
extract_updates(commands)
- [content, @updates, execution_messages_for(commands)]
+ [content, @updates, execution_messages_for(commands), command_names(commands)]
end
# Takes a text and interprets the commands that are extracted from it.
@@ -83,8 +83,10 @@ module QuickActions
args.map! { _1.gsub(/\\_/, '_') }
usernames = (args - ['me']).map { _1.delete_prefix('@') }
found = User.by_username(usernames).to_a.select { can?(:read_user, _1) }
- found_names = found.map(&:username).to_set
- missing = args.reject { |arg| arg == 'me' || found_names.include?(arg.delete_prefix('@')) }.map { "'#{_1}'" }
+ found_names = found.map(&:username).map(&:downcase).to_set
+ missing = args.reject do |arg|
+ arg == 'me' || found_names.include?(arg.downcase.delete_prefix('@'))
+ end.map { "'#{_1}'" }
failed_parse(format(_("Failed to find users for %{missing}"), missing: missing.to_sentence)) if missing.present?
@@ -165,6 +167,15 @@ module QuickActions
end.compact
end
+ def command_names(commands)
+ commands.flatten.map do |name|
+ definition = self.class.definition_by_name(name)
+ next unless definition
+
+ name
+ end.compact
+ end
+
def extract_updates(commands)
commands.each do |name, arg|
definition = self.class.definition_by_name(name)
diff --git a/app/services/service_ping/build_payload_service.rb b/app/services/service_ping/build_payload_service.rb
deleted file mode 100644
index f4ae939fd07..00000000000
--- a/app/services/service_ping/build_payload_service.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-module ServicePing
- class BuildPayloadService
- def execute
- return {} unless allowed_to_report?
-
- raw_payload
- end
-
- private
-
- def allowed_to_report?
- product_intelligence_enabled? && !User.single_user&.requires_usage_stats_consent?
- end
-
- def product_intelligence_enabled?
- ::Gitlab::CurrentSettings.usage_ping_enabled?
- end
-
- def raw_payload
- @raw_payload ||= ::Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values)
- end
- end
-end
-
-ServicePing::BuildPayloadService.prepend_mod_with('ServicePing::BuildPayloadService')
diff --git a/app/services/service_ping/devops_report_service.rb b/app/services/service_ping/devops_report_service.rb
deleted file mode 100644
index 3b8f5dfdb82..00000000000
--- a/app/services/service_ping/devops_report_service.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-module ServicePing
- class DevopsReportService
- def initialize(data)
- @data = data
- end
-
- def execute
- # `conv_index` was previously named `dev_ops_score` in
- # version-gitlab-com, so we check both for backwards compatibility.
- metrics = @data['conv_index'] || @data['dev_ops_score']
-
- # Do not attempt to save a report for the first Service Ping
- # response for a given GitLab instance, which comes without
- # metrics.
- return if metrics.keys == ['usage_data_id']
-
- report = DevOpsReport::Metric.create(
- metrics.slice(*DevOpsReport::Metric::METRICS)
- )
-
- Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ActiveRecord::RecordInvalid.new(report)) unless report.persisted?
- end
- end
-end
diff --git a/app/services/service_ping/permit_data_categories_service.rb b/app/services/service_ping/permit_data_categories_service.rb
deleted file mode 100644
index d8fa255a485..00000000000
--- a/app/services/service_ping/permit_data_categories_service.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-module ServicePing
- class PermitDataCategoriesService
- STANDARD_CATEGORY = 'standard'
- SUBSCRIPTION_CATEGORY = 'subscription'
- OPERATIONAL_CATEGORY = 'operational'
- OPTIONAL_CATEGORY = 'optional'
- CATEGORIES = [
- STANDARD_CATEGORY,
- SUBSCRIPTION_CATEGORY,
- OPERATIONAL_CATEGORY,
- OPTIONAL_CATEGORY
- ].to_set.freeze
-
- def execute
- return [] unless ServicePingSettings.product_intelligence_enabled?
-
- CATEGORIES
- end
- end
-end
-
-ServicePing::PermitDataCategoriesService.prepend_mod_with('ServicePing::PermitDataCategoriesService')
diff --git a/app/services/service_ping/service_ping_settings.rb b/app/services/service_ping/service_ping_settings.rb
deleted file mode 100644
index 6964210b1db..00000000000
--- a/app/services/service_ping/service_ping_settings.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-module ServicePing
- module ServicePingSettings
- extend self
-
- def product_intelligence_enabled?
- enabled? && !User.single_user&.requires_usage_stats_consent?
- end
-
- def enabled?
- ::Gitlab::CurrentSettings.usage_ping_enabled?
- end
- end
-end
-
-ServicePing::ServicePingSettings.extend_mod_with('ServicePing::ServicePingSettings')
diff --git a/app/services/service_ping/submit_service.rb b/app/services/service_ping/submit_service.rb
index c8733bc2f11..343fc00a2f0 100644
--- a/app/services/service_ping/submit_service.rb
+++ b/app/services/service_ping/submit_service.rb
@@ -6,6 +6,7 @@ module ServicePing
STAGING_BASE_URL = 'https://gitlab-services-version-gitlab-com-staging.gs-staging.gitlab.org'
USAGE_DATA_PATH = 'usage_data'
ERROR_PATH = 'usage_ping_errors'
+ METADATA_PATH = 'usage_ping_metadata'
SubmissionError = Class.new(StandardError)
@@ -18,26 +19,27 @@ module ServicePing
start = Time.current
begin
- usage_data = BuildPayloadService.new.execute
+ usage_data = ServicePing::BuildPayload.new.execute
response = submit_usage_data_payload(usage_data)
rescue StandardError => e
return unless Gitlab::CurrentSettings.usage_ping_enabled?
error_payload = {
time: Time.current,
- uuid: Gitlab::UsageData.add_metric('UuidMetric'),
- hostname: Gitlab::UsageData.add_metric('HostnameMetric'),
- version: Gitlab::UsageData.alt_usage_data { Gitlab::VERSION },
- message: e.message,
+ uuid: Gitlab::CurrentSettings.uuid,
+ hostname: Gitlab.config.gitlab.host,
+ version: Gitlab.version_info.to_s,
+ message: "#{e.message.presence || e.class} at #{e.backtrace[0]}",
elapsed: (Time.current - start).round(1)
}
- submit_payload({ error: error_payload }, url: error_url)
+ submit_payload({ error: error_payload }, path: ERROR_PATH)
usage_data = Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values)
response = submit_usage_data_payload(usage_data)
end
- version_usage_data_id = response.dig('conv_index', 'usage_data_id') || response.dig('dev_ops_score', 'usage_data_id')
+ version_usage_data_id =
+ response.dig('conv_index', 'usage_data_id') || response.dig('dev_ops_score', 'usage_data_id')
unless version_usage_data_id.is_a?(Integer) && version_usage_data_id > 0
raise SubmissionError, "Invalid usage_data_id in response: #{version_usage_data_id}"
@@ -46,23 +48,32 @@ module ServicePing
unless @skip_db_write
raw_usage_data = save_raw_usage_data(usage_data)
raw_usage_data.update_version_metadata!(usage_data_id: version_usage_data_id)
- DevopsReportService.new(response).execute
+ ServicePing::DevopsReport.new(response).execute
end
- end
- def url
- URI.join(base_url, USAGE_DATA_PATH)
- end
+ return unless Feature.enabled?(:measure_service_ping_metric_collection)
- def error_url
- URI.join(base_url, ERROR_PATH)
+ submit_payload({ metadata: { metrics: metrics_collection_time(usage_data) } }, path: METADATA_PATH)
end
private
- def submit_payload(payload, url: self.url)
+ def metrics_collection_time(payload, parents = [])
+ return [] unless payload.is_a?(Hash)
+
+ payload.flat_map do |key, metric_value|
+ key_path = parents.dup.append(key)
+ if metric_value.respond_to?(:duration)
+ { name: key_path.join('.'), time_elapsed: metric_value.duration }
+ else
+ metrics_collection_time(metric_value, key_path)
+ end
+ end
+ end
+
+ def submit_payload(payload, path: USAGE_DATA_PATH)
Gitlab::HTTP.post(
- url,
+ URI.join(base_url, path),
body: payload.to_json,
allow_local_requests: true,
headers: { 'Content-type' => 'application/json' }
@@ -80,9 +91,13 @@ module ServicePing
end
def save_raw_usage_data(usage_data)
- RawUsageData.safe_find_or_create_by(recorded_at: usage_data[:recorded_at]) do |record|
+ # safe_find_or_create_by! was originally called here.
+ # We merely switched to `find_or_create_by!`
+ # rubocop: disable CodeReuse/ActiveRecord
+ RawUsageData.find_or_create_by(recorded_at: usage_data[:recorded_at]) do |record|
record.payload = usage_data
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
# See https://gitlab.com/gitlab-org/gitlab/-/issues/233615 for details
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 9db39a5e174..d7e4b53b5de 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -111,6 +111,21 @@ module SystemNoteService
::SystemNotes::TimeTrackingService.new(noteable: noteable, project: project, author: author).change_time_spent
end
+ # Called when a timelog is removed from a Noteable
+ #
+ # noteable - Noteable object
+ # project - Project owning the noteable
+ # author - User performing the change
+ # timelog - The removed timelog
+ #
+ # Example Note text:
+ # "deleted 2h 30m of time spent from 22-03-2022"
+ #
+ # Returns the created Note object
+ def remove_timelog(noteable, project, author, timelog)
+ ::SystemNotes::TimeTrackingService.new(noteable: noteable, project: project, author: author).remove_timelog(timelog)
+ end
+
def close_after_error_tracking_resolve(issue, project, author)
::SystemNotes::IssuablesService.new(noteable: issue, project: project, author: author).close_after_error_tracking_resolve
end
@@ -351,11 +366,27 @@ module SystemNoteService
::SystemNotes::IssuablesService.new(noteable: issue, project: issue.project, author: author).change_issue_type
end
+ def add_timeline_event(timeline_event)
+ incidents_service(timeline_event.incident).add_timeline_event(timeline_event)
+ end
+
+ def edit_timeline_event(timeline_event, author, was_changed:)
+ incidents_service(timeline_event.incident).edit_timeline_event(timeline_event, author, was_changed: was_changed)
+ end
+
+ def delete_timeline_event(noteable, author)
+ incidents_service(noteable).delete_timeline_event(author)
+ end
+
private
def merge_requests_service(noteable, project, author)
::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author)
end
+
+ def incidents_service(incident)
+ ::SystemNotes::IncidentsService.new(noteable: incident)
+ end
end
SystemNoteService.prepend_mod_with('SystemNoteService')
diff --git a/app/services/system_notes/incidents_service.rb b/app/services/system_notes/incidents_service.rb
new file mode 100644
index 00000000000..d5da684a2d8
--- /dev/null
+++ b/app/services/system_notes/incidents_service.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module SystemNotes
+ class IncidentsService < ::SystemNotes::BaseService
+ CHANGED_TEXT = {
+ occurred_at: 'the event time/date on ',
+ note: 'the text on ',
+ occurred_at_and_note: 'the event time/date and text on '
+ }.freeze
+
+ def initialize(noteable:)
+ @noteable = noteable
+ @project = noteable.project
+ end
+
+ def add_timeline_event(timeline_event)
+ author = timeline_event.author
+ anchor = "timeline_event_#{timeline_event.id}"
+ path = url_helpers.project_issues_incident_path(project, noteable, anchor: anchor)
+ body = "added an [incident timeline event](#{path})"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'timeline_event'))
+ end
+
+ def edit_timeline_event(timeline_event, author, was_changed:)
+ anchor = "timeline_event_#{timeline_event.id}"
+ path = url_helpers.project_issues_incident_path(project, noteable, anchor: anchor)
+ changed_text = CHANGED_TEXT.fetch(was_changed, '')
+ body = "edited #{changed_text}[incident timeline event](#{path})"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'timeline_event'))
+ end
+
+ def delete_timeline_event(author)
+ body = 'deleted an incident timeline event'
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'timeline_event'))
+ end
+ end
+end
diff --git a/app/services/system_notes/time_tracking_service.rb b/app/services/system_notes/time_tracking_service.rb
index a804a06fe4c..a9b1f6d3d37 100644
--- a/app/services/system_notes/time_tracking_service.rb
+++ b/app/services/system_notes/time_tracking_service.rb
@@ -76,6 +76,18 @@ module SystemNotes
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
end
+ def remove_timelog(timelog)
+ time_spent = timelog.time_spent
+ spent_at = timelog.spent_at&.to_date
+
+ parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent)
+
+ body = "deleted #{parsed_time} of spent time"
+ body += " from #{spent_at}" if spent_at
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
+ end
+
private
def issue_activity_counter
diff --git a/app/services/timelogs/base_service.rb b/app/services/timelogs/base_service.rb
new file mode 100644
index 00000000000..be46c26e047
--- /dev/null
+++ b/app/services/timelogs/base_service.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Timelogs
+ class BaseService
+ include BaseServiceUtility
+ include Gitlab::Utils::StrongMemoize
+
+ attr_accessor :timelog, :current_user
+
+ def initialize(timelog, user)
+ @timelog = timelog
+ @current_user = user
+ end
+ end
+end
diff --git a/app/services/timelogs/delete_service.rb b/app/services/timelogs/delete_service.rb
new file mode 100644
index 00000000000..0df888a3706
--- /dev/null
+++ b/app/services/timelogs/delete_service.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Timelogs
+ class DeleteService < Timelogs::BaseService
+ def execute
+ unless can?(current_user, :admin_timelog, timelog)
+ return ServiceResponse.error(
+ message: "Timelog doesn't exist or you don't have permission to delete it",
+ http_status: 404)
+ end
+
+ if timelog.destroy
+ issuable = timelog.issuable
+
+ if issuable
+ # Add a system note for the timelog removal
+ SystemNoteService.remove_timelog(issuable, issuable.project, current_user, timelog)
+ end
+
+ ServiceResponse.success(payload: timelog)
+ else
+ ServiceResponse.error(message: 'Failed to remove timelog', http_status: 400)
+ end
+ end
+ end
+end
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index 1ea65049dc2..dfa9316889e 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -63,10 +63,7 @@ module Users
# destroying: https://github.com/rails/rails/issues/22510
# This ensures we delete records in batches.
user.destroy_dependent_associations_in_batches(exclude: [:snippets])
-
- if Feature.enabled?(:nullify_in_batches_on_user_deletion, default_enabled: :yaml)
- user.nullify_dependent_associations_in_batches
- end
+ user.nullify_dependent_associations_in_batches
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
user_data = user.destroy
diff --git a/app/services/namespaces/in_product_marketing_email_records.rb b/app/services/users/in_product_marketing_email_records.rb
index 1237a05ea13..94dbd809496 100644
--- a/app/services/namespaces/in_product_marketing_email_records.rb
+++ b/app/services/users/in_product_marketing_email_records.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-module Namespaces
+module Users
class InProductMarketingEmailRecords
attr_reader :records
@@ -13,9 +13,10 @@ module Namespaces
@records = []
end
- def add(user, track, series)
+ def add(user, campaign: nil, track: nil, series: nil)
@records << Users::InProductMarketingEmail.new(
user: user,
+ campaign: campaign,
track: track,
series: series,
created_at: Time.zone.now,
diff --git a/app/services/users/validate_otp_service.rb b/app/services/users/validate_manual_otp_service.rb
index c8a9f217d22..96a827db13c 100644
--- a/app/services/users/validate_otp_service.rb
+++ b/app/services/users/validate_manual_otp_service.rb
@@ -1,13 +1,13 @@
# frozen_string_literal: true
module Users
- class ValidateOtpService < BaseService
+ class ValidateManualOtpService < BaseService
include ::Gitlab::Auth::Otp::Fortinet
def initialize(current_user)
@current_user = current_user
@strategy = if forti_authenticator_enabled?(current_user)
- ::Gitlab::Auth::Otp::Strategies::FortiAuthenticator.new(current_user)
+ ::Gitlab::Auth::Otp::Strategies::FortiAuthenticator::ManualOtp.new(current_user)
elsif forti_token_cloud_enabled?(current_user)
::Gitlab::Auth::Otp::Strategies::FortiTokenCloud.new(current_user)
else
@@ -19,7 +19,7 @@ module Users
strategy.validate(otp_code)
rescue StandardError => ex
Gitlab::ErrorTracking.log_exception(ex)
- error(message: ex.message)
+ error(ex.message)
end
private
diff --git a/app/services/users/validate_push_otp_service.rb b/app/services/users/validate_push_otp_service.rb
new file mode 100644
index 00000000000..6a914cda28c
--- /dev/null
+++ b/app/services/users/validate_push_otp_service.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Users
+ class ValidatePushOtpService < BaseService
+ include ::Gitlab::Auth::Otp::Fortinet
+
+ def initialize(current_user)
+ @current_user = current_user
+ @strategy = if forti_authenticator_enabled?(current_user)
+ ::Gitlab::Auth::Otp::Strategies::FortiAuthenticator::PushOtp.new(current_user)
+ end
+ end
+
+ def execute
+ strategy.validate
+ rescue StandardError => ex
+ Gitlab::ErrorTracking.log_exception(ex)
+ error(ex.message)
+ end
+
+ private
+
+ attr_reader :strategy
+ end
+end
diff --git a/app/services/work_items/delete_task_service.rb b/app/services/work_items/delete_task_service.rb
new file mode 100644
index 00000000000..3bb23576442
--- /dev/null
+++ b/app/services/work_items/delete_task_service.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module WorkItems
+ class DeleteTaskService
+ def initialize(work_item:, current_user: nil, task_params: {}, lock_version:)
+ @work_item = work_item
+ @current_user = current_user
+ @task_params = task_params
+ @lock_version = lock_version
+ @task = task_params[:task]
+ @errors = []
+ end
+
+ def execute
+ transaction_result = ::WorkItem.transaction do
+ replacement_result = TaskListReferenceRemovalService.new(
+ work_item: @work_item,
+ task: @task,
+ line_number_start: @task_params[:line_number_start],
+ line_number_end: @task_params[:line_number_end],
+ lock_version: @lock_version,
+ current_user: @current_user
+ ).execute
+
+ break ::ServiceResponse.error(message: replacement_result.errors, http_status: 422) if replacement_result.error?
+
+ delete_result = ::WorkItems::DeleteService.new(
+ project: @task.project,
+ current_user: @current_user
+ ).execute(@task)
+
+ if delete_result.error?
+ @errors += delete_result.errors
+ raise ActiveRecord::Rollback
+ end
+
+ delete_result
+ end
+
+ return transaction_result if transaction_result
+
+ ::ServiceResponse.error(message: @errors, http_status: 422)
+ end
+ end
+end
diff --git a/app/services/work_items/task_list_reference_removal_service.rb b/app/services/work_items/task_list_reference_removal_service.rb
new file mode 100644
index 00000000000..e7ec73a96e0
--- /dev/null
+++ b/app/services/work_items/task_list_reference_removal_service.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module WorkItems
+ class TaskListReferenceRemovalService
+ STALE_OBJECT_MESSAGE = 'Stale work item. Check lock version'
+
+ def initialize(work_item:, task:, line_number_start:, line_number_end:, lock_version:, current_user:)
+ @work_item = work_item
+ @task = task
+ @line_number_start = line_number_start
+ @line_number_end = line_number_end
+ @lock_version = lock_version
+ @current_user = current_user
+ end
+
+ def execute
+ return ::ServiceResponse.error(message: 'line_number_start must be greater than 0') if @line_number_start < 1
+ return ::ServiceResponse.error(message: "Work item description can't be blank") if @work_item.description.blank?
+
+ if @line_number_end < @line_number_start
+ return ::ServiceResponse.error(message: 'line_number_end must be greater or equal to line_number_start')
+ end
+
+ source_lines = @work_item.description.split("\n")
+
+ line_matches_reference = (@line_number_start..@line_number_end).any? do |line_number|
+ markdown_line = source_lines[line_number - 1]
+
+ /#{Regexp.escape(@task.to_reference)}(?!\d)/.match?(markdown_line)
+ end
+
+ unless line_matches_reference
+ return ::ServiceResponse.error(
+ message: "Unable to detect a task on lines #{@line_number_start}-#{@line_number_end}"
+ )
+ end
+
+ remove_task_lines!(source_lines)
+
+ ::WorkItems::UpdateService.new(
+ project: @work_item.project,
+ current_user: @current_user,
+ params: { description: source_lines.join("\n"), lock_version: @lock_version }
+ ).execute(@work_item)
+
+ if @work_item.valid?
+ ::ServiceResponse.success
+ else
+ ::ServiceResponse.error(message: @work_item.errors.full_messages)
+ end
+ rescue ActiveRecord::StaleObjectError
+ ::ServiceResponse.error(message: STALE_OBJECT_MESSAGE)
+ end
+
+ private
+
+ def remove_task_lines!(source_lines)
+ source_lines.delete_if.each_with_index do |_line, index|
+ index >= @line_number_start - 1 && index < @line_number_end
+ end
+ end
+ end
+end
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 a0fa69c54c5..f914de138a9 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -19,6 +19,10 @@
= f.label :receive_max_input_size, _('Maximum push size (MB)'), class: 'label-light'
= f.number_field :receive_max_input_size, class: 'form-control gl-form-input qa-receive-max-input-size-field', title: _('Maximum size limit for a single commit.'), data: { toggle: 'tooltip', container: 'body' }
.form-group
+ = f.label :max_export_size, _('Maximum export size (MB)'), class: 'label-light'
+ = f.number_field :max_export_size, class: 'form-control gl-form-input', title: _('Maximum size of export files.'), data: { toggle: 'tooltip', container: 'body' }
+ %span.form-text.text-muted= _('Set to 0 for no size limit.')
+ .form-group
= f.label :max_import_size, _('Maximum import size (MB)'), class: 'label-light'
= f.number_field :max_import_size, class: 'form-control gl-form-input qa-receive-max-import-size-field', title: _('Maximum size of import files.'), data: { toggle: 'tooltip', container: 'body' }
%span.form-text.text-muted= _('Only effective when remote storage is enabled. Set to 0 for no size limit.')
@@ -29,9 +33,7 @@
= 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/enforce_pat_expiration', form: f
= render_if_exists 'admin/application_settings/ssh_key_expiration_policy', form: f
- = render_if_exists 'admin/application_settings/enforce_ssh_key_expiration', form: f
.form-group
= f.label :user_oauth_applications, _('User OAuth applications'), class: 'label-bold'
diff --git a/app/views/admin/application_settings/_issue_limits.html.haml b/app/views/admin/application_settings/_issue_limits.html.haml
index 663e1485749..431e2a64c46 100644
--- a/app/views/admin/application_settings/_issue_limits.html.haml
+++ b/app/views/admin/application_settings/_issue_limits.html.haml
@@ -1,9 +1,9 @@
-= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-issue-limits-settings'), html: { class: 'fieldset-form' } do |f|
+= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-issue-limits-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- = f.label :issues_create_limit, 'Max requests per minute per user', class: 'label-bold'
+ = f.label :issues_create_limit, _('Maximum number of requests per minute')
= f.number_field :issues_create_limit, class: 'form-control gl-form-input'
= f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_note_limits.html.haml b/app/views/admin/application_settings/_note_limits.html.haml
index d4ae0d3944c..40760b3c45e 100644
--- a/app/views/admin/application_settings/_note_limits.html.haml
+++ b/app/views/admin/application_settings/_note_limits.html.haml
@@ -9,7 +9,7 @@
= f.label :notes_create_limit_allowlist, _('Users to exclude from the rate limit'), class: 'label-bold'
= f.text_area :notes_create_limit_allowlist_raw, class: 'form-control gl-form-input', rows: 5, aria: { describedBy: 'note-create-limits-allowlist-field-description' }
.form-text.text-muted{ id: 'note-create-limits-allowlist-field-description' }
- = _('List of users allowed to exceed the rate limit.')
+ = _('List of users who are allowed to exceed the rate limit. Example: username1, username2')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_pipeline_limits.html.haml b/app/views/admin/application_settings/_pipeline_limits.html.haml
new file mode 100644
index 00000000000..e93823172db
--- /dev/null
+++ b/app/views/admin/application_settings/_pipeline_limits.html.haml
@@ -0,0 +1,9 @@
+= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-pipeline-limits-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :pipeline_limit_per_project_user_sha, _('Maximum number of requests per minute')
+ = f.number_field :pipeline_limit_per_project_user_sha, class: 'form-control gl-form-input'
+
+ = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_prometheus.html.haml b/app/views/admin/application_settings/_prometheus.html.haml
index 11830fac336..59681c0278e 100644
--- a/app/views/admin/application_settings/_prometheus.html.haml
+++ b/app/views/admin/application_settings/_prometheus.html.haml
@@ -1,20 +1,17 @@
-= form_for @application_setting, url: metrics_and_profiling_admin_application_settings_path(anchor: 'js-prometheus-settings'), html: { class: 'fieldset-form' } do |f|
+= gitlab_ui_form_for @application_setting, url: metrics_and_profiling_admin_application_settings_path(anchor: 'js-prometheus-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- .form-check
- = f.check_box :prometheus_metrics_enabled, class: 'form-check-input'
- = f.label :prometheus_metrics_enabled, class: 'form-check-label' do
- = _("Enable health and performance metrics endpoint")
- .form-text.text-muted
- = _('Enable a Prometheus endpoint that exposes health and performance statistics. The Health Check menu item appears in the Monitoring section of the Admin Area. Restart required.')
- = link_to _('Learn More.'), help_page_path('administration/monitoring/prometheus/gitlab_metrics.md'), target: '_blank', rel: 'noopener noreferrer'
+ - prometheus_help_link_url = help_page_path('administration/monitoring/prometheus/gitlab_metrics')
+ - prometheus_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: prometheus_help_link_url }
+ = f.gitlab_ui_checkbox_component :prometheus_metrics_enabled,
+ _('Enable health and performance metrics endpoint'),
+ help_text: s_('AdminSettings|Enable a Prometheus endpoint that exposes health and performance statistics. The Health Check menu item appears in the Monitoring section of the Admin Area. Restart required. %{link_start}Learn more.%{link_end}').html_safe % { link_start: prometheus_help_link_start, link_end: '</a>'.html_safe }
+ .form-text.gl-text-gray-500.gl-pl-6
- unless Gitlab::Metrics.metrics_folder_present?
- .form-text.text-muted
- %strong.cred= _("WARNING:")
- = _("Environment variable %{environment_variable} does not exist or is not pointing to a valid directory.").html_safe % { environment_variable: '<code>prometheus_multiproc_dir</code>'.html_safe }
- = link_to sprite_icon('question-o'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory')
+ - icon_link = link_to sprite_icon('question-o'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory'), target: '_blank', rel: 'noopener noreferrer'
+ = s_('AdminSettings|%{strongStart}WARNING:%{strongEnd} Environment variable %{environment_variable} does not exist or is not pointing to a valid directory. %{icon_link}').html_safe % { strongStart: '<strong class="gl-text-red-500">'.html_safe, strongEnd: '</strong>'.html_safe, environment_variable: '<code>prometheus_multiproc_dir</code>'.html_safe, icon_link: icon_link }
.form-group
= f.label :metrics_method_call_threshold, _('Method call threshold (ms)'), class: 'label-bold'
= f.number_field :metrics_method_call_threshold, class: 'form-control gl-form-input'
diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml
index eb1f94a2f04..856db32e088 100644
--- a/app/views/admin/application_settings/_registry.html.haml
+++ b/app/views/admin/application_settings/_registry.html.haml
@@ -13,7 +13,6 @@
= f.gitlab_ui_checkbox_component :container_expiration_policies_enable_historic_entries,
'%{label} %{label_link}'.html_safe % { label: label, label_link: label_link },
help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
- - if container_registry_expiration_policies_throttling?
.form-group
= f.label :container_registry_delete_tags_service_timeout, _('Cleanup policy maximum processing time (seconds)'), class: 'label-bold'
= f.number_field :container_registry_delete_tags_service_timeout, min: 0, class: 'form-control gl-form-input'
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
index 62a90e173ec..b5fa08aed79 100644
--- a/app/views/admin/application_settings/_repository_storage.html.haml
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -1,16 +1,16 @@
-= form_for @application_setting, url: repository_admin_application_settings_path(anchor: 'js-repository-storage-settings'), html: { class: 'fieldset-form' } do |f|
+= gitlab_ui_form_for @application_setting, url: repository_admin_application_settings_path(anchor: 'js-repository-storage-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.sub-section
%h4= _('Hashed repository storage paths')
.form-group
- .form-check
- = f.check_box :hashed_storage_enabled, class: 'form-check-input qa-hashed-storage-checkbox', disabled: @application_setting.hashed_storage_enabled?
- = f.label :hashed_storage_enabled, _('Use hashed storage'), class: 'label-bold form-check-label'
- .form-text.text-muted
- = _('Use hashed storage paths for newly created and renamed repositories. Always enabled since 13.0.')
- = link_to s_('Learn more.'), help_page_path('administration/repository_storage_types.md', anchor: 'hashed-storage'), target: '_blank', rel: 'noopener noreferrer'
+ - repository_storage_help_link_url = help_page_path('administration/repository_storage_types.md')
+ - 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'),
+ checkbox_options: { disabled: @application_setting.hashed_storage_enabled? },
+ help_text: _('Use hashed storage paths for newly created and renamed repositories. Always enabled since 13.0. %{link_start}Learn more.%{link_end}').html_safe % { link_start: repository_storage_help_link_start, link_end: '</a>'.html_safe }
.sub-section
%h4= _("Storage nodes for new repositories")
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 08b3d173d20..1d6051a06ea 100644
--- a/app/views/admin/application_settings/_runner_registrars_form.html.haml
+++ b/app/views/admin/application_settings/_runner_registrars_form.html.haml
@@ -1,16 +1,16 @@
-= form_for @application_setting, url: ci_cd_admin_application_settings_path(anchor: 'js-runner-settings'), html: { class: 'fieldset-form' } do |f|
+= gitlab_ui_form_for @application_setting, url: ci_cd_admin_application_settings_path(anchor: 'js-runner-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- = hidden_field_tag "application_setting[valid_runner_registrars][]", nil
- - ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES.each do |type|
- .form-check
- = f.check_box(:valid_runner_registrars, { multiple: true, checked: valid_runner_registrars.include?(type), class: 'form-check-input' }, type, nil)
- = f.label :valid_runner_registrars, class: 'form-check-label' do
- = s_("Runners|Members of the %{type} can register runners") % { type: type }
- %span.form-text.gl-text-gray-600
+ .gl-form-group
+ %span.form-text.gl-mb-3.gl-mt-0
= _('If no options are selected, only administrators can register runners.')
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'prevent-users-from-registering-runners'), target: '_blank', rel: 'noopener noreferrer'
+ = hidden_field_tag "application_setting[valid_runner_registrars][]", nil
+ - ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES.each do |type|
+ = f.gitlab_ui_checkbox_component :valid_runner_registrars, s_("Runners|Members of the %{type} can register runners") % { type: type },
+ checkbox_options: { multiple: true, checked: valid_runner_registrars.include?(type) },
+ checked_value: type,
+ unchecked_value: nil
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index bce210d28d3..48f0b9b2c31 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -1,39 +1,28 @@
-= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-signin-settings'), html: { class: 'fieldset-form', id: 'signin-settings' } do |f|
+= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-signin-settings'), html: { class: 'fieldset-form', id: 'signin-settings' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
- .form-check
- = f.check_box :password_authentication_enabled_for_web, class: 'form-check-input'
- = f.label :password_authentication_enabled_for_web, class: 'form-check-label' do
- = _('Allow password authentication for the web interface')
- .form-text.text-muted
- = _('Clear this checkbox to use an external authentication provider instead.')
+ = f.gitlab_ui_checkbox_component :password_authentication_enabled_for_web,
+ _('Allow password authentication for the web interface'),
+ help_text: _('Clear this checkbox to use an external authentication provider instead.')
.form-group
- .form-check
- = f.check_box :password_authentication_enabled_for_git, class: 'form-check-input'
- = f.label :password_authentication_enabled_for_git, class: 'form-check-label' do
- = _('Allow password authentication for Git over HTTP(S)')
- .form-text.text-muted
- - if Gitlab::Auth::Ldap::Config.enabled?
- = _('Clear this checkbox to use a personal access token or LDAP password instead.')
- - else
- = _('Clear this checkbox to use a personal access token instead.')
+ = f.gitlab_ui_checkbox_component :password_authentication_enabled_for_git,
+ _('Allow password authentication for Git over HTTP(S)'),
+ help_text: Gitlab::Auth::Ldap::Config.enabled? ? _('Clear this checkbox to use a personal access token or LDAP password instead.') : _('Clear this checkbox to use a personal access token instead.')
- if omniauth_enabled? && button_based_providers.any?
%fieldset.form-group
%legend.gl-font-base.gl-mb-3.gl-border-none.gl-font-weight-bold= _('Enabled OAuth authentication sources')
= hidden_field_tag 'application_setting[enabled_oauth_sign_in_sources][]'
- - oauth_providers_checkboxes.each do |source|
+ - oauth_providers_checkboxes(f).each do |source|
= source
.form-group
= f.label :two_factor_authentication, _('Two-factor authentication'), class: 'label-bold'
- .form-check
- = f.check_box :require_two_factor_authentication, class: 'form-check-input'
- = f.label :require_two_factor_authentication, class: 'form-check-label' do
- = _('Enforce two-factor authentication')
- %p.form-text.text-muted
- = _('Enforce two-factor authentication for all user sign-ins.')
- = link_to _('Learn more.'), help_page_path('security/two_factor_authentication.md'), target: '_blank', rel: 'noopener noreferrer'
+ - 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'
+ = 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 }
.form-group
= f.label :two_factor_authentication, _('Two-factor grace period'), class: 'label-bold'
= f.number_field :two_factor_grace_period, min: 0, class: 'form-control gl-form-input', placeholder: '0'
@@ -42,22 +31,18 @@
.form-group
= f.label :admin_mode, _('Admin Mode'), class: 'label-bold'
= sprite_icon('lock', css_class: 'gl-icon')
- .form-check
- = f.check_box :admin_mode, class: 'form-check-input'
- = f.label :admin_mode, class: 'form-check-label' do
- = _('Enable admin mode')
- %p.form-text.text-muted
- = _('Require additional authentication for administrative tasks.')
- = link_to _('Learn more.'), help_page_path('user/admin_area/settings/sign_in_restrictions', anchor: 'admin-mode'), target: '_blank', rel: 'noopener noreferrer'
+ - help_text = _('Require additional authentication for administrative tasks.')
+ - help_link = link_to _('Learn more.'), help_page_path('user/admin_area/settings/sign_in_restrictions', anchor: 'admin-mode'), target: '_blank', rel: 'noopener noreferrer'
+ = f.gitlab_ui_checkbox_component :admin_mode,
+ _('Enable admin mode'),
+ help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
.form-group
= f.label :unknown_sign_in, _('Email notification for unknown sign-ins'), class: 'label-bold'
- .form-check
- = f.check_box :notify_on_unknown_sign_in, class: 'form-check-input'
- = f.label :notify_on_unknown_sign_in, class: 'form-check-label' do
- = _('Enable email notification')
- %p.form-text.text-muted
- = _('Notify users by email when sign-in location is not recognized.')
- = link_to _('Learn more.'), help_page_path('user/profile/unknown_sign_in_notification.md'), target: '_blank', rel: 'noopener noreferrer'
+ - help_text = _('Notify users by email when sign-in location is not recognized.')
+ - help_link = link_to _('Learn more.'), help_page_path('user/profile/unknown_sign_in_notification.md'), 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 }
.form-group
= f.label :home_page_url, _('Home page URL'), class: 'label-bold'
= f.text_field :home_page_url, class: 'form-control gl-form-input', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block'
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
index a1285a3f467..8b4ac9b79c8 100644
--- a/app/views/admin/application_settings/_usage.html.haml
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -13,7 +13,7 @@
.form-group
- can_be_configured = @application_setting.usage_ping_can_be_configured?
- service_ping_link_start = link_start % { url: help_page_path('development/service_ping/index') }
- - deactivating_service_ping_link_start = link_start % { url: help_page_path('development/service_ping/index', anchor: 'disable-service-ping-using-the-configuration-file') }
+ - deactivating_service_ping_link_start = link_start % { url: help_page_path('user/admin_area/settings/usage_statistics', anchor: 'disable-usage-statistics-with-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'),
@@ -28,7 +28,7 @@
.form-group
- usage_ping_enabled = @application_setting.usage_ping_enabled?
- label = s_('AdminSettings|Enable Registration Features')
- - label_link = link_to sprite_icon('question-o'), help_page_path('development/service_ping/index', anchor: 'registration-features-program')
+ - label_link = link_to sprite_icon('question-o'), help_page_path('user/admin_area/settings/usage_statistics', anchor: 'registration-features-program')
- help_text = usage_ping_enabled ? s_('AdminSettings|You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in the future, you will also need to register with GitLab via a new cloud licensing service.') : s_('AdminSettings|To enable Registration Features, first enable Service Ping.')
= f.gitlab_ui_checkbox_component :usage_ping_features_enabled?, '%{label} %{label_link}'.html_safe % { label: label, label_link: label_link },
help_text: '<span id="service_ping_features_helper_text">%{help_text}</span>'.html_safe % { help_text: help_text },
diff --git a/app/views/admin/application_settings/_users_api_limits.html.haml b/app/views/admin/application_settings/_users_api_limits.html.haml
index 9b3502b3cfd..3918c76b12c 100644
--- a/app/views/admin/application_settings/_users_api_limits.html.haml
+++ b/app/views/admin/application_settings/_users_api_limits.html.haml
@@ -9,6 +9,6 @@
= f.label :users_get_by_id_limit_allowlist_raw, _('Users to exclude from the rate limit'), class: 'label-bold'
= f.text_area :users_get_by_id_limit_allowlist_raw, class: 'form-control gl-form-input', rows: 5, aria: { describedBy: 'users-api-limit-users-allowlist-field-description' }
.form-text.text-muted{ id: 'users-api-limit-users-allowlist-field-description' }
- = _('List of users allowed to exceed the rate limit.')
+ = _('List of users who are allowed to exceed the rate limit. Example: username1, username2')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index 23649bc2d54..e3c044ff979 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -16,29 +16,30 @@
= f.label :default_group_visibility, class: 'label-bold'
= render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new)
.form-group
- = f.label :restricted_visibility_levels, class: 'label-bold'
+ = f.label :restricted_visibility_levels, class: 'label-bold gl-mb-0'
+ %span.form-text.gl-mt-0.gl-mb-3#restricted-visibility-help
+ = _('Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.')
= hidden_field_tag 'application_setting[restricted_visibility_levels][]'
- restricted_level_checkboxes(f).each do |level|
= level
- %span.form-text.text-muted#restricted-visibility-help
- = _('Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.')
.form-group
- = f.label :import_sources, class: 'label-bold'
- = hidden_field_tag 'application_setting[import_sources][]'
- - import_sources_checkboxes(f).each do |source|
- = source
- %span.form-text.text-muted#import-sources-help
+ = f.label :import_sources, s_('AdminSettings|Import sources'), class: 'label-bold gl-mb-0'
+ %span.form-text.gl-mt-0.gl-mb-3#import-sources-help
= _('Enabled sources for code import during project creation. OmniAuth must be configured for GitHub')
= link_to sprite_icon('question-o'), help_page_path("integration/github")
, Bitbucket
= link_to sprite_icon('question-o'), help_page_path("integration/bitbucket")
and GitLab.com
= link_to sprite_icon('question-o'), help_page_path("integration/gitlab")
+ = hidden_field_tag 'application_setting[import_sources][]'
+ - import_sources_checkboxes(f).each do |source|
+ = source
= render_if_exists 'admin/application_settings/ldap_access_setting', form: f
.form-group
- = f.gitlab_ui_checkbox_component :project_export_enabled, s_('AdminSettings|Project export enabled')
+ = f.label :project_export, s_('AdminSettings|Project export'), class: 'label-bold'
+ = f.gitlab_ui_checkbox_component :project_export_enabled, s_('AdminSettings|Enabled')
.form-group
%label.label-bold= _('Enabled Git access protocols')
diff --git a/app/views/admin/application_settings/ci/_header.html.haml b/app/views/admin/application_settings/ci/_header.html.haml
index 1298be9a6cb..a22e67b0522 100644
--- a/app/views/admin/application_settings/ci/_header.html.haml
+++ b/app/views/admin/application_settings/ci/_header.html.haml
@@ -13,7 +13,7 @@
= _('Variables can be:')
%ul
%li
- = html_escape(_('%{code_open}Protected:%{code_close} Only exposed to protected branches or tags.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+ = html_escape(_('%{code_open}Protected:%{code_close} Only exposed to protected branches or protected tags.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%li
= html_escape(_('%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
= link_to _('Learn more.'), help_page_path('ci/variables/index', anchor: 'mask-a-cicd-variable'), target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/admin/application_settings/ci_cd.html.haml b/app/views/admin/application_settings/ci_cd.html.haml
index aab4f44d4d7..e925175b7ea 100644
--- a/app/views/admin/application_settings/ci_cd.html.haml
+++ b/app/views/admin/application_settings/ci_cd.html.haml
@@ -8,7 +8,7 @@
.settings-content
- if ci_variable_protected_by_default?
%p.settings-message.text-center
- - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/index', anchor: 'protect-a-cicd-variable') }
+ - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/index', anchor: 'protected-cicd-variables') }
= s_('Environment variables on this GitLab instance are configured to be %{link_start}protected%{link_end} by default.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
#js-instance-variables{ data: { endpoint: admin_ci_variables_path, group: 'true', maskable_regex: ci_variable_maskable_regex, protected_by_default: ci_variable_protected_by_default?.to_s} }
@@ -38,7 +38,7 @@
.settings-content
= render 'registry'
-- if Feature.enabled?(:runner_registration_control, default_enabled: :yaml)
+- if Feature.enabled?(:runner_registration_control)
%section.settings.as-runner.no-animate#js-runner-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml
index bc2fedec69c..7643f8fa7a7 100644
--- a/app/views/admin/application_settings/general.html.haml
+++ b/app/views/admin/application_settings/general.html.haml
@@ -93,7 +93,7 @@
%p
= _('Manage Web IDE features.')
.settings-content
- = gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: "#js-web-ide-settings"), html: { class: 'fieldset-form', id: 'web-ide-settings' } do |f|
+ = gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: "js-web-ide-settings"), html: { class: 'fieldset-form', id: 'web-ide-settings' } do |f|
= form_errors(@application_setting)
%fieldset
@@ -115,4 +115,4 @@
= render 'admin/application_settings/snowplow'
= render 'admin/application_settings/eks'
= render 'admin/application_settings/floc'
-= render_if_exists 'admin/application_settings/license_file'
+= render_if_exists 'admin/application_settings/add_license'
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 7cb5760f62a..8e4b0b53f28 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -53,9 +53,7 @@
.settings-content
= render 'usage'
-= render_if_exists 'admin/application_settings/pseudonymizer_settings', expanded: expanded_by_default?
-
-- if Feature.enabled?(:configure_sentry_in_application_settings, default_enabled: :yaml)
+- 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' } }
.settings-header
%h4
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
index ea35b7ab9c4..a2497fe122b 100644
--- a/app/views/admin/application_settings/network.html.haml
+++ b/app/views/admin/application_settings/network.html.haml
@@ -157,4 +157,16 @@
.settings-content
= render 'import_export_limits'
+%section.settings.as-pipeline-limits.no-animate#js-pipeline-limits-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Pipeline creation rate limits')
+ %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Limit the number of pipeline creation requests per minute. This limit includes pipelines created through the UI, the API, and by background processing.')
+ = link_to _('Learn more.'), help_page_path('user/admin_area/settings/rate_limit_on_pipelines_creation.md'), target: '_blank', rel: 'noopener noreferrer'
+ .settings-content
+ = render 'pipeline_limits'
+
= render_if_exists 'admin/application_settings/ee_network_settings'
diff --git a/app/views/admin/application_settings/service_usage_data.html.haml b/app/views/admin/application_settings/service_usage_data.html.haml
index ec084c05cf7..55c25ca32d5 100644
--- a/app/views/admin/application_settings/service_usage_data.html.haml
+++ b/app/views/admin/application_settings/service_usage_data.html.haml
@@ -18,9 +18,9 @@
- else
= render Pajamas::AlertComponent.new(variant: :warning,
dismissible: false,
- title: _('Service Ping payload not found in the application cache')) do
+ title: _('Service Ping payload not found in the application cache')) do |c|
- .gl-alert-body
+ = c.body do
- enable_service_ping_link_url = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'enable-or-disable-usage-statistics')
- enable_service_ping_link = '<a href="%{url}">'.html_safe % { url: enable_service_ping_link_url }
- generate_manually_link_url = help_page_path('administration/troubleshooting/gitlab_rails_cheat_sheet', anchor: 'generate-service-ping')
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index 925b3681298..fd73d4c5671 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -21,25 +21,13 @@
.col-sm-2.col-form-label.pt-0
= f.label :trusted
.col-sm-10
- = f.check_box :trusted
- %span.form-text.text-muted
- Trusted applications are automatically authorized on GitLab OAuth flow. It's highly recommended for the security of users that trusted applications have the confidential setting set to true.
+ = f.gitlab_ui_checkbox_component :trusted, _('Trusted applications are automatically authorized on GitLab OAuth flow. It\'s highly recommended for the security of users that trusted applications have the confidential setting set to true.')
= content_tag :div, class: 'form-group row' do
.col-sm-2.col-form-label.pt-0
= f.label :confidential
.col-sm-10
- = f.check_box :confidential
- %span.form-text.text-muted
- = _('The application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential.')
-
- = content_tag :div, class: 'form-group row' do
- .col-sm-2.col-form-label.pt-0
- = f.label :expire_access_tokens
- .col-sm-10
- = f.check_box :expire_access_tokens
- %span.form-text.text-muted
- = _('Access tokens expire after 2 hours. A refresh token may be used at any time to generate a new access token. Non-expiring access tokens are deprecated. Clear this setting to enable backward compatibility.')
+ = f.gitlab_ui_checkbox_component :confidential, _('The application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential.')
.form-group.row
.col-sm-2.col-form-label.pt-0
diff --git a/app/views/admin/background_migrations/_job.html.haml b/app/views/admin/background_migrations/_job.html.haml
new file mode 100644
index 00000000000..e34f73e8b94
--- /dev/null
+++ b/app/views/admin/background_migrations/_job.html.haml
@@ -0,0 +1,10 @@
+%tr{ role: 'row' }
+ %td{ role: 'cell', data: { label: _('Id') } }
+ = link_to admin_background_migration_batched_job_path(job.batched_migration, job, params: { database: params[:database] }) do
+ = job.id
+ %td{ role: 'cell', data: { label: s_('BackgroundMigrations|Started at') } }
+ = job.started_at
+ %td{ role: 'cell', data: { label: s_('BackgroundMigrations|Finished at') } }
+ = job.finished_at
+ %td{ role: 'cell', data: { label: s_('BackgroundMigrations|Batch size') } }
+ = job.batch_size
diff --git a/app/views/admin/background_migrations/_migration.html.haml b/app/views/admin/background_migrations/_migration.html.haml
index 9cef8332259..f4906028e39 100644
--- a/app/views/admin/background_migrations/_migration.html.haml
+++ b/app/views/admin/background_migrations/_migration.html.haml
@@ -1,5 +1,7 @@
%tr{ role: 'row' }
- %td{ role: 'cell', data: { label: _('Migration') } }= migration.job_class_name + ': ' + migration.table_name
+ %td{ role: 'cell', data: { label: _('Migration') } }
+ = link_to admin_background_migration_path(migration, database: params[:database]) do
+ = migration.job_class_name + ': ' + migration.table_name
%td{ role: 'cell', data: { label: _('Progress') } }
- progress = batched_migration_progress(migration, @successful_rows_counts[migration.id])
- if progress
@@ -10,14 +12,14 @@
= gl_badge_tag migration.status_name.to_s.humanize, { size: :sm, variant: batched_migration_status_badge_variant(migration) }
%td{ role: 'cell', data: { label: _('Action') } }
- if migration.active?
- = button_to pause_admin_background_migration_path(migration),
+ = button_to pause_admin_background_migration_path(migration, database: params[:database]),
class: 'gl-button btn btn-icon has-tooltip', title: _('Pause'), 'aria-label' => _('Pause') do
= sprite_icon('pause', css_class: 'gl-button-icon gl-icon')
- elsif migration.paused?
- = button_to resume_admin_background_migration_path(migration),
+ = button_to resume_admin_background_migration_path(migration, database: params[:database]),
class: 'gl-button btn btn-icon has-tooltip', title: _('Resume'), 'aria-label' => _('Resume') do
= sprite_icon('play', css_class: 'gl-button-icon gl-icon')
- elsif migration.failed?
- = button_to retry_admin_background_migration_path(migration),
+ = button_to retry_admin_background_migration_path(migration, database: params[:database]),
class: 'gl-button btn btn-icon has-tooltip', title: _('Retry'), 'aria-label' => _('Retry') do
= sprite_icon('retry', css_class: 'gl-button-icon gl-icon')
diff --git a/app/views/admin/background_migrations/_migration_full_information.html.haml b/app/views/admin/background_migrations/_migration_full_information.html.haml
new file mode 100644
index 00000000000..620274c375f
--- /dev/null
+++ b/app/views/admin/background_migrations/_migration_full_information.html.haml
@@ -0,0 +1,21 @@
+%tr{ role: 'row' }
+ %td{ role: 'cell', data: { label: _('Id') } }
+ = @migration.id
+ %td{ role: 'cell', data: { label: _('Min Value') } }
+ = @migration.min_value
+ %td{ role: 'cell', data: { label: _('Max Value') } }
+ = @migration.max_value
+ %td{ role: 'cell', data: { label: _('Batch size') } }
+ = @migration.batch_size
+ %td{ role: 'cell', data: { label: _('Sub-batch size') } }
+ = @migration.sub_batch_size
+ %td{ role: 'cell', data: { label: _('Interval') } }
+ = @migration.interval
+ %td{ role: 'cell', data: { label: _('Pause time (ms)') } }
+ = @migration.pause_ms
+ %td{ role: 'cell', data: { label: _('Created on') } }
+ = @migration.created_at
+ %td{ role: 'cell', data: { label: _('Last updated') } }
+ = @migration.updated_at
+ %td{ role: 'cell', data: { label: _('Status') } }
+ = gl_badge_tag @migration.status_name.to_s.humanize, { size: :sm, variant: batched_migration_status_badge_variant(@migration) }
diff --git a/app/views/admin/background_migrations/index.html.haml b/app/views/admin/background_migrations/index.html.haml
index afceb6427e0..c8b195219ec 100644
--- a/app/views/admin/background_migrations/index.html.haml
+++ b/app/views/admin/background_migrations/index.html.haml
@@ -1,13 +1,26 @@
-- page_title _('Background Migrations')
+- 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-flex-grow-1
+ %h3= s_('BackgroundMigrations|Background Migrations')
+ %p.light.gl-mb-0
+ - learnmore_link = help_page_path('development/database/batched_background_migrations')
+ - learnmore_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: learnmore_link }
+ = html_escape(s_('BackgroundMigrations|Background migrations are used to perform data migrations whenever a migration exceeds the time limits in our guidelines. %{linkStart}Learn more%{linkEnd}')) % { linkStart: learnmore_link_start, linkEnd: '</a>'.html_safe }
+
+ - if @databases.size > 1
+ .gl-display-flex.gl-align-items-center.gl-flex-grow-0.gl-flex-basis-0.gl-sm-mt-0.gl-mt-5.gl-sm-ml-7.gl-ml-0
+ #js-database-listbox{ data: { databases: @databases, selected_database: @selected_database } }
= gl_tabs_nav do
- = gl_tab_link_to admin_background_migrations_path, item_active: @current_tab == 'queued' do
+ = gl_tab_link_to admin_background_migrations_path({ tab: nil, database: params[:database] }), item_active: @current_tab == 'queued' do
= _('Queued')
= gl_tab_counter_badge limited_counter_with_delimiter(@relations_by_tab['queued'])
- = gl_tab_link_to admin_background_migrations_path(tab: 'failed'), item_active: @current_tab == 'failed' do
+ = gl_tab_link_to admin_background_migrations_path({ tab: 'failed', database: params[:database] }), item_active: @current_tab == 'failed' do
= _('Failed')
= gl_tab_counter_badge limited_counter_with_delimiter(@relations_by_tab['failed'])
- = gl_tab_link_to admin_background_migrations_path(tab: 'finished'), item_active: @current_tab == 'finished' do
+ = gl_tab_link_to admin_background_migrations_path({ tab: 'finished', database: params[:database] }), item_active: @current_tab == 'finished' do
= _('Finished')
= gl_tab_counter_badge limited_counter_with_delimiter(@relations_by_tab['finished'])
@@ -16,10 +29,10 @@
%table.table.b-table.gl-table.b-table-stacked-md{ role: 'table' }
%thead{ role: 'rowgroup' }
%tr{ role: 'row' }
- %th.table-th-transparent.border-bottom{ role: 'cell' }= _('Migration')
- %th.table-th-transparent.border-bottom{ role: 'cell' }= _('Progress')
- %th.table-th-transparent.border-bottom{ role: 'cell' }= _('Status')
- %th.table-th-transparent.border-bottom{ role: 'cell' }
+ %th.border-bottom{ role: 'cell' }= _('Migration')
+ %th.border-bottom{ role: 'cell' }= _('Progress')
+ %th.border-bottom{ role: 'cell' }= _('Status')
+ %th.border-bottom{ role: 'cell' }
%tbody{ role: 'rowgroup' }
= render partial: 'migration', collection: @migrations
diff --git a/app/views/admin/background_migrations/show.html.haml b/app/views/admin/background_migrations/show.html.haml
new file mode 100644
index 00000000000..7915a9d36dc
--- /dev/null
+++ b/app/views/admin/background_migrations/show.html.haml
@@ -0,0 +1,39 @@
+- add_to_breadcrumbs s_('BackgroundMigrations|Background Migrations'), admin_background_migrations_path(database: params[:database])
+- breadcrumb_title @migration.id
+- page_title @migration.job_class_name , s_('BackgroundMigrations|Background Migrations')
+- @breadcrumb_link = admin_background_migration_path(@migration, database: params[:database])
+
+%h3= @migration.job_class_name + ': ' + @migration.table_name
+
+.tab-content.gl-tab-content
+ .tab-pane.active{ role: 'tabpanel' }
+ %table.table.b-table.gl-table.b-table-stacked-md{ role: 'table' }
+ %thead{ role: 'rowgroup' }
+ %tr{ role: 'row' }
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= _('Id')
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= _('Min Value')
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= _('Max Value')
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= _('Batch size')
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= _('Sub-batch size')
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= _('Interval')
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= _('Pause time (ms)')
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= _('Created on')
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= _('Last updated')
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= _('Status')
+ %tbody{ role: 'rowgroup' }
+ = render partial: 'migration_full_information', migration: @migration
+
+- if @migration.batched_jobs.with_status(:failed).any?
+ %h5= s_('BackgroundMigrations|Failed jobs:')
+
+ %table.table.b-table.gl-table.b-table-stacked-md{ role: 'table' }
+ %thead{ role: 'rowgroup' }
+ %tr{ role: 'row' }
+ %th{ role: 'cell' }= _('Id')
+ %th{ role: 'cell' }= s_('BackgroundMigrations|Started at')
+ %th{ role: 'cell' }= s_('BackgroundMigrations|Finished at')
+ %th{ role: 'cell' }= s_('BackgroundMigrations|Batch size')
+ %tbody{ role: 'rowgroup' }
+ = render partial: 'job', collection: @failed_jobs
+
+ = paginate_collection @failed_jobs
diff --git a/app/views/admin/batched_jobs/_job.html.haml b/app/views/admin/batched_jobs/_job.html.haml
new file mode 100644
index 00000000000..512f4062ccf
--- /dev/null
+++ b/app/views/admin/batched_jobs/_job.html.haml
@@ -0,0 +1,17 @@
+%tr{ role: 'row' }
+ %td{ role: 'cell', data: { label: _('Id') } }
+ = @job.id
+ %td{ role: 'cell', data: { label: s_('BatchedJob|Min value') } }
+ = @job.min_value
+ %td{ role: 'cell', data: { label: s_('BatchedJob|Max value') } }
+ = @job.max_value
+ %td{ role: 'cell', data: { label: s_('BatchedJob|Batch size') } }
+ = @job.batch_size
+ %td{ role: 'cell', data: { label: s_('BatchedJob|Started at') } }
+ = @job.started_at
+ %td{ role: 'cell', data: { label: s_('BatchedJob|Finished at') } }
+ = @job.finished_at
+ %td{ role: 'cell', data: { label: s_('BatchedJob|Attempts') } }
+ = @job.attempts
+ %td{ role: 'cell', data: { label: s_('BatchedJob|Pause ms') } }
+ = @job.pause_ms
diff --git a/app/views/admin/batched_jobs/_transition_log.html.haml b/app/views/admin/batched_jobs/_transition_log.html.haml
new file mode 100644
index 00000000000..bd81be4679a
--- /dev/null
+++ b/app/views/admin/batched_jobs/_transition_log.html.haml
@@ -0,0 +1,13 @@
+%tr{ role: 'row' }
+ %td{ role: 'cell', data: { label: _('Id') } }
+ = transition_log.id
+ %td{ role: 'cell', data: { label: s_('BatchedJob|Created at') } }
+ = transition_log.created_at
+ %td{ role: 'cell', data: { label: s_('BatchedJob|Previous status') } }
+ = transition_log.previous_status
+ %td{ role: 'cell', data: { label: s_('BatchedJob|Next status') } }
+ = transition_log.next_status
+ %td{ role: 'cell', data: { label: s_('BatchedJob|Exception class') } }
+ = transition_log.exception_class
+ %td{ role: 'cell', data: { label: s_('BatchedJob|Exception message') } }
+ = transition_log.exception_message
diff --git a/app/views/admin/batched_jobs/show.html.haml b/app/views/admin/batched_jobs/show.html.haml
new file mode 100644
index 00000000000..760635413a5
--- /dev/null
+++ b/app/views/admin/batched_jobs/show.html.haml
@@ -0,0 +1,36 @@
+- add_to_breadcrumbs s_('Batched Job|Background Migrations'), admin_background_migrations_path(database: params[:database])
+- add_to_breadcrumbs @job.batched_background_migration_id, admin_background_migration_path(@job.batched_migration, database: params[:database])
+- breadcrumb_title sprintf(s_('Batched Job|Batched Job (Id: %{id})'), { id: @job.id.to_s})
+- page_title @job.id, s_('BatchedJob|Batched Jobs')
+- @breadcrumb_link = admin_background_migration_batched_job_path(@job.batched_migration, @job, database: params[:database])
+
+%h3= sprintf(s_('Batched Job|Batched Job (Id: %{id})'), { id: @job.id.to_s})
+
+%table.table.b-table.gl-table.b-table-stacked-md{ role: 'table' }
+ %thead{ role: 'rowgroup' }
+ %tr{ role: 'row' }
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= _('Id')
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= s_('BatchedJob|Min Value')
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= s_('BatchedJob|Max Value')
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= s_('BatchedJob|Batch size')
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= s_('BatchedJob|Started at')
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= s_('BatchedJob|Finished at')
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= s_('BatchedJob|Attempts')
+ %th.table-th-transparent.border-bottom{ role: 'cell' }= s_('BatchedJob|Pause time (ms)')
+ %tbody{ role: 'rowgroup' }
+ = render partial: 'job', job: @job
+
+- if @transition_logs.any?
+ %h5= s_('BatchedJob|Transition logs:')
+
+ %table.table.b-table.gl-table.b-table-stacked-md{ role: 'table' }
+ %thead{ role: 'rowgroup' }
+ %tr{ role: 'row' }
+ %th{ role: 'cell' }= _('Id')
+ %th{ role: 'cell' }= s_('BatchedJob|Created At')
+ %th{ role: 'cell' }= s_('BatchedJob|Previous Status')
+ %th{ role: 'cell' }= s_('BatchedJob|Next Status')
+ %th{ role: 'cell' }= s_('BatchedJob|Exception Class')
+ %th{ role: 'cell' }= s_('BatchedJob|Exception Message')
+ %tbody{ role: 'rowgroup' }
+ = render partial: 'transition_log', collection: @transition_logs
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index 4102918931f..dfd3b87c674 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -29,7 +29,7 @@
= f.label :starts_at, _("Dismissable")
.col-sm-10
= f.gitlab_ui_checkbox_component :dismissable, _('Allow users to dismiss the broadcast message')
- - if Feature.enabled?(:role_targeted_broadcast_messages, default_enabled: :yaml)
+ - if Feature.enabled?(:role_targeted_broadcast_messages)
.form-group.row
.col-sm-2.col-form-label
= f.label :target_access_levels, _('Target roles')
diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml
index 54c2a9d5250..8b657eda0c0 100644
--- a/app/views/admin/broadcast_messages/index.html.haml
+++ b/app/views/admin/broadcast_messages/index.html.haml
@@ -1,6 +1,6 @@
- breadcrumb_title _("Messages")
- page_title _("Broadcast Messages")
-- targeted_broadcast_messages_enabled = Feature.enabled?(:role_targeted_broadcast_messages, default_enabled: :yaml)
+- targeted_broadcast_messages_enabled = Feature.enabled?(:role_targeted_broadcast_messages)
%h3.page-title
= _('Broadcast Messages')
diff --git a/app/views/admin/dashboard/_security_newsletter_callout.html.haml b/app/views/admin/dashboard/_security_newsletter_callout.html.haml
index 9b994b757f9..4b1303cc97c 100644
--- a/app/views/admin/dashboard/_security_newsletter_callout.html.haml
+++ b/app/views/admin/dashboard/_security_newsletter_callout.html.haml
@@ -6,9 +6,9 @@
alert_data: { feature_id: Users::CalloutsHelper::SECURITY_NEWSLETTER_CALLOUT,
dismiss_endpoint: callouts_path,
defer_links: 'true' },
- close_button_data: { testid: 'close-security-newsletter-callout' }) do
- .gl-alert-body
+ close_button_data: { testid: 'close-security-newsletter-callout' }) do |c|
+ = c.body do
= s_('AdminArea|Sign up for the GitLab Security Newsletter to get notified for security updates.')
- .gl-alert-actions
+ = c.actions do
= link_to 'https://about.gitlab.com/company/preference-center/', target: '_blank', rel: 'noreferrer noopener', class: 'deferred-link gl-alert-action btn-confirm btn-md gl-button' do
= s_('AdminArea|Sign up for the GitLab newsletter')
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 8ac6f63cdfb..944d7bfced0 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -27,8 +27,8 @@
- if @group.new_record?
.form-group.row
.offset-sm-2.col-sm-10
- = render Pajamas::AlertComponent.new(dismissible: false) do
- .gl-alert-body
+ = render Pajamas::AlertComponent.new(dismissible: false) do |c|
+ = c.body do
= render 'shared/group_tips'
.form-actions
= f.submit _('Create group'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 09f2d431197..39b2fa41c80 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -112,7 +112,7 @@
= form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
%div
- = users_select_tag(:user_ids, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all)
+ = users_select_tag(:user_id, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all)
.gl-mt-3
= select_tag :access_level, options_for_select(@group.access_level_roles), class: "project-access-select select2"
%hr
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index be7055e6f7b..16f6e71d79b 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -16,8 +16,8 @@
.col-md-12
= render Pajamas::AlertComponent.new(variant: :danger,
alert_class: 'gl-mb-5',
- alert_data: { testid: 'last-repository-check-failed-alert' }) do
- .gl-alert-body
+ alert_data: { testid: 'last-repository-check-failed-alert' }) do |c|
+ = c.body do
- last_check_message = _("Last repository check (%{last_check_timestamp}) failed. See the 'repocheck.log' file for error messages.")
- last_check_message = last_check_message % { last_check_timestamp: time_ago_with_tooltip(@project.last_repository_check_at) }
= last_check_message.html_safe
diff --git a/app/views/admin/requests_profiles/index.html.haml b/app/views/admin/requests_profiles/index.html.haml
deleted file mode 100644
index 9d42a2bfa93..00000000000
--- a/app/views/admin/requests_profiles/index.html.haml
+++ /dev/null
@@ -1,22 +0,0 @@
-- page_title _('Requests Profiles')
-
-%h3.page-title
- = page_title
-
-.bs-callout.clearfix
- = html_escape(_('Pass the header %{codeOpen} X-Profile-Token: %{profile_token} %{codeClose} to profile the request')) % { profile_token: @profile_token, codeOpen: '<code>'.html_safe, codeClose: '</code>'.html_safe }
-
-- if @profiles.present?
- .gl-mt-3
- - @profiles.each do |path, profiles|
- .card
- .card-header
- %code= path
- %ul.content-list
- - profiles.each do |profile|
- %li
- = link_to profile.time.to_s(:long) + ' ' + profile.profile_mode.capitalize,
- admin_requests_profile_path(profile)
-- else
- %p
- = _('No profiles found')
diff --git a/app/views/admin/runners/edit.html.haml b/app/views/admin/runners/edit.html.haml
index 5570c46c17f..ccdfe67ea77 100644
--- a/app/views/admin/runners/edit.html.haml
+++ b/app/views/admin/runners/edit.html.haml
@@ -1,95 +1,53 @@
-- add_page_specific_style 'page_bundles/ci_status'
-
- runner_name = "##{@runner.id} (#{@runner.short_sha})"
-- if Feature.enabled?(:runner_read_only_admin_view, default_enabled: :yaml)
- - breadcrumb_title _('Edit')
- - page_title _('Edit'), runner_name
- - add_to_breadcrumbs _('Runners'), admin_runners_path
- - add_to_breadcrumbs runner_name, admin_runner_path(@runner)
-- else
- - breadcrumb_title runner_name
- - page_title runner_name
+- breadcrumb_title _('Edit')
+- page_title _('Edit'), runner_name
+- add_to_breadcrumbs _('Runners'), admin_runners_path
+- add_to_breadcrumbs runner_name, admin_runner_path(@runner)
-#js-admin-runner-edit{ data: {runner_id: @runner.id} }
+#js-admin-runner-edit{ data: {runner_id: @runner.id, runner_path: admin_runner_path(@runner) } }
-.row
- .col-md-6
- %h4= _('Restrict projects for this runner')
- - if @runner.runner_projects.any?
- %table.table{ data: { testid: 'assigned-projects' } }
- %thead
- %tr
- %th= _('Assigned projects')
- - @runner.runner_projects.each do |runner_project|
- - project = runner_project.project
- - if project
- %tr
- %td
- = render Pajamas::AlertComponent.new(variant: :danger,
- dismissible: false,
- title: project.full_name) do
- .gl-alert-actions
- = link_to _('Disable'), admin_namespace_project_runner_project_path(project.namespace, project, runner_project), method: :delete, class: 'btn gl-alert-action btn-confirm btn-md gl-button'
+- if @runner.project_type?
+ .gl-overflow-auto
+ %h4.gl-font-lg.gl-my-5= _('Restrict projects for this runner')
- %table.table{ data: { testid: 'unassigned-projects' } }
+ - if @runner.runner_projects.any?
+ %table.table{ data: { testid: 'assigned-projects' } }
%thead
%tr
- %th= _('Project')
- %th
-
+ %th= _('Assigned projects')
+ - @runner.runner_projects.each do |runner_project|
+ - project = runner_project.project
+ - if project
+ %tr
+ %td
+ = render Pajamas::AlertComponent.new(variant: :danger,
+ dismissible: false,
+ title: project.full_name) do |c|
+ = c.actions do
+ = link_to _('Disable'), admin_namespace_project_runner_project_path(project.namespace, project, runner_project), method: :delete, class: 'btn gl-alert-action btn-confirm btn-md gl-button'
+
+ %table.table{ data: { testid: 'unassigned-projects' } }
+ %thead
+ %tr
+ %th= s_('Runners|Select projects to assign to this runner')
+ %th
+
+ %tr
+ %td
+ = form_tag edit_admin_runner_path(@runner), id: 'runner-projects-search', class: 'form-inline', method: :get do
+ .input-group
+ = search_field_tag :search, params[:search], class: 'form-control gl-form-input', spellcheck: false
+ .input-group-append
+ = submit_tag _('Search'), class: 'gl-button btn btn-default'
+
+ %td
+ - @projects.each do |project|
%tr
%td
- = form_tag edit_admin_runner_path(@runner), id: 'runner-projects-search', class: 'form-inline', method: :get do
- .input-group
- = search_field_tag :search, params[:search], class: 'form-control gl-form-input', spellcheck: false
- .input-group-append
- = submit_tag _('Search'), class: 'gl-button btn btn-default'
-
+ = project.full_name
%td
- - @projects.each do |project|
- %tr
- %td
- = project.full_name
- %td
- .float-right
- = form_for project.runner_projects.new, url: admin_namespace_project_runner_projects_path(project.namespace, project), method: :post do |f|
- = f.hidden_field :runner_id, value: @runner.id
- = f.submit _('Enable'), aria: { label: s_('Runners|Change to project runner') }, class: 'gl-button btn btn-sm', data: { confirm: (s_('Runners|You are about to change this instance runner to a project runner. This operation is not reversible. Are you sure you want to continue?') if @runner.instance_type?), confirm_btn_variant: 'danger' }
- = paginate_without_count @projects
-
- .col-md-6
- %h4= _('Recent jobs served by this runner')
- %table.table.ci-table.runner-builds
- %thead
- %tr
- %th= _('Job')
- %th= _('Status')
- %th= _('Project')
- %th= _('Commit')
- %th= _('Finished at')
-
- - @builds.each do |build|
- - project = build.project
- %tr.build
- %td.id
- - if project
- = link_to project_job_path(project, build) do
- %strong ##{build.id}
- - else
- %strong ##{build.id}
-
- %td.status
- = render 'ci/status/badge', status: build.detailed_status(current_user)
-
- %td.status
- - if project
- = project.full_name
-
- %td.build-link
- - if project
- = link_to pipeline_path(build.pipeline) do
- %strong= build.pipeline.short_sha
-
- %td.timestamp
- - if build.finished_at
- %span= time_ago_with_tooltip build.finished_at
+ .float-right
+ = form_for project.runner_projects.new, url: admin_namespace_project_runner_projects_path(project.namespace, project), method: :post do |f|
+ = f.hidden_field :runner_id, value: @runner.id
+ = f.submit _('Enable'), class: 'gl-button btn btn-sm'
+ = paginate_without_count @projects
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 5c4a7026f50..22351397b9a 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -5,4 +5,4 @@
- page_title title
- add_to_breadcrumbs _('Runners'), admin_runners_path
-#js-admin-runner-show{ data: {runner_id: @runner.id} }
+#js-admin-runner-show{ data: {runner_id: @runner.id, runners_path: admin_runners_path} }
diff --git a/app/views/admin/sessions/_new_base.html.haml b/app/views/admin/sessions/_new_base.html.haml
index c9b002a4dd2..65eb1358b40 100644
--- a/app/views/admin/sessions/_new_base.html.haml
+++ b/app/views/admin/sessions/_new_base.html.haml
@@ -4,4 +4,4 @@
= password_field_tag 'user[password]', nil, class: 'form-control', autocomplete: 'current-password', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
.submit-container.move-submit-down
- = submit_tag _('Enter Admin Mode'), class: 'gl-button btn btn-success', data: { qa_selector: 'enter_admin_mode_button' }
+ = submit_tag _('Enter Admin Mode'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'enter_admin_mode_button' }
diff --git a/app/views/admin/sessions/_signin_box.html.haml b/app/views/admin/sessions/_signin_box.html.haml
index ab7eb8c79dc..9372bae14c3 100644
--- a/app/views/admin/sessions/_signin_box.html.haml
+++ b/app/views/admin/sessions/_signin_box.html.haml
@@ -14,6 +14,6 @@
= render_if_exists 'devise/sessions/new_smartcard'
- if allow_admin_mode_password_authentication_for_web?
- .login-box.tab-pane{ id: 'login-pane', role: 'tabpanel', class: active_when(!any_form_based_providers_enabled?) }
+ .login-box.tab-pane.gl-p-5{ id: 'login-pane', role: 'tabpanel', class: active_when(!any_form_based_providers_enabled?) }
.login-body
= render 'admin/sessions/new_base'
diff --git a/app/views/admin/sessions/_two_factor_otp.html.haml b/app/views/admin/sessions/_two_factor_otp.html.haml
index 3fe6e20a367..40ba79d1a65 100644
--- a/app/views/admin/sessions/_two_factor_otp.html.haml
+++ b/app/views/admin/sessions/_two_factor_otp.html.haml
@@ -6,4 +6,4 @@
= _("Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.")
.submit-container.move-submit-down
- = submit_tag 'Verify code', class: 'gl-button btn btn-success'
+ = submit_tag 'Verify code', class: 'gl-button btn btn-confirm'
diff --git a/app/views/admin/sessions/new.html.haml b/app/views/admin/sessions/new.html.haml
index 67c607270a5..7d07b49c98e 100644
--- a/app/views/admin/sessions/new.html.haml
+++ b/app/views/admin/sessions/new.html.haml
@@ -8,7 +8,7 @@
- if any_form_based_providers_enabled?
= render 'devise/shared/tabs_ldap', show_password_form: allow_admin_mode_password_authentication_for_web?, render_signup_link: false
- else
- = render 'devise/shared/tabs_normal', tab_title: _('Enter Admin Mode'), render_signup_link: false
+ = render 'devise/shared/tab_single', tab_title: page_title
.tab-content
- if allow_admin_mode_password_authentication_for_web? || ldap_sign_in_enabled? || crowd_enabled?
= render 'admin/sessions/signin_box'
diff --git a/app/views/admin/sessions/two_factor.html.haml b/app/views/admin/sessions/two_factor.html.haml
index 531ab206157..3f915846dd8 100644
--- a/app/views/admin/sessions/two_factor.html.haml
+++ b/app/views/admin/sessions/two_factor.html.haml
@@ -5,9 +5,9 @@
.col-md-5.new-session-forms-container
.login-page
#signin-container
- = render 'devise/shared/tabs_normal', tab_title: _('Enter Admin Mode'), render_signup_link: false
+ = render 'devise/shared/tab_single', tab_title: _('Enter Admin Mode')
.tab-content
- .login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' }
+ .login-box.tab-pane.gl-p-5.active{ id: 'login-pane', role: 'tabpanel' }
.login-body
- if current_user.two_factor_otp_enabled?
= render 'admin/sessions/two_factor_otp'
diff --git a/app/views/admin/topics/_form.html.haml b/app/views/admin/topics/_form.html.haml
index 50ef375dd35..9b9d97950cc 100644
--- a/app/views/admin/topics/_form.html.haml
+++ b/app/views/admin/topics/_form.html.haml
@@ -3,13 +3,20 @@
.form-group
= f.label :name do
- = _("Topic name")
- = f.text_field :name, placeholder: _('My topic'), class: 'form-control input-lg', data: { qa_selector: 'topic_name_field' },
+ = _("Topic slug (name)")
+ = f.text_field :name, placeholder: _('my-topic'), class: 'form-control input-lg', data: { qa_selector: 'topic_name_field' },
required: true,
title: _('Please fill in a name for your topic.'),
autofocus: true
.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' },
+ required: true,
+ title: _('Please fill in a title for your topic.')
+
+ .form-group
= f.label :description, _("Description")
= render layout: 'shared/md_preview', locals: { url: preview_markdown_admin_topics_path, referenced_users: false } do
= render 'shared/zen', f: f, attr: :description,
diff --git a/app/views/admin/topics/_topic.html.haml b/app/views/admin/topics/_topic.html.haml
index 959e7ab31fc..462943263df 100644
--- a/app/views/admin/topics/_topic.html.haml
+++ b/app/views/admin/topics/_topic.html.haml
@@ -1,4 +1,5 @@
- 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' } }
.avatar-container.rect-avatar.s40.gl-flex-shrink-0
@@ -6,7 +7,9 @@
.gl-min-w-0.gl-flex-grow-1
.title
- = link_to topic.name, topic_explore_projects_path(topic_name: topic.name)
+ = link_to title, topic_explore_projects_path(topic_name: topic.name)
+ %div
+ = topic.name
.stats.gl-text-gray-500.gl-flex-shrink-0.gl-display-none.gl-sm-display-flex
%span.gl-ml-5.has-tooltip{ title: n_('%d project', '%d projects', topic.total_projects_count) % topic.total_projects_count }
diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml
index 86391b980c0..a7ed7b8c052 100644
--- a/app/views/admin/users/_users.html.haml
+++ b/app/views/admin/users/_users.html.haml
@@ -1,8 +1,8 @@
- if registration_features_can_be_prompted?
= render Pajamas::AlertComponent.new(variant: :tip,
alert_class: 'gl-my-5',
- dismissible: false) do
- .gl-alert-body
+ dismissible: false) do |c|
+ = c.body do
= render 'shared/registration_features_discovery_message', feature_title: s_('RegistrationFeatures|send emails to users')
.top-area
diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml
index f0a9936112b..8f4cc41822b 100644
--- a/app/views/ci/runner/_how_to_setup_runner.html.haml
+++ b/app/views/ci/runner/_how_to_setup_runner.html.haml
@@ -8,13 +8,13 @@
= _("Register the runner with this URL:")
%br
%code#coordinator_address= root_url(only_path: false)
- = clipboard_button(target: '#coordinator_address', title: _("Copy URL"), class: "btn-transparent btn-clipboard")
+ = clipboard_button(target: '#coordinator_address', title: _("Copy URL"))
%br
%br
= _("And this registration token:")
%br
%code#registration_token{ data: {testid: 'registration_token' } }= registration_token
- = clipboard_button(target: '#registration_token', title: _("Copy token"), class: "btn-transparent btn-clipboard")
+ = clipboard_button(target: '#registration_token', title: _("Copy token"))
.gl-mt-3.gl-mb-3
= button_to _("Reset registration token"), reset_token_url,
diff --git a/app/views/ci/runner/_setup_runner_in_aws.html.haml b/app/views/ci/runner/_setup_runner_in_aws.html.haml
index b0a5b40f2ad..09fa0176da6 100644
--- a/app/views/ci/runner/_setup_runner_in_aws.html.haml
+++ b/app/views/ci/runner/_setup_runner_in_aws.html.haml
@@ -8,7 +8,7 @@
= _('Copy this registration token.')
%br
%code#registration_token{ data: { testid: 'registration_token' } }= registration_token
- = clipboard_button(target: '#registration_token', title: _('Copy token'), class: 'btn-transparent btn-clipboard')
+ = clipboard_button(target: '#registration_token', title: _('Copy token'))
%li
= _('Choose the preferred Runner and populate the AWS CFT.')
= link_to _('Learn more.'), 'https://gitlab.com/guided-explorations/aws/gitlab-runner-autoscaling-aws-asg', target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/ci/variables/_content.html.haml b/app/views/ci/variables/_content.html.haml
index 8a2a479486f..b597c2d442a 100644
--- a/app/views/ci/variables/_content.html.haml
+++ b/app/views/ci/variables/_content.html.haml
@@ -4,7 +4,7 @@
= _('Variables can be:')
%ul
%li
- = html_escape(_('%{code_open}Protected:%{code_close} Only exposed to protected branches or tags.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+ = html_escape(_('%{code_open}Protected:%{code_close} Only exposed to protected branches or protected tags.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%li
= html_escape(_('%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
= link_to _('Learn more.'), help_page_path('ci/variables/index', anchor: 'mask-a-cicd-variable'), target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index f289e6a3386..0024526a90c 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -2,7 +2,7 @@
- if ci_variable_protected_by_default?
%p.settings-message.text-center
- - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/index', anchor: 'protect-a-cicd-variable') }
+ - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/index', anchor: 'protected-cicd-variables') }
= s_('Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- is_group = !@group.nil?
@@ -17,9 +17,9 @@
aws_tip_commands_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'run-aws-commands-from-gitlab-cicd'),
aws_tip_learn_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'aws'),
contains_variable_reference_link: help_page_path('ci/variables/index', anchor: 'use-variables-in-other-variables'),
- protected_environment_variables_link: help_page_path('ci/variables/index', anchor: 'protect-a-cicd-variable'),
+ protected_environment_variables_link: help_page_path('ci/variables/index', anchor: 'protected-cicd-variables'),
masked_environment_variables_link: help_page_path('ci/variables/index', anchor: 'mask-a-cicd-variable'),
-} }
+ environment_scope_link: help_page_path('ci/environments/index', anchor: 'scope-environments-with-specs') } }
- if !@group && @project.group
.settings-header.border-top.gl-mt-6
diff --git a/app/views/clusters/clusters/_banner.html.haml b/app/views/clusters/clusters/_banner.html.haml
index b7d1aa6f944..720e3ff08e2 100644
--- a/app/views/clusters/clusters/_banner.html.haml
+++ b/app/views/clusters/clusters/_banner.html.haml
@@ -7,13 +7,13 @@
%span.gl-ml-2= s_('ClusterIntegration|Kubernetes cluster is being created...')
= render Pajamas::AlertComponent.new(variant: :warning,
- alert_class: 'hidden js-cluster-api-unreachable') do
- .gl-alert-body
+ alert_class: 'hidden js-cluster-api-unreachable') do |c|
+ = c.body do
= s_('ClusterIntegration|Your cluster API is unreachable. Please ensure your API URL is correct.')
= render Pajamas::AlertComponent.new(variant: :warning,
- alert_class: 'hidden js-cluster-authentication-failure js-cluster-api-unreachable') do
- .gl-alert-body
+ alert_class: 'hidden js-cluster-authentication-failure js-cluster-api-unreachable') do |c|
+ = c.body do
= s_('ClusterIntegration|There was a problem authenticating with your cluster. Please ensure your CA Certificate and Token are valid.')
.hidden.js-cluster-success.bs-callout.bs-callout-success{ role: 'alert' }
diff --git a/app/views/clusters/clusters/_deprecation_alert.html.haml b/app/views/clusters/clusters/_deprecation_alert.html.haml
index 202e2c14d3f..3a83efec29b 100644
--- a/app/views/clusters/clusters/_deprecation_alert.html.haml
+++ b/app/views/clusters/clusters/_deprecation_alert.html.haml
@@ -1,5 +1,5 @@
-= render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_class: 'gl-mt-6 gl-mb-3') do
- .gl-alert-body
+= render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_class: 'gl-mt-6 gl-mb-3') do |c|
+ = c.body do
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
- issue_link_start = link_start % { url: 'https://gitlab.com/gitlab-org/configure/general/-/issues/199' }
- docs_link_start = link_start % { url: help_page_path('user/clusters/agent/index.md') }
diff --git a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
index ffd910b1b9d..b130e0c7214 100644
--- a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
@@ -2,9 +2,9 @@
= render Pajamas::AlertComponent.new(title: s_('ClusterIntegration|Did you know?'),
alert_class: 'gcp-signup-offer',
- alert_data: { feature_id: Users::CalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: callouts_path }) do
- .gl-alert-body
+ alert_data: { feature_id: Users::CalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: callouts_path }) do |c|
+ = c.body do
= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
- .gl-alert-actions
+ = c.actions do
%a.gl-button.btn-confirm.text-decoration-none{ href: 'https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form', target: '_blank', rel: 'noopener noreferrer' }
= s_("ClusterIntegration|Apply for credit")
diff --git a/app/views/clusters/clusters/_integrations.html.haml b/app/views/clusters/clusters/_integrations.html.haml
index c670dafb947..62ae551fee7 100644
--- a/app/views/clusters/clusters/_integrations.html.haml
+++ b/app/views/clusters/clusters/_integrations.html.haml
@@ -13,15 +13,16 @@
= prometheus_form.gitlab_ui_checkbox_component :enabled,
s_('ClusterIntegration|Enable Prometheus integration'),
help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
- = prometheus_form.submit _('Save changes'), class: 'btn gl-button btn-success'
+ = prometheus_form.submit _('Save changes'), class: 'btn gl-button btn-confirm'
- .sub-section.form-group
- = gitlab_ui_form_for @elastic_stack_integration, as: :integration, namespace: :elastic_stack, url: @cluster.integrations_path, method: :post, html: { class: 'js-cluster-integrations-form' } do |elastic_stack_form|
- = elastic_stack_form.hidden_field :application_type
- .form-group.gl-form-group
- - help_text = s_('ClusterIntegration|Allows GitLab to query a specifically configured in-cluster Elasticsearch for pod logs.')
- - help_link = link_to(_('More information.'), help_page_path("user/clusters/integrations", anchor: "elastic-stack-cluster-integration"), target: '_blank', rel: 'noopener noreferrer')
- = elastic_stack_form.gitlab_ui_checkbox_component :enabled,
- s_('ClusterIntegration|Enable Elastic Stack integration'),
- help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
- = elastic_stack_form.submit _('Save changes'), class: 'btn gl-button btn-success'
+ - if Feature.enabled?(:monitor_logging, @project)
+ .sub-section.form-group
+ = gitlab_ui_form_for @elastic_stack_integration, as: :integration, namespace: :elastic_stack, url: @cluster.integrations_path, method: :post, html: { class: 'js-cluster-integrations-form' } do |elastic_stack_form|
+ = elastic_stack_form.hidden_field :application_type
+ .form-group.gl-form-group
+ - help_text = s_('ClusterIntegration|Allows GitLab to query a specifically configured in-cluster Elasticsearch for pod logs.')
+ - help_link = link_to(_('More information.'), help_page_path("user/clusters/integrations", anchor: "elastic-stack-cluster-integration"), target: '_blank', rel: 'noopener noreferrer')
+ = elastic_stack_form.gitlab_ui_checkbox_component :enabled,
+ s_('ClusterIntegration|Enable Elastic Stack integration'),
+ help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
+ = elastic_stack_form.submit _('Save changes'), class: 'btn gl-button btn-confirm'
diff --git a/app/views/clusters/clusters/_sidebar.html.haml b/app/views/clusters/clusters/_sidebar.html.haml
index 045c03df4fa..e479773e70d 100644
--- a/app/views/clusters/clusters/_sidebar.html.haml
+++ b/app/views/clusters/clusters/_sidebar.html.haml
@@ -1,5 +1,4 @@
- is_connect_page = local_assigns.fetch(:is_connect_page, false)
-- docs_mode = local_assigns.fetch(:docs_mode, false)
- title = is_connect_page ? s_('ClusterIntegration|Connect a Kubernetes cluster') : s_('ClusterIntegration|Create a Kubernetes cluster')
%h3
@@ -7,7 +6,7 @@
%p
= clusterable.sidebar_text
-- if !docs_mode
+- if is_connect_page
%p
= clusterable.learn_more_link
diff --git a/app/views/clusters/clusters/aws/_new.html.haml b/app/views/clusters/clusters/aws/_new.html.haml
deleted file mode 100644
index 7142dd83dce..00000000000
--- a/app/views/clusters/clusters/aws/_new.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-- if !Gitlab::CurrentSettings.eks_integration_enabled?
- - documentation_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/clusters/add_eks_clusters.md',
- anchor: 'additional-requirements-for-self-managed-instances') }
- = s_('Amazon authentication is not %{link_start}correctly configured%{link_end}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_start: documentation_link_start, link_end: '<a/>'.html_safe }
-- else
- .js-create-eks-cluster-form-container{ data: { 'gitlab-managed-cluster-help-path' => help_page_path('user/project/clusters/gitlab_managed_clusters.md'),
- 'namespace-per-environment-help-path' => help_page_path('user/project/clusters/deploy_to_cluster.md', anchor: 'custom-namespace'),
- 'create-role-path' => clusterable.authorize_aws_role_path,
- 'create-cluster-path' => clusterable.create_aws_clusters_path,
- 'account-id' => Gitlab::CurrentSettings.eks_account_id,
- 'external-id' => @aws_role.role_external_id,
- 'role-arn' => @aws_role.role_arn,
- 'instance-types' => @instance_types,
- 'kubernetes-integration-help-path' => help_page_path('user/infrastructure/clusters/index.md'),
- 'account-and-external-ids-help-path' => help_page_path('user/project/clusters/add_eks_clusters.md', anchor: 'how-to-create-a-new-cluster-on-eks-through-cluster-certificates-deprecated'),
- 'create-role-arn-help-path' => help_page_path('user/project/clusters/add_eks_clusters.md', anchor: 'how-to-create-a-new-cluster-on-eks-through-cluster-certificates-deprecated'),
- 'external-link-icon' => sprite_icon('external-link') } }
diff --git a/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml b/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml
index 807f98b7b0a..bed671832f3 100644
--- a/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml
+++ b/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml
@@ -1,15 +1,11 @@
-- provider = local_assigns.fetch(:provider)
-- is_current_provider = provider == params[:provider]
- logo_path = local_assigns.fetch(:logo_path)
- help_path = local_assigns.fetch(:help_path)
- label = local_assigns.fetch(:label)
- last = local_assigns.fetch(:last, false)
-- docs_mode = local_assigns.fetch(:docs_mode, false)
- classes = ["btn btn-confirm gl-button btn-confirm-secondary gl-flex-direction-column gl-w-half"]
-- conditional_classes = [("gl-mr-5" unless last), ("active" if is_current_provider && !docs_mode), ("js-create-#{provider}-cluster-button" if !docs_mode)]
-- link = docs_mode ? help_path : clusterable.new_path(provider: provider)
+- conditional_classes = [("gl-mr-5" unless last)]
-= link_to link, class: classes + conditional_classes do
+= link_to help_path, class: classes + conditional_classes do
.svg-content.gl-p-3= image_tag logo_path, alt: label, class: "gl-w-64 gl-h-64"
%span
= label
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 69250141816..e4128ee22a4 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
@@ -3,13 +3,12 @@
- 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')
- gke_help_path = help_page_path('user/infrastructure/clusters/connect/new_gke_cluster')
-- docs_mode = local_assigns.fetch(:docs_mode, false)
.gl-p-5
%h4.gl-mb-5
= create_cluster_label
.gl-display-flex
= render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
- locals: { provider: 'aws', label: eks_label, logo_path: 'illustrations/logos/amazon_eks.svg', help_path: eks_help_path, docs_mode: docs_mode }
+ locals: { label: eks_label, logo_path: 'illustrations/logos/amazon_eks.svg', help_path: eks_help_path }
= render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
- locals: { provider: 'gcp', label: gke_label, logo_path: 'illustrations/logos/google_gke.svg', help_path: gke_help_path, docs_mode: docs_mode, last: true }
+ locals: { label: gke_label, logo_path: 'illustrations/logos/google_gke.svg', help_path: gke_help_path, last: true }
diff --git a/app/views/clusters/clusters/connect.html.haml b/app/views/clusters/clusters/connect.html.haml
index ec00a9c345a..82750974803 100644
--- a/app/views/clusters/clusters/connect.html.haml
+++ b/app/views/clusters/clusters/connect.html.haml
@@ -9,5 +9,5 @@
.gl-w-quarter.gl-xs-w-full.gl-flex-shrink-0.gl-md-mr-5
= render 'sidebar', is_connect_page: true
.gl-w-full
- #js-cluster-new{ data: js_cluster_new }
+ #js-cluster-new
= render 'clusters/clusters/user/form'
diff --git a/app/views/clusters/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml
deleted file mode 100644
index 58b8e8b1003..00000000000
--- a/app/views/clusters/clusters/gcp/_form.html.haml
+++ /dev/null
@@ -1,87 +0,0 @@
-- external_link_icon = sprite_icon('external-link')
-- zones_link_url = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones'
-- machine_type_link_url = 'https://cloud.google.com/compute/docs/machine-types'
-- pricing_link_url = 'https://cloud.google.com/compute/pricing#machinetype'
-- kubernetes_integration_url = help_page_path('user/infrastructure/clusters/index.md')
-- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
-- help_link_end = ' %{external_link_icon}</a>'.html_safe % { external_link_icon: external_link_icon }
-
-%p
- = s_('ClusterIntegration|Read our %{link_start}help page%{link_end} on Kubernetes cluster integration.').html_safe % { link_start: help_link_start % { url: kubernetes_integration_url }, link_end: '</a>'.html_safe }
-
-%p= link_to('Select a different Google account', @authorize_url)
-
-= bootstrap_form_for @gcp_cluster, html: { class: 'gl-show-field-errors js-gke-cluster-creation prepend-top-20',
- data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field|
- = field.text_field :name, required: true, title: s_('ClusterIntegration|Cluster name is required.'),
- label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold'
- = field.form_group :environment_scope, label: { text: s_('ClusterIntegration|Environment scope'),
- class: 'label-bold' } do
- = field.text_field :environment_scope, required: true, class: 'form-control',
- title: 'Environment scope is required.', wrapper: false
- .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.")
-
- = field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field|
- .form-group
- = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'),
- class: 'label-bold'
- .js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } }
- = provider_gcp_field.hidden_field :gcp_project_id
- .dropdown
- %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
- %span.dropdown-toggle-text
- = _('Select project')
- = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
- %span.form-text.text-muted &nbsp;
-
- .form-group
- = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone'), class: 'label-bold'
- .js-gcp-zone-dropdown-entry-point
- = provider_gcp_field.hidden_field :zone
- .dropdown
- %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
- %span.dropdown-toggle-text
- = _('Select project to choose zone')
- = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
- %p.form-text.text-muted
- = s_('ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: zones_link_url }, help_link_end: help_link_end }
-
- = provider_gcp_field.number_field :num_nodes, required: true, placeholder: '3',
- title: s_('ClusterIntegration|Number of nodes must be a numerical value.'),
- label: s_('ClusterIntegration|Number of nodes'), label_class: 'label-bold'
-
- .form-group
- = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type'), class: 'label-bold'
- .js-gcp-machine-type-dropdown-entry-point
- = provider_gcp_field.hidden_field :machine_type
- .dropdown
- %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
- %span.dropdown-toggle-text
- = _('Select project and zone to choose machine type')
- = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
- %p.form-text.text-muted
- = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end }
-
- .form-group
- = provider_gcp_field.check_box :cloud_run, { label: s_('ClusterIntegration|Enable Cloud Run for Anthos'),
- label_class: 'label-bold' }
- .form-text.text-muted
- = s_('ClusterIntegration|Uses the Cloud Run, Istio, and HTTP Load Balancing addons for this cluster.')
- = link_to _('Learn more.'), help_page_path('user/project/clusters/add_gke_clusters.md', anchor: 'cloud-run-for-anthos'), target: '_blank', rel: 'noopener noreferrer'
-
- .form-group
- = field.check_box :managed, { label: s_('ClusterIntegration|GitLab-managed cluster'),
- label_class: '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'
-
- .form-group
- = field.check_box :namespace_per_environment, { label: s_('ClusterIntegration|Namespace per environment'), label_class: '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'
-
- .form-group.js-gke-cluster-creation-submit-container
- = field.submit s_('ClusterIntegration|Create Kubernetes cluster'),
- class: 'js-gke-cluster-creation-submit gl-button btn btn-confirm', disabled: true
diff --git a/app/views/clusters/clusters/gcp/_gcp_not_configured.html.haml b/app/views/clusters/clusters/gcp/_gcp_not_configured.html.haml
deleted file mode 100644
index f1f26a0aab8..00000000000
--- a/app/views/clusters/clusters/gcp/_gcp_not_configured.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-- documentation_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path("integration/google") }
-- link_end = '<a/>'.html_safe
-= s_('Google authentication is not %{link_start}properly configured%{link_end}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_start: documentation_link_start, link_end: link_end }
diff --git a/app/views/clusters/clusters/gcp/_header.html.haml b/app/views/clusters/clusters/gcp/_header.html.haml
deleted file mode 100644
index a2ad3cd64df..00000000000
--- a/app/views/clusters/clusters/gcp/_header.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-%h4
- = s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
-%p
- = s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:')
-%ul
- %li
- - link_to_kubernetes_engine = link_to(s_('ClusterIntegration|access to Google Kubernetes Engine'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|Your account must have %{link_to_kubernetes_engine}').html_safe % { link_to_kubernetes_engine: link_to_kubernetes_engine }
- %li
- - link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/kubernetes-engine/docs/quickstart?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters').html_safe % { link_to_requirements: link_to_requirements }
- %li
- - link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), 'https://console.cloud.google.com/home/dashboard?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project }
diff --git a/app/views/clusters/clusters/gcp/_new.html.haml b/app/views/clusters/clusters/gcp/_new.html.haml
deleted file mode 100644
index 6c3a230fb93..00000000000
--- a/app/views/clusters/clusters/gcp/_new.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-= render 'clusters/clusters/gcp/header'
-- if @valid_gcp_token
- = render 'clusters/clusters/gcp/form'
-- else
- = render 'clusters/clusters/gcp/gcp_not_configured'
diff --git a/app/views/clusters/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml
deleted file mode 100644
index 53d521fface..00000000000
--- a/app/views/clusters/clusters/new.html.haml
+++ /dev/null
@@ -1,19 +0,0 @@
-- @content_class = 'limit-container-width' unless fluid_layout
-- add_to_breadcrumbs _('Kubernetes Clusters'), clusterable.index_path
-- breadcrumb_title _('Create a cluster')
-- page_title _('Create a Kubernetes cluster')
-- provider = params[:provider]
-
-= render 'deprecation_alert'
-
-= 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
- = render 'sidebar', is_connect_page: false
- .gl-w-full
- = render 'clusters/clusters/cloud_providers/cloud_provider_selector'
-
- - if ['aws', 'gcp'].include?(provider)
- .gl-p-5.gl-border-1.gl-border-t-solid.gl-border-gray-100
- = render "clusters/clusters/#{provider}/new"
diff --git a/app/views/clusters/clusters/new_cluster_docs.html.haml b/app/views/clusters/clusters/new_cluster_docs.html.haml
index 9b99aef0e72..bff371b8d51 100644
--- a/app/views/clusters/clusters/new_cluster_docs.html.haml
+++ b/app/views/clusters/clusters/new_cluster_docs.html.haml
@@ -2,12 +2,11 @@
- add_to_breadcrumbs _('Kubernetes Clusters'), clusterable.index_path
- breadcrumb_title _('Create a cluster')
- page_title _('Create a Kubernetes cluster')
-- docs_mode = true
= 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
- = render 'sidebar', docs_mode: docs_mode, is_connect_page: false
+ = render 'sidebar', is_connect_page: false
.gl-w-full
- = render 'clusters/clusters/cloud_providers/cloud_provider_selector', docs_mode: docs_mode
+ = 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 fb46b4e5064..58e0ef96333 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -28,14 +28,6 @@
= render 'banner'
- .gl-alert.gl-alert-warning{ role: 'alert' }
- = sprite_icon('warning', css_class: "gl-alert-icon gl-alert-icon-no-title gl-icon")
- %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss'), data: { testid: 'dismiss-one-click-application-removal' } }
- = sprite_icon('close', css_class: 'gl-icon')
- .gl-alert-body
- = s_('ClusterApplicationsRemoved|One-click application management was removed in GitLab 14.0. Your applications are still installed in your cluster, and integrations continue working.')
- = link_to _('More information.'), help_page_path("user/clusters/applications"), target: '_blank', rel: 'noopener noreferrer'
-
- if cluster_created?(@cluster)
.js-toggle-container
= gl_tabs_nav do
diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index 2dd15ebd266..bf7b24181c1 100644
--- a/app/views/clusters/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -1,7 +1,5 @@
-- more_info_link = link_to _('Learn more.'), help_page_path('user/project/clusters/add_remove_clusters.md',
- anchor: 'add-existing-cluster'), target: '_blank', rel: 'noopener noreferrer'
-- rbac_help_link = link_to _('Learn more.'), help_page_path('user/project/clusters/add_remove_clusters.md',
- anchor: 'access-controls'), target: '_blank', rel: 'noopener noreferrer'
+- more_info_link = link_to _('Learn more.'), help_page_path('user/project/clusters/add_existing_cluster'), target: '_blank', rel: 'noopener noreferrer'
+- rbac_help_link = link_to _('Learn more.'), help_page_path('user/project/clusters/cluster_access'), target: '_blank', rel: 'noopener noreferrer'
- api_url_help_text = s_('ClusterIntegration|The URL used to access the Kubernetes API.')
- ca_cert_help_text = s_('ClusterIntegration|The Kubernetes certificate used to authenticate to the cluster.')
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 1d711f366c4..4f6ddf10984 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -70,6 +70,8 @@
= sort_title_recently_created
= link_to todos_filter_path(sort: sort_value_oldest_created) do
= sort_title_oldest_created
+ = link_to todos_filter_path(sort: sort_value_recently_updated) do
+ = sort_title_recently_updated
.row.js-todos-all
- if @allowed_todos.any?
diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml
index d5372862128..c90a9e7c672 100644
--- a/app/views/devise/passwords/new.html.haml
+++ b/app/views/devise/passwords/new.html.haml
@@ -3,17 +3,17 @@
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f|
.devise-errors
= render "devise/shared/error_messages", resource: resource
- .form-group
+ .form-group.gl-px-5.gl-pt-5
= f.label :email
= f.email_field :email, class: "form-control gl-form-input", required: true, autocomplete: 'off', value: params[:user_email], autofocus: true, title: _('Please provide a valid email address.')
.form-text.text-muted
= _('Requires your primary GitLab email address.')
- %div
- if recaptcha_enabled?
- = recaptcha_tags nonce: content_security_policy_nonce
+ .gl-px-5
+ = recaptcha_tags nonce: content_security_policy_nonce
- .gl-mt-5
+ .gl-p-5
= f.submit _("Reset password"), class: "gl-button btn-confirm btn"
.clearfix.prepend-top-20
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 83e3fd85511..48b38861e6e 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -1,12 +1,12 @@
= gitlab_ui_form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors js-sign-in-form', aria: { live: 'assertive' }, data: { testid: 'sign-in-form' }}) do |f|
- .form-group
- = f.label _('Username or email'), for: 'user_login', class: 'label-bold'
+ .form-group.gl-px-5.gl-pt-5
+ = render_if_exists 'devise/sessions/new_base_user_login_label', form: f
= f.text_field :login, value: @invite_email, class: 'form-control gl-form-input top js-username-field', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', required: true, title: _('This field is required.'), data: { qa_selector: 'login_field', testid: 'username-field' }
- .form-group
+ .form-group.gl-px-5
= f.label :password, class: 'label-bold'
= f.password_field :password, class: 'form-control gl-form-input bottom', autocomplete: 'current-password', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
- if devise_mapping.rememberable?
- %div
+ .gl-px-5
.gl-display-inline-block
= f.gitlab_ui_checkbox_component :remember_me, _('Remember me')
.gl-float-right
@@ -18,7 +18,8 @@
- if Feature.enabled?(:arkose_labs_login_challenge)
= render_if_exists 'devise/sessions/arkose_labs'
- elsif captcha_enabled? || captcha_on_login_required?
- = recaptcha_tags nonce: content_security_policy_nonce
+ .gl-px-5
+ = recaptcha_tags nonce: content_security_policy_nonce
- .submit-container.move-submit-down
+ .submit-container.move-submit-down.gl-px-5
= f.button _('Sign in'), type: :submit, class: "gl-button btn btn-block btn-confirm js-sign-in-button#{' js-no-auto-disable' if Feature.enabled?(:arkose_labs_login_challenge)}", data: { qa_selector: 'sign_in_button', testid: 'sign-in-button' }
diff --git a/app/views/devise/sessions/_new_base_user_login_label.html.haml b/app/views/devise/sessions/_new_base_user_login_label.html.haml
new file mode 100644
index 00000000000..2aa66684cad
--- /dev/null
+++ b/app/views/devise/sessions/_new_base_user_login_label.html.haml
@@ -0,0 +1 @@
+= local_assigns[:form].label _('Username or email'), for: 'user_login', class: 'label-bold'
diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml
index fb4c011dd49..bdf357c5f74 100644
--- a/app/views/devise/sessions/_new_crowd.html.haml
+++ b/app/views/devise/sessions/_new_crowd.html.haml
@@ -1,13 +1,13 @@
= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user', class: 'gl-show-field-errors') do
- .form-group
+ .form-group.gl-px-5.gl-pt-5
= label_tag :username, _('Username or email')
= text_field_tag :username, nil, { class: "form-control top", title: _("This field is required."), autofocus: "autofocus", required: true }
- .form-group
+ .form-group.gl-px-5
= label_tag :password
= password_field_tag :password, nil, { autocomplete: 'current-password', class: "form-control bottom", title: _("This field is required."), required: true }
- if devise_mapping.rememberable?
- .remember-me
+ .remember-me.gl-px-5
%label{ for: "remember_me" }
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span= _('Remember me')
- = submit_tag _("Sign in"), class: "gl-button btn-confirm btn"
+ = submit_tag _("Sign in"), class: "gl-button btn-confirm btn gl-px-5"
diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml
index fea58779c17..4cde24f4afa 100644
--- a/app/views/devise/sessions/_new_ldap.html.haml
+++ b/app/views/devise/sessions/_new_ldap.html.haml
@@ -3,17 +3,17 @@
- submit_message = local_assigns.fetch(:submit_message, _('Sign in'))
= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do
- .form-group
+ .form-group.gl-px-5.gl-pt-5
= label_tag :username, "#{server['label']} Username"
= text_field_tag :username, nil, { class: "form-control gl-form-input top", title: _("This field is required."), autofocus: "autofocus", data: { qa_selector: 'username_field' }, required: true }
- .form-group
+ .form-group.gl-px-5
= label_tag :password
= password_field_tag :password, nil, { autocomplete: 'current-password', class: "form-control gl-form-input bottom", title: _("This field is required."), data: { qa_selector: 'password_field' }, required: true }
- if !hide_remember_me && devise_mapping.rememberable?
- .remember-me
+ .remember-me.gl-px-5
%label{ for: "remember_me" }
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span= _('Remember me')
- .submit-container.move-submit-down
+ .submit-container.move-submit-down.gl-px-5
= submit_tag submit_message, class: "gl-button btn btn-confirm", data: { qa_selector: 'sign_in_button' }
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index 29bcb3c158b..77a2fda021f 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -1,6 +1,6 @@
%div
= render 'devise/shared/tab_single', tab_title: _('Two-Factor Authentication')
- .login-box
+ .login-box.gl-p-5
.login-body
- if @user.two_factor_otp_enabled?
= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if @user.two_factor_webauthn_u2f_enabled?}" }) do |f|
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index bd7fe41ae8d..32b4a15517e 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -1,6 +1,6 @@
- hide_remember_me = local_assigns.fetch(:hide_remember_me, false)
-.omniauth-container.gl-mt-5
+.omniauth-container.gl-mt-5.gl-p-5
%label.gl-font-weight-bold
= _('Sign in with')
- providers = enabled_button_based_providers
diff --git a/app/views/devise/shared/_tab_single.html.haml b/app/views/devise/shared/_tab_single.html.haml
index 1b5a932a09a..336954d00b0 100644
--- a/app/views/devise/shared/_tab_single.html.haml
+++ b/app/views/devise/shared/_tab_single.html.haml
@@ -1,2 +1,2 @@
= gl_tabs_nav({ class: 'new-session-tabs gl-border-0' }) do
- = gl_tab_link_to tab_title, '#', { item_active: true, class: 'gl-cursor-default!', tab_class: 'gl-bg-transparent!', tabindex: '-1' }
+ = gl_tab_link_to tab_title, '#', { item_active: true, class: 'gl-cursor-default!', tab_class: 'gl-bg-transparent!', tabindex: '-1', data: { qa_selector: 'sign_in_tab' } }
diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml
deleted file mode 100644
index 01dd3748887..00000000000
--- a/app/views/devise/shared/_tabs_normal.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- tab_title = local_assigns.fetch(:tab_title, _('Sign in'))
-- render_signup_link = local_assigns.fetch(:render_signup_link, true)
-
-%ul.nav-links.new-session-tabs.nav-tabs.nav{ role: 'tablist' }
- %li.nav-item{ role: 'presentation' }
- %a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab', qa_selector: 'sign_in_tab' }, role: 'tab' }= tab_title
- - if render_signup_link && allow_signup?
- %li.nav-item{ role: 'presentation' }
- %a.nav-link{ href: '#register-pane', data: { track_label: 'sign_in_register', track_property: '', track_action: 'click_button', track_value: '', toggle: 'tab', qa_selector: 'register_tab' }, role: 'tab' } Register
diff --git a/app/views/doorkeeper/authorizations/redirect.html.haml b/app/views/doorkeeper/authorizations/redirect.html.haml
index 9580f33c88a..a9ac92fd087 100644
--- a/app/views/doorkeeper/authorizations/redirect.html.haml
+++ b/app/views/doorkeeper/authorizations/redirect.html.haml
@@ -5,4 +5,16 @@
= javascript_tag do
:plain
- window.location= "#{redirect_uri}";
+ (function() {
+ // Only permit a basic set of characters in the fragment.
+ const allowedRegex = /^#[\w-]+$/g;
+
+ const hash = window.location.hash;
+ let redirectUri = "#{redirect_uri}";
+
+ if (window.location.hash && window.location.hash.search(allowedRegex) === 0 && redirectUri.indexOf('#') === -1) {
+ redirectUri = redirectUri + hash;
+ }
+
+ window.location = redirectUri;
+ })();
diff --git a/app/views/errors/request_conflict.html.haml b/app/views/errors/request_conflict.html.haml
new file mode 100644
index 00000000000..2f5abaca72f
--- /dev/null
+++ b/app/views/errors/request_conflict.html.haml
@@ -0,0 +1,18 @@
+- message = local_assigns.fetch(:message, nil)
+- content_for(:title, 'Request Conflict')
+
+%img{ :alt => "", :src => image_path('logo.svg') }
+%h1
+ 409
+.container
+ %h2
+ = s_("409|There was a conflict with your request.")
+ - if message
+ %p
+ = message
+ %p
+ = s_('409|Please contact your GitLab administrator if you think this is a mistake.')
+ .action-container.js-go-back{ hidden: true }
+ %button{ type: 'button', class: 'gl-button btn btn-primary' }
+ = _('Go Back')
+= render "errors/footer"
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index d08c3d5ba41..53c59474d83 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -1,3 +1,5 @@
+- return if !event.personal_snippet_note? && event.has_no_project_and_group?
+
= icon_for_profile_event(event)
= event_user_info(event)
diff --git a/app/views/explore/projects/topic.html.haml b/app/views/explore/projects/topic.html.haml
index aeb040ea61f..76e59a49ed1 100644
--- a/app/views/explore/projects/topic.html.haml
+++ b/app/views/explore/projects/topic.html.haml
@@ -1,7 +1,7 @@
- @hide_top_links = false
- @no_container = true
-- page_title @topic.name, _("Topics")
-- max_topic_name_length = 50
+- page_title @topic.title_or_name, _("Topics")
+- max_topic_title_length = 50
= render_dashboard_ultimate_trial(current_user)
@@ -9,12 +9,12 @@
.gl-pb-5.gl-align-items-center.gl-justify-content-center.gl-display-flex
.avatar-container.rect-avatar.s60.gl-flex-shrink-0
= topic_icon(@topic, alt: _('Topic avatar'), class: 'avatar topic-avatar s60')
- - if @topic.name.length > max_topic_name_length
- %h1.gl-mt-3.str-truncated.has-tooltip{ title: @topic.name }
- = truncate(@topic.name, length: max_topic_name_length)
+ - if @topic.title_or_name.length > max_topic_title_length
+ %h1.gl-mt-3.gl-str-truncated.has-tooltip{ title: @topic.title_or_name }
+ = truncate(@topic.title_or_name, length: max_topic_title_length)
- else
%h1.gl-mt-3
- = @topic.name
+ = @topic.title_or_name
- if @topic.description.present?
.topic-description.gl-ml-4.gl-mr-4
= markdown(@topic.description)
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index 9b3a8c31d54..bd893ca3162 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -17,8 +17,7 @@
- if can?(current_user, :read_group, @group)
%span.gl-display-inline-block.gl-vertical-align-middle
= s_("GroupPage|Group ID: %{group_id}") % { group_id: @group.id }
- - button_class = "btn gl-button btn-sm btn-tertiary btn-default-tertiary home-panel-metadata"
- = clipboard_button(title: s_('GroupPage|Copy group ID'), text: @group.id, class: button_class)
+ = clipboard_button(title: s_('GroupPage|Copy group ID'), text: @group.id)
- if current_user
%span.gl-ml-3.gl-mb-3
= render 'shared/members/access_request_links', source: @group
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 95b3ad26e99..654ee70dbee 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
@@ -5,8 +5,8 @@
= s_('GroupsNew|Import groups from another instance of GitLab')
= link_to _('History'), history_import_bulk_imports_path, class: 'gl-link gl-ml-auto'
= render Pajamas::AlertComponent.new(dismissible: false,
- variant: :warning) do
- .gl-alert-body
+ variant: :warning) do |c|
+ = c.body do
- docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index.md') }
- docs_link_end = '</a>'.html_safe
= s_('GroupsNew|Not all related objects are migrated. %{docs_link_start}More info%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: docs_link_end }
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 ddd7481e0bd..04170c30a20 100644
--- a/app/views/groups/_import_group_from_file_panel.html.haml
+++ b/app/views/groups/_import_group_from_file_panel.html.haml
@@ -7,8 +7,8 @@
%h4
= _('Import group from file')
= render Pajamas::AlertComponent.new(variant: :warning,
- dismissible: false) do
- .gl-alert-body
+ dismissible: false) do |c|
+ = c.body do
- docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index.md') }
- link_end = '</a>'.html_safe
= s_('GroupsNew|This feature is deprecated and replaced by %{docs_link_start}group migration%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: link_end }
diff --git a/app/views/groups/_invite_groups_modal.html.haml b/app/views/groups/_invite_groups_modal.html.haml
index 22ef319348a..2e11f6cee4f 100644
--- a/app/views/groups/_invite_groups_modal.html.haml
+++ b/app/views/groups/_invite_groups_modal.html.haml
@@ -1,3 +1,3 @@
- return unless can_admin_group_member?(group)
-.js-invite-groups-modal{ data: common_invite_group_modal_data(group, GroupMember, 'false').merge(group_select_data(group)) }
+.js-invite-groups-modal{ data: common_invite_group_modal_data(group, GroupMember, 'false') }
diff --git a/app/views/groups/dependency_proxies/show.html.haml b/app/views/groups/dependency_proxies/show.html.haml
index 940a504438d..082f637e854 100644
--- a/app/views/groups/dependency_proxies/show.html.haml
+++ b/app/views/groups/dependency_proxies/show.html.haml
@@ -1,7 +1,5 @@
- page_title _("Dependency Proxy")
- @content_class = "limit-container-width" unless fluid_layout
-- dependency_proxy_available = Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true) || @group.public?
#js-dependency-proxy{ data: { group_path: @group.full_path,
- dependency_proxy_available: dependency_proxy_available.to_s,
no_manifests_illustration: image_path('illustrations/docker-empty-state.svg'), group_id: @group.id } }
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 8afa6316c56..209faa937dc 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -5,7 +5,7 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@group.name} issues")
-- if Feature.enabled?(:vue_issues_list, @group, default_enabled: :yaml)
+- if Feature.enabled?(:vue_issues_list, @group)
.js-issues-list{ data: group_issues_list_data(@group, current_user) }
- if @can_bulk_update
= render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :issues
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index 5c0487db0fc..50a1b474504 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -9,7 +9,7 @@
= render 'shared/milestones/search_form'
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @group)
- = link_to _('New milestone'), new_group_milestone_path(@group), class: "btn gl-button btn-confirm", data: { qa_selector: "new_group_milestone_link" }
+ = link_to _('New milestone'), new_group_milestone_path(@group), class: "btn gl-button btn-confirm gl-ml-3", data: { qa_selector: "new_group_milestone_link" }
- if @milestones.blank?
= render 'shared/empty_states/milestones_tab', learn_more_path: help_page_path('user/project/milestones/index') do
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 750030601b7..58a78a8adc1 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -15,7 +15,7 @@
#import-group-pane.tab-pane
- if import_sources_enabled?
- - if Feature.enabled?(:bulk_import, default_enabled: :yaml)
+ - if Feature.enabled?(:bulk_import)
= render 'import_group_from_another_instance_panel'
.gl-mt-7.gl-border-b-solid.gl-border-gray-100.gl-border-1
= render 'import_group_from_file_panel'
diff --git a/app/views/groups/runners/_group_runners.html.haml b/app/views/groups/runners/_group_runners.html.haml
deleted file mode 100644
index 876642474cd..00000000000
--- a/app/views/groups/runners/_group_runners.html.haml
+++ /dev/null
@@ -1,29 +0,0 @@
-- link = link_to _('Runner API'), help_page_path('api/runners.md')
-
-%h4
- = _('Group runners')
-
--# Proper policies should be implemented per
--# https://gitlab.com/gitlab-org/gitlab-foss/issues/45894
-.bs-callout.help-callout
- %p
- = _('These runners are shared across projects in this group.')
- = _('Group runners can be managed with the %{link}.').html_safe % { link: link }
-
- - if can?(current_user, :register_group_runners, @group)
- - if params[:ci_runner_templates]
- %hr
- = render partial: 'ci/runner/setup_runner_in_aws',
- locals: { registration_token: @group.runners_token }
- %hr
- = render partial: 'ci/runner/how_to_setup_runner',
- locals: { registration_token: @group.runners_token,
- type: 'group',
- reset_token_url: reset_registration_token_group_settings_ci_cd_path,
- project_path: '',
- group_path: @group.full_path }
- %br
- - else
- = _('Please contact an admin to register runners.')
- = link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'prevent-users-from-registering-runners'), target: '_blank', rel: 'noopener noreferrer'
-
diff --git a/app/views/groups/runners/_runner.html.haml b/app/views/groups/runners/_runner.html.haml
deleted file mode 100644
index b2d8b9668e7..00000000000
--- a/app/views/groups/runners/_runner.html.haml
+++ /dev/null
@@ -1,80 +0,0 @@
-.gl-responsive-table-row{ id: dom_id(runner) }
- .table-section.section-10.section-wrap
- .table-mobile-header{ role: 'rowheader' }= _('Type')
- .table-mobile-content
- - if runner.group_type?
- = gl_badge_tag s_('Runners|group'), variant: :success, size: :sm
- - else
- = gl_badge_tag s_('Runners|specific'), variant: :info, size: :sm
- - if runner.locked?
- = gl_badge_tag s_('Runners|locked'), variant: :warning, size: :sm
- - unless runner.active?
- = gl_badge_tag s_('Runners|paused'), { variant: :danger, size: :sm }, { title: s_('Runners|Not accepting jobs'), data: { toggle: 'tooltip', container: 'body' } }
-
- .table-section.section-30
- .table-mobile-header{ role: 'rowheader' }= s_('Runners|Runner')
- .table-mobile-content
- = link_to("##{runner.id} (#{runner.short_sha})", group_runner_path(@group, runner))
- .gl-text-truncate
- %span{ title: runner.description, data: { toggle: 'tooltip', container: 'body' } }
- = runner.description
-
- .table-section.section-10
- .table-mobile-header{ role: 'rowheader' }= _('Version')
- .table-mobile-content.str-truncated.has-tooltip{ title: runner.version }
- = runner.version
-
- .table-section.section-10
- .table-mobile-header{ role: 'rowheader' }= _('IP Address')
- .table-mobile-content.str-truncated.has-tooltip{ title: runner.ip_address }
- = runner.ip_address
-
- .table-section.section-5
- .table-mobile-header{ role: 'rowheader' }= _('Projects')
- .table-mobile-content
- - if runner.group_type?
- \-
- - else
- = runner.runner_projects.count(:all)
-
- .table-section.section-5
- .table-mobile-header{ role: 'rowheader' }= _('Jobs')
- .table-mobile-content
- = limited_counter_with_delimiter(runner.builds)
-
- .table-section.section-10.section-wrap
- .table-mobile-header{ role: 'rowheader' }= _('Tags')
- .table-mobile-content
- - runner.tags.map(&:name).sort.each do |tag|
- = gl_badge_tag tag, { variant: :info }, { class: 'str-truncated has-tooltip', title: tag }
-
- .table-section.section-10
- .table-mobile-header{ role: 'rowheader' }= _('Last contact')
- .table-mobile-content
- - contacted_at = runner_contacted_at(runner)
- - if contacted_at
- = time_ago_with_tooltip contacted_at
- - else
- = _('Never')
-
- .table-section.table-button-footer.section-10
- .btn-group.table-action-buttons
- .btn-group
- = link_to edit_group_runner_path(@group, runner), class: 'gl-button btn btn-default btn-icon has-tooltip', title: _('Edit'), ref: 'tooltip', aria: { label: _('Edit') }, data: { placement: 'top', container: 'body'} do
- = sprite_icon('pencil', css_class: 'gl-icon')
- .btn-group
- - if runner.active?
- = link_to pause_group_runner_path(@group, runner), method: :post, class: 'gl-button btn btn-default btn-icon', title: s_('Runners|Pause from accepting jobs'), ref: 'tooltip', aria: { label: _('Pause') }, data: { toggle: 'tooltip', container: 'body', confirm: _('Are you sure?') } do
- = sprite_icon('pause', css_class: 'gl-icon')
- - else
- = link_to resume_group_runner_path(@group, runner), method: :post, class: 'gl-button btn btn-default btn-icon', title: s_('Runners|Resume accepting jobs'), ref: 'tooltip', aria: { label: _('Resume') }, data: { toggle: 'tooltip', container: 'body'} do
- = sprite_icon('play', css_class: 'gl-icon')
- - if runner.belongs_to_more_than_one_project?
- - delete_runner_tooltip = _('Multi-project Runners cannot be removed')
- .btn-group.has-tooltip{ data: { container: 'body', placement: 'top' }, title: delete_runner_tooltip }
- .gl-button.btn.btn-danger.btn-icon{ 'aria-label' => delete_runner_tooltip, disabled: 'disabled' }
- = sprite_icon('close', css_class: 'gl-icon')
- - else
- .btn-group
- = link_to group_runner_path(@group, runner), method: :delete, class: 'gl-button btn btn-danger btn-icon has-tooltip', title: _('Remove'), ref: 'tooltip', aria: { label: _('Remove') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?'), confirm_btn_variant: "danger" } do
- = sprite_icon('close', css_class: 'gl-icon')
diff --git a/app/views/groups/runners/_settings.html.haml b/app/views/groups/runners/_settings.html.haml
index 087b06ee37d..7716a2f125f 100644
--- a/app/views/groups/runners/_settings.html.haml
+++ b/app/views/groups/runners/_settings.html.haml
@@ -1,119 +1,14 @@
-- if Feature.enabled?(:runner_list_group_view_vue_ui, @group, default_enabled: :yaml)
- .gl-mb-6
- #update-shared-runners-form{ data: group_shared_runners_settings_data(@group) }
- .gl-card.gl-px-8.gl-py-6.gl-line-height-20
- .gl-card-body.gl-display-flex{ :class => "gl-p-0!" }
- .gl-banner-illustration
- = image_tag('illustrations/rocket-launch-md.svg', alt: s_('Runners|Rocket launch illustration'))
- .gl-banner-content
- %h1.gl-banner-title
- = s_('Runners|New group runners view')
- %p
- = s_('Runners|The new view gives you more space and better visibility into your fleet of runners.')
- %a.btn.btn-confirm.btn-md.gl-button{ :href => group_runners_path(@group) }
- %span.gl-button-text
- = s_('Runners|Take me there!')
-- else
- = render 'shared/runners/runner_description'
-
- %hr
-
- .row
- .col-sm-6
- = render 'groups/runners/group_runners'
- .col-sm-6
- = render 'groups/runners/shared_runners'
-
- %h4.underlined-title
- = _('Available runners: %{runners}').html_safe % { runners: limited_counter_with_delimiter(@all_group_runners) }
-
- -# haml-lint:disable NoPlainNodes
- .row
- .col-sm-9
- = form_tag group_settings_ci_cd_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do
- .filtered-search-wrapper.d-flex
- .filtered-search-box
- = dropdown_tag(_('Recent searches'),
- options: { wrapper_class: 'filtered-search-history-dropdown-wrapper',
- toggle_class: 'gl-button btn btn-default filtered-search-history-dropdown-toggle-button',
- dropdown_class: 'filtered-search-history-dropdown',
- content_class: 'filtered-search-history-dropdown-content' }) do
- .js-filtered-search-history-dropdown{ data: { full_path: group_settings_ci_cd_path } }
- .filtered-search-box-input-container.droplab-dropdown
- .scroll-container
- %ul.tokens-container.list-unstyled
- %li.input-token
- %input.form-control.filtered-search{ search_filter_input_options('runners') }
- #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
- %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- %li.filter-dropdown-item{ data: {hint: "#{'{{hint}}'}", tag: "#{'{{tag}}'}", action: "#{'{{hint === \'search\' ? \'submit\' : \'\' }}'}" } }
- = button_tag class: 'gl-button btn btn-link' do
- -# Encapsulate static class name `{{icon}}` inside #{} to bypass
- -# haml lint's ClassAttributeWithStaticValue
- %svg
- %use{ 'xlink:href': "#{'{{icon}}'}" }
- %span.js-filter-hint
- {{formattedKey}}
- #js-dropdown-operator.filtered-search-input-dropdown-menu.dropdown-menu
- %ul.filter-dropdown{ data: { dropdown: true, dynamic: true } }
- %li.filter-dropdown-item{ data: { value: "{{ title }}" } }
- = button_tag class: 'gl-button btn btn-link' do
- {{ title }}
- %span.btn-helptext
- {{ help }}
- #js-dropdown-admin-runner-status.filtered-search-input-dropdown-menu.dropdown-menu
- %ul{ data: { dropdown: true } }
- - Ci::Runner::AVAILABLE_STATUSES.each do |status|
- %li.filter-dropdown-item{ data: { value: status } }
- = button_tag class: 'gl-button btn btn-link' do
- = status.titleize
-
- #js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu
- %ul{ data: { dropdown: true } }
- - Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
- - next if runner_type == 'instance_type'
- %li.filter-dropdown-item{ data: { value: runner_type } }
- = button_tag class: 'gl-button btn btn-link' do
- = runner_type.titleize
-
- #js-dropdown-runner-tag.filtered-search-input-dropdown-menu.dropdown-menu
- %ul{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { value: 'none' } }
- = button_tag class: 'gl-button btn btn-link' do
- = _('No Tag')
- %li.divider.droplab-item-ignore
- %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- %li.filter-dropdown-item
- = button_tag class: 'gl-button btn btn-link js-data-value' do
- %span.dropdown-light-content
- {{name}}
-
- = button_tag class: 'clear-search hidden' do
- = sprite_icon('close', size: 16, css_class: 'clear-search-icon')
- .filter-dropdown-container
- = render 'groups/runners/sort_dropdown'
-
- .col-sm-3.text-right-lg
- = _('Runners currently online: %{active_runners_count}') % { active_runners_count: limited_counter_with_delimiter(@all_group_runners.online) }
-
-
- - if @group_runners.any?
- .content-list{ data: { testid: 'runners-table' } }
- .table-holder
- .gl-responsive-table-row.table-row-header{ role: 'row' }
- .table-section.section-10{ role: 'rowheader' }= _('Type/State')
- .table-section.section-30{ role: 'rowheader' }= s_('Runners|Runner')
- .table-section.section-10{ role: 'rowheader' }= _('Version')
- .table-section.section-10{ role: 'rowheader' }= _('IP Address')
- .table-section.section-5{ role: 'rowheader' }= _('Projects')
- .table-section.section-5{ role: 'rowheader' }= _('Jobs')
- .table-section.section-10{ role: 'rowheader' }= _('Tags')
- .table-section.section-10{ role: 'rowheader' }= _('Last contact')
- .table-section.section-10{ role: 'rowheader' }
-
- - @group_runners.each do |runner|
- - runner = runner.present(current_user: current_user)
- = render 'groups/runners/runner', runner: runner
- = paginate @group_runners, theme: 'gitlab', :params => { :anchor => 'runners-settings' }
- - else
- .nothing-here-block= _('No runners found')
+.gl-mb-6
+ #update-shared-runners-form{ data: group_shared_runners_settings_data(@group) }
+.gl-card.gl-px-8.gl-py-6.gl-line-height-20
+ .gl-card-body.gl-display-flex{ :class => "gl-p-0!" }
+ .gl-banner-illustration
+ = image_tag('illustrations/rocket-launch-md.svg', alt: s_('Runners|Rocket launch illustration'))
+ .gl-banner-content
+ %h1.gl-banner-title
+ = s_('Runners|New group runners view')
+ %p
+ = s_('Runners|The new view gives you more space and better visibility into your fleet of runners.')
+ %a.btn.btn-confirm.btn-md.gl-button{ :href => group_runners_path(@group) }
+ %span.gl-button-text
+ = s_('Runners|Take me there!')
diff --git a/app/views/groups/runners/_shared_runners.html.haml b/app/views/groups/runners/_shared_runners.html.haml
deleted file mode 100644
index 15b1199b8c9..00000000000
--- a/app/views/groups/runners/_shared_runners.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-= render 'shared/runners/shared_runners_description'
-
-#update-shared-runners-form{ data: group_shared_runners_settings_data(@group) }
diff --git a/app/views/groups/runners/_sort_dropdown.html.haml b/app/views/groups/runners/_sort_dropdown.html.haml
deleted file mode 100644
index b7b10cecee8..00000000000
--- a/app/views/groups/runners/_sort_dropdown.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-- runners_sort_options = runners_sort_options_hash.map { |value, text| { value: value, text: text, href: page_filter_path(sort: value) } }
-
-= gl_redirect_listbox_tag runners_sort_options, @sort, class: 'gl-ml-3', data: { display: 'static' }
diff --git a/app/views/groups/runners/edit.html.haml b/app/views/groups/runners/edit.html.haml
index 4a5bab94246..04b9d88723f 100644
--- a/app/views/groups/runners/edit.html.haml
+++ b/app/views/groups/runners/edit.html.haml
@@ -1,11 +1,7 @@
- breadcrumb_title _('Edit')
- page_title _('Edit'), "##{@runner.id} (#{@runner.short_sha})"
-- if Feature.enabled?(:runner_list_group_view_vue_ui, @group, default_enabled: :yaml)
- - add_to_breadcrumbs _('Runners'), group_runners_path(@group)
-- else
- - add_to_breadcrumbs _('CI/CD Settings'), group_settings_ci_cd_path(@group)
-
+- add_to_breadcrumbs _('Runners'), group_runners_path(@group)
- add_to_breadcrumbs "#{@runner.short_sha}", group_runner_path(@group, @runner)
diff --git a/app/views/groups/runners/show.html.haml b/app/views/groups/runners/show.html.haml
index 72701491c67..b6c0c8a707f 100644
--- a/app/views/groups/runners/show.html.haml
+++ b/app/views/groups/runners/show.html.haml
@@ -1,6 +1,3 @@
-- if Feature.enabled?(:runner_list_group_view_vue_ui, @group, default_enabled: :yaml)
- - add_to_breadcrumbs _('Runners'), group_runners_path(@group)
-- else
- - add_to_breadcrumbs _('CI/CD Settings'), group_settings_ci_cd_path(@group)
+- add_to_breadcrumbs _('Runners'), group_runners_path(@group)
= render 'shared/runners/runner_details', runner: @runner
diff --git a/app/views/groups/settings/_export.html.haml b/app/views/groups/settings/_export.html.haml
index 62bc574231f..6cae416311e 100644
--- a/app/views/groups/settings/_export.html.haml
+++ b/app/views/groups/settings/_export.html.haml
@@ -3,8 +3,8 @@
.sub-section
%h4= s_('GroupSettings|Export group')
%p= _('Export this group with all related data.')
- = render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_class: 'gl-mb-4') do
- .gl-alert-body
+ = render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_class: 'gl-mb-4') do |c|
+ = c.body do
- docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index.md') }
- docs_link_end = '</a>'.html_safe
= s_('GroupsNew|This feature is deprecated and replaced by %{docs_link_start}group migration%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: docs_link_end }
@@ -12,8 +12,8 @@
- export_information = _('After the export is complete, download the data file from a notification email or from this page. You can then import the data file from the %{strong_text_start}Create new group%{strong_text_end} page of another GitLab instance.') % { strong_text_start: '<strong>'.html_safe, strong_text_end: '</strong>'.html_safe}
= export_information.html_safe
= link_to _('Learn more.'), help_page_path('user/group/settings/import_export.md'), target: '_blank', rel: 'noopener noreferrer'
- = render Pajamas::AlertComponent.new(dismissible: false, alert_class: 'gl-mb-5') do
- .gl-alert-body
+ = render Pajamas::AlertComponent.new(dismissible: false, alert_class: 'gl-mb-5') do |c|
+ = c.body do
%p.gl-mb-0
%p= _('The following items will be exported:')
%ul
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index 1a2f770cd59..ecb31b37fd3 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -34,7 +34,7 @@
= render 'groups/settings/ip_restriction_registration_features_cta', f: f
= render_if_exists 'groups/settings/ip_restriction', f: f, group: @group
= render_if_exists 'groups/settings/allowed_email_domain', f: f, group: @group
- - if Feature.enabled?(:group_wiki_settings_toggle, @group, default_enabled: :yaml)
+ - if @group.licensed_feature_available?(:group_wikis)
= render_if_exists 'groups/settings/wiki', f: f, group: @group
= render 'groups/settings/lfs', f: f
= render 'groups/settings/project_creation_level', f: f, group: @group
diff --git a/app/views/groups/settings/_remove_button.html.haml b/app/views/groups/settings/_remove_button.html.haml
index 66d6b516a86..e765638953a 100644
--- a/app/views/groups/settings/_remove_button.html.haml
+++ b/app/views/groups/settings/_remove_button.html.haml
@@ -1,8 +1,8 @@
- remove_form_id = local_assigns.fetch(:remove_form_id, nil)
- if group.paid?
- = render Pajamas::AlertComponent.new(dismissible: false, alert_class: 'gl-mb-5', alert_data: { testid: 'group-has-linked-subscription-alert' }) do
- .gl-alert-body
+ = render Pajamas::AlertComponent.new(dismissible: false, alert_class: 'gl-mb-5', alert_data: { testid: 'group-has-linked-subscription-alert' }) do |c|
+ = c.body do
= html_escape(_("This group can't be removed because it is linked to a subscription. To remove this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }
.js-confirm-danger{ data: group_settings_confirm_modal_data(group, remove_form_id) }
diff --git a/app/views/groups/settings/_transfer.html.haml b/app/views/groups/settings/_transfer.html.haml
index f5d9d0e2587..e65c3cd13f6 100644
--- a/app/views/groups/settings/_transfer.html.haml
+++ b/app/views/groups/settings/_transfer.html.haml
@@ -13,7 +13,7 @@
%li= s_('GroupSettings|You will need to update your local repositories to point to the new location.')
%li= s_("GroupSettings|If the parent group's visibility is lower than the group's current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.")
- if group.paid?
- = render Pajamas::AlertComponent.new(dismissible: false, alert_class: 'gl-mb-5') do
- .gl-alert-body
+ = render Pajamas::AlertComponent.new(dismissible: false, alert_class: 'gl-mb-5') do |c|
+ = c.body do
= html_escape(_("This group can't be transferred because it is linked to a subscription. To transfer this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }
.js-transfer-group-form{ data: initial_data }
diff --git a/app/views/groups/settings/packages_and_registries/show.html.haml b/app/views/groups/settings/packages_and_registries/show.html.haml
index 78ce981eb07..c4ce76c43ec 100644
--- a/app/views/groups/settings/packages_and_registries/show.html.haml
+++ b/app/views/groups/settings/packages_and_registries/show.html.haml
@@ -1,9 +1,7 @@
- breadcrumb_title _('Packages & Registries')
- page_title _('Packages & Registries')
- @content_class = 'limit-container-width' unless fluid_layout
-- dependency_proxy_available = Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true) || @group.public?
%section#js-packages-and-registries-settings{ data: { default_expanded: expanded_by_default?.to_s,
group_path: @group.full_path,
- dependency_proxy_available: dependency_proxy_available.to_s,
group_dependency_proxy_path: group_dependency_proxy_path(@group) } }
diff --git a/app/views/help/instance_configuration.html.haml b/app/views/help/instance_configuration.html.haml
index 411a81cb976..b6d27123be4 100644
--- a/app/views/help/instance_configuration.html.haml
+++ b/app/views/help/instance_configuration.html.haml
@@ -10,6 +10,7 @@
= render 'help/instance_configuration/size_limits'
= render 'help/instance_configuration/package_registry'
= render 'help/instance_configuration/rate_limits'
+ = render 'help/instance_configuration/ci_cd_limits'
%p
%strong= _("Table of contents")
diff --git a/app/views/help/instance_configuration/_ci_cd_limits.html.haml b/app/views/help/instance_configuration/_ci_cd_limits.html.haml
new file mode 100644
index 00000000000..bd5b8a6f10d
--- /dev/null
+++ b/app/views/help/instance_configuration/_ci_cd_limits.html.haml
@@ -0,0 +1,52 @@
+- ci_cd_limits = @instance_configuration.settings[:ci_cd_limits]
+- return unless ci_cd_limits.present?
+
+- content_for :table_content do
+ %li= link_to _('CI/CD limits'), '#ci-cd-limits'
+
+- content_for :settings_content do
+ %h2#ci-cd-limits
+ = _('CI/CD limits')
+
+ %p
+ = s_('CICD|There are several CI/CD limits in place.')
+ .table-responsive
+ %table
+ %thead
+ %tr
+ %th= s_('CICD|Limit')
+ - ci_cd_limits.each_key do |title|
+ %th= title.to_s.humanize
+ %tbody
+ %tr
+ %td= s_('AdminSettings|Maximum number of jobs in a single pipeline')
+ - ci_cd_limits.each_value do |limits|
+ %td= instance_configuration_disabled_cell_html(limits[:ci_pipeline_size])
+ %tr
+ %td= s_('AdminSettings|Total number of jobs in currently active pipelines')
+ - ci_cd_limits.each_value do |limits|
+ %td= instance_configuration_disabled_cell_html(limits[:ci_active_jobs])
+ %tr
+ %td= s_('AdminSettings|Maximum number of active pipelines per project')
+ - ci_cd_limits.each_value do |limits|
+ %td= instance_configuration_disabled_cell_html(limits[:ci_active_pipelines])
+ %tr
+ %td= s_('AdminSettings|Maximum number of pipeline subscriptions to and from a project')
+ - ci_cd_limits.each_value do |limits|
+ %td= instance_configuration_disabled_cell_html(limits[:ci_project_subscriptions])
+ %tr
+ %td= s_('AdminSettings|Maximum number of pipeline schedules')
+ - ci_cd_limits.each_value do |limits|
+ %td= instance_configuration_disabled_cell_html(limits[:ci_pipeline_schedules])
+ %tr
+ %td= s_('AdminSettings|Maximum number of DAG dependencies that a job can have')
+ - ci_cd_limits.each_value do |limits|
+ %td= instance_configuration_disabled_cell_html(limits[:ci_needs_size_limit])
+ %tr
+ %td= s_('AdminSettings|Maximum number of runners registered per group')
+ - ci_cd_limits.each_value do |limits|
+ %td= instance_configuration_disabled_cell_html(limits[:ci_registered_group_runners])
+ %tr
+ %td= s_('AdminSettings|Maximum number of runners registered per project')
+ - ci_cd_limits.each_value do |limits|
+ %td= instance_configuration_disabled_cell_html(limits[:ci_registered_project_runners])
diff --git a/app/views/help/instance_configuration/_size_limits.html.haml b/app/views/help/instance_configuration/_size_limits.html.haml
index b592eeed020..90501450385 100644
--- a/app/views/help/instance_configuration/_size_limits.html.haml
+++ b/app/views/help/instance_configuration/_size_limits.html.haml
@@ -24,6 +24,9 @@
%td= _('Maximum push size')
%td= instance_configuration_human_size_cell(size_limits[:receive_max_input_size])
%tr
+ %td= _('Maximum export size')
+ %td= instance_configuration_human_size_cell(size_limits[:max_export_size])
+ %tr
%td= _('Maximum import size')
%td= instance_configuration_human_size_cell(size_limits[:max_import_size])
%tr
diff --git a/app/views/import/bulk_imports/status.html.haml b/app/views/import/bulk_imports/status.html.haml
index 2aae8811678..71866bab30b 100644
--- a/app/views/import/bulk_imports/status.html.haml
+++ b/app/views/import/bulk_imports/status.html.haml
@@ -8,4 +8,5 @@
jobs_path: realtime_changes_import_bulk_imports_path(format: :json),
source_url: @source_url,
group_path_regex: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
+ history_path: history_import_bulk_imports_path,
group_url_error_message: group_url_error_message } }
diff --git a/app/views/import/shared/_errors.html.haml b/app/views/import/shared/_errors.html.haml
index aa6fcc445fd..ae54d0544f3 100644
--- a/app/views/import/shared/_errors.html.haml
+++ b/app/views/import/shared/_errors.html.haml
@@ -1,7 +1,7 @@
- if @errors.present?
= render Pajamas::AlertComponent.new(variant: :danger,
dismissible: false,
- alert_class: 'gl-mb-5') do
- .gl-alert-body
+ alert_class: 'gl-mb-5') do |c|
+ = c.body do
- @errors.each do |error|
= error
diff --git a/app/views/jira_connect/users/show.html.haml b/app/views/jira_connect/users/show.html.haml
index 29805a2c42d..569c4587f14 100644
--- a/app/views/jira_connect/users/show.html.haml
+++ b/app/views/jira_connect/users/show.html.haml
@@ -11,7 +11,7 @@
= s_('JiraService|You can now close this window and%{br}return to the GitLab for Jira application.').html_safe % { br: '<br>'.html_safe }
- if @jira_app_link
- %p= external_link s_('Integrations|Return to GitLab for Jira'), @jira_app_link, class: 'gl-button btn btn-confirm'
+ %p= link_to s_('Integrations|Return to GitLab for Jira'), @jira_app_link, class: 'gl-button btn btn-confirm'
%p= link_to _('Sign out'), destroy_user_session_path, method: :post
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 15cd9bece71..55c66454d0b 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -3,32 +3,14 @@
%head{ prefix: "og: http://ogp.me/ns#" }
%meta{ charset: "utf-8" }
+ %title= page_title(site_name)
+
= render 'layouts/loading_hints'
%meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' }
= render 'layouts/startup_js'
- -# Open Graph - http://ogp.me/
- %meta{ property: 'og:type', content: "object" }
- %meta{ property: 'og:site_name', content: site_name }
- %meta{ property: 'og:title', content: page_title }
- %meta{ property: 'og:description', content: page_description }
- %meta{ property: 'og:image', content: page_image }
- %meta{ property: 'og:image:width', content: '64' }
- %meta{ property: 'og:image:height', content: '64' }
- %meta{ property: 'og:url', content: request.base_url + request.fullpath }
-
- -# Twitter Card - https://dev.twitter.com/cards/types/summary
- %meta{ property: 'twitter:card', content: "summary" }
- %meta{ property: 'twitter:title', content: page_title }
- %meta{ property: 'twitter:description', content: page_description }
- %meta{ property: 'twitter:image', content: page_image }
- = page_card_meta_tags
-
- %title= page_title(site_name)
- %meta{ name: "description", content: page_description }
-
- if page_canonical_link
%link{ rel: 'canonical', href: page_canonical_link }
@@ -67,27 +49,38 @@
= yield :project_javascripts
- = csrf_meta_tags
- = csp_meta_tag
- = action_cable_meta_tag
+ -# Open Graph - http://ogp.me/
+ %meta{ property: 'og:type', content: "object" }
+ %meta{ property: 'og:site_name', content: site_name }
+ %meta{ property: 'og:title', content: page_title }
+ %meta{ property: 'og:description', content: page_description }
+ %meta{ property: 'og:image', content: page_image }
+ %meta{ property: 'og:image:width', content: '64' }
+ %meta{ property: 'og:image:height', content: '64' }
+ %meta{ property: 'og:url', content: request.base_url + request.fullpath }
+
+ -# Twitter Card - https://dev.twitter.com/cards/types/summary
+ %meta{ property: 'twitter:card', content: "summary" }
+ %meta{ property: 'twitter:title', content: page_title }
+ %meta{ property: 'twitter:description', content: page_description }
+ %meta{ property: 'twitter:image', content: page_image }
+ = page_card_meta_tags
+
+ %meta{ name: "description", content: page_description }
%meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' }
%meta{ name: 'theme-color', content: user_theme_primary_color }
+ = csrf_meta_tags
+ = csp_meta_tag
+ = action_cable_meta_tag
+
-# Apple Safari/iOS home screen icons
- = favicon_link_tag 'touch-icon-iphone.png', rel: 'apple-touch-icon'
- = favicon_link_tag 'touch-icon-ipad.png', rel: 'apple-touch-icon', sizes: '76x76'
- = favicon_link_tag 'touch-icon-iphone-retina.png', rel: 'apple-touch-icon', sizes: '120x120'
- = favicon_link_tag 'touch-icon-ipad-retina.png', rel: 'apple-touch-icon', sizes: '152x152'
- %link{ rel: 'mask-icon', href: image_path('logo.svg'), color: 'rgb(226, 67, 41)' }
+ = favicon_link_tag 'apple-touch-icon.png', rel: 'apple-touch-icon'
-# OpenSearch
%link{ href: search_opensearch_path(format: :xml), rel: 'search', title: 'Search GitLab', type: 'application/opensearchdescription+xml' }
- -# Windows 8 pinned site tile
- %meta{ name: 'msapplication-TileImage', content: image_path('msapplication-tile.png') }
- %meta{ name: 'msapplication-TileColor', content: '#30353E' }
-
= yield :meta_tags
= render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id')
diff --git a/app/views/layouts/_header_search.html.haml b/app/views/layouts/_header_search.html.haml
index f7b7aac6de4..3c62180214b 100644
--- a/app/views/layouts/_header_search.html.haml
+++ b/app/views/layouts/_header_search.html.haml
@@ -1,4 +1,4 @@
-#js-header-search.header-search.is-not-active.gl-relative{ data: { 'search-context' => header_search_context.to_json,
+#js-header-search.header-search.is-not-active.gl-relative.gl-w-full{ data: { 'search-context' => header_search_context.to_json,
'search-path' => search_path,
'issues-path' => issues_dashboard_path,
'mr-path' => merge_requests_dashboard_path,
diff --git a/app/views/layouts/_loading_hints.html.haml b/app/views/layouts/_loading_hints.html.haml
index 9b2815ea9bc..b3bb474ea43 100644
--- a/app/views/layouts/_loading_hints.html.haml
+++ b/app/views/layouts/_loading_hints.html.haml
@@ -12,4 +12,4 @@
= preload_link_tag(path_to_stylesheet('application'), crossorigin: css_crossorigin)
= preload_link_tag(path_to_stylesheet("highlight/themes/#{user_color_scheme}"), crossorigin: css_crossorigin)
- if Gitlab::Tracking.enabled? && Gitlab::Tracking.collector_hostname
- %link{ rel: 'preconnect', href: Gitlab::Tracking.collector_hostname, crossorigin: '' }
+ %link{ rel: 'preconnect', href: "https://#{Gitlab::Tracking.collector_hostname}", crossorigin: '' }
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 5c9c6a06ac1..cee5c1b6b69 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -16,7 +16,7 @@
%h1.mb-3.font-weight-normal
= current_appearance&.title.presence || _('GitLab')
.row.mb-3
- .col-sm-7.order-12.order-sm-1.brand-holder
+ .col-md-6.order-12.order-sm-1.brand-holder
- unless recently_confirmed_com?
= brand_image
- if current_appearance&.description?
@@ -36,7 +36,7 @@
= render_if_exists 'layouts/devise_help_text'
- .col-sm-5.order-1.new-session-forms-container{ class: recently_confirmed_com? ? 'order-sm-first' : 'order-sm-12' }
+ .col-md-6.order-1.new-session-forms-container{ class: recently_confirmed_com? ? 'order-sm-first' : 'order-sm-12' }
= yield
= render 'devise/shared/footer', footer_message: footer_message
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index c15a5e54a42..3cae8186750 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -4,16 +4,15 @@
%header.navbar.navbar-gitlab.navbar-expand-sm.js-navbar{ data: { qa_selector: 'navbar' } }
%a.gl-sr-only.gl-accessibility{ href: "#content-body" } Skip to content
.container-fluid
- .header-content
- .title-container.hide-when-top-nav-responsive-open
+ .header-content.js-header-content
+ .title-container.hide-when-top-nav-responsive-open.gl-transition-medium.gl-display-flex.gl-align-items-stretch.gl-pt-0
%h1.title
%span.gl-sr-only GitLab
= link_to root_path, title: _('Dashboard'), id: 'logo', **tracking_attrs('main_navigation', 'click_gitlab_logo_link', 'navigation') do
- = brand_header_logo
- - logo_text = brand_header_logo_type
- - if logo_text.present?
- %span.logo-text.d-none.d-lg-block.gl-ml-3
- = logo_text
+ %span{ :class => "gl-display-none gl-lg-display-flex" }
+ = brand_header_logo({add_gitlab_white_text: true})
+ %span{ :class => "gl-lg-display-none! gl-display-flex" }
+ = brand_header_logo
- if Gitlab.com_and_canary?
= link_to Gitlab::Saas.canary_toggle_com_url, class: 'canary-badge bg-transparent', data: { qa_selector: 'canary_badge_link' }, target: :_blank, rel: 'noopener noreferrer' do
= gl_badge_tag({ variant: :success, size: :sm }) do
@@ -32,15 +31,15 @@
.gl-display-none.gl-sm-display-block
= render "layouts/nav/top_nav"
- .navbar-collapse.collapse
- %ul.nav.navbar-nav
+ .navbar-collapse.gl-transition-medium.collapse
+ %ul.nav.navbar-nav.gl-w-full
- if current_user
- = render 'layouts/header/new_dropdown', class: 'gl-display-none gl-sm-display-block'
+ = render 'layouts/header/new_dropdown', class: 'gl-display-none gl-sm-display-block gl-white-space-nowrap gl-text-right'
- if top_nav_show_search
- search_menu_item = top_nav_search_menu_item_attrs
- %li.nav-item.header-search-new.d-none.d-lg-block.m-auto
+ %li.nav-item.header-search-new.gl-display-none.gl-lg-display-block.m-auto.gl-w-full
- unless current_controller?(:search)
- - if Feature.enabled?(:new_header_search, default_enabled: :yaml)
+ - if Feature.enabled?(:new_header_search)
= render 'layouts/header_search'
- else
= render 'layouts/search'
@@ -61,7 +60,7 @@
= number_with_delimiter(issues_count)
- if header_link?(:merge_requests)
= nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter dropdown" }) do
- - top_level_link = Feature.enabled?(:mr_attention_requests, default_enabled: :yaml) ? attention_requested_mrs_dashboard_path : assigned_mrs_dashboard_path
+ - top_level_link = current_user.mr_attention_requests_enabled? ? attention_requested_mrs_dashboard_path : assigned_mrs_dashboard_path
= link_to top_level_link, class: 'dashboard-shortcuts-merge_requests', title: _('Merge requests'), aria: { label: _('Merge requests') },
data: { qa_selector: 'merge_requests_shortcut_button',
toggle: "dropdown",
@@ -78,7 +77,7 @@
%ul
%li.dropdown-header
= _('Merge requests')
- - if Feature.enabled?(:mr_attention_requests, default_enabled: :yaml)
+ - if current_user.mr_attention_requests_enabled?
%li#js-need-attention-nav
#js-need-attention-nav-onboarding
= link_to attention_requested_mrs_dashboard_path, class: 'gl-display-flex! gl-align-items-center js-prefetch-document' do
@@ -87,12 +86,18 @@
%li.divider
%li
= link_to assigned_mrs_dashboard_path, class: 'gl-display-flex! gl-align-items-center js-prefetch-document' do
- = _('Assigned to you')
+ - if current_user.mr_attention_requests_enabled?
+ = _('Assignee')
+ - else
+ = _('Assigned to you')
= gl_badge_tag({ variant: :neutral, size: :sm }, { class: "js-assigned-mr-count gl-ml-auto" }) do
= user_merge_requests_counts[:assigned]
%li
= link_to reviewer_mrs_dashboard_path, class: 'gl-display-flex! gl-align-items-center js-prefetch-document' do
- = _('Review requests for you')
+ - if current_user.mr_attention_requests_enabled?
+ = _('Reviewer')
+ - else
+ = _('Review requests for you')
= gl_badge_tag({ variant: :neutral, size: :sm }, { class: "js-reviewer-mr-count gl-ml-auto" }) do
= user_merge_requests_counts[:review_requested]
- if header_link?(:todos)
diff --git a/app/views/layouts/header/_logo_with_title.html.haml b/app/views/layouts/header/_logo_with_title.html.haml
index 0b9d4e2eea4..66614bdb21e 100644
--- a/app/views/layouts/header/_logo_with_title.html.haml
+++ b/app/views/layouts/header/_logo_with_title.html.haml
@@ -1,4 +1,5 @@
%header.navbar.fixed-top.navbar-gitlab.justify-content-center
- = render partial: 'shared/logo', formats: :svg
- %span.logo-text.d-none.d-lg-block.gl-ml-3.pt-1
- = render partial: 'shared/logo_type', formats: :svg
+ .gl-display-none.gl-lg-display-block
+ = render partial: 'shared/logo_with_white_text', formats: :svg
+ .gl-lg-display-none
+ = render partial: 'shared/logo', formats: :svg
diff --git a/app/views/layouts/header/_new_dropdown.html.haml b/app/views/layouts/header/_new_dropdown.html.haml
index a0b271fdafa..e5b03acbe3b 100644
--- a/app/views/layouts/header/_new_dropdown.html.haml
+++ b/app/views/layouts/header/_new_dropdown.html.haml
@@ -6,8 +6,8 @@
- return if menu_sections.empty?
-%li.header-new.dropdown{ class: top_class, data: { track_label: "new_dropdown", track_action: "click_dropdown" } }
- = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", id: "js-onboarding-new-project-link", title: title, ref: 'tooltip', aria: { label: title }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static', qa_selector: 'new_menu_toggle' } do
+%li.header-new.gl-flex-grow-1.gl-flex-shrink-1.dropdown{ class: top_class, data: { track_label: "new_dropdown", track_action: "click_dropdown" } }
+ = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip gl-display-inline-block!", id: "js-onboarding-new-project-link", title: title, ref: 'tooltip', aria: { label: title }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static', qa_selector: 'new_menu_toggle' } do
= sprite_icon('plus-square')
= sprite_icon('chevron-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right.dropdown-extended-height
diff --git a/app/views/layouts/header/_registration_enabled_callout.html.haml b/app/views/layouts/header/_registration_enabled_callout.html.haml
index affee15c4d0..03e961bda8f 100644
--- a/app/views/layouts/header/_registration_enabled_callout.html.haml
+++ b/app/views/layouts/header/_registration_enabled_callout.html.haml
@@ -5,10 +5,10 @@
alert_class: 'js-registration-enabled-callout',
alert_data: { feature_id: Users::CalloutsHelper::REGISTRATION_ENABLED_CALLOUT,
dismiss_endpoint: callouts_path },
- close_button_data: { testid: 'close-registration-enabled-callout' }) do
- .gl-alert-body
+ close_button_data: { testid: 'close-registration-enabled-callout' }) do |c|
+ = c.body do
= _('Only allow anyone to register for accounts on GitLab instances that you intend to be used by anyone. Allowing anyone to register makes GitLab instances more vulnerable.')
- .gl-alert-actions
+ = c.actions do
= link_to general_admin_application_settings_path(anchor: 'js-signup-settings'), class: 'btn gl-alert-action btn-confirm btn-md gl-button' do
%span.gl-button-text
= _('Turn off')
diff --git a/app/views/layouts/header/_storage_enforcement_banner.html.haml b/app/views/layouts/header/_storage_enforcement_banner.html.haml
index 92c02d6ecfd..6613130fdf3 100644
--- a/app/views/layouts/header/_storage_enforcement_banner.html.haml
+++ b/app/views/layouts/header/_storage_enforcement_banner.html.haml
@@ -8,7 +8,7 @@
alert_data: { feature_id: banner_info[:callouts_feature_name],
dismiss_endpoint: banner_info[:callouts_path],
group_id: namespace.id,
- defer_links: "true" }) do
- .gl-alert-body
+ defer_links: "true" }) do |c|
+ = c.body do
= banner_info[:text]
= banner_info[:learn_more_link]
diff --git a/app/views/layouts/in_product_marketing_mailer.html.haml b/app/views/layouts/in_product_marketing_mailer.html.haml
index 679a2d4b8b3..65c68c95d9a 100644
--- a/app/views/layouts/in_product_marketing_mailer.html.haml
+++ b/app/views/layouts/in_product_marketing_mailer.html.haml
@@ -170,7 +170,7 @@
%table{ border: "0", cellpadding: "0", cellspacing: "0", role: "presentation", width: "100%" }
%tr
%td{ align: "left", style: "padding: 0 20px;" }
- = about_link('mailers/in_product_marketing/gitlab-logo-gray-rgb.png', 200)
+ = about_link('mailers/gitlab_logo_black_text.png', 200)
%tr
%td{ "aria-hidden" => "true", height: "30", style: "font-size: 0; line-height: 0;" }
diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml
index c2eb6b68024..580b8e67a3c 100644
--- a/app/views/layouts/mailer.html.haml
+++ b/app/views/layouts/mailer.html.haml
@@ -1,7 +1,7 @@
= content_for :footer do
%tr.footer
%td
- %img.footer-logo{ alt: "GitLab", src: image_url('mailers/gitlab_footer_logo.gif') }
+ %img.footer-logo{ alt: "GitLab", src: image_url('mailers/gitlab_logo_black_text.png') }
%div
- manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, class: 'mng-notif-link')
- help_link = link_to(_("Help"), help_url, class: 'help-link')
diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml
index 3c52c430868..fde4e74fb7a 100644
--- a/app/views/layouts/nav/_breadcrumbs.html.haml
+++ b/app/views/layouts/nav/_breadcrumbs.html.haml
@@ -18,9 +18,8 @@
= breadcrumb_list_item link_to(extra[:text], extra[:link])
= render "layouts/nav/breadcrumbs/collapsed_inline_list", location: :after
- unless @skip_current_level_breadcrumb
- %li
- %h2.breadcrumbs-sub-title{ data: { qa_selector: 'breadcrumb_sub_title_content' } }
- = link_to @breadcrumb_title, breadcrumb_title_link
+ %li{ data: { testid: 'breadcrumb-current-link', qa_selector: 'breadcrumb_current_link' } }
+ = link_to @breadcrumb_title, breadcrumb_title_link
-# haml-lint:disable InlineJavaScript
%script{ type: 'application/ld+json' }
:plain
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 94c708783e4..02565a8f573 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -103,10 +103,6 @@
= link_to admin_health_check_path, title: _('Health Check') do
%span
= _('Health Check')
- = nav_link(controller: :requests_profiles) do
- = link_to admin_requests_profiles_path, title: _('Requests Profiles') do
- %span
- = _('Requests Profiles')
- if Gitlab::CurrentSettings.current_application_settings.grafana_enabled?
= nav_link do
= link_to Gitlab::CurrentSettings.current_application_settings.grafana_url, target: '_blank', title: _('Metrics Dashboard'), rel: 'noopener noreferrer' do
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
index caa46b7bc56..91301e1e226 100644
--- a/app/views/layouts/terms.html.haml
+++ b/app/views/layouts/terms.html.haml
@@ -1,10 +1,11 @@
!!! 5
- add_page_specific_style 'page_bundles/terms'
- @hide_breadcrumbs = true
+- body_classes = [user_application_theme]
%html{ lang: I18n.locale, class: page_class }
= render "layouts/head"
- %body{ data: { page: body_data_page } }
+ %body{ class: body_classes, data: { page: body_data_page } }
.layout-page.terms{ class: page_class }
.content-wrapper.gl-pb-5
.mobile-overlay
@@ -17,11 +18,7 @@
.content{ id: "content-body" }
.gl-card
.gl-card-header
- = brand_header_logo
- - logo_text = brand_header_logo_type
- - if logo_text.present?
- %span.logo-text.gl-ml-3
- = logo_text
+ = brand_header_logo({add_gitlab_black_text: true})
- if header_link?(:user_dropdown)
.navbar-collapse
%ul.nav.navbar-nav
diff --git a/app/views/layouts/unknown_user_mailer.html.haml b/app/views/layouts/unknown_user_mailer.html.haml
index 2eb7b400604..7f0d1dc01dd 100644
--- a/app/views/layouts/unknown_user_mailer.html.haml
+++ b/app/views/layouts/unknown_user_mailer.html.haml
@@ -1,7 +1,7 @@
= content_for :footer do
%tr.footer
%td.gitlab-info
- %img.footer-logo{ alt: "GitLab", src: image_url('mailers/gitlab_footer_logo.gif') }
+ %img.footer-logo{ alt: "GitLab", src: image_url('mailers/gitlab_logo_black_text.png') }
%p.gitlab-info-text
= html_escape(_("GitLab is a complete DevOps platform, delivered as a single application, fundamentally changing the way%{br_tag}Development, Security, and Ops teams collaborate")) % { br_tag: '<br/>'.html_safe }
diff --git a/app/views/notify/approved_merge_request_email.html.haml b/app/views/notify/approved_merge_request_email.html.haml
new file mode 100644
index 00000000000..4393186a8ad
--- /dev/null
+++ b/app/views/notify/approved_merge_request_email.html.haml
@@ -0,0 +1,157 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+%html{ lang: "en" }
+ %head
+ %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
+ %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
+ %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
+ %title= message.subject
+ :css
+ /* CLIENT-SPECIFIC STYLES */
+ body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
+ table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
+ img { -ms-interpolation-mode: bicubic; }
+
+ /* iOS BLUE LINKS */
+ a[x-apple-data-detectors] {
+ color: inherit !important;
+ text-decoration: none !important;
+ font-size: inherit !important;
+ font-family: inherit !important;
+ font-weight: inherit !important;
+ line-height: inherit !important;
+ }
+
+ /* ANDROID MARGIN HACK */
+ body { margin:0 !important; }
+ div[style*="margin: 16px 0"] { margin:0 !important; }
+
+ @media only screen and (max-width: 639px) {
+ body, #body {
+ min-width: 320px !important;
+ }
+ table.wrapper {
+ width: 100% !important;
+ min-width: 320px !important;
+ }
+ table.wrapper > tbody > tr > td {
+ border-left: 0 !important;
+ border-right: 0 !important;
+ border-radius: 0 !important;
+ padding-left: 10px !important;
+ padding-right: 10px !important;
+ }
+ }
+
+ ul.users-list {
+ list-style: none;
+ padding: 0px;
+ display: block;
+ margin-top: 0px;
+ }
+ ul.users-list li {
+ display: inline-block;
+ padding-right: 12px;
+ padding-top: 8px;
+ }
+
+ %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
+ %tbody
+ %tr.line
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }
+ %tr.header
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+ %img{ alt: "GitLab", height: "55", src: image_url('mailers/gitlab_logo.png'), width: "55" }/
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
+ %tbody
+ %tr.success
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
+ %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
+ - if @merge_request.respond_to? :approvals_required
+ %span Merge request was approved (#{@merge_request.approvals.count}/#{@merge_request.approvals_required})
+ - else
+ %span Merge request was approved
+ %tr.spacer
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
+ &nbsp;
+ %tr.section
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;line-height:1.4;text-align:center;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;width:100%;" }
+ %tbody
+ %tr{ style: 'width:100%;' }
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;text-align:center;" }
+ %img{ src: image_url('mailers/approval/icon-merge-request-gray.gif'), style: "height:18px;width:18px;margin-bottom:-4px;", alt: "Merge request icon" }
+ %span{ style: "font-weight: 600;color:#333333;" } Merge request
+ %a{ href: merge_request_url(@merge_request), style: "font-weight: 600;color:#3777b0;text-decoration:none" }= @merge_request.to_reference
+ %span was approved by
+ %img.avatar{ height: "24", src: avatar_icon_for_user(@approved_by, 24, only_path: false), style: "border-radius:12px;margin:-7px 0 -7px 3px;", width: "24", alt: "Avatar" }/
+ %a.muted{ href: user_url(@approved_by), style: "color:#333333;text-decoration:none;" }
+ = @approved_by.name
+ %tr.spacer
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
+ &nbsp;
+ %tr.section
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
+ - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
+ - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
+ %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
+ = namespace_name
+ \/
+ %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
+ = @project.name
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %span.muted{ style: "color:#333333;text-decoration:none;" }
+ = @merge_request.source_branch
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img.avatar{ height: "24", src: avatar_icon_for_user(@merge_request.author, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %a.muted{ href: user_url(@merge_request.author), style: "color:#333333;text-decoration:none;" }
+ = @merge_request.author.name
+
+ - if @merge_request.assignees.any?
+ = render 'users_list', users: @merge_request.assignees, user_label: assignees_label(@merge_request, include_value: false)
+
+ - if @merge_request.reviewers.any?
+ = render 'users_list', users: @merge_request.reviewers, user_label: reviewers_label(@merge_request, include_value: false)
+ - if Gitlab.ee?
+ -# EE-specific start
+ = render 'layouts/mailer/additional_text'
+ -# EE-specific end
+
+ %tr.footer
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+ %img{ alt: "GitLab", src: image_url('mailers/gitlab_logo_black_text.png'), style: "display:block;margin:0 auto 1em;", width: "90" }/
+ %div
+ - manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, style: "color:#3777b0;text-decoration:none;")
+ - help_link = link_to(_("Help"), help_url, style: "color:#3777b0;text-decoration:none;")
+ = _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} &middot; %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link }
diff --git a/app/views/notify/approved_merge_request_email.text.haml b/app/views/notify/approved_merge_request_email.text.haml
new file mode 100644
index 00000000000..476da7f9af7
--- /dev/null
+++ b/app/views/notify/approved_merge_request_email.text.haml
@@ -0,0 +1,9 @@
+Merge request #{@merge_request.to_reference} was approved by #{sanitize_name(@approved_by.name)}
+
+Merge request URL: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
+
+= merge_path_description(@merge_request, 'to')
+
+Author: #{sanitize_name(@merge_request.author_name)}
+= assignees_label(@merge_request)
+= reviewers_label(@merge_request)
diff --git a/app/views/notify/build_ios_app_guide_email.html.haml b/app/views/notify/build_ios_app_guide_email.html.haml
new file mode 100644
index 00000000000..e9f23d3c0f9
--- /dev/null
+++ b/app/views/notify/build_ios_app_guide_email.html.haml
@@ -0,0 +1,13 @@
+%tr
+ %td{ bgcolor: "#ffffff", height: "auto", style: "max-width: 600px; width: 100%; text-align: center; height: 200px; padding: 25px 15px; mso-line-height-rule: exactly; min-height: 40px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;", valign: "middle", width: "100%" }
+ = inline_image_link(@message.logo_path, { width: '150', style: 'width: 150px;' })
+ %h1{ style: "font-size: 40px; line-height: 46x; color: #000000; padding: 20px 0 0 0; font-weight: normal;" }
+ = @message.title
+%tr
+ %td{ style: "padding: 10px 20px 30px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; color:#000000; font-size: 18px; line-height: 24px;" }
+ %p{ style: "margin: 0 0 20px 0;" }
+ = @message.body_line1.html_safe
+%tr
+ %td{ align: "center", style: "padding: 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" }
+ .cta_link.cta_link_primary= @message.cta_link
+ .cta_link.cta_link_secondary= @message.cta2_link
diff --git a/app/views/notify/build_ios_app_guide_email.text.erb b/app/views/notify/build_ios_app_guide_email.text.erb
new file mode 100644
index 00000000000..59757b7c1b0
--- /dev/null
+++ b/app/views/notify/build_ios_app_guide_email.text.erb
@@ -0,0 +1,13 @@
+<%= @message.title %>
+
+<%= @message.body_line1 %>
+
+<%= @message.cta_link %>
+
+<%= @message.cta2_link %>
+
+<%= @message.footer_links %>
+
+<%= @message.address %>
+
+<%= @message.unsubscribe %>
diff --git a/app/views/notify/inactive_project_deletion_warning_email.html.haml b/app/views/notify/inactive_project_deletion_warning_email.html.haml
new file mode 100644
index 00000000000..52253ce3076
--- /dev/null
+++ b/app/views/notify/inactive_project_deletion_warning_email.html.haml
@@ -0,0 +1,28 @@
+- project_link = link_to(_("%{project_name}") % { project_name: @project.name }, @project.http_url_to_repo)
+- projects_api_link = link_to(_("Projects API"), help_page_url('api/projects'))
+- events_api_link = link_to(_("Events API"), help_page_url('api/events', anchor: 'list-a-projects-visible-events'))
+
+%p
+ = _('Hi %{username},') % { username: sanitize_name(@user.name) }
+
+%p
+ = html_escape(_("Due to inactivity, the %{project_link} project is scheduled to be deleted on %{b_open}%{deletion_date}%{b_close}. To unschedule the deletion of %{project_link}, perform some activity on it. For example:")) % { project_link: project_link.html_safe, deletion_date: @deletion_date, b_open: '<b>'.html_safe, b_close: '</b>'.html_safe }
+
+%p
+ %ul
+ %li= _("Create or close an issue.")
+ %li= _("Create, update, or delete a merge request.")
+ %li= _("Push code to the repository.")
+ %li= _("Add or remove a user.")
+
+%p
+ = html_escape(_("To ensure %{project_link} is unscheduled for deletion, check that activity has been logged by GitLab. For example:")) %{project_link: project_link.html_safe}
+
+%p
+ %ul
+ %li= html_escape(_("Go to the %{b_open}Activity%{b_close} page for %{project_link}.")) % { project_link: project_link, b_open: '<b>'.html_safe, b_close: '</b>'.html_safe }
+ %li= html_escape(_("View the %{code_open}last_activity_at%{code_close} attribute for %{project_link} using the %{projects_api_link}.")) % { project_link: project_link.html_safe, projects_api_link: projects_api_link.html_safe, code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+ %li= html_escape(_("List the visible events for %{project_link} using the %{events_api_link}.")) % { project_link: project_link.html_safe, events_api_link: events_api_link.html_safe }
+
+%p
+ = html_escape(_("This email supersedes any previous emails about scheduled deletion you may have received for %{project_link}.")) % { project_link: project_link.html_safe }
diff --git a/app/views/notify/inactive_project_deletion_warning_email.text.erb b/app/views/notify/inactive_project_deletion_warning_email.text.erb
new file mode 100644
index 00000000000..a0b79967817
--- /dev/null
+++ b/app/views/notify/inactive_project_deletion_warning_email.text.erb
@@ -0,0 +1,17 @@
+<%= _('Hi %{username},') % { username: sanitize_name(@user.name) } %>
+
+<%= _("Due to inactivity, the %{project_name} (%{project_link}) project is scheduled to be deleted on %{deletion_date}. To unschedule the deletion of %{project_name}, perform some activity on it. For example:") %
+ { project_name: @project.name, project_link: @project.http_url_to_repo, deletion_date: @deletion_date } %>
+
+<%= _("- Create or close an issue.") %>
+<%= _("- Create, update, or delete a merge request.") %>
+<%= _("- Push code to the repository.") %>
+<%= _("- Add or remove a user.") %>
+
+<%= _("To ensure %{project_name} is unscheduled for deletion, check that activity has been logged by GitLab. For example:") % { project_name: @project.name } %>
+
+<%= _("- Go to the Activity page for %{project_name}.") % { project_name: @project.name } %>
+<%= _("- View the last_activity_at attribute for %{project_name} using the Project API %{projects_api_link}.") % { project_name: @project.name, projects_api_link: help_page_url('api/projects') } %>
+<%= _("- List the visible events for %{project_name} using the Events API %{events_api_link}.") % { project_name: @project.name, events_api_link: help_page_url('api/events', anchor: 'list-a-projects-visible-events') } %>
+
+<%= _("This email supersedes any previous emails about scheduled deletion you may have received for %{project_name}.") % { project_name: @project.name } %>
diff --git a/app/views/notify/merge_request_unmergeable_email.html.haml b/app/views/notify/merge_request_unmergeable_email.html.haml
index 6bcff28985c..f0a5e5d4367 100644
--- a/app/views/notify/merge_request_unmergeable_email.html.haml
+++ b/app/views/notify/merge_request_unmergeable_email.html.haml
@@ -1,2 +1,10 @@
%p
= sprintf(s_('Notify|Merge request %{merge_request} can no longer be merged due to conflict.'), { merge_request: merge_request_reference_link(@merge_request) }).html_safe
+%p
+ = merge_path_description(@merge_request, 'to')
+%p
+ = sprintf(s_('Author: %{author_name}'), { author_name: sanitize_name(@merge_request.author_name) })
+%p
+ = assignees_label(@merge_request)
+%p
+ = reviewers_label(@merge_request)
diff --git a/app/views/notify/merge_when_pipeline_succeeds_email.html.haml b/app/views/notify/merge_when_pipeline_succeeds_email.html.haml
index e7c51c8fb13..e4d138cce96 100644
--- a/app/views/notify/merge_when_pipeline_succeeds_email.html.haml
+++ b/app/views/notify/merge_when_pipeline_succeeds_email.html.haml
@@ -61,7 +61,7 @@
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }
%tr.header
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
- %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }
+ %img{ alt: "GitLab", height: "55", src: image_url('mailers/gitlab_logo.png'), width: "55" }
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
@@ -146,7 +146,7 @@
%tr.footer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
- %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }
+ %img{ alt: "GitLab", src: image_url('mailers/gitlab_logo_black_text.png'), style: "display:block;margin:0 auto 1em;", width: "90" }
%div
- manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, style: "color:#3777b0;text-decoration:none;")
- help_link = link_to(_("Help"), help_url, style: "color:#3777b0;text-decoration:none;")
diff --git a/app/views/notify/merged_merge_request_email.html.haml b/app/views/notify/merged_merge_request_email.html.haml
index 0622e2f6ffb..e2e4d6d937f 100644
--- a/app/views/notify/merged_merge_request_email.html.haml
+++ b/app/views/notify/merged_merge_request_email.html.haml
@@ -1,2 +1,16 @@
%p
= sprintf(s_('Notify|Merge request %{merge_request} was merged'), { merge_request: merge_request_reference_link(@merge_request) }).html_safe
+
+%p
+ = merge_path_description(@merge_request, 'to')
+
+%div
+ = sprintf(s_('Notify|Author: %{author_name}'), { author_name: sanitize_name(@merge_request.author_name) })
+
+ - if @merge_request.assignees.any?
+ %div
+ = assignees_label(@merge_request)
+
+ - if @merge_request.reviewers.any?
+ %div
+ = reviewers_label(@merge_request)
diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml
index d6ec916641d..9b9eb566903 100644
--- a/app/views/notify/merged_merge_request_email.text.haml
+++ b/app/views/notify/merged_merge_request_email.text.haml
@@ -5,5 +5,9 @@
= merge_path_description(@merge_request, 'to')
= sprintf(s_('Notify|Author: %{author_name}'), { author_name: sanitize_name(@merge_request.author_name) })
-= assignees_label(@merge_request)
-= reviewers_label(@merge_request)
+
+- if @merge_request.assignees.any?
+ = assignees_label(@merge_request)
+
+- if @merge_request.reviewers.any?
+ = reviewers_label(@merge_request)
diff --git a/app/views/notify/unapproved_merge_request_email.html.haml b/app/views/notify/unapproved_merge_request_email.html.haml
new file mode 100644
index 00000000000..8a4138b7515
--- /dev/null
+++ b/app/views/notify/unapproved_merge_request_email.html.haml
@@ -0,0 +1,156 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+%html{ lang: "en" }
+ %head
+ %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
+ %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
+ %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
+ %title= message.subject
+ :css
+ /* CLIENT-SPECIFIC STYLES */
+ body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
+ table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
+ img { -ms-interpolation-mode: bicubic; }
+
+ /* iOS BLUE LINKS */
+ a[x-apple-data-detectors] {
+ color: inherit !important;
+ text-decoration: none !important;
+ font-size: inherit !important;
+ font-family: inherit !important;
+ font-weight: inherit !important;
+ line-height: inherit !important;
+ }
+
+ /* ANDROID MARGIN HACK */
+ body { margin:0 !important; }
+ div[style*="margin: 16px 0"] { margin:0 !important; }
+
+ @media only screen and (max-width: 639px) {
+ body, #body {
+ min-width: 320px !important;
+ }
+ table.wrapper {
+ width: 100% !important;
+ min-width: 320px !important;
+ }
+ table.wrapper > tbody > tr > td {
+ border-left: 0 !important;
+ border-right: 0 !important;
+ border-radius: 0 !important;
+ padding-left: 10px !important;
+ padding-right: 10px !important;
+ }
+ }
+
+ ul.users-list {
+ list-style: none;
+ padding: 0px;
+ display: block;
+ margin-top: 0px;
+ }
+ ul.users-list li {
+ display: inline-block;
+ padding-right: 12px;
+ padding-top: 8px;
+ }
+
+ %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
+ %tbody
+ %tr.line
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }
+ %tr.header
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+ %img{ alt: "GitLab", height: "55", src: image_url('mailers/gitlab_logo.png'), width: "55" }/
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
+ %tbody
+ %tr.success
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#FC6D26;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
+ %img{ alt: "✗", height: "13", src: image_url('mailers/approval/icon-x-orange-inverted.gif'), style: "display:block;", width: "13" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
+ - if @merge_request.respond_to? :approvals_required
+ %span Merge request was unapproved (#{@merge_request.approvals.count}/#{@merge_request.approvals_required})
+ - else
+ %span Merge request was unapproved
+ %tr.spacer
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
+ &nbsp;
+ %tr.section
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;line-height:1.4;text-align:center;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;width:100%;" }
+ %tbody
+ %tr{ style: 'width:100%;' }
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;text-align:center;" }
+ %img{ src: image_url('mailers/approval/icon-merge-request-gray.gif'), style: "height:18px;width:18px;margin-bottom:-4px;", alt: "Merge request icon" }
+ %span{ style: "font-weight: 600;color:#333333;" } Merge request
+ %a{ href: merge_request_url(@merge_request), style: "font-weight: 600;color:#3777b0;text-decoration:none" }= @merge_request.to_reference
+ %span was unapproved by
+ %img.avatar{ height: "24", src: avatar_icon_for_user(@unapproved_by, 24), style: "border-radius:12px;margin:-7px 0 -7px 3px;", width: "24", alt: "Avatar" }/
+ %a.muted{ href: user_url(@unapproved_by), style: "color:#333333;text-decoration:none;" }
+ = @unapproved_by.name
+ %tr.spacer
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
+ &nbsp;
+ %tr.section
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
+ - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
+ - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
+ %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
+ = namespace_name
+ \/
+ %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
+ = @project.name
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %span.muted{ style: "color:#333333;text-decoration:none;" }
+ = @merge_request.source_branch
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img.avatar{ height: "24", src: avatar_icon_for_user(@merge_request.author, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %a.muted{ href: user_url(@merge_request.author), style: "color:#333333;text-decoration:none;" }
+ = @merge_request.author.name
+ - if @merge_request.assignees.any?
+ = render 'users_list', users: @merge_request.assignees, user_label: assignees_label(@merge_request, include_value: false)
+
+ - if @merge_request.reviewers.any?
+ = render 'users_list', users: @merge_request.reviewers, user_label: reviewers_label(@merge_request, include_value: false)
+ - if Gitlab.ee?
+ -# EE-specific start
+ = render 'layouts/mailer/additional_text'
+ -# EE-specific end
+
+ %tr.footer
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+ %img{ alt: "GitLab", src: image_url('mailers/gitlab_logo_black_text.png'), style: "display:block;margin:0 auto 1em;", width: "90" }/
+ %div
+ - manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, style: "color:#3777b0;text-decoration:none;")
+ - help_link = link_to(_("Help"), help_url, style: "color:#3777b0;text-decoration:none;")
+ = _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} &middot; %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link }
diff --git a/app/views/notify/unapproved_merge_request_email.text.haml b/app/views/notify/unapproved_merge_request_email.text.haml
new file mode 100644
index 00000000000..4e34b883906
--- /dev/null
+++ b/app/views/notify/unapproved_merge_request_email.text.haml
@@ -0,0 +1,9 @@
+Merge request #{@merge_request.to_reference} was unapproved by #{@unapproved_by.name}
+
+Merge request URL: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
+
+= merge_path_description(@merge_request, 'to')
+
+Author: #{sanitize_name(@merge_request.author_name)}
+= assignees_label(@merge_request)
+= reviewers_label(@merge_request)
diff --git a/app/views/profiles/_email_settings.html.haml b/app/views/profiles/_email_settings.html.haml
index 1057e96f442..35cad79b6fd 100644
--- a/app/views/profiles/_email_settings.html.haml
+++ b/app/views/profiles/_email_settings.html.haml
@@ -5,15 +5,34 @@
- help_text = email_change_disabled ? s_("Your account uses dedicated credentials for the \"%{group_name}\" group and can only be updated through SSO.") % { group_name: @user.managing_group.name } : read_only_help_text
- password_automatically_set = @user.password_automatically_set?
-= form.text_field :email, required: true, class: 'input-lg gl-form-input', value: (@user.email unless @user.temp_oauth_email?), help: help_text.html_safe, readonly: readonly || email_change_disabled
-- unless password_automatically_set
- = hidden_field_tag 'user[validation_password]', :validation_password, class: 'js-password-prompt-field', help: s_("Profiles|Enter your password to confirm the email change")
-= form.select :public_email, options_for_select(@user.public_verified_emails, selected: @user.public_email),
- { help: s_("Profiles|This email will be displayed on your public profile"), include_blank: s_("Profiles|Do not show on profile") },
- control_class: 'select2 input-lg', disabled: email_change_disabled
-- commit_email_link_url = help_page_path('user/profile/index', anchor: 'change-the-email-displayed-on-your-commits', target: '_blank')
-- commit_email_link_start = '<a href="%{url}">'.html_safe % { url: commit_email_link_url }
-- commit_email_docs_link = s_('Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more%{commit_email_link_end}').html_safe % { commit_email_link_start: commit_email_link_start, commit_email_link_end: '</a>'.html_safe }
-= form.select :commit_email, options_for_select(commit_email_select_options(@user), selected: @user.commit_email),
- { help: commit_email_docs_link },
- control_class: 'select2 input-lg', disabled: email_change_disabled
+.form-group.gl-form-group
+ = form.label :email, _('Email')
+ = form.text_field :email, required: true, class: 'gl-form-input form-control gl-form-input-lg', value: (@user.email unless @user.temp_oauth_email?), readonly: readonly || email_change_disabled
+ %small.form-text.text-gl-muted
+ = help_text.html_safe
+
+ - unless password_automatically_set
+ = hidden_field_tag 'user[validation_password]', :validation_password, class: 'js-password-prompt-field', help: s_("Profiles|Enter your password to confirm the email change")
+
+.form-group.gl-form-group
+ = form.label :public_email, s_('Profiles|Public email')
+ .gl-form-input-lg
+ = form.select :public_email,
+ options_for_select(@user.public_verified_emails, selected: @user.public_email),
+ { include_blank: s_("Profiles|Do not show on profile") },
+ { class: 'gl-form-select custom-select', disabled: email_change_disabled }
+ %small.form-text.text-gl-muted
+ = s_("Profiles|This email will be displayed on your public profile")
+
+.form-group.gl-form-group
+ - commit_email_link_url = help_page_path('user/profile/index', anchor: 'change-the-email-displayed-on-your-commits', target: '_blank')
+ - commit_email_link_start = '<a href="%{url}">'.html_safe % { url: commit_email_link_url }
+ - commit_email_docs_link = s_('Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more%{commit_email_link_end}').html_safe % { commit_email_link_start: commit_email_link_start, commit_email_link_end: '</a>'.html_safe }
+ = form.label :commit_email, s_('Profiles|Commit email')
+ .gl-form-input-lg
+ = form.select :commit_email,
+ options_for_select(commit_email_select_options(@user), selected: @user.commit_email),
+ {},
+ { class: 'gl-form-select custom-select', disabled: email_change_disabled }
+ %small.form-text.text-gl-muted
+ = commit_email_docs_link
diff --git a/app/views/profiles/_name.html.haml b/app/views/profiles/_name.html.haml
index aea38bf4c3b..5af4fe24d62 100644
--- a/app/views/profiles/_name.html.haml
+++ b/app/views/profiles/_name.html.haml
@@ -1,5 +1,9 @@
+= form.label :name, s_('Profiles|Full name')
- if user.read_only_attribute?(:name)
- = form.text_field :name, class: 'gl-form-input', required: true, readonly: true, wrapper: { class: 'col-md-9 qa-full-name rspec-full-name' },
- help: s_("Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you") % { provider_label: attribute_provider_label(:name) }
+ = form.text_field :name, class: 'gl-form-input form-control', required: true, readonly: true
+ %small.form-text.text-gl-muted
+ = s_("Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you") % { provider_label: attribute_provider_label(:name) }
- else
- = form.text_field :name, class: 'gl-form-input', label: s_('Profiles|Full name'), required: true, title: s_("Profiles|Using emojis in names seems fun, but please try to set a status message instead"), wrapper: { class: 'col-md-9 qa-full-name rspec-full-name' }, help: s_("Profiles|Enter your name, so people you know can recognize you")
+ = form.text_field :name, class: 'gl-form-input form-control', required: true, title: s_("Profiles|Using emojis in names seems fun, but please try to set a status message instead")
+ %small.form-text.text-gl-muted
+ = s_("Profiles|Enter your name, so people you know can recognize you")
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 8568e61aa33..bbbb8154c51 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -3,15 +3,15 @@
- if current_user.ldap_user?
= render Pajamas::AlertComponent.new(alert_class: 'gl-my-5',
- dismissible: false) do
- .gl-alert-body
+ dismissible: false) do |c|
+ = c.body do
= s_('Profiles|Some options are unavailable for LDAP accounts')
- if params[:two_factor_auth_enabled_successfully]
= render Pajamas::AlertComponent.new(variant: :success,
alert_class: 'gl-my-5',
- close_button_class: 'js-close-2fa-enabled-success-alert') do
- .gl-alert-body
+ close_button_class: 'js-close-2fa-enabled-success-alert') do |c|
+ = c.body do
= html_escape(_('You have set up 2FA for your account! If you lose access to your 2FA device, you can use your recovery codes to access your account. Alternatively, if you upload an SSH key, you can %{anchorOpen}use that key to generate additional recovery codes%{anchorClose}.')) % { anchorOpen: '<a href="%{href}">'.html_safe % { href: help_page_path('user/profile/account/two_factor_authentication', anchor: 'generate-new-recovery-codes-using-ssh') }, anchorClose: '</a>'.html_safe }
.row.gl-mt-3.js-search-settings-section
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 5d74bbe9971..26c9b2f0ee1 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -3,8 +3,8 @@
%div
- if @user.errors.any?
- = render Pajamas::AlertComponent.new(variant: :danger) do
- .gl-alert-body
+ = render Pajamas::AlertComponent.new(variant: :danger) do |c|
+ = c.body do
%ul
- @user.errors.full_messages.each do |msg|
%li= msg
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 3fb48f3d3e3..8c799a5e3fe 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -127,7 +127,12 @@
= _('Language')
= f.select :preferred_language, language_choices, {}, class: 'select2'
.form-text.text-muted
- = s_('Preferences|This feature is experimental and translations are not complete yet')
+ = s_('Preferences|This feature is experimental and translations are not yet complete.')
+ %p
+ = link_to help_page_url('development/i18n/translation'), class: 'text-nowrap', target: '_blank', rel: 'noopener noreferrer' do
+ = _("Help translate GitLab into your language")
+ %span{ aria: { label: _('Open new window') } }
+ = sprite_icon('external-link')
.form-group
= f.label :first_day_of_week, class: 'label-bold' do
= _('First day of the week')
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 531e72b7cc2..107c7cebc61 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -5,7 +5,7 @@
- availability = availability_values
- custom_emoji = show_status_emoji?(@user.status)
-= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user gl-mt-3 js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
+= gitlab_ui_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user gl-mt-3 js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
= form_errors(@user)
.row.js-search-settings-section
@@ -34,7 +34,7 @@
.gl-my-3
%button.gl-button.btn.btn-default.js-choose-user-avatar-button{ type: 'button' }= s_("Profiles|Choose file...")
%span.avatar-file-name.gl-ml-3.js-avatar-filename= s_("Profiles|No file chosen.")
- = f.file_field_without_bootstrap :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*'
+ = f.file_field :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*'
.gl-text-gray-500= s_("Profiles|The maximum file size allowed is 200KB.")
- if @user.avatar?
= link_to s_("Profiles|Remove avatar"), profile_avatar_path, data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") }, method: :delete, class: 'gl-button btn btn-danger-secondary btn-sm gl-mt-5'
@@ -62,16 +62,24 @@
= sprite_icon("close")
= status_form.hidden_field :emoji, id: 'js-status-emoji-field'
- = status_form.text_field :message,
- id: 'js-status-message-field',
- class: 'form-control gl-form-input input-lg',
- label: s_("Profiles|Your status"),
- prepend: emoji_button,
- append: reset_message_button,
- placeholder: s_("Profiles|What's your status?")
- .checkbox-icon-inline-wrapper
- = status_form.check_box :availability, { data: { testid: "user-availability-checkbox" }, label: s_("Profiles|Busy"), wrapper_class: 'gl-mr-0 gl-font-weight-bold' }, availability["busy"], availability["not_set"]
- .gl-text-gray-600.gl-ml-5= s_('Profiles|An indicator appears next to your name and avatar')
+ .form-group.gl-form-group
+ = status_form.label :message, s_("Profiles|Your status")
+ .input-group{ role: 'group' }
+ .input-group-prepend
+ = emoji_button
+ = status_form.text_field :message,
+ id: 'js-status-message-field',
+ class: 'form-control gl-form-input input-lg',
+ placeholder: s_("Profiles|What's your status?")
+ .input-group-append
+ = reset_message_button
+ .form-group.gl-form-group
+ = status_form.gitlab_ui_checkbox_component :availability,
+ s_("Profiles|Busy"),
+ help_text: s_('Profiles|An indicator appears next to your name and avatar'),
+ checkbox_options: { data: { testid: "user-availability-checkbox" } },
+ checked_value: availability["busy"],
+ unchecked_value: availability["not_set"]
.col-lg-12
%hr
.row.user-time-preferences.js-search-settings-section
@@ -94,35 +102,71 @@
= s_("Profiles|Some options are unavailable for LDAP accounts")
.col-lg-8
.row
- = render 'profiles/name', form: f, user: @user
- = f.text_field :id, class: 'gl-form-input', readonly: true, label: s_('Profiles|User ID'), wrapper: { class: 'col-md-3' }
-
- = f.text_field :pronouns, label: s_('Profiles|Pronouns'), class: 'input-md gl-form-input', help: s_("Profiles|Enter your pronouns to let people know how to refer to you")
- = f.text_field :pronunciation, label: s_('Profiles|Pronunciation'), class: 'input-md gl-form-input', help: s_("Profiles|Enter how your name is pronounced to help people address you correctly")
+ .form-group.gl-form-group.col-md-9.rspec-full-name
+ = render 'profiles/name', form: f, user: @user
+ .form-group.gl-form-group.col-md-3
+ = f.label :id, s_('Profiles|User ID')
+ = f.text_field :id, class: 'gl-form-input form-control', readonly: true
+ .form-group.gl-form-group
+ = f.label :pronouns, s_('Profiles|Pronouns')
+ = f.text_field :pronouns, class: 'gl-form-input form-control gl-form-input-lg'
+ %small.form-text.text-gl-muted
+ = s_("Profiles|Enter your pronouns to let people know how to refer to you")
+ .form-group.gl-form-group
+ = f.label :pronunciation, s_('Profiles|Pronunciation')
+ = f.text_field :pronunciation, class: 'gl-form-input form-control gl-form-input-lg'
+ %small.form-text.text-gl-muted
+ = s_("Profiles|Enter how your name is pronounced to help people address you correctly")
= render_if_exists 'profiles/extra_settings', form: f
= render_if_exists 'profiles/email_settings', form: f
- = f.text_field :skype, class: 'input-md gl-form-input', placeholder: s_("Profiles|username")
- = f.text_field :linkedin, class: 'input-md gl-form-input', help: s_("Profiles|Your LinkedIn profile name from linkedin.com/in/profilename")
- = f.text_field :twitter, class: 'input-md gl-form-input', placeholder: s_("Profiles|@username")
- = f.text_field :website_url, label: s_('Profiles|Website url'), class: 'input-lg gl-form-input', placeholder: s_("Profiles|https://website.com")
- - if @user.read_only_attribute?(:location)
- = f.text_field :location, class: 'gl-form-input', readonly: true, help: s_("Profiles|Your location was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:location) }
- - else
- = f.text_field :location, label: s_('Profiles|Location'), class: 'input-lg gl-form-input', placeholder: s_("Profiles|City, country")
- = f.text_field :job_title, label: s_('Profiles|Job title'), class: 'input-md gl-form-input'
- = f.text_field :organization, label: s_('Profiles|Organization'), class: 'input-md gl-form-input', help: s_("Profiles|Who you represent or work for")
- = f.text_area :bio, class: 'gl-form-input', label: s_('Profiles|Bio'), rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters")
+ .form-group.gl-form-group
+ = f.label :skype
+ = f.text_field :skype, class: 'gl-form-input form-control gl-form-input-lg', placeholder: s_("Profiles|username")
+ .form-group.gl-form-group
+ = f.label :linkedin
+ = f.text_field :linkedin, class: 'gl-form-input form-control gl-form-input-lg'
+ %small.form-text.text-gl-muted
+ = s_("Profiles|Your LinkedIn profile name from linkedin.com/in/profilename")
+ .form-group.gl-form-group
+ = f.label :twitter
+ = f.text_field :twitter, class: 'gl-form-input form-control gl-form-input-lg', placeholder: s_("Profiles|@username")
+ .form-group.gl-form-group
+ = f.label :website_url, s_('Profiles|Website url')
+ = f.text_field :website_url, class: 'gl-form-input form-control gl-form-input-lg', placeholder: s_("Profiles|https://website.com")
+ .form-group.gl-form-group
+ = f.label :location, s_('Profiles|Location')
+ - if @user.read_only_attribute?(:location)
+ = f.text_field :location, class: 'gl-form-input form-control gl-form-input-lg', readonly: true
+ %small.form-text.text-gl-muted
+ = s_("Profiles|Your location was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:location) }
+ - else
+ = f.text_field :location, class: 'gl-form-input form-control gl-form-input-lg', placeholder: s_("Profiles|City, country")
+ .form-group.gl-form-group
+ = f.label :job_title, s_('Profiles|Job title')
+ = f.text_field :job_title, class: 'gl-form-input form-control gl-form-input-lg'
+ .form-group.gl-form-group
+ = f.label :organization, s_('Profiles|Organization')
+ = f.text_field :organization, class: 'gl-form-input form-control gl-form-input-lg'
+ %small.form-text.text-gl-muted
+ = s_("Profiles|Who you represent or work for")
+ .form-group.gl-form-group
+ = f.label :bio, s_('Profiles|Bio')
+ = f.text_area :bio, class: 'gl-form-input gl-form-textarea form-control', rows: 4, maxlength: 250
+ %small.form-text.text-gl-muted
+ = s_("Profiles|Tell us about yourself in fewer than 250 characters")
%hr
- %h5= _('Private profile')
- .checkbox-icon-inline-wrapper
- - private_profile_label = capture do
- = s_("Profiles|Don't display activity-related personal information on your profile")
- = f.check_box :private_profile, label: private_profile_label, inline: true, wrapper_class: 'mr-0'
- = link_to sprite_icon('question-o'), help_page_path('user/profile/index.md', anchor: 'make-your-user-profile-page-private')
- %h5= s_("Profiles|Private contributions")
- = f.check_box :include_private_contributions, label: s_('Profiles|Include private contributions on my profile'), wrapper_class: 'mb-2', inline: true
- .help-block
- = s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information")
+ %fieldset.form-group.gl-form-group
+ %legend.col-form-label.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')
+ = 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.col-form-label
+ = s_("Profiles|Private contributions")
+ = f.gitlab_ui_checkbox_component :include_private_contributions,
+ s_('Profiles|Include private contributions on my profile'),
+ help_text: s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information")
%hr
= f.submit s_("Profiles|Update profile settings"), class: 'gl-button btn btn-confirm gl-mr-3 js-password-prompt-btn'
= link_to _("Cancel"), user_path(current_user), class: 'gl-button btn btn-default btn-cancel'
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 3ae64643420..ace644a493b 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -2,7 +2,7 @@
- page_title _('Two-Factor Authentication'), _('Account')
- add_to_breadcrumbs _('Account'), profile_account_path
- @content_class = "limit-container-width" unless fluid_layout
-- webauthn_enabled = Feature.enabled?(:webauthn, default_enabled: :yaml)
+- webauthn_enabled = Feature.enabled?(:webauthn)
.js-two-factor-auth{ 'data-two-factor-skippable' => "#{two_factor_skippable?}", 'data-two_factor_skip_url' => skip_profile_two_factor_auth_path }
.row.gl-mt-3
@@ -42,8 +42,8 @@
- if @error
= render Pajamas::AlertComponent.new(title: @error[:message],
variant: :danger,
- dismissible: false) do
- .gl-alert-body
+ dismissible: false) do |c|
+ = c.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'
.form-group
diff --git a/app/views/projects/_deletion_failed.html.haml b/app/views/projects/_deletion_failed.html.haml
index f9d8a2d2989..85a7b9eb22b 100644
--- a/app/views/projects/_deletion_failed.html.haml
+++ b/app/views/projects/_deletion_failed.html.haml
@@ -3,7 +3,7 @@
= render Pajamas::AlertComponent.new(variant: :warning,
dismissible: false,
- alert_class: 'project-deletion-failed-message') do
- .gl-alert-body
+ alert_class: 'project-deletion-failed-message') do |c|
+ = c.body do
This project was scheduled for deletion, but failed with the following message:
= project.delete_error
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index 74ace549bb1..bea5d548e03 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -8,7 +8,7 @@
#tree-holder.tree-holder.clearfix
.nav-block.gl-display-flex.gl-xs-flex-direction-column.gl-align-items-stretch
- = render 'projects/tree/tree_header', tree: @tree
+ = render 'projects/tree/tree_header', tree: @tree, is_project_overview: is_project_overview
.info-well.gl-display-none.gl-sm-display-flex.project-last-commit.gl-flex-direction-column
#js-last-commit.gl-m-auto
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 8e6cc6da65d..eee9cfe0618 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,7 +1,7 @@
- empty_repo = @project.empty_repo?
- show_auto_devops_callout = show_auto_devops_callout?(@project)
- emails_disabled = @project.emails_disabled?
-- cache_enabled = Feature.enabled?(:cache_home_panel, @project, type: :development, default_enabled: :yaml)
+- cache_enabled = Feature.enabled?(:cache_home_panel, @project, type: :development)
.project-home-panel.js-show-on-project-root.gl-my-5{ class: [("empty-project" if empty_repo)] }
.gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-sm-flex-direction-column.gl-mb-3
@@ -19,8 +19,7 @@
- if can?(current_user, :read_project, @project)
%span.gl-display-inline-block.gl-vertical-align-middle
= s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id }
- - button_class = "btn gl-button btn-sm btn-tertiary btn-default-tertiary home-panel-metadata"
- = clipboard_button(title: s_('ProjectPage|Copy project ID'), text: @project.id, class: button_class)
+ = clipboard_button(title: s_('ProjectPage|Copy project ID'), text: @project.id)
- if current_user
%span.gl-ml-3.gl-mb-3
= render 'shared/members/access_request_links', source: @project
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 66857dadb65..5a2add9de1e 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -2,8 +2,8 @@
- if event && show_last_push_widget?(event)
= render Pajamas::AlertComponent.new(variant: :success,
alert_class: 'gl-mt-3',
- close_button_class: 'js-close-banner') do
- .gl-alert-body
+ close_button_class: 'js-close-banner') do |c|
+ = c.body do
%span= s_("LastPushEvent|You pushed to")
%strong.gl-display-inline-flex.gl-max-w-50p{ data: { toggle: 'tooltip' }, title: event.ref_name }
= link_to event.ref_name, project_commits_path(event.project, event.ref_name), class: 'ref-name gl-text-truncate'
@@ -15,6 +15,6 @@
#{time_ago_with_tooltip(event.created_at)}
- if can?(current_user, :create_merge_request_in, event.project.default_merge_request_target)
- .gl-alert-actions
+ = c.actions do
= link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn gl-button btn-confirm qa-create-merge-request" do
#{ _('Create merge request') }
diff --git a/app/views/projects/_merge_request_merge_checks_settings.html.haml b/app/views/projects/_merge_request_merge_checks_settings.html.haml
index 4f9af40f711..3345b3043b8 100644
--- a/app/views/projects/_merge_request_merge_checks_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_checks_settings.html.haml
@@ -3,21 +3,16 @@
.form-group
%b= s_('ProjectSettings|Merge checks')
%p.text-secondary= s_('ProjectSettings|These checks must pass before merge requests can be merged.')
- .form-check.mb-2.builds-feature
- = form.check_box :only_allow_merge_if_pipeline_succeeds, class: 'form-check-input'
- = form.label :only_allow_merge_if_pipeline_succeeds, class: 'form-check-label' do
- = s_('ProjectSettings|Pipelines must succeed')
- .text-secondary
- = s_("ProjectSettings|Merge requests can't be merged if the latest pipeline did not succeed or is still running.")
- .form-check.mb-2
- .gl-pl-6
- = form.check_box :allow_merge_on_skipped_pipeline, class: 'form-check-input'
- = form.label :allow_merge_on_skipped_pipeline, class: 'form-check-label' do
- = s_('ProjectSettings|Skipped pipelines are considered successful')
- .text-secondary
- = s_('ProjectSettings|Introduces the risk of merging changes that do not pass the pipeline.')
- .form-check.mb-2
- = form.check_box :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-input', data: { qa_selector: 'allow_merge_if_all_discussions_are_resolved_checkbox' }
- = form.label :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-label' do
- = s_('ProjectSettings|All discussions must be resolved')
+ .builds-feature
+ = form.gitlab_ui_checkbox_component :only_allow_merge_if_pipeline_succeeds,
+ s_('ProjectSettings|Pipelines must succeed'),
+ help_text: s_("ProjectSettings|Merge requests can't be merged if the latest pipeline did not succeed or is still running.")
+ .gl-pl-6
+ = form.gitlab_ui_checkbox_component :allow_merge_on_skipped_pipeline,
+ s_('ProjectSettings|Skipped pipelines are considered successful'),
+ help_text: s_('ProjectSettings|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_('ProjectSettings|All threads must be resolved'),
+ checkbox_options: { data: { qa_selector: 'allow_merge_if_all_discussions_are_resolved_checkbox' } }
= render_if_exists 'projects/merge_request_merge_checks_jira_enforcement', form: form, project: @project
diff --git a/app/views/projects/_merge_request_merge_method_settings.html.haml b/app/views/projects/_merge_request_merge_method_settings.html.haml
index 250f7e94e84..cb660750632 100644
--- a/app/views/projects/_merge_request_merge_method_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_method_settings.html.haml
@@ -4,7 +4,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|Learn about commit history.'), help_page_path('user/project/merge_requests/commits.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to s_('ProjectSettings|How do they differ?'), help_page_path('user/project/merge_requests/methods/index.md'), target: '_blank', rel: 'noopener noreferrer'
.form-check.mb-2
= form.radio_button :merge_method, :merge, class: "js-merge-method-radio form-check-input"
= label_tag :project_merge_method_merge, class: 'form-check-label' do
diff --git a/app/views/projects/_merge_request_merge_options_settings.html.haml b/app/views/projects/_merge_request_merge_options_settings.html.haml
index 20f3933d0a8..e91c001ea3d 100644
--- a/app/views/projects/_merge_request_merge_options_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_options_settings.html.haml
@@ -5,17 +5,10 @@
%p.text-secondary= s_('ProjectSettings|Additional settings that influence how and when merges are done.')
= render_if_exists 'projects/merge_pipelines_settings', form: form
= render_if_exists 'projects/merge_trains_settings', form: form
- .form-check.mb-2
- = form.check_box :resolve_outdated_diff_discussions, class: 'form-check-input'
- = form.label :resolve_outdated_diff_discussions, class: 'form-check-label' do
- = s_('ProjectSettings|Automatically resolve merge request diff discussions when they become outdated')
- .form-check.mb-2
- = form.check_box :printing_merge_request_link_enabled, class: 'form-check-input'
- = form.label :printing_merge_request_link_enabled, class: 'form-check-label' do
- = s_('ProjectSettings|Show link to create or view a merge request when pushing from the command line')
- .form-check.mb-2
- = form.check_box :remove_source_branch_after_merge, class: 'form-check-input'
- = form.label :remove_source_branch_after_merge, class: 'form-check-label' do
- = s_('ProjectSettings|Enable "Delete source branch" option by default')
- .descr.text-secondary
- = s_('ProjectSettings|Existing merge requests and protected branches are not affected.')
+ = form.gitlab_ui_checkbox_component :resolve_outdated_diff_discussions,
+ s_('ProjectSettings|Automatically resolve merge request diff threads when they become outdated')
+ = form.gitlab_ui_checkbox_component :printing_merge_request_link_enabled,
+ s_('ProjectSettings|Show link to create or view a merge request when pushing from the command line')
+ = form.gitlab_ui_checkbox_component :remove_source_branch_after_merge,
+ s_('ProjectSettings|Enable "Delete source branch" option by default'),
+ help_text: s_('ProjectSettings|Existing merge requests and protected branches are not affected.')
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index e79825bdfc4..66fa1a69ef9 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -39,8 +39,8 @@
= project_tip.html_safe
= render Pajamas::AlertComponent.new(alert_class: "gl-mb-4 gl-display-none js-user-readme-repo",
dismissible: false,
- variant: :success) do
- .gl-alert-body
+ variant: :success) do |c|
+ = c.body do
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/profile/index', anchor: 'add-details-to-your-profile-with-a-readme') }
= html_escape(_('%{project_path} is a project that you can use to add a README to your GitLab profile. Create a public project and initialize the repository with a README to get started. %{help_link_start}Learn more.%{help_link_end}')) % { project_path: "<strong>#{current_user.username} / #{current_user.username}</strong>".html_safe, help_link_start: help_link_start, help_link_end: '</a>'.html_safe }
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 7f72438c3f9..bc1e62a8980 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -58,3 +58,6 @@
#{line}
- current_line += line_count
+
+ - if blame_pagination
+ = paginate(blame_pagination, theme: "gitlab")
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 85b9a69ab4c..2c3aade1068 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -14,7 +14,7 @@
#blob-content-holder.blob-content-holder
- if @code_navigation_path
#js-code-navigation{ data: { code_navigation_path: @code_navigation_path, blob_path: blob.path, definition_path_prefix: project_blob_path(@project, @ref) } }
- - if Feature.enabled?(:refactor_blob_viewer, @project, default_enabled: :yaml) && !expanded
+ - if Feature.enabled?(:refactor_blob_viewer, @project) && !expanded
-# Data info will be removed once we migrate this to use GraphQL
-# Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/330406
#js-view-blob-app{ data: { blob_path: blob.path,
diff --git a/app/views/projects/blob/_breadcrumb.html.haml b/app/views/projects/blob/_breadcrumb.html.haml
index e666bb237bd..7c2caf34fd1 100644
--- a/app/views/projects/blob/_breadcrumb.html.haml
+++ b/app/views/projects/blob/_breadcrumb.html.haml
@@ -2,7 +2,7 @@
.nav-block
.tree-ref-container
.tree-ref-holder
- = render 'shared/ref_switcher', destination: 'blob', path: @path
+ = render 'shared/ref_switcher', destination: 'blob'
%ul.breadcrumb.repo-breadcrumb
%li.breadcrumb-item
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index c9303e19d5d..09a275c24a1 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -40,6 +40,8 @@
Soft wrap
.file-editor.code
+ - if Feature.enabled?(:source_editor_toolbar, current_user)
+ #editor-toolbar
.js-edit-mode-pane.qa-editor#editor{ data: { 'editor-loading': true } }<
%pre.editor-loading-content= params[:content] || local_assigns[:blob_data]
- if local_assigns[:path]
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index aefa4a41ab5..f80601ef221 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -6,12 +6,13 @@
- if @conflict
= render Pajamas::AlertComponent.new(alert_class: 'gl-mb-5 gl-mt-5',
variant: :danger,
- dismissible: false) do
+ dismissible: false) do |c|
- blob_url = project_blob_path(@project, @id)
- external_link_icon = content_tag 'span', { aria: { label: _('Opens new window') }} do
- sprite_icon('external-link', css_class: 'gl-icon').html_safe
- blob_link_start = '<a href="%{url}" class="gl-link" target="_blank" rel="noopener noreferrer">'.html_safe % { url: blob_url }
- = _('Someone edited the file the same time you did. Please check out %{link_start}the file %{icon}%{link_end} and make sure your changes will not unintentionally remove theirs.').html_safe % { link_start: blob_link_start, link_end: '</a>'.html_safe , icon: external_link_icon }
+ = c.body do
+ = _('Someone edited the file the same time you did. Please check out %{link_start}the file %{icon}%{link_end} and make sure your changes will not unintentionally remove theirs.').html_safe % { link_start: blob_link_start, link_end: '</a>'.html_safe , icon: external_link_icon }
%h3.page-title.blob-edit-page-title
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 85a0346e691..295b2de9bd2 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -39,7 +39,7 @@
%ul.content-list.all-branches
- @branches.each do |branch|
= render "projects/branches/branch", branch: branch, merged: @merged_branch_names.include?(branch.name), commit_status: @branch_pipeline_statuses[branch.name], show_commit_status: @branch_pipeline_statuses.any?
- - if Feature.enabled?(:branch_list_keyset_pagination, @project, default_enabled: :yaml)
+ - if Feature.enabled?(:branch_list_keyset_pagination, @project)
= render('kaminari/gitlab/without_count', previous_path: @prev_path, next_path: @next_path)
- else
= paginate @branches, theme: 'gitlab'
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index 07bae7819a4..c06f60bd05d 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -2,8 +2,8 @@
- default_ref = params[:ref] || @project.default_branch
- if @error
- = render Pajamas::AlertComponent.new(variant: :danger) do
- .gl-alert-body
+ = render Pajamas::AlertComponent.new(variant: :danger) do |c|
+ = c.body do
= @error
%h3.page-title
= _('New Branch')
diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml
index 74d10f11898..5fd1c5cd403 100644
--- a/app/views/projects/buttons/_clone.html.haml
+++ b/app/views/projects/buttons/_clone.html.haml
@@ -30,13 +30,23 @@
%label.label-bold{ class: 'gl-px-4!' }
= _('Open in your IDE')
- if ssh_enabled?
- %a.dropdown-item.open-with-link{ href: 'vscode://vscode.git/clone?url=' + CGI.escape(project.ssh_url_to_repo) }
+ - escaped_ssh_url_to_repo = CGI.escape(project.ssh_url_to_repo)
+ %a.dropdown-item.open-with-link{ href: 'vscode://vscode.git/clone?url=' + escaped_ssh_url_to_repo }
.gl-new-dropdown-item-text-wrapper
= _('Visual Studio Code (SSH)')
- if http_enabled?
- %a.dropdown-item.open-with-link{ href: 'vscode://vscode.git/clone?url=' + CGI.escape(project.http_url_to_repo) }
+ - escaped_http_url_to_repo = CGI.escape(project.http_url_to_repo)
+ %a.dropdown-item.open-with-link{ href: 'vscode://vscode.git/clone?url=' + escaped_http_url_to_repo }
.gl-new-dropdown-item-text-wrapper
= _('Visual Studio Code (HTTPS)')
+ - if ssh_enabled?
+ %a.dropdown-item.open-with-link{ href: 'jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo=' + escaped_ssh_url_to_repo }
+ .gl-new-dropdown-item-text-wrapper
+ = _('IntelliJ IDEA (SSH)')
+ - if http_enabled?
+ %a.dropdown-item.open-with-link{ href: 'jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo=' + escaped_http_url_to_repo }
+ .gl-new-dropdown-item-text-wrapper
+ = _('IntelliJ IDEA (HTTPS)')
- if show_xcode_link?(@project)
%a.dropdown-item.open-with-link{ href: xcode_uri_to_repo(@project) }
.gl-new-dropdown-item-text-wrapper
diff --git a/app/views/projects/ci/pipeline_editor/show.html.haml b/app/views/projects/ci/pipeline_editor/show.html.haml
index c4757ea9c26..18eac48d42a 100644
--- a/app/views/projects/ci/pipeline_editor/show.html.haml
+++ b/app/views/projects/ci/pipeline_editor/show.html.haml
@@ -1,5 +1,6 @@
- @force_fluid_layout = true
- add_page_specific_style 'page_bundles/pipelines'
+- add_page_specific_style 'page_bundles/pipeline_editor'
- page_title s_('Pipelines|Pipeline Editor')
- content_for :prefetch_asset_tags do
diff --git a/app/views/projects/ci/secure_files/show.html.haml b/app/views/projects/ci/secure_files/show.html.haml
index db0734be6bd..1a87ccd753c 100644
--- a/app/views/projects/ci/secure_files/show.html.haml
+++ b/app/views/projects/ci/secure_files/show.html.haml
@@ -1,5 +1,3 @@
-- @content_class = "limit-container-width"
-
- page_title s_('Secure Files')
-#js-ci-secure-files{ data: { project_id: @project.id } }
+#js-ci-secure-files{ data: { project_id: @project.id, admin: can?(current_user, :admin_secure_files, @project).to_s, file_size_limit: Ci::SecureFile::FILE_SIZE_LIMIT.to_mb } }
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index 82d3bfbcfe6..c6fb3bcd559 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -14,7 +14,7 @@
%li.commits-row{ data: { day: day } }
%ul.content-list.commit-list.flex-list
- - if Feature.enabled?(:cached_commits, project, default_enabled: :yaml)
+ - if Feature.enabled?(:cached_commits, project)
= render partial: 'projects/commits/commit', collection: daily_commits, locals: { project: project, ref: ref, merge_request: merge_request }, cached: -> (commit) { commit_partial_cache_key(commit, ref: ref, merge_request: merge_request, request: request) }
- else
= render partial: 'projects/commits/commit', collection: daily_commits, locals: { project: project, ref: ref, merge_request: merge_request }
@@ -28,7 +28,7 @@
%li.commits-row
%ul.content-list.commit-list.flex-list
- - if Feature.enabled?(:cached_commits, project, default_enabled: :yaml)
+ - if Feature.enabled?(:cached_commits, project)
= render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }, cached: -> (commit) { commit_partial_cache_key(commit, ref: ref, merge_request: merge_request, request: request) }
- else
= render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }
@@ -36,8 +36,8 @@
- if hidden > 0
%li
= render Pajamas::AlertComponent.new(variant: :warning,
- dismissible: false) do
- .gl-alert-body
+ dismissible: false) do |c|
+ = c.body do
= n_('%s additional commit has been omitted to prevent performance issues.', '%s additional commits have been omitted to prevent performance issues.', hidden) % number_with_delimiter(hidden)
- if can_update_merge_request && context_commits&.empty?
diff --git a/app/views/projects/default_branch/_show.html.haml b/app/views/projects/default_branch/_show.html.haml
index f9d3af7aa36..2d3d36a9157 100644
--- a/app/views/projects/default_branch/_show.html.haml
+++ b/app/views/projects/default_branch/_show.html.haml
@@ -9,7 +9,7 @@
= _('Set the default branch for this project. All merge requests and commits are made against this branch unless you specify a different one.')
.settings-content
- = form_for @project, remote: true, html: { multipart: true, anchor: 'default-branch-settings' }, authenticity_token: true do |f|
+ = gitlab_ui_form_for @project, remote: true, html: { multipart: true, anchor: 'default-branch-settings' }, authenticity_token: true do |f|
%fieldset
- if @project.empty_repo?
.text-secondary
@@ -20,12 +20,10 @@
= f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide', data: { qa_selector: 'default_branch_dropdown' }})
.form-group
- .form-check
- = f.check_box :autoclose_referenced_issues, class: 'form-check-input'
- = f.label :autoclose_referenced_issues, class: 'form-check-label' do
- %strong= _("Auto-close referenced issues on default branch")
- .form-text.text-muted
- = _("When merge requests and commits in the default branch close, any issues they reference also close.")
- = 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_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'
+ = f.gitlab_ui_checkbox_component :autoclose_referenced_issues,
+ _("Auto-close referenced issues on default branch"),
+ help_text: (help_text + "&nbsp;" + help_icon).html_safe
= f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index 504bbf3a304..e92297a5a6a 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -46,3 +46,4 @@
.btn-group.table-action-buttons
= render 'projects/deployments/actions', deployment: deployment
= render 'projects/deployments/rollback', deployment: deployment
+ = render_if_exists 'projects/deployments/approvals', deployment: deployment
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 6f4ffecd5e0..d596199f816 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -34,8 +34,7 @@
- if load_diff_files_async
- url = url_for(safe_params.merge(action: 'diff_files'))
.js-diffs-batch{ data: { diff_files_path: url } }
- .text-center
- %span.gl-spinner.gl-spinner-md
+ = gl_loading_icon( size: "md", css_class: "gl-mt-4" )
- else
= render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, diff_page_context: diff_page_context }
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index 2cd215c5518..9b64afa8c60 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -4,7 +4,7 @@
%a.show-suppressed-diff.cursor-pointer.js-show-suppressed-diff= _("Changes suppressed. Click to show.")
%table.text-file.diff-wrap-lines.code.code-commit.js-syntax-highlight.commit-diff{ data: diff_view_data, class: too_big ? 'hide' : '' }
- - if Feature.enabled?(:inline_haml_diff_line_rendering, @project, default_enabled: :yaml)
+ - if Feature.enabled?(:inline_haml_diff_line_rendering, @project)
- diff_file.highlighted_diff_lines.each do |line|
- line_code = diff_file.line_code(line)
diff --git a/app/views/projects/feature_flags/edit.html.haml b/app/views/projects/feature_flags/edit.html.haml
index ac8c0575077..121dcd31a13 100644
--- a/app/views/projects/feature_flags/edit.html.haml
+++ b/app/views/projects/feature_flags/edit.html.haml
@@ -2,6 +2,6 @@
- add_to_breadcrumbs s_('FeatureFlags|Feature Flags'), project_feature_flags_path(@project)
- breadcrumb_title @feature_flag.name
-- page_title s_('FeatureFlags|Edit Feature Flag')
+- page_title s_('FeatureFlags|Edit Feature Flag'), @feature_flag.name
#js-edit-feature-flag{ data: edit_feature_flag_data }
diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml
index 9b64f158a1b..13fd4cee0cc 100644
--- a/app/views/projects/forks/error.html.haml
+++ b/app/views/projects/forks/error.html.haml
@@ -3,8 +3,8 @@
= render Pajamas::AlertComponent.new(title: _('Fork Error!'),
variant: :danger,
alert_class: 'gl-mt-5',
- dismissible: false) do
- .gl-alert-body
+ dismissible: false) do |c|
+ = c.body do
%p
= _("You tried to fork %{link_to_the_project} but it failed for the following reason:").html_safe % { link_to_the_project: link_to_project(@project) }
@@ -17,5 +17,5 @@
- else
= error
- .gl-alert-actions
- = link_to _('Try to fork again'), new_project_fork_path(@project), title: _("Fork"), class: "btn gl-alert-action btn-info btn-md gl-button"
+ = c.actions do
+ = link_to _('Try to fork again'), new_project_fork_path(@project), title: _("Fork"), class: "btn gl-alert-action btn-info btn-md gl-button"
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 9e3d9b4258a..a3569d41714 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
@@ -39,10 +39,9 @@
.label-container
- if generic_commit_status.tags.any?
- generic_commit_status.tags.each do |tag|
- %span.badge.badge-primary
- = tag
+ = gl_badge_tag tag, variant: :info, size: :sm
- if retried
- %span.badge.badge-warning retried
+ = gl_badge_tag retried, variant: :warning, size: :sm
- if pipeline_link
%td
diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml
index 40280e0787f..ca0307aed60 100644
--- a/app/views/projects/graphs/charts.html.haml
+++ b/app/views/projects/graphs/charts.html.haml
@@ -46,7 +46,7 @@
.col-md-6
.tree-ref-container
.tree-ref-holder
- = render 'shared/ref_switcher', destination: 'graphs_commits'
+ = render 'shared/ref_switcher', destination: 'graphs_commits', path: @path
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
diff --git a/app/views/projects/issues/_alert_moved_from_service_desk.html.haml b/app/views/projects/issues/_alert_moved_from_service_desk.html.haml
index f28b951ad62..291edf014c3 100644
--- a/app/views/projects/issues/_alert_moved_from_service_desk.html.haml
+++ b/app/views/projects/issues/_alert_moved_from_service_desk.html.haml
@@ -3,6 +3,6 @@
- service_desk_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: service_desk_link_url }
= render Pajamas::AlertComponent.new(variant: :warning,
- alert_class: 'hide js-alert-moved-from-service-desk-warning gl-mt-5') do
- .gl-alert-body.gl-mr-3
+ alert_class: 'hide js-alert-moved-from-service-desk-warning gl-mt-5') do |c|
+ = c.body do
= s_('This project does not have %{service_desk_link_start}Service Desk%{service_desk_link_end} enabled, so the user who created the issue will no longer receive email notifications about new activity.').html_safe % { service_desk_link_start: service_desk_link_start, service_desk_link_end: '</a>'.html_safe }
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index f1c19756474..a904b53515c 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,4 +1,4 @@
-- add_page_startup_api_call Feature.enabled?(:paginated_issue_discussions, @project, default_enabled: :yaml) ? discussions_path(@issue, per_page: 20) : discussions_path(@issue)
+- add_page_startup_api_call Feature.enabled?(:paginated_issue_discussions, @project) ? discussions_path(@issue, per_page: 20) : discussions_path(@issue)
- @gfm_form = true
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index d74b6c0639c..fe2be0f73c9 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -13,7 +13,7 @@
issues_path: project_issues_path(@project),
project_path: @project.full_path } }
-- if Feature.enabled?(:vue_issues_list, @project&.group, default_enabled: :yaml)
+- if Feature.enabled?(:vue_issues_list, @project&.group)
.js-issues-list{ data: project_issues_list_data(@project, current_user) }
- if @can_bulk_update
= render 'shared/issuable/bulk_update_sidebar', type: :issues
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 6c6f98e0b20..3572d1d6556 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -2,6 +2,7 @@
- add_to_breadcrumbs _("Issues"), project_issues_path(@project)
- breadcrumb_title @issue.to_reference
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues")
+- add_page_specific_style 'page_bundles/issues_show'
= render 'projects/issuable/show', issuable: @issue, api_awards_path: award_emoji_issue_api_path(@issue)
= render 'projects/invite_members_modal', project: @project
diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml
index 9a2a1e57165..e725e8e6889 100644
--- a/app/views/projects/jobs/index.html.haml
+++ b/app/views/projects/jobs/index.html.haml
@@ -4,7 +4,7 @@
- add_page_specific_style 'page_bundles/ci_status'
- admin = local_assigns.fetch(:admin, false)
-- if Feature.enabled?(:jobs_table_vue, @project, default_enabled: :yaml)
+- if Feature.enabled?(:jobs_table_vue, @project)
#js-jobs-table{ data: { admin: admin, full_path: @project.full_path, job_statuses: job_statuses.to_json, pipeline_editor_path: project_ci_pipeline_editor_path(@project), empty_state_svg_path: image_path('jobs-empty-state.svg') } }
- else
.top-area
diff --git a/app/views/projects/mattermosts/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml
index 3d901c6f59b..1f008496a34 100644
--- a/app/views/projects/mattermosts/_no_teams.html.haml
+++ b/app/views/projects/mattermosts/_no_teams.html.haml
@@ -1,9 +1,3 @@
-- if @teams_error_message
- = content_for :flash_message do
- = render Pajamas::AlertComponent.new(variant: :danger) do
- .gl-alert-body
- = @teams_error_message
-
%p
You aren’t a member of any team on the Mattermost instance at
%strong= Gitlab.config.mattermost.host
diff --git a/app/views/projects/mattermosts/new.html.haml b/app/views/projects/mattermosts/new.html.haml
index 9e293d07cb7..8254198bd41 100644
--- a/app/views/projects/mattermosts/new.html.haml
+++ b/app/views/projects/mattermosts/new.html.haml
@@ -1,10 +1,20 @@
-- @body_class = 'card-content'
+- add_to_breadcrumbs _('Integrations'), scoped_integrations_path(project: @project, group: @group)
+- add_to_breadcrumbs @integration.title, scoped_edit_integration_path(@integration, project: @project, group: @group)
+- breadcrumb_title _('New')
+- page_title @integration.title, _('Integrations')
+- @content_class = 'limit-container-width' unless fluid_layout
-.service-installation
- .inline.float-right
+- if @teams_error_message
+ = render Pajamas::AlertComponent.new(variant: :danger) do |c|
+ = c.body do
+ = @teams_error_message
+
+%h3
+ Install Mattermost Command
+ .gl-float-right
= custom_icon('mattermost_logo', size: 48)
- %h3 Install Mattermost Command
- - if @teams.empty?
- = render 'no_teams'
- - else
- = render 'team_selection'
+
+- if @teams.empty?
+ = render 'no_teams'
+- else
+ = render 'team_selection'
diff --git a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
index f6afac493d5..282faf7714e 100644
--- a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
+++ b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
@@ -1,36 +1,46 @@
- display_issuable_type = issuable_display_type(@merge_request)
-- button_class = "btn gl-button #{!@merge_request.closed? && 'js-draft-toggle-button'}"
-- toggle_class = "btn gl-button dropdown-toggle"
-.float-left.btn-group.gl-ml-3.gl-display-none.gl-md-display-flex
- = link_to @merge_request.closed? ? reopen_issuable_path(@merge_request) : toggle_draft_merge_request_path(@merge_request), method: :put, class: "#{button_class} btn-confirm-secondary" do
- - if @merge_request.closed?
- = _('Reopen')
- = display_issuable_type
- - else
- = @merge_request.work_in_progress? ? _('Mark as ready') : _('Mark as draft')
+.float-left.btn-group.gl-md-ml-3.gl-display-flex.dropdown.gl-new-dropdown.gl-md-w-auto.gl-w-full
+ = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret gl-display-none! gl-md-display-inline-flex!", data: { 'toggle' => 'dropdown' } do
+ %span.gl-sr-only= _('Toggle dropdown')
+ = sprite_icon "ellipsis_v", size: 16, css_class: "dropdown-icon gl-icon"
+ = 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-new-dropdown-button-text= _('Merge request actions')
+ = sprite_icon "chevron-down", size: 16, css_class: "dropdown-icon gl-icon"
+ .dropdown-menu.dropdown-menu-right
+ .gl-new-dropdown-inner
+ .gl-new-dropdown-contents
+ %ul
+ - if can?(current_user, :update_merge_request, @merge_request)
+ %li.gl-new-dropdown-item{ class: "gl-md-display-none!" }
+ = link_to edit_project_merge_request_path(@project, @merge_request), class: 'dropdown-item' do
+ .gl-new-dropdown-item-text-wrapper
+ = _('Edit')
+ - if @merge_request.open?
+ %li.gl-new-dropdown-item
+ = link_to toggle_draft_merge_request_path(@merge_request), method: :put, class: 'dropdown-item js-draft-toggle-button' do
+ .gl-new-dropdown-item-text-wrapper
+ = @merge_request.work_in_progress? ? _('Mark as ready') : _('Mark as draft')
+ %li.gl-new-dropdown-item.js-close-item
+ = link_to close_issuable_path(@merge_request), method: :put, class: 'dropdown-item' do
+ .gl-new-dropdown-item-text-wrapper
+ = _('Close')
+ = display_issuable_type
+ - elsif !@merge_request.source_project_missing?
+ %li.gl-new-dropdown-item
+ = link_to reopen_issuable_path(@merge_request), method: :put, class: 'dropdown-item' do
+ .gl-new-dropdown-item-text-wrapper
+ = _('Reopen')
+ = display_issuable_type
- - if !@merge_request.closed? || !issuable_author_is_current_user(@merge_request)
- = button_tag type: 'button', class: "#{toggle_class} btn-confirm-secondary btn-icon", data: { 'toggle' => 'dropdown' } do
- %span.gl-sr-only= _('Toggle dropdown')
- = sprite_icon "chevron-down", size: 12, css_class: "gl-button-icon"
-
- %ul.dropdown-menu.dropdown-menu-right
- - if @merge_request.open?
- %li
- = link_to close_issuable_path(@merge_request), method: :put do
- .description
- %strong.title
- = _('Close')
- = display_issuable_type
-
- - unless issuable_author_is_current_user(@merge_request)
- - unless @merge_request.closed?
- %li.divider.droplab-item-ignore
-
- %li
- %a{ href: new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)) }
- .description
- %strong.title= _('Report abuse')
- %p.text.gl-mb-0
- = _('Report %{display_issuable_type} that are abusive, inappropriate or spam.') % { display_issuable_type: display_issuable_type.pluralize }
+ - if current_user && moved_mr_sidebar_enabled?
+ %li.gl-new-dropdown-divider
+ %hr.dropdown-divider
+ %li.gl-new-dropdown-item.js-sidebar-subscriptions-entry-point
+ - unless issuable_author_is_current_user(@merge_request)
+ %li.gl-new-dropdown-item
+ = link_to new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)), class: 'dropdown-item' do
+ .gl-new-dropdown-item-text-wrapper
+ = _('Report abuse')
+ - if moved_mr_sidebar_enabled?
+ %li.gl-new-dropdown-item#js-lock-entry-point
diff --git a/app/views/projects/merge_requests/_code_dropdown.html.haml b/app/views/projects/merge_requests/_code_dropdown.html.haml
new file mode 100644
index 00000000000..0bd28e315d9
--- /dev/null
+++ b/app/views/projects/merge_requests/_code_dropdown.html.haml
@@ -0,0 +1,39 @@
+.float-left.gl-md-ml-3.dropdown.gl-new-dropdown{ class: "gl-display-none! gl-md-display-flex!" }
+ #js-check-out-modal{ data: how_merge_modal_data(@merge_request) }
+ = button_tag type: 'button', class: "btn dropdown-toggle btn-confirm gl-button gl-dropdown-toggle", data: { toggle: 'dropdown', qa_selector: 'mr_code_dropdown' } do
+ %span.gl-new-dropdown-button-text= _('Code')
+ = sprite_icon "chevron-down", size: 16, css_class: "dropdown-icon gl-icon gl-ml-2 gl-mr-0!"
+ .dropdown-menu.dropdown-menu-right
+ .gl-new-dropdown-inner
+ .gl-new-dropdown-contents
+ %ul
+ %li.gl-new-dropdown-section-header
+ %header.dropdown-header
+ = _('Review changes')
+ %li.gl-new-dropdown-item
+ %button.dropdown-item.js-check-out-modal-trigger{ type: 'button' }
+ .gl-new-dropdown-item-text-wrapper
+ = _('Check out branch')
+ - if current_user
+ %li.gl-new-dropdown-item
+ = link_to ide_merge_request_path(@merge_request), class: 'dropdown-item', data: { qa_selector: 'open_in_web_ide_button' } do
+ .gl-new-dropdown-item-text-wrapper
+ = _('Open in Web IDE')
+ - if Gitlab::CurrentSettings.gitpod_enabled && current_user&.gitpod_enabled
+ %li.gl-new-dropdown-item
+ = link_to "#{Gitlab::CurrentSettings.gitpod_url}##{merge_request_url(@merge_request)}", class: 'dropdown-item' do
+ .gl-new-dropdown-item-text-wrapper
+ = _('Open in Gitpod')
+ %li.gl-new-dropdown-divider
+ %hr.dropdown-divider
+ %li.gl-new-dropdown-section-header
+ %header.dropdown-header
+ = _('Download')
+ %li.gl-new-dropdown-item
+ = link_to merge_request_path(@merge_request, format: :patch), class: 'dropdown-item', download: '', data: { qa_selector: 'download_email_patches_menu_item' } do
+ .gl-new-dropdown-item-text-wrapper
+ = _('Email patches')
+ %li.gl-new-dropdown-item
+ = link_to merge_request_path(@merge_request, format: :diff), class: 'dropdown-item', data: { qa_selector: 'download_plain_diff_menu_item' } do
+ .gl-new-dropdown-item-text-wrapper
+ = _('Plain diff')
diff --git a/app/views/projects/merge_requests/_mr_box.html.haml b/app/views/projects/merge_requests/_mr_box.html.haml
index 916b841e350..e16631b4943 100644
--- a/app/views/projects/merge_requests/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/_mr_box.html.haml
@@ -1,3 +1,7 @@
.detail-page-description.py-2
- %h2.title.mb-0{ data: { qa_selector: 'title_content' } }
- = markdown_field(@merge_request, :title)
+ - if Feature.enabled?(:updated_mr_header, @project)
+ = render 'shared/issuable/status_box', issuable: @merge_request
+ = merge_request_header(@project, @merge_request)
+ - else
+ %h2.title.mb-0{ data: { qa_selector: 'title_content' } }
+ = markdown_field(@merge_request, :title)
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 49b7320d630..638c520e210 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -2,51 +2,40 @@
- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
- can_reopen_merge_request = can?(current_user, :reopen_merge_request, @merge_request)
- are_close_and_open_buttons_hidden = merge_request_button_hidden?(@merge_request, true) && merge_request_button_hidden?(@merge_request, false)
-- cache_key = [@project, @merge_request, can_update_merge_request, can_reopen_merge_request, are_close_and_open_buttons_hidden, current_user&.preferred_language]
+- updated_mr_header_enabled = Feature.enabled?(:updated_mr_header, @project)
+- cache_key = [@project, @merge_request, can_update_merge_request, can_reopen_merge_request, are_close_and_open_buttons_hidden, current_user&.preferred_language, updated_mr_header_enabled]
= cache(cache_key, expires_in: 1.day) do
- if @merge_request.closed_or_merged_without_fork?
= render Pajamas::AlertComponent.new(alert_class: 'gl-mb-5',
variant: :danger,
- dismissible: false) do
- .gl-alert-body
+ dismissible: false) do |c|
+ = c.body do
= _('The source project of this merge request has been removed.')
- .detail-page-header.border-bottom-0.pt-0.pb-0
+ .detail-page-header.border-bottom-0.pt-0.pb-0{ class: "#{'gl-display-block gl-md-display-flex!' if updated_mr_header_enabled}" }
.detail-page-header-body
- = render "shared/issuable/status_box", issuable: @merge_request
+ - unless updated_mr_header_enabled
+ = render "shared/issuable/status_box", issuable: @merge_request
+ .issuable-meta{ class: "#{'gl-display-flex' if updated_mr_header_enabled}" }
+ - if updated_mr_header_enabled
+ #js-issuable-header-warnings
+ %h2.title.gl-my-0.gl-display-inline-block{ data: { qa_selector: 'title_content' } }
+ = markdown_field(@merge_request, :title)
+ - else
+ #js-issuable-header-warnings
+ = issuable_meta(@merge_request, @project)
- .issuable-meta
- #js-issuable-header-warnings
- = issuable_meta(@merge_request, @project)
+ %div
+ %button.gl-button.btn.btn-default.btn-icon.float-right.gl-display-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ type: 'button', class: "#{'gl-md-display-none!' if moved_mr_sidebar_enabled? } #{'gl-sm-display-none!' unless moved_mr_sidebar_enabled?}" }
+ = sprite_icon('chevron-double-lg-left')
- %a.gl-button.btn.btn-default.btn-icon.float-right.d-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
- = sprite_icon('chevron-double-lg-left')
+ .detail-page-header-actions.js-issuable-actions{ class: "#{'gl-align-self-start is-merge-request' if updated_mr_header_enabled}" }
+ - if can_update_merge_request
+ = link_to _('Edit'), edit_project_merge_request_path(@project, @merge_request), class: "gl-display-none gl-md-display-block btn gl-button btn-default btn-grouped js-issuable-edit", data: { qa_selector: "edit_button" }
- .detail-page-header-actions.js-issuable-actions
- .clearfix.dropdown
- %button.gl-button.btn.btn-default.float-left.gl-md-display-none.gl-w-full{ type: "button", data: { toggle: "dropdown" } }
- Options
- = sprite_icon('chevron-down', css_class: 'gl-text-gray-500')
- .dropdown-menu.dropdown-menu-right
- %ul
- - if can_update_merge_request
- %li= link_to _('Edit'), edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
- - if @merge_request.opened?
- %li
- = link_to @merge_request.work_in_progress? ? _('Mark as ready') : _('Mark as draft'), toggle_draft_merge_request_path(@merge_request), method: :put, class: "js-draft-toggle-button"
- %li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] }
- = link_to _('Close'), merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
- - if can_reopen_merge_request
- %li{ class: merge_request_button_visibility(@merge_request, false) }
- = link_to _('Reopen'), merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, title: 'Reopen merge request'
- - unless @merge_request.merged? || current_user == @merge_request.author
- %li= link_to _('Report abuse'), new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request))
+ - if @merge_request.source_project
+ = render 'projects/merge_requests/code_dropdown'
- - if can_update_merge_request
- = link_to _('Edit'), edit_project_merge_request_path(@project, @merge_request), class: "gl-display-none gl-md-display-block btn gl-button btn-default btn-grouped js-issuable-edit", data: { qa_selector: "edit_button" }
-
- - if can_update_merge_request && !are_close_and_open_buttons_hidden
- = render 'projects/merge_requests/close_reopen_draft_report_toggle'
- - elsif !@merge_request.merged?
- = link_to _('Report abuse'), new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)), class: 'gl-display-none gl-md-display-block gl-button btn btn-default gl-float-right gl-ml-3', title: _('Report abuse')
+ - if current_user
+ = render 'projects/merge_requests/close_reopen_draft_report_toggle'
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index e2ac8ef5abc..811b45ef8af 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -22,7 +22,7 @@
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
- = dropdown_toggle f.object.source_branch.presence || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch }, { toggle_class: "js-compare-dropdown js-source-branch monospace" }
+ = dropdown_toggle f.object.source_branch.presence || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch, qa_selector: "source_branch_dropdown" }, { toggle_class: "js-compare-dropdown js-source-branch monospace" }
.dropdown-menu.dropdown-menu-selectable.js-source-branch-dropdown.git-revision-dropdown
= dropdown_title(_("Select source branch"))
= dropdown_filter(_("Search branches"))
@@ -62,4 +62,4 @@
- if @merge_request.errors.any?
= form_errors(@merge_request)
- = f.submit 'Compare branches and continue', class: "gl-button btn btn-confirm mr-compare-btn"
+ = f.submit 'Compare branches and continue', class: "gl-button btn btn-confirm mr-compare-btn", data: { qa_selector: "compare_branches_button" }
diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml
index ce5a042fbf8..4596fcd280d 100644
--- a/app/views/projects/merge_requests/invalid.html.haml
+++ b/app/views/projects/merge_requests/invalid.html.haml
@@ -10,8 +10,8 @@
= render "projects/merge_requests/mr_box"
= render Pajamas::AlertComponent.new(variant: :danger,
- dismissible: false) do
- .gl-alert-body
+ dismissible: false) do |c|
+ = c.body do
- if @merge_request.for_fork? && !@merge_request.source_project
= err_fork_project_removed
- elsif !@merge_request.source_branch_exists?
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 008f2588dbd..13e5451df98 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -1,5 +1,6 @@
- @gfm_form = true
-- @content_class = "merge-request-container#{' limit-container-width' unless fluid_layout}"
+- unless moved_mr_sidebar_enabled?
+ - @content_class = "merge-request-container#{' limit-container-width' unless fluid_layout}"
- add_to_breadcrumbs _("Merge requests"), project_merge_requests_path(@project)
- breadcrumb_title @merge_request.to_reference
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", _("Merge requests")
@@ -41,14 +42,14 @@
= _("Changes")
= gl_badge_tag @diffs_count, { size: :sm }
.d-flex.flex-wrap.align-items-center.justify-content-lg-end
- #js-vue-discussion-counter
+ #js-vue-discussion-counter{ data: { blocks_merge: @project.only_allow_merge_if_all_discussions_are_resolved?.to_s } }
.tab-content#diff-notes-app
#js-diff-file-finder
#js-code-navigation
= render "projects/merge_requests/tabs/pane", id: "notes", class: "notes voting_notes" do
- .row
- %section.col-md-12
+ %div{ class: "#{'merge-request-overview' if moved_mr_sidebar_enabled?}" }
+ %section
.issuable-discussion.js-vue-notes-event
- if @merge_request.description.present?
.detail-page-description
@@ -70,6 +71,8 @@
help_page_path: suggest_changes_help_path,
current_user_data: @current_user_data,
is_locked: @merge_request.discussion_locked.to_s } }
+ - if moved_mr_sidebar_enabled?
+ = render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, reviewers: @merge_request.reviewers, source_branch: @merge_request.source_branch
= render "projects/merge_requests/tabs/pane", name: "commits", id: "commits", class: "commits" do
-# This tab is always loaded via AJAX
@@ -83,7 +86,8 @@
.loading.hide
= gl_loading_icon(size: 'lg')
-= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, reviewers: @merge_request.reviewers, source_branch: @merge_request.source_branch
+- unless moved_mr_sidebar_enabled?
+ = render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, reviewers: @merge_request.reviewers, source_branch: @merge_request.source_branch
- if @merge_request.can_be_reverted?(current_user)
= render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit
@@ -92,7 +96,7 @@
#js-review-bar
-- if Feature.enabled?(:mr_attention_requests, default_enabled: :yaml)
+- if current_user&.mr_attention_requests_enabled?
#js-need-attention-sidebar-onboarding
= render 'projects/invite_members_modal', project: @project
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index 154a92e6ec8..326a7c4027f 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -9,7 +9,7 @@
= render 'shared/milestones/search_form'
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @project)
- = link_to new_project_milestone_path(@project), class: 'gl-button btn btn-confirm', data: { qa_selector: "new_project_milestone_link" }, title: _('New milestone') do
+ = link_to new_project_milestone_path(@project), class: 'gl-button btn btn-confirm gl-ml-3', data: { qa_selector: "new_project_milestone_link" }, title: _('New milestone') do
= _('New milestone')
- if @milestones.blank?
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 13aa8f56d20..4ec72176202 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -14,8 +14,8 @@
- if can?(current_user, :read_issue, @project) && @milestone.total_issues_count == 0
= render Pajamas::AlertComponent.new(dismissible: false,
alert_data: { testid: 'no-issues-alert' },
- alert_class: 'gl-mt-3 gl-mb-5') do
- .gl-alert-body
+ alert_class: 'gl-mt-3 gl-mb-5') do |c|
+ = c.body do
= _('Assign some issues to this milestone.')
- else
= render 'shared/milestones/milestone_complete_alert', milestone: @milestone do
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index bc8400a63f9..d689b54678e 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -37,8 +37,8 @@
.panel-footer
= f.submit _('Mirror repository'), class: 'gl-button btn btn-confirm js-mirror-submit qa-mirror-repository-button', name: :update_remote_mirror
- else
- = render Pajamas::AlertComponent.new(dismissible: false) do
- .gl-alert-body
+ = render Pajamas::AlertComponent.new(dismissible: false) do |c|
+ = c.body do
= _('Mirror settings are only available to GitLab administrators.')
.panel.panel-default
diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml
index 67a2eeb7e7b..c5efacb21af 100644
--- a/app/views/projects/pages/_access.html.haml
+++ b/app/views/projects/pages/_access.html.haml
@@ -15,7 +15,7 @@
= external_link(domain.url, domain.url)
- unless @project.public_pages?
.card-footer.gl-alert-warning
- - help_page = help_page_path('/user/project/pages/pages_access_control')
+ - help_page = help_page_path('user/project/pages/pages_access_control')
- link_start = '<a href="%{url}" target="_blank" class="gl-alert-link" rel="noopener noreferrer">'.html_safe % { url: help_page }
- link_end = '</a>'.html_safe
= html_escape_once(s_('GitLabPages|Access Control is enabled for this Pages website; only authorized users will be able to access it. To make your website publicly available, navigate to your project\'s %{strong_start}Settings &gt; General &gt; Visibility%{strong_end} and select %{strong_start}Everyone%{strong_end} in pages section. Read the %{link_start}documentation%{link_end} for more information.')).html_safe % { link_start: link_start, link_end: link_end, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml
index 99efb0b98c6..993026d2884 100644
--- a/app/views/projects/pages/_destroy.haml
+++ b/app/views/projects/pages/_destroy.haml
@@ -8,7 +8,7 @@
%p.gl-mb-0
= s_('GitLabPages|Removing pages will prevent them from being exposed to the outside world.')
.card-footer
- = link_to s_('GitLabPages|Remove pages'), project_pages_path(@project), data: { confirm: s_('GitLabPages|Are you sure?')}, method: :delete, class: "btn gl-button btn-danger"
+ = link_to s_('GitLabPages|Remove pages'), project_pages_path(@project), data: { confirm: s_('GitLabPages|Are you sure?'), 'confirm-btn-variant': 'danger'}, method: :delete, class: "btn gl-button btn-danger", "aria-label": s_('GitLabPages|Remove pages')
- else
.nothing-here-block
= s_('GitLabPages|Only project maintainers can remove pages')
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
index 4e9c77564da..04178804de4 100644
--- a/app/views/projects/pages/_list.html.haml
+++ b/app/views/projects/pages/_list.html.haml
@@ -20,7 +20,7 @@
= gl_badge_tag s_('GitLabPages|Expired'), variant: :danger
%div
= link_to s_('GitLabPages|Edit'), project_pages_domain_path(@project, domain), class: "btn gl-button btn-sm btn-grouped btn-confirm btn-inverted"
- = link_to s_('GitLabPages|Remove'), project_pages_domain_path(@project, domain), data: { confirm: s_('GitLabPages|Are you sure?')}, method: :delete, class: "btn gl-button btn-danger btn-sm btn-grouped"
+ = link_to s_('GitLabPages|Remove'), project_pages_domain_path(@project, domain), data: { confirm: s_('GitLabPages|Are you sure?'), 'confirm-btn-variant': 'danger'}, "aria-label": s_("GitLabPages|Remove domain"), method: :delete, class: "btn gl-button btn-danger btn-sm btn-grouped"
- if domain.needs_verification?
%li.list-group-item.bs-callout-warning
- details_link_start = "<a href='#{project_pages_domain_path(@project, domain)}'>".html_safe
diff --git a/app/views/projects/pages_domains/_certificate.html.haml b/app/views/projects/pages_domains/_certificate.html.haml
index 861305dc93b..4ba3e084dc4 100644
--- a/app/views/projects/pages_domains/_certificate.html.haml
+++ b/app/views/projects/pages_domains/_certificate.html.haml
@@ -38,7 +38,8 @@
= domain_presenter.pages_domain.subject || _('missing')
= link_to _('Remove'),
clean_certificate_project_pages_domain_path(@project, domain_presenter),
- data: { confirm: _('Are you sure?') },
+ data: { confirm: _('Are you sure?'), 'confirm-btn-variant': 'danger' },
+ 'aria-label': s_("GitLabPages|Remove certificate"),
class: 'gl-button btn btn-danger btn-sm',
method: :delete
- else
diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml
index d3e2854ff19..9d9603b0947 100644
--- a/app/views/projects/pages_domains/_form.html.haml
+++ b/app/views/projects/pages_domains/_form.html.haml
@@ -1,7 +1,8 @@
- if domain_presenter.errors.any?
- = render Pajamas::AlertComponent.new(variant: :danger, dismissible: false) do
- - domain_presenter.errors.full_messages.each do |msg|
- = msg
+ = render Pajamas::AlertComponent.new(variant: :danger, dismissible: false) do |c|
+ = c.body do
+ - domain_presenter.errors.full_messages.each do |msg|
+ = msg
.form-group.border-section
.row
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 88e6b98b115..6b26c9f3f00 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -32,40 +32,45 @@
#js-pipeline-jobs-vue{ data: { full_path: @project.full_path, pipeline_iid: @pipeline.iid } }
- if @pipeline.failed_builds.present?
- #js-tab-failures.build-failures.tab-pane.build-page
- %table.table.responsive-table.ci-table.responsive-table-sm-rounded
- %thead
- %th.table-th-transparent
- %th.table-th-transparent= _('Name')
- %th.table-th-transparent= _('Stage')
- %th.table-th-transparent= _('Failure')
+ #js-tab-failures.tab-pane
+ - if Feature.enabled?(:failed_jobs_tab_vue, @project)
+ #js-pipeline-failed-jobs-vue{ data: { full_path: @project.full_path, pipeline_iid: @pipeline.iid, failed_jobs_summary_data: prepare_failed_jobs_summary_data(@pipeline.failed_builds) } }
+ - else
+ .build-failures.build-page
+ %table.table.gl-table.responsive-table.ci-table.responsive-table-sm-rounded
+ %thead
+ %th
+ %th= _('Name')
+ %th= _('Stage')
+ %th= _('Failure')
+ %th
- %tbody
- - @pipeline.failed_builds.each_with_index do |build, index|
- - job = build.present(current_user: current_user)
- %tr.build-state.responsive-table-border-start
- %td.responsive-table-cell.ci-status-icon-failed{ data: { column: _('Status')} }
- .d-none.d-md-block.build-icon
- = sprite_icon("status_#{build.status}")
- .d-md-none.build-badge
- = render "ci/status/badge", link: false, status: job.detailed_status(current_user)
- %td.responsive-table-cell.build-name{ data: { column: _('Name')} }
- = link_to build.name, pipeline_job_url(pipeline, build)
- %td.responsive-table-cell.build-stage{ data: { column: _('Stage')} }
- = build.stage.titleize
- %td.responsive-table-cell.build-failure{ data: { column: _('Failure')} }
- = build.present.callout_failure_message
- %td.responsive-table-cell.build-actions
- - if can?(current_user, :update_build, job) && job.retryable?
- = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'gl-button btn btn-default btn-icon' do
- = sprite_icon('repeat', css_class: 'gl-icon')
- - if can?(current_user, :read_build, job)
- %tr.build-log-row.responsive-table-border-end
- %td
- %td.responsive-table-cell.build-log-container{ colspan: 4 }
- %pre.build-log.build-log-rounded
- %code.bash.js-build-output
- = build_summary(build)
+ %tbody
+ - @pipeline.failed_builds.each_with_index do |build, index|
+ - job = build.present(current_user: current_user)
+ %tr.build-state.responsive-table-border-start
+ %td.responsive-table-cell.ci-status-icon-failed{ data: { column: _('Status')} }
+ .d-none.d-md-block.build-icon
+ = sprite_icon("status_#{build.status}")
+ .d-md-none.build-badge
+ = render "ci/status/badge", link: false, status: job.detailed_status(current_user)
+ %td.responsive-table-cell.build-name{ data: { column: _('Name')} }
+ = link_to build.name, pipeline_job_url(pipeline, build)
+ %td.responsive-table-cell.build-stage{ data: { column: _('Stage')} }
+ = build.stage.titleize
+ %td.responsive-table-cell.build-failure{ data: { column: _('Failure')} }
+ = build.present.callout_failure_message
+ %td.responsive-table-cell.build-actions
+ - if can?(current_user, :update_build, job) && job.retryable?
+ = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'gl-button btn btn-default btn-icon' do
+ = sprite_icon('repeat', css_class: 'gl-icon')
+ - if can?(current_user, :read_build, job)
+ %tr.build-log-row.responsive-table-border-end
+ %td
+ %td.responsive-table-cell.build-log-container{ colspan: 4 }
+ %pre.build-log.build-log-rounded
+ %code.bash.js-build-output
+ = build_summary(build)
#js-tab-dag.tab-pane
#js-pipeline-dag-vue{ data: { pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, empty_svg_path: image_path('illustrations/empty-state/empty-dag-md.svg'), about_dag_doc_path: help_page_path('ci/directed_acyclic_graph/index.md'), dag_doc_path: help_page_path('ci/yaml/index.md', anchor: 'needs')} }
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 2b0a0fc1253..30b224a60da 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -27,7 +27,7 @@
= s_('You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}').html_safe % { gitlab_ci_yml: '.gitlab-ci.yml', lint_link_start: lint_link_start, lint_link_end: '</a>'.html_safe }
#js-pipeline-notification{ data: { deprecated_keywords_doc_path: help_page_path('ci/yaml/index.md', anchor: 'deprecated-keywords'), full_path: @project.full_path, pipeline_iid: @pipeline.iid } }
- - if Feature.enabled?(:pipeline_tabs_vue, @project, default_enabled: :yaml)
+ - if Feature.enabled?(:pipeline_tabs_vue, @project)
#js-pipeline-tabs{ data: js_pipeline_tabs_data(@project, @pipeline) }
- else
= render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors
diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml
index 8134ee8f417..449b6c25f50 100644
--- a/app/views/projects/runners/_group_runners.html.haml
+++ b/app/views/projects/runners/_group_runners.html.haml
@@ -28,10 +28,7 @@
= _('This group does not have any group runners yet.')
- if can?(current_user, :admin_group_runners, @project.group)
- - if Feature.enabled?(:runner_list_group_view_vue_ui, @group, default_enabled: :yaml)
- - register_runners_path = group_runners_path(@project.group)
- - else
- - register_runners_path = group_settings_ci_cd_path(@project.group)
+ - register_runners_path = group_runners_path(@project.group)
- group_link = link_to _("group's CI/CD settings."), register_runners_path
= _('Group owners can register group runners in the %{link}').html_safe % { link: group_link }
- else
diff --git a/app/views/projects/serverless/functions/index.html.haml b/app/views/projects/serverless/functions/index.html.haml
deleted file mode 100644
index 03a1b0ee7bb..00000000000
--- a/app/views/projects/serverless/functions/index.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-- @content_class = "limit-container-width" unless fluid_layout
-- breadcrumb_title _('Serverless')
-- page_title _('Serverless')
-- status_path = project_serverless_functions_path(@project, format: :json)
-- clusters_path = project_clusters_path(@project)
-
-.serverless-functions-page.js-serverless-functions-page{ data: { status_path: status_path,
- installed: @installed,
- clusters_path: clusters_path,
- help_path: help_page_path('user/project/clusters/serverless/index'),
- empty_image_path: image_path('illustrations/empty-state/empty-serverless-lg.svg') } }
-
-.js-serverless-functions-notice
- .flash-container
-
-.top-area.adjust.d-flex.justify-content-center.gl-border-none
- .serverless-functions-table#js-serverless-functions
diff --git a/app/views/projects/serverless/functions/show.html.haml b/app/views/projects/serverless/functions/show.html.haml
deleted file mode 100644
index dd81d957e51..00000000000
--- a/app/views/projects/serverless/functions/show.html.haml
+++ /dev/null
@@ -1,19 +0,0 @@
-- @content_class = "limit-container-width" unless fluid_layout
-- clusters_path = project_clusters_path(@project)
-- help_path = help_page_path('user/project/clusters/serverless/index')
-
-- add_to_breadcrumbs('Serverless', project_serverless_functions_path(@project))
-
-- page_title @service[:name]
-
-.serverless-function-details-page.js-serverless-function-details-page{ data: { service: @service.as_json,
- prometheus: @prometheus,
- clusters_path: clusters_path,
- help_path: help_path } }
-
-.serverless-function-details#js-serverless-function-details
-
-.js-serverless-function-notice
- .flash-container
-
-.function-holder.js-function-holder.input-group
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index d1d9a220068..9d74f99bb19 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -1,5 +1,5 @@
-- if lookup_context.template_exists?('top', "projects/services/#{integration.to_param}", true)
- = render "projects/services/#{integration.to_param}/top", integration: integration
+- if lookup_context.template_exists?('top', "shared/integrations/#{integration.to_param}", true)
+ = render "shared/integrations/#{integration.to_param}/top", integration: integration
- if integration.activate_disabled_reason.present? && integration.activate_disabled_reason[:trackers].any?
-# When using integration.activate_disabled_reason[:trackers], it's potentially insecure to use the raw records
@@ -7,8 +7,8 @@
-# For example, we can get the link to each tracker with scoped_edit_integration_path(tracker, tracker.project)
= render Pajamas::AlertComponent.new(title: s_('ExternalIssueIntegration|Another issue tracker is already in use'),
variant: :warning,
- dismissible: false) do
- .gl-alert-body
+ dismissible: false) do |c|
+ = c.body do
= s_('ExternalIssueIntegration|Only one issue tracker integration can be active at a time. Please disable the active tracker first and try again.')
%h2.gl-mb-4
@@ -17,6 +17,6 @@
= sprite_icon('check', css_class: 'gl-text-green-500')
= render 'shared/integration_settings', integration: integration
-- if lookup_context.template_exists?('show', "projects/services/#{integration.to_param}", true)
+- if lookup_context.template_exists?('show', "shared/integrations/#{integration.to_param}", true)
%hr
- = render "projects/services/#{integration.to_param}/show", integration: integration
+ = render "shared/integrations/#{integration.to_param}/show", integration: integration
diff --git a/app/views/projects/services/prometheus/_external_alerts.html.haml b/app/views/projects/services/prometheus/_external_alerts.html.haml
deleted file mode 100644
index 168b4853a9a..00000000000
--- a/app/views/projects/services/prometheus/_external_alerts.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-- return unless can?(current_user, :read_prometheus_alerts, @project)
-- return unless integration.manual_configuration?
-
-- notify_url = notify_project_prometheus_alerts_url(@project, format: :json)
-- authorization_key = @project.alerting_setting.try(:token)
-- learn_more_url = help_page_path('operations/metrics/alerts.md', anchor: 'external-prometheus-instances')
-
-#js-settings-prometheus-alerts{ data: { notify_url: notify_url, authorization_key: authorization_key, change_key_url: reset_alerting_token_project_settings_operations_path(@project), learn_more_url: learn_more_url, disabled: true } }
diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml
deleted file mode 100644
index 3350ac8a6c5..00000000000
--- a/app/views/projects/services/prometheus/_show.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-.row
- .col-lg-3
- %h4.gl-mt-0
- = s_('PrometheusService|Metrics')
-
-.row.gl-mb-3.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring
- = render 'projects/services/prometheus/metrics', project: @project, integration: integration
-
-= render 'projects/services/prometheus/external_alerts', project: @project, integration: integration
diff --git a/app/views/projects/services/prometheus/_top.html.haml b/app/views/projects/services/prometheus/_top.html.haml
deleted file mode 100644
index 52b29ea2e8f..00000000000
--- a/app/views/projects/services/prometheus/_top.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- return unless integration.manual_configuration?
-
-.row
- .col-lg-12
- = render Pajamas::AlertComponent.new(dismissible: false) do
- .gl-alert-body
- = s_('AlertSettings|You can now set up alert endpoints for manually configured Prometheus instances in the Alerts section on the Operations settings page. Alert endpoint fields on this page have been deprecated.')
- .gl-alert-actions
- = link_to _('Visit settings page'), project_settings_operations_path(@project, anchor: 'js-alert-management-settings'), class: 'gl-button btn gl-alert-action btn-info'
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 5ef56cda6d2..508e63f77d8 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -1,6 +1,7 @@
- help_link_public_pipelines = link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'change-which-users-can-view-your-pipelines'), target: '_blank', rel: 'noopener noreferrer'
- help_link_auto_canceling = link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'auto-cancel-redundant-pipelines'), target: '_blank', rel: 'noopener noreferrer'
-- help_link_skip_outdated =link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'skip-outdated-deployment-jobs'), target: '_blank', rel: 'noopener noreferrer'
+- help_link_skip_outdated = link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'skip-outdated-deployment-jobs'), target: '_blank', rel: 'noopener noreferrer'
+- help_link_separated_caches = link_to sprite_icon('question-o'), help_page_path('ci/caching/index', anchor: 'cache-key-names'), target: '_blank', rel: 'noopener noreferrer'
.row.gl-mt-3
.col-lg-12
@@ -25,6 +26,11 @@
help_text: (_('When a deployment job is successful, skip older deployment jobs that are still pending.') + ' ' + help_link_skip_outdated).html_safe
.form-group
+ = f.gitlab_ui_checkbox_component :ci_separated_caches,
+ s_("CICD|Use separate caches for protected branches"),
+ help_text: (s_('CICD|Unprotected branches will not have access to the cache from protected branches.') + ' ' + help_link_separated_caches).html_safe
+
+ .form-group
= f.label :ci_config_path, _('CI/CD configuration file'), class: 'label-bold'
= f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml'
%p.form-text.text-muted
@@ -77,19 +83,7 @@
= _("The maximum file size in megabytes for individual job artifacts.")
= link_to sprite_icon('question-o'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size'), target: '_blank', rel: 'noopener noreferrer'
- .form-group
- = f.label :build_coverage_regex, _("Test coverage parsing"), class: 'label-bold'
- .input-group
- %span.input-group-prepend
- .input-group-text /
- = f.text_field :build_coverage_regex, class: 'form-control gl-form-input', placeholder: 'Regular expression', data: { qa_selector: 'build_coverage_regex_field' }
- %span.input-group-append
- .input-group-text /
- %p.form-text.text-muted
- = html_escape(_('The regular expression used to find test coverage output in the job log. For example, use %{regex} for Simplecov (Ruby). Leave blank to disable.')) % { regex: '<code>\(\d+.\d+%\)</code>'.html_safe }
- = link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'add-test-coverage-results-using-project-settings-deprecated'), target: '_blank', rel: 'noopener noreferrer'
-
- = f.submit _('Save changes'), class: "btn gl-button btn-confirm", data: { qa_selector: 'save_general_pipelines_changes_button' }
+ = f.submit _('Save changes'), class: "btn gl-button btn-confirm"
%hr
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 28cde994d00..87ca13a7bd6 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -7,14 +7,14 @@
- expanded = expanded_by_default?
- general_expanded = @project.errors.empty? ? expanded : true
-%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded), data: { qa_selector: 'general_pipelines_settings_content' } }
+%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("General pipelines")
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
- = _("Customize your pipeline configuration and coverage report.")
+ = _("Customize your pipeline configuration.")
.settings-content
= render 'form'
@@ -109,3 +109,15 @@
= link_to _('Learn more'), help_page_path('ci/jobs/ci_job_token'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'ci/token_access/index'
+
+- if show_secure_files_setting(@project, current_user)
+ %section.settings
+ .settings-header
+ %h4.settings-title
+ = _("Secure Files")
+ = button_to project_ci_secure_files_path(@project), method: :get, class: 'btn gl-button btn-default' do
+ = _('Manage')
+ %p
+ = _("Use Secure Files to store files used by your pipelines such as Android keystores, or Apple provisioning profiles and signing certificates.")
+ = link_to _('Learn more'), help_page_path('ci/secure_files/index'), target: '_blank', rel: 'noopener noreferrer'
+
diff --git a/app/views/projects/settings/operations/_prometheus.html.haml b/app/views/projects/settings/operations/_prometheus.html.haml
deleted file mode 100644
index 93281cc225b..00000000000
--- a/app/views/projects/settings/operations/_prometheus.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-%section.settings.no-animate.js-prometheus-settings
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
- = _('Prometheus')
- %button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = _('Expand')
- %p
- = _('Link Prometheus monitoring to GitLab.')
- = link_to _('More information'), help_page_path('user/project/integrations/prometheus'), target: '_blank', rel: 'noopener noreferrer'
- .settings-content
- - if @project
- = render 'shared/prometheus_configuration_banner', project: @project, integration: service, header_tag: :b, info_well_classes: 'gl-p-3 gl-mt-3'
-
- %b.gl-mb-3
- = s_('PrometheusService|Manual configuration')
- %p
- = s_('PrometheusService|Auto configuration settings are used unless you override their values here.')
diff --git a/app/views/projects/settings/operations/show.html.haml b/app/views/projects/settings/operations/show.html.haml
index 9a31666c316..80c22604e49 100644
--- a/app/views/projects/settings/operations/show.html.haml
+++ b/app/views/projects/settings/operations/show.html.haml
@@ -15,13 +15,14 @@
= s_('Deprecations|Feature deprecation and removal')
.gl-alert-body
%p
- = html_escape(s_('Deprecations|The metrics feature was deprecated in GitLab 14.7. The logs and tracing features were also deprecated in GitLab 14.7, and are %{removal_link_start} scheduled for removal %{link_end} in GitLab 15.0. For information on a possible replacement, %{opstrace_link_start} learn more about Opstrace %{link_end}.')) % {removal_link_start: removal_epic_link_start, opstrace_link_start: opstrace_link_start, link_end: link_end }
+ = html_escape(s_('Deprecations|The metrics feature was deprecated in GitLab 14.7.'))
+ = html_escape(s_('Deprecations|The logs and tracing features were also deprecated in GitLab 14.7, and are %{removal_link_start} scheduled for removal %{link_end} in GitLab 15.0.')) % {removal_link_start: removal_epic_link_start, link_end: link_end } if Feature.enabled?(:monitor_tracing, @project)
+ = html_escape(s_('Deprecations|For information on a possible replacement, %{opstrace_link_start} learn more about Opstrace %{link_end}.')) % {opstrace_link_start: opstrace_link_start, link_end: link_end }
= render 'projects/settings/operations/metrics_dashboard'
-= render 'projects/settings/operations/tracing'
+= render 'projects/settings/operations/tracing' if Feature.enabled?(:monitor_tracing, @project)
= render 'projects/settings/operations/error_tracking'
= render 'projects/settings/operations/alert_management'
= render 'projects/settings/operations/incidents'
= render 'projects/settings/operations/grafana_integration'
= render_if_exists 'projects/settings/operations/status_page'
-= render 'projects/settings/operations/prometheus', service: prometheus_integration if Feature.enabled?(:settings_operations_prometheus_service)
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 202c0f22420..29bdca1c876 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -1,6 +1,8 @@
+- is_project_overview = local_assigns.fetch(:is_project_overview, false)
+
.tree-ref-container.gl-display-flex.mb-2.mb-md-0
.tree-ref-holder
- = render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
+ = render 'shared/ref_switcher', destination: 'tree', show_create: true
#js-repo-breadcrumb{ data: breadcrumb_data_attributes }
@@ -8,7 +10,7 @@
.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'
- #js-tree-history-link{ data: { history_link: project_commits_path(@project, @ref) } }
+ #js-tree-history-link{ data: { history_link: project_commits_path(@project, @ref), is_project_overview: is_project_overview.to_s } }
= render 'projects/find_file_link'
= render 'shared/web_ide_button', blob: nil
diff --git a/app/views/projects/work_items/index.html.haml b/app/views/projects/work_items/index.html.haml
index 0efd7a740d3..356f93c6ed5 100644
--- a/app/views/projects/work_items/index.html.haml
+++ b/app/views/projects/work_items/index.html.haml
@@ -1,3 +1,3 @@
- page_title s_('WorkItem|Work Items')
-#js-work-items{ data: { full_path: @project.full_path } }
+#js-work-items{ data: { full_path: @project.full_path, issues_list_path: project_issues_path(@project) } }
diff --git a/app/views/pwa/offline.html.haml b/app/views/pwa/offline.html.haml
new file mode 100644
index 00000000000..5eae546bea9
--- /dev/null
+++ b/app/views/pwa/offline.html.haml
@@ -0,0 +1,31 @@
+= link_to root_path do
+ = render 'shared/logo.svg'
+%h1= _('Offline')
+.container
+ %h3= _('You are currently offline, or the GitLab instance is not reachable.')
+ %p= _("In the background, we're attempting to connect you again.")
+ -# haml-lint:disable InlineJavaScript
+ :javascript
+ window.addEventListener('online', () => {
+ window.location.reload();
+ });
+
+ async function checkNetworkAndReload() {
+ try {
+ const response = await fetch('.');
+ // Verify we get a valid response from the server
+ if (response.status >= 200 && response.status < 500) {
+ window.location.reload();
+ return;
+ }
+ } catch {
+ // Unable to connect to the server, ignore.
+ }
+ window.setTimeout(checkNetworkAndReload, 2500);
+ }
+
+ if (window.location.pathname.endsWith('/-/offline')) {
+ return;
+ }
+
+ checkNetworkAndReload();
diff --git a/app/views/registrations/welcome/show.html.haml b/app/views/registrations/welcome/show.html.haml
index 6235cce5d80..62499f1a6b6 100644
--- a/app/views/registrations/welcome/show.html.haml
+++ b/app/views/registrations/welcome/show.html.haml
@@ -17,7 +17,7 @@
%p.gl-text-center= html_escape(_('%{gitlab_experience_text}. We won\'t share this information with anyone.')) % { gitlab_experience_text: gitlab_experience_text }
- else
%p.gl-text-center= html_escape(_('%{gitlab_experience_text}. Don\'t worry, this information isn\'t shared outside of your self-managed GitLab instance.')) % { gitlab_experience_text: gitlab_experience_text }
- = gitlab_ui_form_for(current_user, url: users_sign_up_welcome_path, html: { class: 'card gl-w-full! gl-p-5', 'aria-live' => 'assertive' }) do |f|
+ = gitlab_ui_form_for(current_user, url: users_sign_up_welcome_path, html: { class: 'card gl-w-full! gl-p-5 js-users-signup-welcome', 'aria-live' => 'assertive' }) do |f|
.devise-errors
= render 'devise/shared/error_messages', resource: current_user
.row
@@ -27,7 +27,7 @@
- if Feature.enabled?(:user_other_role_details)
.row
.form-group.col-sm-12.js-other-role-group.hidden
- = f.label :other_role, _('What is your job title? (optional)'), class: 'form-check-label gl-mb-3'
+ = f.label :other_role, _('What is your job title? (optional)')
= f.text_field :other_role, class: 'form-control'
= render_if_exists "registrations/welcome/jobs_to_be_done", f: f
= render_if_exists "registrations/welcome/setup_for_company", f: f
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 fdd4dfba616..d69f54608e9 100644
--- a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
+++ b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
@@ -1,12 +1,12 @@
- if show_auto_devops_implicitly_enabled_banner?(project, current_user)
= render Pajamas::AlertComponent.new(alert_class: 'qa-auto-devops-banner auto-devops-implicitly-enabled-banner',
close_button_class: 'hide-auto-devops-implicitly-enabled-banner',
- close_button_data: { project_id: project.id }) do
- .gl-alert-body
+ close_button_data: { project_id: project.id }) do |c|
+ = c.body do
= s_("AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found.")
- unless Gitlab.config.registry.enabled
%div
= _('Container registry is not enabled on this GitLab instance. Ask an administrator to enable it in order for Auto DevOps to work.')
- .gl-alert-actions
+ = c.actions do
= link_to _('Settings'), project_settings_ci_cd_path(project), class: 'alert-link btn gl-button btn-confirm'
= link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank', class: 'alert-link btn gl-button btn-default gl-ml-3'
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 850d58920db..7248403d6c9 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -2,6 +2,19 @@
- import_url = Gitlab::UrlSanitizer.new(f.object.import_url)
.import-url-data
+ .info-well.prepend-top-20
+ .well-segment
+ %ul
+ %li
+ = html_escape(_('The repository must be accessible over %{code_open}http://%{code_close}, %{code_open}https://%{code_close} or %{code_open}git://%{code_close}.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+ %li= html_escape(_('When using the %{code_open}http://%{code_close} or %{code_open}https://%{code_close} protocols, please provide the exact URL to the repository. HTTP redirects will not be followed.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+ %li
+ = _('If your HTTP repository is not publicly accessible, add your credentials.')
+ %li
+ = import_will_timeout_message(ci_cd_only)
+ %li
+ = import_svn_message(ci_cd_only)
+ = render_if_exists 'shared/ci_cd_only_link', ci_cd_only: ci_cd_only
.form-group
= f.label :import_url, class: 'label-bold' do
%span
@@ -11,9 +24,10 @@
= render Pajamas::AlertComponent.new(variant: :danger,
alert_class: 'gl-mt-3 js-import-url-error hide',
dismissible: false,
- close_button_class: 'js-close-2fa-enabled-success-alert') do
- .gl-alert-body
+ close_button_class: 'js-close-2fa-enabled-success-alert') do |c|
+ = c.body do
= s_('Import|There is not a valid Git repository at this URL. If your HTTP repository is not publicly accessible, verify your credentials.')
+ = render_if_exists 'shared/ee/import_form', f: f, ci_cd_only: ci_cd_only
.row
.form-group.col-md-6
= f.label :import_url_user, class: 'label-bold' do
@@ -26,19 +40,3 @@
%span
= _('Password (optional)')
= f.password_field :import_url_password, class: 'form-control gl-form-input', required: false, autocomplete: 'new-password'
-
- .info-well.prepend-top-20
- .well-segment
- %ul
- %li
- = html_escape(_('The repository must be accessible over %{code_open}http://%{code_close}, %{code_open}https://%{code_close} or %{code_open}git://%{code_close}.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- %li= html_escape(_('When using the %{code_open}http://%{code_close} or %{code_open}https://%{code_close} protocols, please provide the exact URL to the repository. HTTP redirects will not be followed.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- %li
- = _('If your HTTP repository is not publicly accessible, add your credentials.')
- %li
- = import_will_timeout_message(ci_cd_only)
- %li
- = import_svn_message(ci_cd_only)
- = render_if_exists 'shared/ci_cd_only_link', ci_cd_only: ci_cd_only
-
-= render_if_exists 'shared/ee/import_form', f: f, ci_cd_only: ci_cd_only
diff --git a/app/views/shared/_integration_settings.html.haml b/app/views/shared/_integration_settings.html.haml
index 93606ca0aba..84710b2ecc7 100644
--- a/app/views/shared/_integration_settings.html.haml
+++ b/app/views/shared/_integration_settings.html.haml
@@ -6,8 +6,8 @@
.js-vue-integration-settings{ data: integration_form_data(integration, group: @group, project: @project) }
.js-integration-help-html.gl-display-none
-# All content below will be repositioned in Vue
- - if lookup_context.template_exists?('help', "projects/services/#{integration.to_param}", true)
- = render "projects/services/#{integration.to_param}/help", integration: integration
+ - if lookup_context.template_exists?('help', "shared/integrations/#{integration.to_param}", true)
+ = render "shared/integrations/#{integration.to_param}/help", integration: integration
- elsif integration.help.present?
.info-well
.well-segment
diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg
index 0ef9de5fed6..83f6fe5c16c 100644
--- a/app/views/shared/_logo.svg
+++ b/app/views/shared/_logo.svg
@@ -1,9 +1,10 @@
-<svg width="24" height="24" class="tanuki-logo" viewBox="0 0 36 36">
- <path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/>
- <path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/>
- <path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/>
- <path class="tanuki-shape tanuki-left-eye" fill="#fc6d26" d="M18,34.38 11.38,14 2,14 6,25Z"/>
- <path class="tanuki-shape tanuki-right-eye" fill="#fc6d26" d="M18,34.38 24.62,14 34,14 30,25Z"/>
- <path class="tanuki-shape tanuki-left-cheek" fill="#fca326" d="M2 14L.1 20.16c-.18.565 0 1.2.5 1.56l17.42 12.66z"/>
- <path class="tanuki-shape tanuki-right-cheek" fill="#fca326" d="M34 14l1.9 6.16c.18.565 0 1.2-.5 1.56L18 34.38z"/>
+<svg class="tanuki-logo" width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path class="tanuki-shape tanuki" d="m24.507 9.5-.034-.09L21.082.562a.896.896 0 0 0-1.694.091l-2.29 7.01H7.825L5.535.653a.898.898 0 0 0-1.694-.09L.451 9.411.416 9.5a6.297 6.297 0 0 0 2.09 7.278l.012.01.03.022 5.16 3.867 2.56 1.935 1.554 1.176a1.051 1.051 0 0 0 1.268 0l1.555-1.176 2.56-1.935 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5Z"
+ fill="#E24329"/>
+ <path class="tanuki-shape right-cheek" d="m24.507 9.5-.034-.09a11.44 11.44 0 0 0-4.56 2.051l-7.447 5.632 4.742 3.584 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5Z"
+ fill="#FC6D26"/>
+ <path class="tanuki-shape chin" d="m7.707 20.677 2.56 1.935 1.555 1.176a1.051 1.051 0 0 0 1.268 0l1.555-1.176 2.56-1.935-4.743-3.584-4.755 3.584Z"
+ fill="#FCA326"/>
+ <path class="tanuki-shape left-cheek" d="M5.01 11.461a11.43 11.43 0 0 0-4.56-2.05L.416 9.5a6.297 6.297 0 0 0 2.09 7.278l.012.01.03.022 5.16 3.867 4.745-3.584-7.444-5.632Z"
+ fill="#FC6D26"/>
</svg>
diff --git a/app/views/shared/_logo_type.svg b/app/views/shared/_logo_type.svg
deleted file mode 100644
index cb07e2634a9..00000000000
--- a/app/views/shared/_logo_type.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 617 169"><path d="M315.26 2.97h-21.8l.1 162.5h88.3v-20.1h-66.5l-.1-142.4M465.89 136.95c-5.5 5.7-14.6 11.4-27 11.4-16.6 0-23.3-8.2-23.3-18.9 0-16.1 11.2-23.8 35-23.8 4.5 0 11.7.5 15.4 1.2v30.1h-.1m-22.6-98.5c-17.6 0-33.8 6.2-46.4 16.7l7.7 13.4c8.9-5.2 19.8-10.4 35.5-10.4 17.9 0 25.8 9.2 25.8 24.6v7.9c-3.5-.7-10.7-1.2-15.1-1.2-38.2 0-57.6 13.4-57.6 41.4 0 25.1 15.4 37.7 38.7 37.7 15.7 0 30.8-7.2 36-18.9l4 15.9h15.4v-83.2c-.1-26.3-11.5-43.9-44-43.9M557.63 149.1c-8.2 0-15.4-1-20.8-3.5V70.5c7.4-6.2 16.6-10.7 28.3-10.7 21.1 0 29.2 14.9 29.2 39 0 34.2-13.1 50.3-36.7 50.3m9.2-110.6c-19.5 0-30 13.3-30 13.3v-21l-.1-27.8h-21.3l.1 158.5c10.7 4.5 25.3 6.9 41.2 6.9 40.7 0 60.3-26 60.3-70.9-.1-35.5-18.2-59-50.2-59M77.9 20.6c19.3 0 31.8 6.4 39.9 12.9l9.4-16.3C114.5 6 97.3 0 78.9 0 32.5 0 0 28.3 0 85.4c0 59.8 35.1 83.1 75.2 83.1 20.1 0 37.2-4.7 48.4-9.4l-.5-63.9V75.1H63.6v20.1h38l.5 48.5c-5 2.5-13.6 4.5-25.3 4.5-32.2 0-53.8-20.3-53.8-63-.1-43.5 22.2-64.6 54.9-64.6M231.43 2.95h-21.3l.1 27.3v94.3c0 26.3 11.4 43.9 43.9 43.9 4.5 0 8.9-.4 13.1-1.2v-19.1c-3.1.5-6.4.7-9.9.7-17.9 0-25.8-9.2-25.8-24.6v-65h35.7v-17.8h-35.7l-.1-38.5M155.96 165.47h21.3v-124h-21.3v124M155.96 24.37h21.3V3.07h-21.3v21.3"/></svg>
diff --git a/app/views/shared/_logo_ukraine.svg b/app/views/shared/_logo_ukraine.svg
deleted file mode 100644
index e2c2bb3855d..00000000000
--- a/app/views/shared/_logo_ukraine.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-<svg width="24" height="24" class="tanuki-logo" viewBox="0 0 24 24">
- <path d="M4.89929534,0.3165 L7.56629534,8.5025 L16.3922953,8.5025 L19.0592953,0.3165 C19.1962953,-0.1055 19.8432953,-0.1055 19.9792953,0.3165 L23.9122953,12.6095 C23.9722953,12.7935 23.9722953,12.9895 23.9192953,13.1695 L0.0392953418,13.1695 C-0.0143874393,12.9863283 -0.0119492421,12.7912726 0.0462953418,12.6095 L3.97929534,0.3165 C4.11529534,-0.1055 4.76229534,-0.1055 4.89929534,0.3165 Z" id="Path" fill="#005BBB"></path>
- <path d="M7.20329534,9.0025 L16.7552953,9.0025 L16.8682953,8.6575 L19.5182953,0.5185 L23.4362953,12.7615 C23.4961172,12.9376949 23.435535,13.1323657 23.2862953,13.2435 L23.2852953,13.2455 L11.9852953,21.4655 L11.9792953,21.4715 L0.673295342,13.2455 C0.522422013,13.1321007 0.462258936,12.9374792 0.522295342,12.7615 L4.43929534,0.5185 L7.09029534,8.6585 L7.20329534,9.0025 Z" id="Shape" stroke="#FFFFFF" opacity="0.32" stroke-linejoin="round"></path>
- <path d="M0.0012953418,12.8575 C-0.0152229638,13.1685309 0.127095079,13.4667211 0.379295342,13.6495 L11.9792953,22.0895 L11.9862953,22.0845 L11.9922953,22.0895 L11.9872953,22.0835 L23.5792953,13.6495 C23.8319507,13.466647 23.9743476,13.1679148 23.9572953,12.8565 L0.0012953418,12.8565 L0.0012953418,12.8575 Z" id="Path" fill="#FFD500"></path>
-</svg> \ No newline at end of file
diff --git a/app/views/shared/_logo_with_black_text.svg b/app/views/shared/_logo_with_black_text.svg
new file mode 100644
index 00000000000..f5b0b70618b
--- /dev/null
+++ b/app/views/shared/_logo_with_black_text.svg
@@ -0,0 +1,12 @@
+<svg class="tanuki-logo" width="111" height="24" viewBox="0 0 111 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path class="logo-text" d="M44.814 9.042h3.645c-.608-3.875-3.963-6.574-8.33-6.574-5.166 0-9.043 3.798-9.043 10.16 0 6.248 3.703 10.123 9.15 10.123 4.887 0 8.386-3.144 8.386-8.234v-2.37h-8.01v2.794h4.55c-.058 2.816-1.938 4.599-4.908 4.599-3.305 0-5.57-2.477-5.57-6.95 0-4.445 2.303-6.913 5.494-6.913 2.38 0 4.01 1.272 4.636 3.365Zm6.218 13.438h3.49V7.68h-3.49v14.8Zm1.76-17.151c1.109 0 2.014-.85 2.014-1.89s-.905-1.9-2.014-1.9c-1.109 0-2.024.849-2.024 1.9s.9 1.89 2.017 1.89h.007ZM64.971 7.68H62.05V4.126h-3.49v3.556h-2.1v2.699h2.1v8.233c-.018 2.786 2.007 4.16 4.628 4.079a7.089 7.089 0 0 0 2.055-.348l-.59-2.73a4.247 4.247 0 0 1-1.02.137c-.878 0-1.582-.309-1.582-1.717v-7.662h2.921V7.68Zm2.701 14.8h12.272v-2.998H71.25V2.737h-3.578V22.48Zm18.957.3c2.323 0 3.71-1.09 4.347-2.333h.115v2.033h3.36v-9.91c0-3.913-3.19-5.09-6.016-5.09-3.113 0-5.504 1.388-6.275 4.087l3.26.464c.345-1.013 1.329-1.88 3.04-1.88 1.62 0 2.506.829 2.506 2.285v.057c0 1.002-1.05 1.051-3.664 1.33-2.872.309-5.619 1.166-5.619 4.502-.01 2.912 2.12 4.455 4.946 4.455Zm1.147-2.56c-1.456 0-2.498-.666-2.498-1.948 0-1.34 1.167-1.899 2.72-2.121.917-.125 2.75-.357 3.2-.722v1.744c.01 1.643-1.321 3.042-3.422 3.042v.005Zm9.244 2.26h3.433v-2.332h.201c.551 1.08 1.698 2.593 4.244 2.593 3.489 0 6.102-2.768 6.102-7.644 0-4.936-2.69-7.616-6.112-7.616-2.613 0-3.702 1.57-4.234 2.641h-.147V2.737h-3.486V22.48Zm3.423-7.403c0-2.88 1.234-4.734 3.48-4.734 2.323 0 3.52 1.976 3.52 4.734 0 2.759-1.214 4.8-3.52 4.8-2.227 0-3.48-1.928-3.48-4.8Z"
+ fill="#171321"/>
+ <path class="tanuki-shape tanuki" d="m24.507 9.5-.034-.09L21.082.562a.896.896 0 0 0-1.694.091l-2.29 7.01H7.825L5.535.653a.898.898 0 0 0-1.694-.09L.451 9.411.416 9.5a6.297 6.297 0 0 0 2.09 7.278l.012.01.03.022 5.16 3.867 2.56 1.935 1.554 1.176a1.051 1.051 0 0 0 1.268 0l1.555-1.176 2.56-1.935 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5Z"
+ fill="#E24329"/>
+ <path class="tanuki-shape right-cheek" d="m24.507 9.5-.034-.09a11.44 11.44 0 0 0-4.56 2.051l-7.447 5.632 4.742 3.584 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5Z"
+ fill="#FC6D26"/>
+ <path class="tanuki-shape chin" d="m7.707 20.677 2.56 1.935 1.555 1.176a1.051 1.051 0 0 0 1.268 0l1.555-1.176 2.56-1.935-4.743-3.584-4.755 3.584Z"
+ fill="#FCA326"/>
+ <path class="tanuki-shape left-cheek" d="M5.01 11.461a11.43 11.43 0 0 0-4.56-2.05L.416 9.5a6.297 6.297 0 0 0 2.09 7.278l.012.01.03.022 5.16 3.867 4.745-3.584-7.444-5.632Z"
+ fill="#FC6D26"/>
+</svg>
diff --git a/app/views/shared/_logo_with_white_text.svg b/app/views/shared/_logo_with_white_text.svg
new file mode 100644
index 00000000000..d0067538058
--- /dev/null
+++ b/app/views/shared/_logo_with_white_text.svg
@@ -0,0 +1,12 @@
+<svg class="tanuki-logo" width="111" height="24" viewBox="0 0 111 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path class="logo-text" d="M44.814 9.042h3.645c-.608-3.875-3.963-6.574-8.33-6.574-5.166 0-9.043 3.798-9.043 10.16 0 6.248 3.703 10.123 9.15 10.123 4.887 0 8.386-3.144 8.386-8.234v-2.37h-8.01v2.794h4.55c-.058 2.816-1.938 4.599-4.908 4.599-3.305 0-5.57-2.477-5.57-6.95 0-4.445 2.303-6.913 5.494-6.913 2.38 0 4.01 1.272 4.636 3.365Zm6.218 13.438h3.49V7.68h-3.49v14.8Zm1.76-17.151c1.109 0 2.014-.85 2.014-1.89s-.905-1.9-2.014-1.9c-1.109 0-2.024.849-2.024 1.9s.9 1.89 2.017 1.89h.007ZM64.971 7.68H62.05V4.126h-3.49v3.556h-2.1v2.699h2.1v8.233c-.018 2.786 2.007 4.16 4.628 4.079a7.089 7.089 0 0 0 2.055-.348l-.59-2.73a4.247 4.247 0 0 1-1.02.137c-.878 0-1.582-.309-1.582-1.717v-7.662h2.921V7.68Zm2.701 14.8h12.272v-2.998H71.25V2.737h-3.578V22.48Zm18.957.3c2.323 0 3.71-1.09 4.347-2.333h.115v2.033h3.36v-9.91c0-3.913-3.19-5.09-6.016-5.09-3.113 0-5.504 1.388-6.275 4.087l3.26.464c.345-1.013 1.329-1.88 3.04-1.88 1.62 0 2.506.829 2.506 2.285v.057c0 1.002-1.05 1.051-3.664 1.33-2.872.309-5.619 1.166-5.619 4.502-.01 2.912 2.12 4.455 4.946 4.455Zm1.147-2.56c-1.456 0-2.498-.666-2.498-1.948 0-1.34 1.167-1.899 2.72-2.121.917-.125 2.75-.357 3.2-.722v1.744c.01 1.643-1.321 3.042-3.422 3.042v.005Zm9.244 2.26h3.433v-2.332h.201c.551 1.08 1.698 2.593 4.244 2.593 3.489 0 6.102-2.768 6.102-7.644 0-4.936-2.69-7.616-6.112-7.616-2.613 0-3.702 1.57-4.234 2.641h-.147V2.737h-3.486V22.48Zm3.423-7.403c0-2.88 1.234-4.734 3.48-4.734 2.323 0 3.52 1.976 3.52 4.734 0 2.759-1.214 4.8-3.52 4.8-2.227 0-3.48-1.928-3.48-4.8Z"
+ fill="#fff"/>
+ <path class="tanuki-shape tanuki" d="m24.507 9.5-.034-.09L21.082.562a.896.896 0 0 0-1.694.091l-2.29 7.01H7.825L5.535.653a.898.898 0 0 0-1.694-.09L.451 9.411.416 9.5a6.297 6.297 0 0 0 2.09 7.278l.012.01.03.022 5.16 3.867 2.56 1.935 1.554 1.176a1.051 1.051 0 0 0 1.268 0l1.555-1.176 2.56-1.935 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5Z"
+ fill="#E24329"/>
+ <path class="tanuki-shape right-cheek" d="m24.507 9.5-.034-.09a11.44 11.44 0 0 0-4.56 2.051l-7.447 5.632 4.742 3.584 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5Z"
+ fill="#FC6D26"/>
+ <path class="tanuki-shape chin" d="m7.707 20.677 2.56 1.935 1.555 1.176a1.051 1.051 0 0 0 1.268 0l1.555-1.176 2.56-1.935-4.743-3.584-4.755 3.584Z"
+ fill="#FCA326"/>
+ <path class="tanuki-shape left-cheek" d="M5.01 11.461a11.43 11.43 0 0 0-4.56-2.05L.416 9.5a6.297 6.297 0 0 0 2.09 7.278l.012.01.03.022 5.16 3.867 4.745-3.584-7.444-5.632Z"
+ fill="#FC6D26"/>
+</svg>
diff --git a/app/views/shared/_milestones_sort_dropdown.html.haml b/app/views/shared/_milestones_sort_dropdown.html.haml
index 1c6eb7aa96b..b68022bfeda 100644
--- a/app/views/shared/_milestones_sort_dropdown.html.haml
+++ b/app/views/shared/_milestones_sort_dropdown.html.haml
@@ -1,4 +1,4 @@
- milestones_sort_options = milestones_sort_options_hash.map { |value, text| { value: value, text: text, href: page_filter_path(sort: value) } }
%div{ data: {testid: 'milestone_sort_by_dropdown'} }
- = gl_redirect_listbox_tag milestones_sort_options, @sort, class: 'gl-ml-3'
+ = gl_redirect_listbox_tag milestones_sort_options, @sort
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index 74a397d7a03..821f1ede422 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -1,7 +1,6 @@
- if any_projects?(@projects)
- .project-item-select-holder.btn-group.gl-ml-auto.gl-mr-auto.gl-relative.gl-overflow-hidden{ class: 'gl-display-flex!' }
- %a.btn.gl-button.btn-confirm.js-new-project-item-link.block-truncated.qa-new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] }, class: "gl-m-0!" }
+ .dropdown.b-dropdown.gl-new-dropdown.btn-group.project-item-select-holder{ class: 'gl-display-inline-flex!' }
+ %a.btn.gl-button.btn-confirm.split-content-button.js-new-project-item-link.block-truncated.qa-new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } }
= gl_loading_icon(inline: true, color: 'light')
= project_select_tag :project_path, class: "project-item-select gl-absolute! gl-visibility-hidden", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path], with_shared: local_assigns[:with_shared], include_projects_in_subgroups: local_assigns[:include_projects_in_subgroups] }, with_feature_enabled: local_assigns[:with_feature_enabled]
- %button.btn.dropdown-toggle.btn-confirm.btn-md.gl-button.gl-dropdown-toggle.dropdown-toggle-split.new-project-item-select-button.qa-new-project-item-select-button.gl-p-0.gl-w-100{ class: "gl-m-0!", 'aria-label': _('Toggle project select') }
- = sprite_icon('chevron-down')
+ %button.btn.dropdown-toggle.btn-confirm.btn-md.gl-button.gl-dropdown-toggle.dropdown-toggle-split.new-project-item-select-button.qa-new-project-item-select-button{ 'aria-label': _('Toggle project select') }
diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml
index 195bd15f840..91cd91ec38b 100644
--- a/app/views/shared/_no_password.html.haml
+++ b/app/views/shared/_no_password.html.haml
@@ -1,9 +1,9 @@
- if show_no_password_message?
= render Pajamas::AlertComponent.new(variant: :warning,
alert_class: 'js-no-password-message',
- close_button_class: 'js-hide-no-password-message') do
- .gl-alert-body
+ close_button_class: 'js-hide-no-password-message') do |c|
+ = c.body do
= no_password_message
- .gl-alert-actions
+ = c.actions do
= link_to _('Remind later'), '#', class: 'js-hide-no-password-message gl-alert-action btn btn-confirm btn-md gl-button'
= link_to _("Don't show again"), profile_path(user: { hide_no_password: true }), method: :put, role: 'button', class: 'gl-alert-action btn btn-default btn-md gl-button'
diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml
index d30679b4305..c4d8cb092dc 100644
--- a/app/views/shared/_no_ssh.html.haml
+++ b/app/views/shared/_no_ssh.html.haml
@@ -1,9 +1,9 @@
- if show_no_ssh_key_message?
= render Pajamas::AlertComponent.new(variant: :warning,
alert_class: 'js-no-ssh-message',
- close_button_class: 'js-hide-no-ssh-message') do
- .gl-alert-body
+ close_button_class: 'js-hide-no-ssh-message') do |c|
+ = c.body do
= s_("MissingSSHKeyWarningLink|You can't push or pull repositories using SSH until you add an SSH key to your profile.")
- .gl-alert-actions
+ = c.actions do
= link_to s_('MissingSSHKeyWarningLink|Add SSH key'), profile_keys_path, class: "gl-alert-action btn btn-confirm btn-md gl-button"
= link_to s_("MissingSSHKeyWarningLink|Don't show again"), profile_path(user: { hide_no_ssh_key: true }), method: :put, role: 'button', class: 'gl-alert-action btn btn-default btn-md gl-button'
diff --git a/app/views/shared/_outdated_browser.html.haml b/app/views/shared/_outdated_browser.html.haml
index 76fb34985c0..0af378cb883 100644
--- a/app/views/shared/_outdated_browser.html.haml
+++ b/app/views/shared/_outdated_browser.html.haml
@@ -1,6 +1,6 @@
- if outdated_browser?
- = render Pajamas::AlertComponent.new(variant: :danger, dismissible: false) do
- .gl-alert-body
+ = render Pajamas::AlertComponent.new(variant: :danger, dismissible: false) do |c|
+ = c.body do
= s_('OutdatedBrowser|GitLab may not work properly, because you are using an outdated web browser.')
%br
- browser_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('install/requirements', anchor: 'supported-web-browsers') }
diff --git a/app/views/shared/_project_limit.html.haml b/app/views/shared/_project_limit.html.haml
index 7e1874f3416..b630c829c76 100644
--- a/app/views/shared/_project_limit.html.haml
+++ b/app/views/shared/_project_limit.html.haml
@@ -1,9 +1,9 @@
- if cookies[:hide_project_limit_message].blank? && !current_user.hide_project_limit && !current_user.can_create_project? && current_user.projects_limit > 0
= render Pajamas::AlertComponent.new(variant: :warning,
dismissible: false,
- alert_class: 'project-limit-message') do
- .gl-alert-body
+ alert_class: 'project-limit-message') do |c|
+ = c.body do
= _("You won't be able to create new projects because you have reached your project limit.")
- .gl-alert-actions
+ = c.actions do
= link_to _('Remind later'), '#', class: 'alert-link hide-project-limit-message btn gl-button btn-confirm'
= link_to _("Don't show again"), profile_path(user: {hide_project_limit: true}), method: :put, class: 'alert-link btn gl-button btn-default 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 e5b1ad88a7f..053c511830c 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('development/service_ping/index.md', anchor: 'registration-features-program')
+- registration_features_docs_path = help_page_path('user/admin_area/settings/usage_statistics.md', 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/_service_ping_consent.html.haml b/app/views/shared/_service_ping_consent.html.haml
index 96f015c7a4b..8de7552c39a 100644
--- a/app/views/shared/_service_ping_consent.html.haml
+++ b/app/views/shared/_service_ping_consent.html.haml
@@ -1,10 +1,10 @@
- if session[:ask_for_usage_stats_consent]
- = render Pajamas::AlertComponent.new(alert_class: 'service-ping-consent-message') do
- .gl-alert-body
+ = render Pajamas::AlertComponent.new(alert_class: 'service-ping-consent-message') do |c|
+ = c.body do
- docs_link = link_to _('collect usage information'), help_page_path('user/admin_area/settings/usage_statistics.md'), class: 'gl-link'
- settings_link = link_to _('your settings'), metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), class: 'gl-link'
= s_('To help improve GitLab, we would like to periodically %{docs_link}. This can be changed at any time in %{settings_link}.').html_safe % { docs_link: docs_link, settings_link: settings_link }
- .gl-alert-actions.gl-mt-3
+ = c.actions do
- send_service_data_path = admin_application_settings_path(application_setting: { version_check_enabled: 1, usage_ping_enabled: 1 })
- not_now_path = admin_application_settings_path(application_setting: { version_check_enabled: 0, usage_ping_enabled: 0 })
= link_to _("Send service data"), send_service_data_path, 'data-url' => admin_application_settings_path, method: :put, 'data-check-enabled': true, 'data-service-ping-enabled': true, class: 'js-service-ping-consent-action alert-link btn gl-button btn-info'
diff --git a/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml b/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml
index 2294c44d49f..0899756d088 100644
--- a/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml
+++ b/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml
@@ -3,10 +3,10 @@
alert_data: { feature_id: Users::CalloutsHelper::TWO_FACTOR_AUTH_RECOVERY_SETTINGS_CHECK,
dismiss_endpoint: callouts_path,
defer_links: 'true' },
- close_button_data: { testid: 'close-account-recovery-regular-check-callout' }) do
- .gl-alert-body
+ close_button_data: { testid: 'close-account-recovery-regular-check-callout' }) do |c|
+ = c.body do
= s_('Profiles|Ensure you have two-factor authentication recovery codes stored in a safe place.')
= link_to _('Learn more.'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'recovery-codes'), target: '_blank', rel: 'noopener noreferrer'
- .gl-alert-actions
+ = c.actions do
= link_to profile_two_factor_auth_path, class: 'deferred-link btn gl-alert-action btn-confirm btn-md gl-button' do
= s_('Profiles|Manage two-factor authentication')
diff --git a/app/views/shared/access_tokens/_table.html.haml b/app/views/shared/access_tokens/_table.html.haml
index 7f7dafbe5b0..5ca9cf8d9a4 100644
--- a/app/views/shared/access_tokens/_table.html.haml
+++ b/app/views/shared/access_tokens/_table.html.haml
@@ -1,23 +1,16 @@
- no_active_tokens_message = local_assigns.fetch(:no_active_tokens_message, _('This user has no active %{type}.') % { type: type_plural })
- impersonation = local_assigns.fetch(:impersonation, false)
- resource = local_assigns.fetch(:resource, false)
-- personal = !impersonation && !resource
%hr
%h5
= _('Active %{type} (%{token_length})') % { type: type_plural, token_length: active_tokens.length }
-- if personal && !personal_access_token_expiration_enforced?
- %p.profile-settings-content
- = _("Personal access tokens are not revoked upon expiration.")
- if impersonation
%p.profile-settings-content
= _("To see all the user's personal access tokens you must impersonate them first.")
-- if personal
- = render_if_exists 'profiles/personal_access_tokens/token_expiry_notification', active_tokens: active_tokens
-
- if active_tokens.present?
.table-responsive
%table.table.active-tokens
@@ -46,12 +39,8 @@
%span.token-never-used-label= _('Never')
%td
- if token.expires?
- - if token.expired? || token.expired_but_not_enforced?
- %span{ class: 'text-danger has-tooltip', title: _('Token valid until revoked') }
- = _('Expired')
- - else
- %span{ class: ('text-warning' if token.expires_soon?) }
- = time_ago_with_tooltip(token.expires_at)
+ %span{ class: ('text-warning' if token.expires_soon?) }
+ = time_ago_with_tooltip(token.expires_at)
- else
%span.token-never-expires-label= _('Never')
- if resource
diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml
index 7289121d9eb..2e04bbf3605 100644
--- a/app/views/shared/deploy_tokens/_form.html.haml
+++ b/app/views/shared/deploy_tokens/_form.html.haml
@@ -3,7 +3,7 @@
- group_deploy_tokens_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_deploy_tokens_help_link_url }
= s_('DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}').html_safe % { link_start: group_deploy_tokens_help_link_start, link_end: '</a>'.html_safe }
-= form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post, remote: Feature.enabled?(:ajax_new_deploy_token, group_or_project) do |f|
+= gitlab_ui_form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post, remote: Feature.enabled?(:ajax_new_deploy_token, group_or_project) do |f|
.form-group
= f.label :name, class: 'label-bold'
@@ -23,33 +23,15 @@
.form-group
= f.label :scopes, _('Scopes (select at least one)'), class: 'label-bold'
- %fieldset.form-group.form-check
- = f.check_box :read_repository, class: 'form-check-input', data: { qa_selector: 'deploy_token_read_repository_checkbox' }
- = f.label :read_repository, 'read_repository', class: 'label-bold form-check-label'
- .text-secondary
- = s_('DeployTokens|Allows read-only access to the repository.')
+ = 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' } }
- if container_registry_enabled?(group_or_project)
- %fieldset.form-group.form-check
- = f.check_box :read_registry, class: 'form-check-input', data: { qa_selector: 'deploy_token_read_registry_checkbox' }
- = f.label :read_registry, 'read_registry', class: 'label-bold form-check-label'
- .text-secondary= s_('DeployTokens|Allows read-only access to registry images.')
-
- %fieldset.form-group.form-check
- = f.check_box :write_registry, class: 'form-check-input', data: { qa_selector: 'deploy_token_write_registry_checkbox' }
- = f.label :write_registry, 'write_registry', class: 'label-bold form-check-label'
- .text-secondary= s_('DeployTokens|Allows write access to registry images.')
+ = 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' } }
- if packages_registry_enabled?(group_or_project)
- %fieldset.form-group.form-check
- = f.check_box :read_package_registry, class: 'form-check-input', data: { qa_selector: 'deploy_token_read_package_registry_checkbox' }
- = f.label :read_package_registry, 'read_package_registry', class: 'label-bold form-check-label'
- .text-secondary= s_('DeployTokens|Allows read-only access to the package registry.')
-
- %fieldset.form-group.form-check
- = f.check_box :write_package_registry, class: 'form-check-input', data: { qa_selector: 'deploy_token_write_package_registry_checkbox' }
- = f.label :write_package_registry, 'write_package_registry', class: 'label-bold form-check-label'
- .text-secondary= s_('DeployTokens|Allows read and write access to the package registry.')
+ = 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' } }
.gl-mt-3
= f.submit s_('DeployTokens|Create deploy token'), class: 'btn gl-button btn-confirm', data: { qa_selector: 'create_deploy_token_button' }
diff --git a/app/views/shared/doorkeeper/applications/_form.html.haml b/app/views/shared/doorkeeper/applications/_form.html.haml
index c1650405776..b40e2630011 100644
--- a/app/views/shared/doorkeeper/applications/_form.html.haml
+++ b/app/views/shared/doorkeeper/applications/_form.html.haml
@@ -17,12 +17,6 @@
help_text: _('Enable only for confidential applications exclusively used by a trusted backend server that can securely store the client secret. Do not enable for native-mobile, single-page, or other JavaScript applications because they cannot keep the client secret confidential.')
.form-group
- - help_text = _('Enable access tokens to expire after 2 hours. If disabled, tokens do not expire.')
- - help_link = link_to _('Learn more.'), help_page_path('integration/oauth_provider.md', anchor: 'expiring-access-tokens'), target: '_blank', rel: 'noopener noreferrer'
- = f.gitlab_ui_checkbox_component :expire_access_tokens, _('Expire access tokens'),
- help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
-
- .form-group
= f.label :scopes, class: 'label-bold'
= render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: @application, scopes: @scopes, f: f
diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml
index d0c4fb2432c..fe602db4393 100644
--- a/app/views/shared/empty_states/_merge_requests.html.haml
+++ b/app/views/shared/empty_states/_merge_requests.html.haml
@@ -10,7 +10,7 @@
.row.empty-state.merge-requests
.col-12
.svg-content
- = image_tag 'illustrations/merge_requests.svg'
+ = image_tag 'illustrations/merge_requests.svg', { auto_dark: true }
.col-12
.text-content
- if has_filter_bar_param?
@@ -42,4 +42,4 @@
- if project_select_button
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: _('merge request'), type: :merge_requests, with_feature_enabled: 'merge_requests'
- else
- = link_to _('New merge request'), button_path, class: 'gl-button btn btn-confirm', title: _('New merge request'), id: 'new_merge_request_link'
+ = link_to _('New merge request'), button_path, class: 'gl-button btn btn-confirm', title: _('New merge request'), id: 'new_merge_request_link', data: { qa_selector: "new_merge_request_button" }
diff --git a/app/views/shared/errors/_gitaly_unavailable.html.haml b/app/views/shared/errors/_gitaly_unavailable.html.haml
index e99c41f2496..c9d7920b9c2 100644
--- a/app/views/shared/errors/_gitaly_unavailable.html.haml
+++ b/app/views/shared/errors/_gitaly_unavailable.html.haml
@@ -1,6 +1,6 @@
= render Pajamas::AlertComponent.new(alert_class: 'gl-my-5',
variant: :danger,
dismissible: false,
- title: reason) do
- .gl-alert-body
+ title: reason) do |c|
+ = c.body do
= s_('The git server, Gitaly, is not available at this time. Please contact your administrator.')
diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml
index ce04e24b09f..932971402a2 100644
--- a/app/views/shared/hook_logs/_content.html.haml
+++ b/app/views/shared/hook_logs/_content.html.haml
@@ -12,8 +12,8 @@
- if hook_log.internal_error_message.present?
= render Pajamas::AlertComponent.new(title: _('Internal error occurred while delivering this webhook.'),
variant: :danger,
- dismissible: false) do
- .gl-alert-body
+ dismissible: false) do |c|
+ = c.body do
= _('Error: %{error}') % { error: hook_log.internal_error_message }
%h4= _('Response')
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/shared/integrations/mattermost_slash_commands/_detailed_help.html.haml
index 9d8ce186232..fec443738c3 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/shared/integrations/mattermost_slash_commands/_detailed_help.html.haml
@@ -34,14 +34,14 @@
.col-12.input-group
= text_field_tag :display_name, "GitLab / #{pretty_name}".html_safe, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#display_name', class: 'input-group-text')
+ = clipboard_button(target: '#display_name', class: 'gl-button btn-default btn-icon input-group-text')
.form-group
= label_tag :description, _('Description'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#description', class: 'input-group-text')
+ = clipboard_button(target: '#description', class: 'gl-button btn-default btn-icon input-group-text')
.form-group
= label_tag nil, s_('MattermostService|Command trigger word'), class: 'col-12 col-form-label label-bold'
@@ -59,7 +59,7 @@
.col-12.input-group
= text_field_tag :request_url, service_trigger_url(integration), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#request_url', class: 'input-group-text')
+ = clipboard_button(target: '#request_url', class: 'gl-button btn-default btn-icon input-group-text')
.form-group
= label_tag nil, s_('MattermostService|Request method'), class: 'col-12 col-form-label label-bold'
@@ -70,14 +70,14 @@
.col-12.input-group
= text_field_tag :response_username, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#response_username', class: 'input-group-text')
+ = clipboard_button(target: '#response_username', class: 'gl-button btn-default btn-icon input-group-text')
.form-group
= label_tag :response_icon, s_('MattermostService|Response icon'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#response_icon', class: 'input-group-text')
+ = clipboard_button(target: '#response_icon', class: 'gl-button btn-default btn-icon input-group-text')
.form-group
= label_tag nil, _('Autocomplete'), class: 'col-12 col-form-label label-bold'
@@ -88,11 +88,11 @@
.col-12.input-group
= text_field_tag :autocomplete_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#autocomplete_hint', class: 'input-group-text')
+ = clipboard_button(target: '#autocomplete_hint', class: 'gl-button btn-default btn-icon input-group-text')
.form-group
= label_tag :autocomplete_description, _('Autocomplete description'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
+ = clipboard_button(target: '#autocomplete_description', class: 'gl-button btn-default btn-icon input-group-text')
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/shared/integrations/mattermost_slash_commands/_help.html.haml
index 993df389fb0..6ce1c65a8dc 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/shared/integrations/mattermost_slash_commands/_help.html.haml
@@ -11,7 +11,7 @@
= s_("MattermostService|After you configure the integration, view your new Mattermost commands by entering")
%kbd.inline /&lt;trigger&gt; help
- if !enabled && integration.project_level?
- = render 'projects/services/mattermost_slash_commands/detailed_help', integration: integration
+ = render 'shared/integrations/mattermost_slash_commands/detailed_help', integration: integration
- if enabled && integration.project_level?
- = render 'projects/services/mattermost_slash_commands/installation_info', integration: integration
+ = render 'shared/integrations/mattermost_slash_commands/installation_info', integration: integration
diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/shared/integrations/mattermost_slash_commands/_installation_info.html.haml
index 38adc69dd5e..38adc69dd5e 100644
--- a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml
+++ b/app/views/shared/integrations/mattermost_slash_commands/_installation_info.html.haml
diff --git a/app/views/projects/services/prometheus/_custom_metrics.html.haml b/app/views/shared/integrations/prometheus/_custom_metrics.html.haml
index 896249c6163..896249c6163 100644
--- a/app/views/projects/services/prometheus/_custom_metrics.html.haml
+++ b/app/views/shared/integrations/prometheus/_custom_metrics.html.haml
diff --git a/app/views/projects/services/prometheus/_help.html.haml b/app/views/shared/integrations/prometheus/_help.html.haml
index f40d8638845..f40d8638845 100644
--- a/app/views/projects/services/prometheus/_help.html.haml
+++ b/app/views/shared/integrations/prometheus/_help.html.haml
diff --git a/app/views/projects/services/prometheus/_metrics.html.haml b/app/views/shared/integrations/prometheus/_metrics.html.haml
index 8794f3e24da..8ee0ddfa1b1 100644
--- a/app/views/projects/services/prometheus/_metrics.html.haml
+++ b/app/views/shared/integrations/prometheus/_metrics.html.haml
@@ -1,6 +1,6 @@
- project = local_assigns.fetch(:project)
-= render 'projects/services/prometheus/custom_metrics', project: project, integration: integration
+= render 'shared/integrations/prometheus/custom_metrics', project: project, integration: integration
.col-lg-3
%p
diff --git a/app/views/shared/integrations/prometheus/_show.html.haml b/app/views/shared/integrations/prometheus/_show.html.haml
new file mode 100644
index 00000000000..0e133e66794
--- /dev/null
+++ b/app/views/shared/integrations/prometheus/_show.html.haml
@@ -0,0 +1,7 @@
+.row
+ .col-lg-3
+ %h4.gl-mt-0
+ = s_('PrometheusService|Metrics')
+
+.row.gl-mb-3.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring
+ = render 'shared/integrations/prometheus/metrics', project: @project, integration: integration
diff --git a/app/views/projects/services/slack/_help.haml b/app/views/shared/integrations/slack/_help.haml
index c5fcd5ca5fe..c5fcd5ca5fe 100644
--- a/app/views/projects/services/slack/_help.haml
+++ b/app/views/shared/integrations/slack/_help.haml
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/shared/integrations/slack_slash_commands/_help.html.haml
index fee0ca15808..fee0ca15808 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/shared/integrations/slack_slash_commands/_help.html.haml
diff --git a/app/views/shared/issuable/_assignees.html.haml b/app/views/shared/issuable/_assignees.html.haml
index 73f1e35f03f..112b0368a3a 100644
--- a/app/views/shared/issuable/_assignees.html.haml
+++ b/app/views/shared/issuable/_assignees.html.haml
@@ -3,7 +3,7 @@
- render_count = assignees_rendering_overflow ? max_render - 1 : max_render
- more_assignees_count = issuable.assignees.size - render_count
-- if issuable.instance_of?(MergeRequest) && Feature.enabled?(:mr_attention_requests, default_enabled: :yaml)
+- if issuable.instance_of?(MergeRequest) && current_user&.mr_attention_requests_enabled?
= render 'shared/issuable/merge_request_assignees', issuable: issuable, count: render_count
- else
- issuable.assignees.take(render_count).each do |assignee| # rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index e0d5f738273..62e1a930ee6 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -8,8 +8,8 @@
- if @conflict
= render Pajamas::AlertComponent.new(variant: :danger,
dismissible: false,
- alert_class: 'gl-mb-5') do
- .gl-alert-body
+ alert_class: 'gl-mb-5') do |c|
+ = c.body do
Someone edited the #{issuable.class.model_name.human.downcase} the same time you did.
Please check out
= link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project, issuable]), target: "_blank", rel: 'noopener noreferrer'
diff --git a/app/views/shared/issuable/_reviewers.html.haml b/app/views/shared/issuable/_reviewers.html.haml
index 4af2cb00859..3bf923eb946 100644
--- a/app/views/shared/issuable/_reviewers.html.haml
+++ b/app/views/shared/issuable/_reviewers.html.haml
@@ -3,7 +3,7 @@
- render_count = reviewers_rendering_overflow ? max_render - 1 : max_render
- more_reviewers_count = issuable.reviewers.size - render_count
-- if issuable.instance_of?(MergeRequest) && Feature.enabled?(:mr_attention_requests, default_enabled: :yaml)
+- if issuable.instance_of?(MergeRequest) && current_user&.mr_attention_requests_enabled?
= render 'shared/issuable/merge_request_reviewers', issuable: issuable, count: render_count
- else
- issuable.reviewers.take(render_count).each do |reviewer| # rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 7fdf8ea7796..6394e05ae24 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -88,7 +88,7 @@
= render 'shared/issuable/user_dropdown_item',
user: User.new(username: '{{username}}', name: '{{name}}'),
avatar: { lazy: true, url: '{{avatar_url}}' }
- - if Feature.enabled?(:mr_attention_requests, default_enabled: :yaml)
+ - if current_user&.mr_attention_requests_enabled?
#js-dropdown-attention-requested.filtered-search-input-dropdown-menu.dropdown-menu
- if current_user
%ul{ data: { dropdown: true } }
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index b99294f504c..feffc7eb011 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -8,17 +8,25 @@
- add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras"
- reviewers = local_assigns.fetch(:reviewers, nil)
- in_group_context_with_iterations = @project.group.present? && issuable_sidebar[:supports_iterations]
+- is_merge_request = issuable_type === 'merge_request'
+- moved_sidebar_enabled = moved_mr_sidebar_enabled? && is_merge_request
-%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in }, issuable_type: issuable_type }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite', 'aria-label': issuable_type }
- .issuable-sidebar
- .issuable-sidebar-header.gl-py-3
- %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
+%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in }, issuable_type: issuable_type }, class: "#{sidebar_gutter_collapsed_class} #{'right-sidebar-merge-requests' if moved_sidebar_enabled}", 'aria-live' => 'polite', 'aria-label': issuable_type }
+ .issuable-sidebar{ class: "#{'is-merge-request' if moved_sidebar_enabled}" }
+ .issuable-sidebar-header{ class: "#{'gl-pb-2! gl-md-display-flex gl-justify-content-end gl-md-display-none!' if moved_sidebar_enabled}" }
+ %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", class: "#{'gl-display-block' if moved_sidebar_enabled}", href: "#", "aria-label" => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
= sidebar_gutter_toggle_icon
- - if signed_in
+ - if signed_in && !moved_sidebar_enabled
.js-issuable-todo{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } }
= form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
- .block.assignee.qa-assignee-block
+ - if signed_in && moved_sidebar_enabled
+ .block.to-do
+ .title.hide-collapsed.gl-font-weight-bold.gl-display-flex.gl-align-items-center.gl-justify-content-space-between.gl-mt-2{ class: 'gl-mb-0!' }
+ = _('To-Do')
+ .js-issuable-todo{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } }
+
+ .block.assignee.qa-assignee-block{ class: "#{'gl-mt-3' if !signed_in && moved_sidebar_enabled}" }
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees, signed_in: signed_in
- if reviewers
@@ -33,6 +41,8 @@
- if @project.group.present?
= render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type
+ .js-sidebar-labels{ data: sidebar_labels_data(issuable_sidebar, @project) }
+
- if issuable_sidebar[:supports_milestone]
.block.milestone{ :class => ("gl-border-b-0!" if in_group_context_with_iterations), data: { qa_selector: 'milestone_block', testid: 'sidebar-milestones' } }
.js-milestone-select{ data: { can_edit: can_edit_issuable.to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid] } }
@@ -43,7 +53,12 @@
- if issuable_sidebar[:show_crm_contacts]
.block.contact
- #js-issue-crm-contacts{ data: { issue_id: issuable_sidebar[:id] } }
+ #js-issue-crm-contacts{ data: { issue_id: issuable_sidebar[:id], group_issues_path: issues_group_path(@project.group) } }
+
+ = render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar, can_edit: can_edit_issuable.to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid]
+
+ - if issuable_sidebar.has_key?(:due_date)
+ #js-due-date-entry-point
- if issuable_sidebar[:supports_time_tracking]
#issuable-time-tracker.block
@@ -51,12 +66,6 @@
.title.hide-collapsed
= _('Time tracking')
= gl_loading_icon(inline: true)
- - if issuable_sidebar.has_key?(:due_date)
- #js-due-date-entry-point
-
- .js-sidebar-labels{ data: sidebar_labels_data(issuable_sidebar, @project) }
-
- = render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar, can_edit: can_edit_issuable.to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid]
- if issuable_sidebar[:supports_severity]
#js-severity
@@ -71,25 +80,23 @@
= render_if_exists 'shared/issuable/sidebar_cve_id_request', issuable_sidebar: issuable_sidebar
- -# haml-lint:disable InlineJavaScript
- %script#js-lock-issue-data{ type: "application/json" }= { is_locked: !!issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe
- #js-lock-entry-point
+ - if !moved_sidebar_enabled
+ #js-lock-entry-point
+ - if signed_in
+ .js-sidebar-subscriptions-entry-point
.js-sidebar-participants-entry-point
- - if signed_in
- .js-sidebar-subscriptions-entry-point
-
.block.with-sub-blocks
#js-reference-entry-point
- - if issuable_type == 'merge_request'
+ - if issuable_type == 'merge_request' && !moved_sidebar_enabled
.sub-block.js-sidebar-source-branch
.sidebar-collapsed-icon.js-dont-change-state
- = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport')
+ = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
.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
= _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<span class='gl-font-monospace' data-testid='ref-name' title='#{html_escape(source_branch)}'>".html_safe, source_branch_close: "</span>".html_safe, source_branch: html_escape(source_branch) }
- = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport')
+ = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
- if show_forwarding_email
.block
diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml
index f6d7ed6764d..e36c4cd6be0 100644
--- a/app/views/shared/issuable/_sort_dropdown.html.haml
+++ b/app/views/shared/issuable/_sort_dropdown.html.haml
@@ -1,26 +1,9 @@
-- sort_value = @sort
-- sort_title = issuable_sort_option_title(sort_value)
- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
- viewing_merge_requests = controller.controller_name == 'merge_requests'
+- items = issuable_sort_options(viewing_issues, viewing_merge_requests)
+- selected = issuable_sort_option_overrides[@sort] || @sort
-.dropdown.inline.gl-ml-3.issue-sort-dropdown
+.gl-ml-3
.btn-group{ role: 'group' }
- .btn-group{ role: 'group' }
- %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'gl-button btn btn-default' }
- = sort_title
- = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
- %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
- %li
- = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority), sort_title)
- = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date), sort_title)
- = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated), sort_title)
- = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone), sort_title)
- = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date), sort_title) if viewing_issues
- = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity), sort_title)
- = sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority), sort_title)
- = sortable_item(sort_title_merged_date, page_filter_path(sort: sort_value_merged_date), sort_title) if viewing_merge_requests
- = sortable_item(sort_title_closed_date, page_filter_path(sort: sort_value_closed_date), sort_title) if viewing_merge_requests
- = sortable_item(sort_title_relative_position, page_filter_path(sort: sort_value_relative_position), sort_title) if viewing_issues
- = sortable_item(sort_title_title, page_filter_path(sort: sort_value_title), sort_title)
- = render_if_exists('shared/ee/issuable/sort_dropdown', viewing_issues: viewing_issues, sort_title: sort_title)
- = issuable_sort_direction_button(sort_value)
+ = gl_redirect_listbox_tag(items, selected, data: { right: true })
+ = issuable_sort_direction_button(@sort)
diff --git a/app/views/shared/issuable/_status_box.html.haml b/app/views/shared/issuable/_status_box.html.haml
index c0e972684d2..4fda1f11545 100644
--- a/app/views/shared/issuable/_status_box.html.haml
+++ b/app/views/shared/issuable/_status_box.html.haml
@@ -1,6 +1,10 @@
-- state_human_name, state_icon_name = state_name_with_icon(issuable)
+- badge_text = state_name_with_icon(issuable)[0]
+- badge_icon = state_name_with_icon(issuable)[1]
+- badge_variant = issuable.open? ? :success : issuable.merged? ? :info : :danger
+- badge_status_class = issuable.open? ? 'issuable-status-badge-open' : issuable.merged? ? 'issuable-status-badge-merged' : 'issuable-status-badge-closed'
+- updated_mr_header_enabled = Feature.enabled?(:updated_mr_header, @project) && issuable.is_a?(MergeRequest)
+- badge_classes = "js-mr-status-box issuable-status-badge gl-mr-3 #{badge_status_class} #{'gl-vertical-align-bottom' if updated_mr_header_enabled}"
-.issuable-status-box.status-box.js-mr-status-box{ class: status_box_class(issuable), data: { project_path: issuable.project.path_with_namespace, iid: issuable.iid, state: issuable.state } }
- = sprite_icon(state_icon_name, css_class: 'gl-display-block gl-sm-display-none!')
- %span.gl-display-none.gl-sm-display-block
- = state_human_name
+= gl_badge_tag({ variant: badge_variant, icon: badge_icon, icon_classes: 'gl-mr-0!' }, { class: badge_classes, data: { project_path: issuable.project.path_with_namespace, iid: issuable.iid, issuable_type: 'merge_request', state: issuable.state } }) do
+ %span.gl-display-none.gl-sm-display-block.gl-ml-2
+ = badge_text
diff --git a/app/views/shared/issue_type/_details_header.html.haml b/app/views/shared/issue_type/_details_header.html.haml
index eca61819cca..08fba712d5e 100644
--- a/app/views/shared/issue_type/_details_header.html.haml
+++ b/app/views/shared/issue_type/_details_header.html.haml
@@ -1,17 +1,16 @@
-- link = issue_closed_link(@issue, current_user, css_class: 'text-white text-underline')
+- link = issue_closed_link(@issue, current_user, css_class: 'text-underline gl-reset-color!')
+- badge_classes = 'issuable-status-badge gl-mr-3'
.detail-page-header
.detail-page-header-body
- .issuable-status-box.status-box.status-box-issue-closed{ class: issue_status_visibility(issuable, status_box: :closed) }
- = sprite_icon('issue-close', css_class: 'gl-display-block gl-sm-display-none!')
- .gl-display-none.gl-sm-display-block
+ = gl_badge_tag({ variant: :info, icon: 'issue-closed', icon_classes: 'gl-mr-0!' }, { class: "#{issue_status_visibility(issuable, status_box: :closed)} #{badge_classes} issuable-status-badge-closed" }) do
+ .gl-display-none.gl-sm-display-block.gl-ml-2
= issue_closed_text(issuable, current_user)
- - if link
- %span.text-white.gl-pl-2.gl-sm-display-none
- = "(#{link})"
- .issuable-status-box.status-box.status-box-open{ class: issue_status_visibility(issuable, status_box: :open) }
- = sprite_icon('issue-open-m', css_class: 'gl-display-block gl-sm-display-none!')
- %span.gl-display-none.gl-sm-display-block
+ - if link
+ %span.gl-pl-2.gl-sm-display-none
+ = "(#{link})"
+ = gl_badge_tag({ variant: :success, icon: 'issues', icon_classes: 'gl-mr-0!' }, { class: "#{issue_status_visibility(issuable, status_box: :open)} #{badge_classes} issuable-status-badge-open" }) do
+ %span.gl-display-none.gl-sm-display-block.gl-ml-2
= _('Open')
.issuable-meta
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 4e06b7902bd..45699808b6b 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -4,7 +4,7 @@
%li{ class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id }
.row
- .col-sm-6
+ .col-md-6
.gl-mb-2
%strong{ data: { qa_selector: "milestone_link", qa_milestone_title: milestone.title } }
= link_to truncate(milestone.title, length: 100), milestone_path(milestone)
@@ -33,18 +33,18 @@
%div
= render('shared/milestone_expired', milestone: milestone)
- if milestone.group_milestone?
- = gl_badge_tag milestone.group.full_name, variant: :info
+ = gl_badge_tag milestone.group.full_name, { variant: :info }, { class: 'gl-white-space-normal gl-text-left' }
- if milestone.project_milestone?
- = gl_badge_tag milestone.project.full_name, variant: :muted
+ = gl_badge_tag milestone.project.full_name, { variant: :muted }, { class: 'gl-white-space-normal gl-text-left' }
- .col-sm-4.milestone-progress
+ .col-md-4.milestone-progress
= milestone_progress_bar(milestone)
= link_to pluralize(milestone.total_issues_count, _('Issue')), issues_path
- if milestone.merge_requests_enabled?
&middot;
= link_to pluralize(milestone.total_merge_requests_count, _('Merge request')), merge_requests_path
.float-lg-right.light #{milestone.percent_complete}% complete
- .col-sm-2
+ .col-md-2
.milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end
- if @project # if in milestones list on project level
- if can_admin_group_milestones?
diff --git a/app/views/shared/milestones/_milestone_complete_alert.html.haml b/app/views/shared/milestones/_milestone_complete_alert.html.haml
index 4685a93a343..86f9193cc7a 100644
--- a/app/views/shared/milestones/_milestone_complete_alert.html.haml
+++ b/app/views/shared/milestones/_milestone_complete_alert.html.haml
@@ -3,6 +3,6 @@
- if milestone.complete? && milestone.active?
= render Pajamas::AlertComponent.new(variant: :success,
alert_data: { testid: 'all-issues-closed-alert' },
- dismissible: false) do
- .gl-alert-body
- = yield
+ dismissible: false) do |c|
+ = c.body do
+ = yield
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index a1e94172ec3..12026b89429 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -163,9 +163,9 @@
.block.reference
.sidebar-collapsed-icon.js-dont-change-state
= clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport')
- .cross-project-reference.hide-collapsed
- %span.gl-display-inline-block.gl-text-truncate
+ .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
- = clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport', class: 'btn-clipboard btn-transparent gl-float-right gl-bg-gray-10')
+ = clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport')
diff --git a/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml
index cfa87351689..d167ffb5582 100644
--- a/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml
+++ b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml
@@ -9,10 +9,7 @@
- lock_attribute = "lock_#{attribute}"
-.gl-form-checkbox.custom-control.custom-checkbox
- = form.check_box lock_attribute, checked: group.namespace_settings.public_send(lock_attribute), class: 'custom-control-input', data: { testid: 'enforce-for-all-subgroups-checkbox' }
- = form.label lock_attribute, class: 'custom-control-label' do
- %span
- = yield.presence || s_('CascadingSettings|Enforce for all subgroups')
- %p.help-text
- = help_text
+= form.gitlab_ui_checkbox_component lock_attribute,
+ s_('CascadingSettings|Enforce for all subgroups'),
+ help_text: help_text,
+ checkbox_options: { checked: group.namespace_settings.public_send(lock_attribute), data: { testid: 'enforce-for-all-subgroups-checkbox' } }
diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml
index 8a79a17b166..c845d4df7df 100644
--- a/app/views/shared/notes/_hints.html.haml
+++ b/app/views/shared/notes/_hints.html.haml
@@ -2,15 +2,12 @@
- supports_file_upload = local_assigns.fetch(:supports_file_upload, true)
.comment-toolbar.clearfix
.toolbar-text
- = link_to _('Markdown'), help_page_path('user/markdown'), target: '_blank', rel: 'noopener noreferrer'
+ - markdownLinkStart = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/markdown') }
+ - quickActionsLinkStart = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/quick_actions') }
- if supports_quick_actions
- and
- = link_to _('quick actions'), help_page_path('user/project/quick_actions'), target: '_blank', rel: 'noopener noreferrer'
- are
+ = html_escape(s_('NoteToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}. For %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd}, type %{keyboardStart}/%{keyboardEnd}.')) % { markdownDocsLinkStart: markdownLinkStart, markdownDocsLinkEnd: '</a>'.html_safe, quickActionsDocsLinkStart: quickActionsLinkStart, quickActionsDocsLinkEnd: '</a>'.html_safe, keyboardStart: '<kbd>'.html_safe, keyboardEnd: '</kbd>'.html_safe }
- else
- is
- supported
-
+ = html_escape(s_('MarkdownToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}')) % { markdownDocsLinkStart: markdownLinkStart, markdownDocsLinkEnd: '</a>'.html_safe }
- if supports_file_upload
%span.uploading-container
%span.uploading-progress-container.hide
diff --git a/app/views/shared/projects/_topics.html.haml b/app/views/shared/projects/_topics.html.haml
index b7df369327c..e3895663033 100644
--- a/app/views/shared/projects/_topics.html.haml
+++ b/app/views/shared/projects/_topics.html.haml
@@ -7,25 +7,25 @@
= sprite_icon('tag', css_class: 'icon gl-relative gl-mr-2')
- project.topics_to_show.each do |topic|
- - explore_project_topic_path = topic_explore_projects_path(topic_name: topic)
- - if topic.length > max_project_topic_length
- %a.gl-mr-3.has-tooltip{ data: { container: "body" }, title: topic, href: explore_project_topic_path, itemprop: 'keywords' }
- = gl_badge_tag truncate(topic, length: max_project_topic_length)
+ - explore_project_topic_path = topic_explore_projects_path(topic_name: topic[:name])
+ - if topic[:title].length > max_project_topic_length
+ %a.gl-mr-3.has-tooltip{ data: { container: "body" }, title: topic[:title], href: explore_project_topic_path, itemprop: 'keywords' }
+ = gl_badge_tag truncate(topic[:title], length: max_project_topic_length)
- else
%a.gl-mr-3{ href: explore_project_topic_path, itemprop: 'keywords' }
- = gl_badge_tag topic
+ = gl_badge_tag topic[:title]
- if project.has_extra_topics?
- title = _('More topics')
- content = capture do
%span.gl-display-inline-flex.gl-flex-wrap
- project.topics_not_shown.each do |topic|
- - explore_project_topic_path = topic_explore_projects_path(topic_name: topic)
- - if topic.length > max_project_topic_length
- %a.gl-mr-3.gl-mb-3.has-tooltip{ data: { container: "body" }, title: topic, href: explore_project_topic_path, itemprop: 'keywords' }
- = gl_badge_tag truncate(topic, length: max_project_topic_length)
+ - explore_project_topic_path = topic_explore_projects_path(topic_name: topic[:name])
+ - if topic[:title].length > max_project_topic_length
+ %a.gl-mr-3.gl-mb-3.has-tooltip{ data: { container: "body" }, title: topic[:title], href: explore_project_topic_path, itemprop: 'keywords' }
+ = gl_badge_tag truncate(topic[:title], length: max_project_topic_length)
- else
%a.gl-mr-3.gl-mb-3{ href: explore_project_topic_path, itemprop: 'keywords' }
- = gl_badge_tag topic
+ = gl_badge_tag topic[:title]
.text-nowrap{ role: 'button', tabindex: 0, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
= _("+ %{count} more") % { count: project.count_of_extra_topics_not_shown }
diff --git a/app/views/shared/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml
index 8d0069a7664..024b06fe97a 100644
--- a/app/views/shared/runners/_form.html.haml
+++ b/app/views/shared/runners/_form.html.haml
@@ -1,30 +1,22 @@
-= form_for runner, url: runner_form_url do |f|
+= gitlab_ui_form_for runner, url: runner_form_url do |f|
= form_errors(runner)
.form-group.row
= label :active, _("Active"), class: 'col-form-label col-sm-2'
.col-sm-10
- .form-check
- = f.check_box :active, { class: 'form-check-input' }
- %label.light{ for: :runner_active }= _("Paused runners don't accept new jobs")
+ = f.gitlab_ui_checkbox_component :active, _("Paused runners don't accept new jobs")
.form-group.row
= label :protected, _("Protected"), class: 'col-form-label col-sm-2'
.col-sm-10
- .form-check
- = f.check_box :access_level, { class: 'form-check-input' }, 'ref_protected', 'not_protected'
- %label.light{ for: :runner_access_level }= _('This runner will only run on pipelines triggered on protected branches')
+ = f.gitlab_ui_checkbox_component :access_level, _('This runner will only run on pipelines triggered on protected branches'), checked_value: 'ref_protected', unchecked_value: 'not_protected'
.form-group.row
= label :run_untagged, _('Run untagged jobs'), class: 'col-form-label col-sm-2'
.col-sm-10
- .form-check
- = f.check_box :run_untagged, { class: 'form-check-input' }
- %label.light{ for: :runner_run_untagged }= _('Indicates whether this runner can pick jobs without tags')
+ = f.gitlab_ui_checkbox_component :run_untagged, _('Indicates whether this runner can pick jobs without tags')
- unless runner.group_type?
.form-group.row
= label :locked, _('Lock to current projects'), class: 'col-form-label col-sm-2'
.col-sm-10
- .form-check
- = f.check_box :locked, { class: 'form-check-input' }
- %label.light{ for: :runner_locked }= _('When a runner is locked, it cannot be assigned to other projects')
+ = f.gitlab_ui_checkbox_component :locked, _('When a runner is locked, it cannot be assigned to other projects')
.form-group.row
= label_tag :ip_address, class: 'col-form-label col-sm-2' do
= _('IP Address')
diff --git a/app/views/shared/runners/_runner_type_alert.html.haml b/app/views/shared/runners/_runner_type_alert.html.haml
index 365cee5fadc..4bf02b71109 100644
--- a/app/views/shared/runners/_runner_type_alert.html.haml
+++ b/app/views/shared/runners/_runner_type_alert.html.haml
@@ -3,14 +3,14 @@
- if runner.group_type?
= render Pajamas::AlertComponent.new(alert_class: alert_class,
title: s_('Runners|This runner is available to all projects and subgroups in a group.'),
- dismissible: false) do
- .gl-alert-body
+ dismissible: false) do |c|
+ = c.body do
= s_('Runners|Use Group runners when you want all projects in a group to have access to a set of runners.')
= link_to _('Learn more.'), help_page_path('ci/runners/runners_scope', anchor: 'group-runners'), target: '_blank', rel: 'noopener noreferrer'
- else
= render Pajamas::AlertComponent.new(alert_class: alert_class,
title: s_('Runners|This runner is associated with specific projects.'),
- dismissible: false) do
- .gl-alert-body
+ dismissible: false) do |c|
+ = c.body do
= s_('Runners|You can set up a specific runner to be used by multiple projects but you cannot make this a shared runner.')
= link_to _('Learn more.'), help_page_path('ci/runners/runners_scope', anchor: 'specific-runners'), target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 4e373dda013..3cd70dab4d5 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -4,21 +4,20 @@
%li.snippet-row.py-3{ data: { qa_selector: 'snippet_link', qa_snippet_title: snippet.title } }
= image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: ''
- .title
- = link_to gitlab_snippet_path(snippet) do
- = snippet.title
+ = link_to gitlab_snippet_path(snippet), class: "title" do
+ = snippet.title
- %ul.controls{ data: { qa_selector: 'snippet_file_count_content', qa_snippet_files: snippet.statistics&.file_count } }
- %li
- = snippet_file_count(snippet)
- %li
- = link_to gitlab_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if notes_count == 0) do
- = sprite_icon('comments', css_class: 'gl-vertical-align-text-bottom')
- = notes_count
- %li
- %span.sr-only{ data: { qa_selector: 'snippet_visibility_content', qa_snippet_visibility: visibility_level_label(snippet.visibility_level) } }
- = visibility_level_label(snippet.visibility_level)
- = visibility_level_icon(snippet.visibility_level)
+ %ul.controls{ data: { qa_selector: 'snippet_file_count_content', qa_snippet_files: snippet.statistics&.file_count } }
+ %li
+ = snippet_file_count(snippet)
+ %li
+ = link_to gitlab_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if notes_count == 0) do
+ = sprite_icon('comments', css_class: 'gl-vertical-align-text-bottom')
+ = notes_count
+ %li
+ %span.sr-only{ data: { qa_selector: 'snippet_visibility_content', qa_snippet_visibility: visibility_level_label(snippet.visibility_level) } }
+ = visibility_level_label(snippet.visibility_level)
+ = visibility_level_icon(snippet.visibility_level)
.snippet-info
#{snippet.to_reference} &middot;
diff --git a/app/views/shared/topics/_topic.html.haml b/app/views/shared/topics/_topic.html.haml
index a47d4495777..ca1098511da 100644
--- a/app/views/shared/topics/_topic.html.haml
+++ b/app/views/shared/topics/_topic.html.haml
@@ -1,4 +1,4 @@
-- max_topic_name_length = 30
+- max_topic_title_length = 30
- detail_page_link = topic_explore_projects_path(topic_name: topic.name)
.col-lg-3.col-md-4.col-sm-12
@@ -8,9 +8,9 @@
= link_to detail_page_link do
= topic_icon(topic, class: "avatar s40")
= link_to detail_page_link do
- - if topic.name.length > max_topic_name_length
- %h5.str-truncated.has-tooltip{ title: topic.name }
- = truncate(topic.name, length: max_topic_name_length)
+ - if topic.title_or_name.length > max_topic_title_length
+ %h5.gl-str-truncated.has-tooltip{ title: topic.title_or_name }
+ = truncate(topic.title_or_name, length: max_topic_title_length)
- else
%h5
- = topic.name
+ = topic.title_or_name
diff --git a/app/views/shared/web_hooks/_hook_errors.html.haml b/app/views/shared/web_hooks/_hook_errors.html.haml
index a100a620cea..d95efe83e15 100644
--- a/app/views/shared/web_hooks/_hook_errors.html.haml
+++ b/app/views/shared/web_hooks/_hook_errors.html.haml
@@ -11,13 +11,13 @@
support_link_start: link_start % { url: support_path },
support_link_end: link_end }
= render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook was automatically disabled'),
- variant: :danger) do
- .gl-alert-body
+ variant: :danger) do |c|
+ = c.body do
= s_('Webhooks|The webhook was triggered more than %{limit} times per minute and is now disabled. To re-enable this webhook, fix the problems shown in %{strong_start}Recent events%{strong_end}, then re-test your settings. %{support_link_start}Contact Support%{support_link_end} if you need help re-enabling your webhook.').html_safe % placeholders
- elsif hook.permanently_disabled?
= render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook failed to connect'),
- variant: :danger) do
- .gl-alert-body
+ variant: :danger) do |c|
+ = c.body do
= s_('Webhooks|The webhook failed to connect, and is disabled. To re-enable it, check %{strong_start}Recent events%{strong_end} for error details, then test your settings below.').html_safe % { strong_start: strong_start, strong_end: strong_end }
- elsif hook.temporarily_disabled?
- help_path = help_page_path('user/project/integrations/webhooks', anchor: 'webhook-fails-or-multiple-webhook-requests-are-triggered')
@@ -27,6 +27,6 @@
help_link_start: link_start % { url: help_path },
help_link_end: link_end }
= render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook fails to connect'),
- variant: :warning) do
- .gl-alert-body
+ variant: :warning) do |c|
+ = c.body do
= s_('Webhooks|The webhook %{help_link_start}failed to connect%{help_link_end}, and will retry in %{retry_time}. To re-enable it, check %{strong_start}Recent events%{strong_end} for error details, then test your settings below.').html_safe % placeholders
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index bfb70e0d496..8a525e455fd 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -3,24 +3,6 @@
#
# Do not edit it manually!
---
-- :name: authorized_project_update:authorized_project_update_project_create
- :worker_name: AuthorizedProjectUpdate::ProjectCreateWorker
- :feature_category: :authentication_and_authorization
- :has_external_dependencies:
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: true
- :tags: []
-- :name: authorized_project_update:authorized_project_update_project_group_link_create
- :worker_name: AuthorizedProjectUpdate::ProjectGroupLinkCreateWorker
- :feature_category: :authentication_and_authorization
- :has_external_dependencies:
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: true
- :tags: []
- :name: authorized_project_update:authorized_project_update_project_recalculate
:worker_name: AuthorizedProjectUpdate::ProjectRecalculateWorker
:feature_category: :authentication_and_authorization
@@ -597,6 +579,15 @@
:weight: 1
:idempotent:
:tags: []
+- :name: cronjob:projects_inactive_projects_deletion_cron
+ :worker_name: Projects::InactiveProjectsDeletionCronWorker
+ :feature_category: :compliance_management
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:projects_schedule_refresh_build_artifacts_size_statistics
:worker_name: Projects::ScheduleRefreshBuildArtifactsSizeStatisticsWorker
:feature_category: :build_artifacts
@@ -678,15 +669,6 @@
:weight: 1
:idempotent:
:tags: []
-- :name: cronjob:requests_profiles
- :worker_name: RequestsProfilesWorker
- :feature_category: :source_code_management
- :has_external_dependencies:
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent:
- :tags: []
- :name: cronjob:schedule_merge_request_cleanup_refs
:worker_name: ScheduleMergeRequestCleanupRefsWorker
:feature_category: :code_review
@@ -2290,15 +2272,6 @@
:weight: 1
:idempotent: true
:tags: []
-- :name: expire_build_instance_artifacts
- :worker_name: ExpireBuildInstanceArtifactsWorker
- :feature_category: :build_artifacts
- :has_external_dependencies:
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent:
- :tags: []
- :name: export_csv
:worker_name: ExportCsvWorker
:feature_category: :team_planning
@@ -2533,6 +2506,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: merge_requests_close_issue
+ :worker_name: MergeRequests::CloseIssueWorker
+ :feature_category: :code_review
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: merge_requests_delete_source_branch
:worker_name: MergeRequests::DeleteSourceBranchWorker
:feature_category: :source_code_management
@@ -2803,6 +2785,15 @@
:weight: 1
:idempotent:
:tags: []
+- :name: projects_after_import
+ :worker_name: Projects::AfterImportWorker
+ :feature_category: :importers
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: projects_git_garbage_collect
:worker_name: Projects::GitGarbageCollectWorker
:feature_category: :gitaly
@@ -2812,6 +2803,15 @@
:weight: 1
:idempotent:
:tags: []
+- :name: projects_inactive_projects_deletion_notification
+ :worker_name: Projects::InactiveProjectsDeletionNotificationWorker
+ :feature_category: :compliance_management
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: projects_post_creation
:worker_name: Projects::PostCreationWorker
:feature_category: :source_code_management
@@ -3123,7 +3123,7 @@
:worker_name: WebHooks::DestroyWorker
:feature_category: :integrations
:has_external_dependencies:
- :urgency: :low
+ :urgency: :high
:resource_boundary: :unknown
:weight: 1
:idempotent: true
diff --git a/app/workers/authorized_project_update/project_create_worker.rb b/app/workers/authorized_project_update/project_create_worker.rb
deleted file mode 100644
index 1f19168cd36..00000000000
--- a/app/workers/authorized_project_update/project_create_worker.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module AuthorizedProjectUpdate
- class ProjectCreateWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
-
- feature_category :authentication_and_authorization
- urgency :low
- queue_namespace :authorized_project_update
-
- idempotent!
-
- def perform(project_id)
- project = Project.find(project_id)
-
- AuthorizedProjectUpdate::ProjectCreateService.new(project).execute
- end
- end
-end
diff --git a/app/workers/authorized_project_update/project_group_link_create_worker.rb b/app/workers/authorized_project_update/project_group_link_create_worker.rb
deleted file mode 100644
index d83981c4ce1..00000000000
--- a/app/workers/authorized_project_update/project_group_link_create_worker.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-module AuthorizedProjectUpdate
- class ProjectGroupLinkCreateWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
-
- feature_category :authentication_and_authorization
- urgency :low
- queue_namespace :authorized_project_update
-
- idempotent!
-
- def perform(project_id, group_id, group_access = nil)
- project = Project.find(project_id)
- group = Group.find(group_id)
-
- AuthorizedProjectUpdate::ProjectGroupLinkCreateService
- .new(project, group, group_access)
- .execute
- end
- end
-end
diff --git a/app/workers/bulk_imports/pipeline_worker.rb b/app/workers/bulk_imports/pipeline_worker.rb
index 1a98705c151..b515f0fa202 100644
--- a/app/workers/bulk_imports/pipeline_worker.rb
+++ b/app/workers/bulk_imports/pipeline_worker.rb
@@ -4,7 +4,7 @@ module BulkImports
class PipelineWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- NDJSON_PIPELINE_PERFORM_DELAY = 10.seconds
+ FILE_EXTRACTION_PIPELINE_PERFORM_DELAY = 10.seconds
data_consistency :always
feature_category :importers
@@ -46,13 +46,13 @@ module BulkImports
raise(Entity::FailedError, 'Failed entity status')
end
- if ndjson_pipeline?(pipeline_tracker)
- status = ExportStatus.new(pipeline_tracker, pipeline_tracker.pipeline_class.relation)
+ if file_extraction_pipeline?(pipeline_tracker)
+ export_status = ExportStatus.new(pipeline_tracker, pipeline_tracker.pipeline_class.relation)
raise(Pipeline::ExpiredError, 'Pipeline timeout') if job_timeout?(pipeline_tracker)
- raise(Pipeline::FailedError, status.error) if status.failed?
+ raise(Pipeline::FailedError, export_status.error) if export_status.failed?
- return reenqueue(pipeline_tracker) if status.started?
+ return reenqueue(pipeline_tracker) if export_status.started?
end
pipeline_tracker.update!(status_event: 'start', jid: jid)
@@ -104,15 +104,15 @@ module BulkImports
@logger ||= Gitlab::Import::Logger.build
end
- def ndjson_pipeline?(pipeline_tracker)
- pipeline_tracker.pipeline_class.ndjson_pipeline?
+ def file_extraction_pipeline?(pipeline_tracker)
+ pipeline_tracker.pipeline_class.file_extraction_pipeline?
end
def job_timeout?(pipeline_tracker)
(Time.zone.now - pipeline_tracker.entity.created_at) > Pipeline::NDJSON_EXPORT_TIMEOUT
end
- def reenqueue(pipeline_tracker, delay: NDJSON_PIPELINE_PERFORM_DELAY)
+ def reenqueue(pipeline_tracker, delay: FILE_EXTRACTION_PIPELINE_PERFORM_DELAY)
self.class.perform_in(
delay,
pipeline_tracker.id,
diff --git a/app/workers/ci/build_finished_worker.rb b/app/workers/ci/build_finished_worker.rb
index 70c234bd4c7..2d7f3a67004 100644
--- a/app/workers/ci/build_finished_worker.rb
+++ b/app/workers/ci/build_finished_worker.rb
@@ -61,7 +61,7 @@ module Ci
end
def archive_trace_worker_class(build)
- if Feature.enabled?(:ci_build_finished_worker_namespace_changed, build.project, default_enabled: :yaml)
+ if Feature.enabled?(:ci_build_finished_worker_namespace_changed, build.project)
Ci::ArchiveTraceWorker
else
::ArchiveTraceWorker
diff --git a/app/workers/cleanup_container_repository_worker.rb b/app/workers/cleanup_container_repository_worker.rb
index 7274ecf62f9..73501315575 100644
--- a/app/workers/cleanup_container_repository_worker.rb
+++ b/app/workers/cleanup_container_repository_worker.rb
@@ -23,31 +23,17 @@ class CleanupContainerRepositoryWorker
return unless valid?
- if run_by_container_expiration_policy?
- container_repository.start_expiration_policy!
- end
-
- result = Projects::ContainerRepository::CleanupTagsService
+ Projects::ContainerRepository::CleanupTagsService
.new(container_repository, current_user, params)
.execute
-
- if run_by_container_expiration_policy? && result[:status] == :success
- container_repository.reset_expiration_policy_started_at!
- end
end
private
def valid?
- return true if run_by_container_expiration_policy?
-
current_user && container_repository && project
end
- def run_by_container_expiration_policy?
- @params['container_expiration_policy'] && container_repository.present? && project.present?
- end
-
def project
container_repository&.project
end
diff --git a/app/workers/concerns/git_garbage_collect_methods.rb b/app/workers/concerns/git_garbage_collect_methods.rb
index 308ffacfc6b..5c0493c9be5 100644
--- a/app/workers/concerns/git_garbage_collect_methods.rb
+++ b/app/workers/concerns/git_garbage_collect_methods.rb
@@ -37,7 +37,7 @@ module GitGarbageCollectMethods
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(resource) if gc?(task)
- update_repository_statistics(resource) if task != :pack_refs
+ update_repository_statistics(resource, task)
# In case pack files are deleted, release libgit2 cache and open file
# descriptors ASAP instead of waiting for Ruby garbage collection
@@ -83,7 +83,7 @@ module GitGarbageCollectMethods
def gitaly_call(task, resource)
repository = resource.repository.raw_repository
- if Feature.enabled?(:optimized_housekeeping, container(resource), default_enabled: :yaml)
+ if Feature.enabled?(:optimized_housekeeping, container(resource))
client = repository.gitaly_repository_client
if task == :prune
@@ -135,15 +135,25 @@ module GitGarbageCollectMethods
resource.repository.has_visible_content?
end
- def update_repository_statistics(resource)
+ def update_repository_statistics(resource, task)
+ return if task == :pack_refs
+
resource.repository.expire_statistics_caches
return if Gitlab::Database.read_only? # GitGarbageCollectWorker may be run on a Geo secondary
- update_db_repository_statistics(resource)
+ stats_to_update = stats
+
+ stats_to_update.delete(:repository_size) if task == :incremental_repack
+
+ update_db_repository_statistics(resource, stats_to_update)
end
- def update_db_repository_statistics(resource)
+ def update_db_repository_statistics(resource, stats)
# no-op
end
+
+ def stats
+ []
+ end
end
diff --git a/app/workers/concerns/reenqueuer.rb b/app/workers/concerns/reenqueuer.rb
index 641ca691868..9c0f8f94a24 100644
--- a/app/workers/concerns/reenqueuer.rb
+++ b/app/workers/concerns/reenqueuer.rb
@@ -41,6 +41,8 @@ module Reenqueuer
end
def perform(*args)
+ set_custom_lease_key(*args) if self.respond_to?(:set_custom_lease_key)
+
try_obtain_lease do
reenqueue(*args) do
ensure_minimum_duration(minimum_duration) do
diff --git a/app/workers/concerns/worker_attributes.rb b/app/workers/concerns/worker_attributes.rb
index 8f7a3da5429..5d7251e9a98 100644
--- a/app/workers/concerns/worker_attributes.rb
+++ b/app/workers/concerns/worker_attributes.rb
@@ -104,7 +104,7 @@ module WorkerAttributes
def get_data_consistency_feature_flag_enabled?
return true unless class_attributes[:data_consistency_feature_flag]
- Feature.enabled?(class_attributes[:data_consistency_feature_flag], default_enabled: :yaml)
+ Feature.enabled?(class_attributes[:data_consistency_feature_flag])
end
# Set this attribute on a job when it will call to services outside of the
@@ -175,7 +175,7 @@ module WorkerAttributes
def deduplication_enabled?
return true unless get_deduplication_options[:feature_flag]
- Feature.enabled?(get_deduplication_options[:feature_flag], default_enabled: :yaml)
+ Feature.enabled?(get_deduplication_options[:feature_flag])
end
def big_payload!
diff --git a/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb b/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb
index cd3ed5d4c9b..f40855a7455 100644
--- a/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb
+++ b/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb
@@ -27,7 +27,6 @@ module ContainerExpirationPolicies
].freeze
def perform_work
- return unless throttling_enabled?
return unless container_repository
log_extra_metadata_on_done(:container_repository_id, container_repository.id)
@@ -45,8 +44,6 @@ module ContainerExpirationPolicies
end
def max_running_jobs
- return 0 unless throttling_enabled?
-
::Gitlab::CurrentSettings.container_registry_expiration_policies_worker_capacity
end
@@ -122,10 +119,6 @@ module ContainerExpirationPolicies
policy.next_run_at < now || (now + max_cleanup_execution_time.seconds < policy.next_run_at)
end
- def throttling_enabled?
- Feature.enabled?(:container_registry_expiration_policies_throttling, default_enabled: :yaml)
- end
-
def max_cleanup_execution_time
::Gitlab::CurrentSettings.container_registry_delete_tags_service_timeout
end
diff --git a/app/workers/container_expiration_policy_worker.rb b/app/workers/container_expiration_policy_worker.rb
index 308ccfe2cb3..80499aff431 100644
--- a/app/workers/container_expiration_policy_worker.rb
+++ b/app/workers/container_expiration_policy_worker.rb
@@ -5,7 +5,11 @@ class ContainerExpirationPolicyWorker # rubocop:disable Scalability/IdempotentWo
data_consistency :always
+ # rubocop:disable Scalability/CronWorkerContext
+ # This worker does not perform work scoped to a context
include CronjobQueue
+ # rubocop:enable Scalability/CronWorkerContext
+
include ExclusiveLeaseGuard
feature_category :container_registry
@@ -17,7 +21,9 @@ class ContainerExpirationPolicyWorker # rubocop:disable Scalability/IdempotentWo
def perform
process_stale_ongoing_cleanups
disable_policies_without_container_repositories
- throttling_enabled? ? perform_throttled : perform_unthrottled
+ try_obtain_lease do
+ ContainerExpirationPolicies::CleanupContainerRepositoryWorker.perform_with_capacity
+ end
log_counts
end
@@ -54,54 +60,6 @@ class ContainerExpirationPolicyWorker # rubocop:disable Scalability/IdempotentWo
.update_all(expiration_policy_cleanup_status: :cleanup_unfinished)
end
- def perform_unthrottled
- with_runnable_policy(preloaded: true) do |policy|
- with_context(project: policy.project,
- user: nil) do |project:, user:|
- ContainerExpirationPolicyService.new(project, user)
- .execute(policy)
- end
- end
- end
-
- def perform_throttled
- try_obtain_lease do
- ContainerExpirationPolicies::CleanupContainerRepositoryWorker.perform_with_capacity
- end
- end
-
- # TODO : remove the preload option when cleaning FF container_registry_expiration_policies_throttling
- def with_runnable_policy(preloaded: false)
- ContainerExpirationPolicy.runnable_schedules.each_batch(of: BATCH_SIZE) do |policies|
- # rubocop: disable CodeReuse/ActiveRecord
- cte = Gitlab::SQL::CTE.new(:batched_policies, policies.limit(BATCH_SIZE))
- # rubocop: enable CodeReuse/ActiveRecord
- scope = cte.apply_to(ContainerExpirationPolicy.all).with_container_repositories
-
- scope = scope.preloaded if preloaded
-
- scope.each do |policy|
- if policy.valid?
- yield policy
- else
- disable_invalid_policy!(policy)
- end
- end
- end
- end
-
- def disable_invalid_policy!(policy)
- policy.disable!
- Gitlab::ErrorTracking.log_exception(
- ::ContainerExpirationPolicyWorker::InvalidPolicyError.new,
- container_expiration_policy_id: policy.id
- )
- end
-
- def throttling_enabled?
- Feature.enabled?(:container_registry_expiration_policies_throttling, default_enabled: :yaml)
- end
-
def lease_timeout
5.hours
end
diff --git a/app/workers/container_registry/migration/enqueuer_worker.rb b/app/workers/container_registry/migration/enqueuer_worker.rb
index 8705deb0cb2..a0babb98e82 100644
--- a/app/workers/container_registry/migration/enqueuer_worker.rb
+++ b/app/workers/container_registry/migration/enqueuer_worker.rb
@@ -13,10 +13,37 @@ module ContainerRegistry
data_consistency :always
feature_category :container_registry
urgency :low
- deduplicate :until_executing, including_scheduled: true
+ deduplicate :until_executing, ttl: DEFAULT_LEASE_TIMEOUT
idempotent!
def perform
+ migration.enqueuer_loop? ? perform_with_loop : perform_without_loop
+ end
+
+ def self.enqueue_a_job
+ perform_async
+ perform_in(7.seconds) if ::ContainerRegistry::Migration.enqueue_twice?
+ end
+
+ private
+
+ def perform_with_loop
+ try_obtain_lease do
+ while runnable? && Time.zone.now < loop_deadline && migration.enqueuer_loop?
+ repository_handled = handle_aborted_migration || handle_next_migration
+
+ # no repository was found: stop the loop
+ break unless repository_handled
+
+ # we're going for another iteration so we need to clear memoization
+ clear_memoization(:next_repository)
+ clear_memoization(:next_aborted_repository)
+ clear_memoization(:last_step_completed_repository)
+ end
+ end
+ end
+
+ def perform_without_loop
re_enqueue = false
try_obtain_lease do
break unless runnable?
@@ -26,12 +53,10 @@ module ContainerRegistry
re_enqueue_if_capacity if re_enqueue
end
- private
-
def handle_aborted_migration
return unless next_aborted_repository
- log_extra_metadata_on_done(:import_type, 'retry')
+ log_on_done(:import_type, 'retry')
log_repository(next_aborted_repository)
next_aborted_repository.retry_aborted_migration
@@ -40,15 +65,16 @@ module ContainerRegistry
rescue StandardError => e
Gitlab::ErrorTracking.log_exception(e, next_aborted_repository_id: next_aborted_repository&.id)
- true
+ migration.enqueuer_loop? ? false : true
ensure
log_repository_migration_state(next_aborted_repository)
+ log_repository_info(next_aborted_repository, import_type: 'retry')
end
def handle_next_migration
return unless next_repository
- log_extra_metadata_on_done(:import_type, 'next')
+ log_on_done(:import_type, 'next')
log_repository(next_repository)
# We return true because the repository was successfully processed (migration_state is changed)
@@ -63,14 +89,16 @@ module ContainerRegistry
false
ensure
log_repository_migration_state(next_repository)
+ log_repository_info(next_repository, import_type: 'next')
end
def tag_count_too_high?
+ return false if migration.max_tags_count == 0
return false unless next_repository.tags_count > migration.max_tags_count
next_repository.skip_import(reason: :too_many_tags)
- log_extra_metadata_on_done(:tags_count_too_high, true)
- log_extra_metadata_on_done(:max_tags_count_setting, migration.max_tags_count)
+ log_on_done(:tags_count_too_high, true)
+ log_on_done(:max_tags_count_setting, migration.max_tags_count)
true
end
@@ -122,7 +150,11 @@ module ContainerRegistry
def next_repository
strong_memoize(:next_repository) do
- ContainerRepository.ready_for_import.take # rubocop:disable CodeReuse/ActiveRecord
+ # Using .limit(2)[0] instead of take here. Using a LIMIT 1 caused the query planner to
+ # use an inefficient sequential scan instead of picking an index. LIMIT 2 works around
+ # this issue.
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87733 for details.
+ ContainerRepository.ready_for_import.limit(2)[0] # rubocop:disable CodeReuse/ActiveRecord
end
end
@@ -145,18 +177,51 @@ module ContainerRegistry
def re_enqueue_if_capacity
return unless below_capacity?
- self.class.perform_async
+ self.class.enqueue_a_job
end
def log_repository(repository)
- log_extra_metadata_on_done(:container_repository_id, repository&.id)
- log_extra_metadata_on_done(:container_repository_path, repository&.path)
+ log_on_done(:container_repository_id, repository&.id)
+ log_on_done(:container_repository_path, repository&.path)
end
def log_repository_migration_state(repository)
return unless repository
- log_extra_metadata_on_done(:container_repository_migration_state, repository.migration_state)
+ log_on_done(:container_repository_migration_state, repository.migration_state)
+ end
+
+ def log_on_done(key, value)
+ return if migration.enqueuer_loop?
+
+ log_extra_metadata_on_done(key, value)
+ end
+
+ def log_info(extras)
+ logger.info(structured_payload(extras))
+ end
+
+ def log_repository_info(repository, extras = {})
+ return unless migration.enqueuer_loop?
+ return unless repository
+
+ repository_info = {
+ container_repository_id: repository.id,
+ container_repository_path: repository.path,
+ container_repository_migration_state: repository.migration_state
+ }
+
+ if repository.import_skipped?
+ repository_info[:container_repository_migration_skipped_reason] = repository.migration_skipped_reason
+ end
+
+ log_info(extras.merge(repository_info))
+ end
+
+ def loop_deadline
+ strong_memoize(:loop_deadline) do
+ 250.seconds.from_now
+ end
end
# used by ExclusiveLeaseGuard
diff --git a/app/workers/container_registry/migration/guard_worker.rb b/app/workers/container_registry/migration/guard_worker.rb
index bab6b8c2a72..1111061a89b 100644
--- a/app/workers/container_registry/migration/guard_worker.rb
+++ b/app/workers/container_registry/migration/guard_worker.rb
@@ -13,7 +13,7 @@ module ContainerRegistry
feature_category :container_registry
urgency :low
worker_resource_boundary :unknown
- deduplicate :until_executed
+ deduplicate :until_executed, ttl: 5.minutes
idempotent!
def perform
@@ -64,7 +64,17 @@ module ContainerRegistry
end
def long_running_migration?(repository)
- migration_start_timestamp(repository).before?(long_running_migration_threshold)
+ timeout = long_running_migration_threshold
+
+ if Feature.enabled?(:registry_migration_guard_thresholds)
+ timeout = if repository.migration_state == 'pre_importing'
+ migration.pre_import_timeout.seconds
+ else
+ migration.import_timeout.seconds
+ end
+ end
+
+ migration_start_timestamp(repository).before?(timeout.ago)
end
def external_state_matches_migration_state?(repository)
@@ -83,17 +93,21 @@ module ContainerRegistry
end
def step_before_timestamp
- ::ContainerRegistry::Migration.max_step_duration.seconds.ago
+ migration.max_step_duration.seconds.ago
end
def max_capacity
# doubling the actual capacity to prevent issues in case the capacity
# is not properly applied
- ::ContainerRegistry::Migration.capacity * 2
+ migration.capacity * 2
+ end
+
+ def migration
+ ::ContainerRegistry::Migration
end
def long_running_migration_threshold
- @threshold ||= 30.minutes.ago
+ @threshold ||= 10.minutes
end
def cancel_long_running_migration(repository)
@@ -101,7 +115,11 @@ module ContainerRegistry
case result[:status]
when :ok
- repository.skip_import(reason: :migration_canceled)
+ if repository.nearing_or_exceeded_retry_limit?
+ repository.skip_import(reason: :migration_canceled)
+ else
+ repository.abort_import
+ end
when :bad_request
repository.reconcile_import_status(result[:state]) do
repository.abort_import
diff --git a/app/workers/database/batched_background_migration/ci_database_worker.rb b/app/workers/database/batched_background_migration/ci_database_worker.rb
index 13314cf95e2..ee9cbba7076 100644
--- a/app/workers/database/batched_background_migration/ci_database_worker.rb
+++ b/app/workers/database/batched_background_migration/ci_database_worker.rb
@@ -5,7 +5,7 @@ module Database
include SingleDatabaseWorker
def self.enabled?
- Feature.enabled?(:execute_batched_migrations_on_schedule_ci_database, type: :ops, default_enabled: :yaml)
+ Feature.enabled?(:execute_batched_migrations_on_schedule_ci_database, type: :ops)
end
def self.tracking_database
diff --git a/app/workers/database/batched_background_migration_worker.rb b/app/workers/database/batched_background_migration_worker.rb
index 6a41fe70915..31208d7a473 100644
--- a/app/workers/database/batched_background_migration_worker.rb
+++ b/app/workers/database/batched_background_migration_worker.rb
@@ -5,7 +5,7 @@ module Database
include BatchedBackgroundMigration::SingleDatabaseWorker
def self.enabled?
- Feature.enabled?(:execute_batched_migrations_on_schedule, type: :ops, default_enabled: :yaml)
+ Feature.enabled?(:execute_batched_migrations_on_schedule, type: :ops)
end
def self.tracking_database
diff --git a/app/workers/database/ci_namespace_mirrors_consistency_check_worker.rb b/app/workers/database/ci_namespace_mirrors_consistency_check_worker.rb
index 2b4253947ac..b2174be1402 100644
--- a/app/workers/database/ci_namespace_mirrors_consistency_check_worker.rb
+++ b/app/workers/database/ci_namespace_mirrors_consistency_check_worker.rb
@@ -13,7 +13,7 @@ module Database
version 1
def perform
- return if Feature.disabled?(:ci_namespace_mirrors_consistency_check, default_enabled: :yaml)
+ return if Feature.disabled?(:ci_namespace_mirrors_consistency_check)
results = ConsistencyCheckService.new(
source_model: Namespace,
@@ -22,6 +22,16 @@ module Database
target_columns: %w[namespace_id traversal_ids]
).execute
+ if results[:mismatches_details].any?
+ ConsistencyFixService.new(
+ source_model: Namespace,
+ target_model: Ci::NamespaceMirror,
+ sync_event_class: Namespaces::SyncEvent,
+ source_sort_key: :id,
+ target_sort_key: :namespace_id
+ ).execute(ids: results[:mismatches_details].map { |h| h[:id] })
+ end
+
log_extra_metadata_on_done(:results, results)
end
end
diff --git a/app/workers/database/ci_project_mirrors_consistency_check_worker.rb b/app/workers/database/ci_project_mirrors_consistency_check_worker.rb
index e9413256617..84052ab238b 100644
--- a/app/workers/database/ci_project_mirrors_consistency_check_worker.rb
+++ b/app/workers/database/ci_project_mirrors_consistency_check_worker.rb
@@ -13,7 +13,7 @@ module Database
version 1
def perform
- return if Feature.disabled?(:ci_project_mirrors_consistency_check, default_enabled: :yaml)
+ return if Feature.disabled?(:ci_project_mirrors_consistency_check)
results = ConsistencyCheckService.new(
source_model: Project,
@@ -22,6 +22,16 @@ module Database
target_columns: %w[project_id namespace_id]
).execute
+ if results[:mismatches_details].any?
+ ConsistencyFixService.new(
+ source_model: Project,
+ target_model: Ci::ProjectMirror,
+ sync_event_class: Projects::SyncEvent,
+ source_sort_key: :id,
+ target_sort_key: :project_id
+ ).execute(ids: results[:mismatches_details].map { |h| h[:id] })
+ end
+
log_extra_metadata_on_done(:results, results)
end
end
diff --git a/app/workers/deployments/hooks_worker.rb b/app/workers/deployments/hooks_worker.rb
index 31c57e5c001..608601b4eb9 100644
--- a/app/workers/deployments/hooks_worker.rb
+++ b/app/workers/deployments/hooks_worker.rb
@@ -13,6 +13,9 @@ module Deployments
params = params.with_indifferent_access
if (deploy = Deployment.find_by_id(params[:deployment_id]))
+ log_extra_metadata_on_done(:deployment_project_id, deploy.project.id)
+ log_extra_metadata_on_done(:deployment_id, params[:deployment_id])
+
deploy.execute_hooks(params[:status_changed_at].to_time)
end
end
diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb
deleted file mode 100644
index 948e1a59b07..00000000000
--- a/app/workers/expire_build_instance_artifacts_worker.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-class ExpireBuildInstanceArtifactsWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
-
- feature_category :build_artifacts
-
- def perform(build_id)
- # rubocop: disable CodeReuse/ActiveRecord
- build = Ci::Build
- .with_expired_artifacts
- .reorder(nil)
- .find_by_id(build_id)
- # rubocop: enable CodeReuse/ActiveRecord
-
- return unless build&.project && !build.project.pending_delete
-
- Gitlab::AppLogger.info("Removing artifacts for build #{build.id}...")
- build.erase_erasable_artifacts!
- 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 5188bda03e2..34996b710d4 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
@@ -36,7 +36,7 @@ module Gitlab
private
def diff_notes_importer(project)
- if project.group.present? && Feature.enabled?(:github_importer_single_endpoint_notes_import, project.group, type: :ops, default_enabled: :yaml)
+ if project.group.present? && Feature.enabled?(:github_importer_single_endpoint_notes_import, project.group, type: :ops)
Importer::SingleEndpointDiffNotesImporter
else
Importer::DiffNotesImporter
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 90a1337169f..167b3e147a3 100644
--- a/app/workers/gitlab/github_import/stage/import_notes_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
@@ -29,7 +29,7 @@ module Gitlab
end
def importers(project)
- if project.group.present? && Feature.enabled?(:github_importer_single_endpoint_notes_import, project.group, type: :ops, default_enabled: :yaml)
+ if project.group.present? && Feature.enabled?(:github_importer_single_endpoint_notes_import, project.group, type: :ops)
[
Importer::SingleEndpointMergeRequestNotesImporter,
Importer::SingleEndpointIssueNotesImporter
diff --git a/app/workers/merge_request_cleanup_refs_worker.rb b/app/workers/merge_request_cleanup_refs_worker.rb
index 27bd5774b8d..db6f4649f47 100644
--- a/app/workers/merge_request_cleanup_refs_worker.rb
+++ b/app/workers/merge_request_cleanup_refs_worker.rb
@@ -18,7 +18,7 @@ class MergeRequestCleanupRefsWorker
FAILURE_THRESHOLD = 3
def perform_work
- return unless Feature.enabled?(:merge_request_refs_cleanup, default_enabled: false)
+ return unless Feature.enabled?(:merge_request_refs_cleanup)
unless merge_request
logger.error('No existing merge request to be cleaned up.')
diff --git a/app/workers/merge_requests/close_issue_worker.rb b/app/workers/merge_requests/close_issue_worker.rb
new file mode 100644
index 00000000000..86d63e571ac
--- /dev/null
+++ b/app/workers/merge_requests/close_issue_worker.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class CloseIssueWorker
+ include ApplicationWorker
+
+ data_consistency :always
+ feature_category :code_review
+ urgency :low
+ idempotent!
+
+ # Issues:CloseService execute webhooks which are treated as external dependencies
+ worker_has_external_dependencies!
+
+ # This worker only accepts ID of an Issue. We are intentionally using this
+ # worker to close Issues asynchronously as we only experience SQL timeouts
+ # when closing an Issue.
+ def perform(project_id, user_id, issue_id, merge_request_id)
+ project = Project.find_by_id(project_id)
+
+ unless project
+ logger.info(structured_payload(message: 'Project not found.', project_id: project_id))
+ return
+ end
+
+ user = User.find_by_id(user_id)
+
+ unless user
+ logger.info(structured_payload(message: 'User not found.', user_id: user_id))
+ return
+ end
+
+ issue = Issue.find_by_id(issue_id)
+
+ unless issue
+ logger.info(structured_payload(message: 'Issue not found.', issue_id: issue_id))
+ return
+ end
+
+ merge_request = MergeRequest.find_by_id(merge_request_id)
+
+ unless merge_request
+ logger.info(structured_payload(message: 'Merge request not found.', merge_request_id: merge_request_id))
+ return
+ end
+
+ Issues::CloseService
+ .new(project: project, current_user: user)
+ .execute(issue, commit: merge_request)
+ end
+ end
+end
diff --git a/app/workers/packages/cleanup_package_file_worker.rb b/app/workers/packages/cleanup_package_file_worker.rb
index f188017ee7a..68a145920ca 100644
--- a/app/workers/packages/cleanup_package_file_worker.rb
+++ b/app/workers/packages/cleanup_package_file_worker.rb
@@ -32,7 +32,7 @@ module Packages
end
def next_item
- model.next_pending_destruction
+ model.next_pending_destruction(order_by: :id)
end
def log_metadata(package_file)
diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb
index 6d809dfb22b..f73958a6ef9 100644
--- a/app/workers/project_service_worker.rb
+++ b/app/workers/project_service_worker.rb
@@ -13,10 +13,13 @@ class ProjectServiceWorker # rubocop:disable Scalability/IdempotentWorker
def perform(hook_id, data)
data = data.with_indifferent_access
- integration = Integration.find(hook_id)
- integration.execute(data)
- rescue StandardError => error
- integration_class = integration&.class&.name || "Not Found"
- Gitlab::ErrorTracking.log_exception(error, integration_class: integration_class)
+ integration = Integration.find_by_id(hook_id)
+ return unless integration
+
+ begin
+ integration.execute(data)
+ rescue StandardError => error
+ integration.log_exception(error)
+ end
end
end
diff --git a/app/services/projects/after_import_service.rb b/app/workers/projects/after_import_worker.rb
index bb0d084d191..06211b2d991 100644
--- a/app/services/projects/after_import_service.rb
+++ b/app/workers/projects/after_import_worker.rb
@@ -1,14 +1,19 @@
# frozen_string_literal: true
module Projects
- class AfterImportService
+ class AfterImportWorker
+ include ApplicationWorker
+
RESERVED_REF_PREFIXES = Repository::RESERVED_REFS_NAMES.map { |n| File.join('refs', n, '/') }
- def initialize(project)
- @project = project
- end
+ data_consistency :always
+ idempotent!
+ urgency :low
+ feature_category :importers
+
+ def perform(project_id)
+ @project = Project.find(project_id)
- def execute
service = Repositories::HousekeepingService.new(@project)
service.execute do
diff --git a/app/workers/projects/git_garbage_collect_worker.rb b/app/workers/projects/git_garbage_collect_worker.rb
index a70c52abde2..9ac3953e83c 100644
--- a/app/workers/projects/git_garbage_collect_worker.rb
+++ b/app/workers/projects/git_garbage_collect_worker.rb
@@ -44,8 +44,12 @@ module Projects
end
override :update_db_repository_statistics
- def update_db_repository_statistics(resource)
- Projects::UpdateStatisticsService.new(resource, nil, statistics: [:repository_size, :lfs_objects_size]).execute
+ def update_db_repository_statistics(resource, stats)
+ Projects::UpdateStatisticsService.new(resource, nil, statistics: stats).execute
+ end
+
+ def stats
+ [:repository_size, :lfs_objects_size]
end
end
end
diff --git a/app/workers/projects/inactive_projects_deletion_cron_worker.rb b/app/workers/projects/inactive_projects_deletion_cron_worker.rb
new file mode 100644
index 00000000000..2c3f4191502
--- /dev/null
+++ b/app/workers/projects/inactive_projects_deletion_cron_worker.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module Projects
+ class InactiveProjectsDeletionCronWorker
+ include ApplicationWorker
+ include Gitlab::Utils::StrongMemoize
+ include CronjobQueue
+
+ idempotent!
+ data_consistency :always
+ feature_category :compliance_management
+
+ INTERVAL = 2.seconds.to_i
+
+ def perform
+ return unless ::Gitlab::CurrentSettings.delete_inactive_projects?
+
+ admin_user = User.admins.active.first
+
+ return unless admin_user
+
+ notified_inactive_projects = Gitlab::InactiveProjectsDeletionWarningTracker.notified_projects
+
+ Project.inactive.without_deleted.find_each(batch_size: 100).with_index do |project, index| # rubocop: disable CodeReuse/ActiveRecord
+ next unless Feature.enabled?(:inactive_projects_deletion, project.root_namespace)
+
+ delay = index * INTERVAL
+
+ with_context(project: project, user: admin_user) do
+ deletion_warning_email_sent_on = notified_inactive_projects["project:#{project.id}"]
+
+ if send_deletion_warning_email?(deletion_warning_email_sent_on, project)
+ send_notification(delay, project, admin_user)
+ elsif deletion_warning_email_sent_on && delete_due_to_inactivity?(deletion_warning_email_sent_on)
+ Gitlab::InactiveProjectsDeletionWarningTracker.new(project.id).reset
+ delete_project(project, admin_user)
+ end
+ end
+ end
+ end
+
+ private
+
+ def grace_months_after_deletion_notification
+ strong_memoize(:grace_months_after_deletion_notification) do
+ (::Gitlab::CurrentSettings.inactive_projects_delete_after_months -
+ ::Gitlab::CurrentSettings.inactive_projects_send_warning_email_after_months).months
+ end
+ end
+
+ def send_deletion_warning_email?(deletion_warning_email_sent_on, project)
+ deletion_warning_email_sent_on.blank?
+ end
+
+ def delete_due_to_inactivity?(deletion_warning_email_sent_on)
+ deletion_warning_email_sent_on < grace_months_after_deletion_notification.ago
+ end
+
+ def deletion_date
+ grace_months_after_deletion_notification.from_now.to_date.to_s
+ end
+
+ def delete_project(project, user)
+ ::Projects::DestroyService.new(project, user, {}).async_execute
+ end
+
+ def send_notification(delay, project, user)
+ ::Projects::InactiveProjectsDeletionNotificationWorker.perform_in(delay, project.id, deletion_date)
+ end
+ end
+end
+
+Projects::InactiveProjectsDeletionCronWorker.prepend_mod
diff --git a/app/workers/projects/inactive_projects_deletion_notification_worker.rb b/app/workers/projects/inactive_projects_deletion_notification_worker.rb
new file mode 100644
index 00000000000..0bf808fd753
--- /dev/null
+++ b/app/workers/projects/inactive_projects_deletion_notification_worker.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Projects
+ class InactiveProjectsDeletionNotificationWorker
+ include ApplicationWorker
+ include ExceptionBacktrace
+
+ idempotent!
+ data_consistency :sticky
+ sidekiq_options retry: 3
+ feature_category :compliance_management
+
+ def perform(project_id, deletion_date)
+ return if Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).notified?
+
+ project = Project.find(project_id)
+
+ notification_service.inactive_project_deletion_warning(project, deletion_date)
+
+ Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).mark_notified
+ rescue ActiveRecord::RecordNotFound => error
+ Gitlab::ErrorTracking.log_exception(error, project_id: project_id)
+ end
+
+ private
+
+ def notification_service
+ @notification_service ||= NotificationService.new
+ end
+ end
+end
diff --git a/app/workers/projects/record_target_platforms_worker.rb b/app/workers/projects/record_target_platforms_worker.rb
index 5b1f85ecca0..899721492e9 100644
--- a/app/workers/projects/record_target_platforms_worker.rb
+++ b/app/workers/projects/record_target_platforms_worker.rb
@@ -7,6 +7,7 @@ module Projects
LEASE_TIMEOUT = 1.hour.to_i
APPLE_PLATFORM_LANGUAGES = %w(swift objective-c).freeze
+ ANDROID_PLATFORM_LANGUAGES = %w(java kotlin).freeze
feature_category :experimentation_activation
data_consistency :always
@@ -18,10 +19,10 @@ module Projects
@project = Project.find_by_id(project_id)
return unless project
- return unless uses_apple_platform_languages?
+ return unless detector_service
try_obtain_lease do
- @target_platforms = Projects::RecordTargetPlatformsService.new(project).execute
+ @target_platforms = Projects::RecordTargetPlatformsService.new(project, detector_service).execute
log_target_platforms_metadata
end
end
@@ -30,8 +31,29 @@ module Projects
attr_reader :target_platforms, :project
+ def detector_service
+ if uses_apple_platform_languages?
+ AppleTargetPlatformDetectorService
+ elsif uses_android_platform_languages? && detect_android_projects_enabled?
+ AndroidTargetPlatformDetectorService
+ end
+ end
+
+ def detect_android_projects_enabled?
+ Feature.enabled?(:detect_android_projects, project)
+ end
+
def uses_apple_platform_languages?
- project.repository_languages.with_programming_language(*APPLE_PLATFORM_LANGUAGES).present?
+ target_languages.with_programming_language(*APPLE_PLATFORM_LANGUAGES).present?
+ end
+
+ def uses_android_platform_languages?
+ target_languages.with_programming_language(*ANDROID_PLATFORM_LANGUAGES).present?
+ end
+
+ def target_languages
+ languages = APPLE_PLATFORM_LANGUAGES + ANDROID_PLATFORM_LANGUAGES
+ @target_languages ||= project.repository_languages.with_programming_language(*languages)
end
def log_target_platforms_metadata
diff --git a/app/workers/prometheus/create_default_alerts_worker.rb b/app/workers/prometheus/create_default_alerts_worker.rb
index 94ac02c4c04..1a0fe7e8d56 100644
--- a/app/workers/prometheus/create_default_alerts_worker.rb
+++ b/app/workers/prometheus/create_default_alerts_worker.rb
@@ -13,19 +13,7 @@ module Prometheus
idempotent!
def perform(project_id)
- project = Project.find_by_id(project_id)
-
- return unless project
-
- result = ::Prometheus::CreateDefaultAlertsService.new(project: project).execute
-
- log_info(result.message) if result.error?
- end
-
- private
-
- def log_info(message)
- logger.info(structured_payload(message: message))
+ # No-op Will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/360756
end
end
end
diff --git a/app/workers/requests_profiles_worker.rb b/app/workers/requests_profiles_worker.rb
deleted file mode 100644
index e02b63fb621..00000000000
--- a/app/workers/requests_profiles_worker.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-class RequestsProfilesWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- # rubocop:disable Scalability/CronWorkerContext
- # This worker does not perform work scoped to a context
- include CronjobQueue
- # rubocop:enable Scalability/CronWorkerContext
-
- feature_category :source_code_management
-
- def perform
- Gitlab::RequestProfiler.remove_all_profiles
- end
-end
diff --git a/app/workers/schedule_merge_request_cleanup_refs_worker.rb b/app/workers/schedule_merge_request_cleanup_refs_worker.rb
index 58cd8f7ade3..b3d0067471a 100644
--- a/app/workers/schedule_merge_request_cleanup_refs_worker.rb
+++ b/app/workers/schedule_merge_request_cleanup_refs_worker.rb
@@ -12,7 +12,7 @@ class ScheduleMergeRequestCleanupRefsWorker
def perform
return if Gitlab::Database.read_only?
- return unless Feature.enabled?(:merge_request_refs_cleanup, default_enabled: false)
+ return unless Feature.enabled?(:merge_request_refs_cleanup)
MergeRequestCleanupRefsWorker.perform_with_capacity
end
diff --git a/app/workers/web_hooks/destroy_worker.rb b/app/workers/web_hooks/destroy_worker.rb
index f457cd11e54..822a1e770d7 100644
--- a/app/workers/web_hooks/destroy_worker.rb
+++ b/app/workers/web_hooks/destroy_worker.rb
@@ -7,7 +7,7 @@ module WebHooks
data_consistency :always
sidekiq_options retry: 3
feature_category :integrations
- urgency :low
+ urgency :high
idempotent!
diff --git a/app/workers/wikis/git_garbage_collect_worker.rb b/app/workers/wikis/git_garbage_collect_worker.rb
index b00190c6b98..977493834c9 100644
--- a/app/workers/wikis/git_garbage_collect_worker.rb
+++ b/app/workers/wikis/git_garbage_collect_worker.rb
@@ -19,8 +19,12 @@ module Wikis
end
override :update_db_repository_statistics
- def update_db_repository_statistics(resource)
- Projects::UpdateStatisticsService.new(resource.container, nil, statistics: [:wiki_size]).execute
+ def update_db_repository_statistics(resource, stats)
+ Projects::UpdateStatisticsService.new(resource.container, nil, statistics: stats).execute
+ end
+
+ def stats
+ [:wiki_size]
end
end
end